From 9cae7241eccfea076bc50ac4e5e3f89c8ce3bb9a Mon Sep 17 00:00:00 2001 From: kunitoki Date: Tue, 2 Jun 2026 23:13:45 +0200 Subject: [PATCH 1/2] More tests --- tests/yup_core.cpp | 1 + tests/yup_core/yup_Logger.cpp | 265 +++++++++++++ tests/yup_core/yup_Socket.cpp | 274 +++++++++++-- tests/yup_events.cpp | 1 + .../yup_events/yup_InterprocessConnection.cpp | 369 ++++++++++++++++++ 5 files changed, 883 insertions(+), 27 deletions(-) create mode 100644 tests/yup_core/yup_Logger.cpp create mode 100644 tests/yup_events/yup_InterprocessConnection.cpp diff --git a/tests/yup_core.cpp b/tests/yup_core.cpp index 4be76f061..eebb22ea1 100644 --- a/tests/yup_core.cpp +++ b/tests/yup_core.cpp @@ -54,6 +54,7 @@ #include "yup_core/yup_LinkedListPointer.cpp" #include "yup_core/yup_ListenerList.cpp" #include "yup_core/yup_LocalisedStrings.cpp" +#include "yup_core/yup_Logger.cpp" #include "yup_core/yup_MACAddress.cpp" #include "yup_core/yup_MathFunctions.cpp" #include "yup_core/yup_MemoryInputStream.cpp" diff --git a/tests/yup_core/yup_Logger.cpp b/tests/yup_core/yup_Logger.cpp new file mode 100644 index 000000000..f2bbc1d26 --- /dev/null +++ b/tests/yup_core/yup_Logger.cpp @@ -0,0 +1,265 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +namespace +{ + +class RecordingLogger : public Logger +{ +public: + void logMessage (const String& message) override + { + const ScopedLock sl (lock); + messages.add (message); + } + + StringArray getMessages() + { + const ScopedLock sl (lock); + return messages; + } + +private: + CriticalSection lock; + StringArray messages; +}; + +} // namespace + +class LoggerTests : public ::testing::Test +{ +protected: + void SetUp() override + { + Logger::setCurrentLogger (nullptr); + } + + void TearDown() override + { + Logger::setCurrentLogger (nullptr); + } +}; + +TEST_F (LoggerTests, DefaultStateHasNoLogger) +{ + EXPECT_EQ (Logger::getCurrentLogger(), nullptr); +} + +TEST_F (LoggerTests, SetAndGetCurrentLogger) +{ + RecordingLogger logger; + Logger::setCurrentLogger (&logger); + EXPECT_EQ (Logger::getCurrentLogger(), &logger); + Logger::setCurrentLogger (nullptr); +} + +TEST_F (LoggerTests, WriteToLogDispatchesToCurrentLogger) +{ + RecordingLogger logger; + Logger::setCurrentLogger (&logger); + + Logger::writeToLog ("hello"); + Logger::writeToLog ("world"); + + auto messages = logger.getMessages(); + EXPECT_EQ (messages.size(), 2); + EXPECT_EQ (messages[0], "hello"); + EXPECT_EQ (messages[1], "world"); + Logger::setCurrentLogger (nullptr); +} + +TEST_F (LoggerTests, WriteToLogWithNoLoggerDoesNotCrash) +{ + EXPECT_EQ (Logger::getCurrentLogger(), nullptr); + Logger::writeToLog ("no logger set"); +} + +TEST_F (LoggerTests, SetCurrentLoggerToNullRemovesLogger) +{ + RecordingLogger logger; + Logger::setCurrentLogger (&logger); + EXPECT_NE (Logger::getCurrentLogger(), nullptr); + + Logger::setCurrentLogger (nullptr); + EXPECT_EQ (Logger::getCurrentLogger(), nullptr); +} + +TEST_F (LoggerTests, WriteAfterRemovingLoggerDoesNotDispatch) +{ + RecordingLogger logger; + Logger::setCurrentLogger (&logger); + Logger::writeToLog ("first"); + + Logger::setCurrentLogger (nullptr); + Logger::writeToLog ("second"); + + EXPECT_EQ (logger.getMessages().size(), 1); + EXPECT_EQ (logger.getMessages()[0], "first"); +} + +TEST_F (LoggerTests, OutputDebugStringDoesNotCrash) +{ + Logger::outputDebugString ("debug output"); +} + +// ============================================================================= + +class FileLoggerTests : public ::testing::Test +{ +protected: + void SetUp() override + { + Logger::setCurrentLogger (nullptr); + tempFile = File::getSpecialLocation (File::tempDirectory) + .getChildFile ("YUP_FileLoggerTests_" + String::toHexString (Random::getSystemRandom().nextInt()) + ".log"); + tempFile.deleteFile(); + } + + void TearDown() override + { + Logger::setCurrentLogger (nullptr); + tempFile.deleteFile(); + } + + File tempFile; +}; + +TEST_F (FileLoggerTests, ConstructionCreatesFile) +{ + FileLogger logger (tempFile, "welcome", -1); + EXPECT_TRUE (tempFile.exists()); + EXPECT_EQ (logger.getLogFile(), tempFile); +} + +TEST_F (FileLoggerTests, GetLogFileReturnsCorrectFile) +{ + FileLogger logger (tempFile, "test", -1); + EXPECT_EQ (logger.getLogFile().getFullPathName(), tempFile.getFullPathName()); +} + +TEST_F (FileLoggerTests, WelcomeMessageIsWritten) +{ + { + FileLogger logger (tempFile, "my welcome message", -1); + } + + auto content = tempFile.loadFileAsString(); + EXPECT_TRUE (content.contains ("my welcome message")); +} + +TEST_F (FileLoggerTests, LogMessageWritesToFile) +{ + { + FileLogger logger (tempFile, "start", -1); + Logger::setCurrentLogger (&logger); + Logger::writeToLog ("test message"); + Logger::setCurrentLogger (nullptr); + } + + EXPECT_TRUE (tempFile.loadFileAsString().contains ("test message")); +} + +TEST_F (FileLoggerTests, MultipleMessagesAllWritten) +{ + { + FileLogger logger (tempFile, "start", -1); + logger.logMessage ("line one"); + logger.logMessage ("line two"); + logger.logMessage ("line three"); + } + + auto content = tempFile.loadFileAsString(); + EXPECT_TRUE (content.contains ("line one")); + EXPECT_TRUE (content.contains ("line two")); + EXPECT_TRUE (content.contains ("line three")); +} + +TEST_F (FileLoggerTests, TrimFileSizeTruncatesLargeFile) +{ + { + FileLogger logger (tempFile, "start", -1); + for (int i = 0; i < 200; ++i) + logger.logMessage ("line " + String (i) + " some extra padding content to make lines longer"); + } + + auto originalSize = tempFile.getSize(); + ASSERT_GT (originalSize, 0); + + auto trimTarget = originalSize / 2; + FileLogger::trimFileSize (tempFile, trimTarget); + + EXPECT_LE (tempFile.getSize(), trimTarget + 512); + EXPECT_GT (tempFile.getSize(), 0); +} + +TEST_F (FileLoggerTests, TrimFileSizeWithZeroDeletesFile) +{ + { + FileLogger logger (tempFile, "start", -1); + logger.logMessage ("some content"); + } + + ASSERT_TRUE (tempFile.exists()); + FileLogger::trimFileSize (tempFile, 0); + EXPECT_FALSE (tempFile.exists()); +} + +TEST_F (FileLoggerTests, TrimFileSizeDoesNothingWhenFileIsSmallerThanLimit) +{ + { + FileLogger logger (tempFile, "start", -1); + logger.logMessage ("short"); + } + + auto originalSize = tempFile.getSize(); + FileLogger::trimFileSize (tempFile, originalSize * 2); + EXPECT_EQ (tempFile.getSize(), originalSize); +} + +TEST_F (FileLoggerTests, GetSystemLogFileFolderReturnsNonEmptyPath) +{ + auto folder = FileLogger::getSystemLogFileFolder(); + EXPECT_FALSE (folder.getFullPathName().isEmpty()); +} + +TEST_F (FileLoggerTests, MaxInitialFileSizeTruncatesExistingLargeFile) +{ + { + FileLogger firstLogger (tempFile, "first session", -1); + for (int i = 0; i < 200; ++i) + firstLogger.logMessage ("padding line " + String (i) + " with some extra content to grow the file"); + } + + auto sizeBeforeSecondOpen = tempFile.getSize(); + ASSERT_GT (sizeBeforeSecondOpen, 1024); + + { + FileLogger secondLogger (tempFile, "second session", 512); + } + + EXPECT_LE (tempFile.getSize(), sizeBeforeSecondOpen); +} diff --git a/tests/yup_core/yup_Socket.cpp b/tests/yup_core/yup_Socket.cpp index 4be10cae7..e3a461295 100644 --- a/tests/yup_core/yup_Socket.cpp +++ b/tests/yup_core/yup_Socket.cpp @@ -43,56 +43,276 @@ using namespace yup; -#if 0 -TEST (SocketTests, StreamingSocket) +// ============================================================================= +// SocketOptions Tests +// ============================================================================= + +TEST (SocketOptionsTests, DefaultConstructionHasNoBufferSizes) { - auto localHost = IPAddress::local(); - int portNum = 12345; + SocketOptions opts; + EXPECT_FALSE (opts.getReceiveBufferSize().has_value()); + EXPECT_FALSE (opts.getSendBufferSize().has_value()); +} - StreamingSocket socketServer; +TEST (SocketOptionsTests, WithReceiveBufferSize) +{ + auto opts = SocketOptions {}.withReceiveBufferSize (131072); + EXPECT_TRUE (opts.getReceiveBufferSize().has_value()); + EXPECT_EQ (*opts.getReceiveBufferSize(), 131072); + EXPECT_FALSE (opts.getSendBufferSize().has_value()); +} - EXPECT_FALSE (socketServer.isConnected()); - EXPECT_TRUE (socketServer.getHostName().isEmpty()); - EXPECT_EQ (socketServer.getBoundPort(), -1); - EXPECT_EQ (static_cast (socketServer.getRawSocketHandle()), invalidSocket); +TEST (SocketOptionsTests, WithSendBufferSize) +{ + auto opts = SocketOptions {}.withSendBufferSize (65536); + EXPECT_FALSE (opts.getReceiveBufferSize().has_value()); + EXPECT_TRUE (opts.getSendBufferSize().has_value()); + EXPECT_EQ (*opts.getSendBufferSize(), 65536); +} - EXPECT_TRUE (socketServer.createListener (portNum, localHost.toString())); +TEST (SocketOptionsTests, WithBothBufferSizesChained) +{ + auto opts = SocketOptions {} + .withReceiveBufferSize (131072) + .withSendBufferSize (65536); - StreamingSocket socket; + EXPECT_EQ (*opts.getReceiveBufferSize(), 131072); + EXPECT_EQ (*opts.getSendBufferSize(), 65536); +} - EXPECT_TRUE (socket.connect (localHost.toString(), portNum)); +TEST (SocketOptionsTests, WithImmutableStyleDoesNotModifyOriginal) +{ + SocketOptions original; + auto modified = original.withReceiveBufferSize (65536); - EXPECT_TRUE (socket.isConnected()); - EXPECT_EQ (socket.getHostName(), localHost.toString()); - EXPECT_NE (socket.getBoundPort(), -1); - EXPECT_NE (static_cast (socket.getRawSocketHandle()), invalidSocket); + EXPECT_FALSE (original.getReceiveBufferSize().has_value()); + EXPECT_TRUE (modified.getReceiveBufferSize().has_value()); +} - socket.close(); +#if ! YUP_WASM +// ============================================================================= +// StreamingSocket Tests +// ============================================================================= + +TEST (StreamingSocketTests, DefaultConstruction) +{ + StreamingSocket socket; EXPECT_FALSE (socket.isConnected()); EXPECT_TRUE (socket.getHostName().isEmpty()); + EXPECT_EQ (socket.getPort(), 0); EXPECT_EQ (socket.getBoundPort(), -1); - EXPECT_EQ (static_cast (socket.getRawSocketHandle()), invalidSocket); + EXPECT_EQ (socket.getRawSocketHandle(), -1); +} + +TEST (StreamingSocketTests, ConstructionWithOptions) +{ + auto opts = SocketOptions {}.withReceiveBufferSize (131072); + StreamingSocket socket (opts); + EXPECT_FALSE (socket.isConnected()); + EXPECT_EQ (socket.getRawSocketHandle(), -1); +} + +TEST (StreamingSocketTests, CreateListenerAssignsPort) +{ + StreamingSocket server; + EXPECT_TRUE (server.createListener (0, "127.0.0.1")); + EXPECT_NE (server.getBoundPort(), -1); + EXPECT_NE (server.getBoundPort(), 0); +} + +TEST (StreamingSocketTests, ConnectToListener) +{ + StreamingSocket server; + ASSERT_TRUE (server.createListener (0, "127.0.0.1")); + + StreamingSocket client; + EXPECT_TRUE (client.connect ("127.0.0.1", server.getBoundPort(), 3000)); + EXPECT_TRUE (client.isConnected()); + EXPECT_EQ (client.getHostName(), "127.0.0.1"); +} + +TEST (StreamingSocketTests, IsLocalForLocalhostConnection) +{ + StreamingSocket server; + ASSERT_TRUE (server.createListener (0, "127.0.0.1")); + + StreamingSocket client; + ASSERT_TRUE (client.connect ("127.0.0.1", server.getBoundPort(), 3000)); + EXPECT_TRUE (client.isLocal()); +} + +TEST (StreamingSocketTests, CloseResetsState) +{ + StreamingSocket server; + ASSERT_TRUE (server.createListener (0, "127.0.0.1")); + + StreamingSocket client; + ASSERT_TRUE (client.connect ("127.0.0.1", server.getBoundPort(), 3000)); + ASSERT_TRUE (client.isConnected()); + + client.close(); + EXPECT_FALSE (client.isConnected()); + EXPECT_TRUE (client.getHostName().isEmpty()); + EXPECT_EQ (client.getBoundPort(), -1); + EXPECT_EQ (client.getRawSocketHandle(), -1); +} + +TEST (StreamingSocketTests, ConnectToInvalidHostFails) +{ + StreamingSocket socket; + EXPECT_FALSE (socket.connect ("invalid.host.local", 12345, 200)); + EXPECT_FALSE (socket.isConnected()); +} + +TEST (StreamingSocketTests, WaitUntilReadyTimesOutOnUnconnectedSocket) +{ + StreamingSocket socket; + // Waiting on an unconnected socket should return error or timeout + int result = socket.waitUntilReady (true, 0); + EXPECT_NE (result, 1); } -TEST (SocketTests, DatagramSocket) +TEST (StreamingSocketTests, ReadAndWriteDataOverConnection) { - auto localHost = IPAddress::local(); - int portNum = 12345; + StreamingSocket server; + ASSERT_TRUE (server.createListener (0, "127.0.0.1")); + const int port = server.getBoundPort(); + + struct ClientThread : public Thread + { + explicit ClientThread (int p) + : Thread ("SocketWriteThread") + , port (p) + { + } + + ~ClientThread() override { stopThread (2000); } + + void run() override + { + StreamingSocket client; + if (! client.connect ("127.0.0.1", port, 3000)) + return; + + const int data = 0x12345678; + client.write (&data, sizeof (data)); + } + int port; + }; + + ClientThread writer (port); + writer.startThread(); + + std::unique_ptr conn (server.waitForNextConnection()); + ASSERT_NE (conn.get(), nullptr); + + int received = 0; + int bytesRead = conn->read (&received, sizeof (received), true); + + writer.stopThread (2000); + + EXPECT_EQ (bytesRead, (int) sizeof (received)); + EXPECT_EQ (received, 0x12345678); +} + +// ============================================================================= +// DatagramSocket Tests +// ============================================================================= + +TEST (DatagramSocketTests, DefaultConstruction) +{ DatagramSocket socket; + EXPECT_EQ (socket.getBoundPort(), -1); +} +TEST (DatagramSocketTests, ConstructionWithBroadcasting) +{ + DatagramSocket socket (true); EXPECT_EQ (socket.getBoundPort(), -1); - EXPECT_NE (static_cast (socket.getRawSocketHandle()), invalidSocket); +} - EXPECT_TRUE (socket.bindToPort (portNum, localHost.toString())); +TEST (DatagramSocketTests, ConstructionWithOptions) +{ + auto opts = SocketOptions {}.withReceiveBufferSize (131072); + DatagramSocket socket (false, opts); + EXPECT_EQ (socket.getBoundPort(), -1); +} - EXPECT_EQ (socket.getBoundPort(), portNum); - EXPECT_NE (static_cast (socket.getRawSocketHandle()), invalidSocket); +TEST (DatagramSocketTests, BindToPortZeroAssignsPort) +{ + DatagramSocket socket; + EXPECT_TRUE (socket.bindToPort (0)); + EXPECT_NE (socket.getBoundPort(), -1); + EXPECT_NE (socket.getBoundPort(), 0); +} - socket.shutdown(); +TEST (DatagramSocketTests, BindToPortWithLocalhostAddress) +{ + DatagramSocket socket; + EXPECT_TRUE (socket.bindToPort (0, "127.0.0.1")); + EXPECT_NE (socket.getBoundPort(), -1); +} + +TEST (DatagramSocketTests, ShutdownResetsBoundPort) +{ + DatagramSocket socket; + ASSERT_TRUE (socket.bindToPort (0)); + ASSERT_NE (socket.getBoundPort(), -1); + socket.shutdown(); EXPECT_EQ (socket.getBoundPort(), -1); - EXPECT_EQ (static_cast (socket.getRawSocketHandle()), invalidSocket); } + +TEST (DatagramSocketTests, SendAndReceiveLoopback) +{ + DatagramSocket receiver; + ASSERT_TRUE (receiver.bindToPort (0, "127.0.0.1")); + const int receiverPort = receiver.getBoundPort(); + + const int sendData = 0xdeadbeef; + DatagramSocket sender; + int bytesSent = sender.write ("127.0.0.1", receiverPort, &sendData, sizeof (sendData)); + EXPECT_EQ (bytesSent, (int) sizeof (sendData)); + + EXPECT_EQ (receiver.waitUntilReady (true, 2000), 1); + + int recvData = 0; + int bytesRead = receiver.read (&recvData, sizeof (recvData), false); + + EXPECT_EQ (bytesRead, (int) sizeof (recvData)); + EXPECT_EQ (recvData, (int) 0xdeadbeef); +} + +TEST (DatagramSocketTests, SendAndReceiveWithSenderInfo) +{ + DatagramSocket receiver; + ASSERT_TRUE (receiver.bindToPort (0, "127.0.0.1")); + const int receiverPort = receiver.getBoundPort(); + + const int sendData = 42; + DatagramSocket sender; + sender.write ("127.0.0.1", receiverPort, &sendData, sizeof (sendData)); + + ASSERT_EQ (receiver.waitUntilReady (true, 2000), 1); + + int recvData = 0; + String senderAddr; + int senderPort = -1; + int bytesRead = receiver.read (&recvData, sizeof (recvData), false, senderAddr, senderPort); + + EXPECT_EQ (bytesRead, (int) sizeof (recvData)); + EXPECT_EQ (recvData, sendData); + EXPECT_FALSE (senderAddr.isEmpty()); + EXPECT_GT (senderPort, 0); +} + +TEST (DatagramSocketTests, SetEnablePortReuse) +{ + DatagramSocket socket; + EXPECT_TRUE (socket.setEnablePortReuse (true)); + EXPECT_TRUE (socket.setEnablePortReuse (false)); +} + #endif diff --git a/tests/yup_events.cpp b/tests/yup_events.cpp index dc4f6635b..9b9f226a8 100644 --- a/tests/yup_events.cpp +++ b/tests/yup_events.cpp @@ -21,3 +21,4 @@ #include "yup_events/yup_Timer.cpp" #include "yup_events/yup_MessageManager.cpp" +#include "yup_events/yup_InterprocessConnection.cpp" diff --git a/tests/yup_events/yup_InterprocessConnection.cpp b/tests/yup_events/yup_InterprocessConnection.cpp new file mode 100644 index 000000000..3fd269ded --- /dev/null +++ b/tests/yup_events/yup_InterprocessConnection.cpp @@ -0,0 +1,369 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +#if ! YUP_WASM + +using namespace yup; + +namespace +{ + +class TestConnection : public InterprocessConnection +{ +public: + TestConnection() + : InterprocessConnection (false) // callbacks on connection thread, no message loop needed + { + } + + ~TestConnection() override + { + disconnect (2000); + } + + void connectionMade() override + { + madeCount.fetch_add (1); + madeEvent.signal(); + } + + void connectionLost() override + { + lostCount.fetch_add (1); + lostEvent.signal(); + } + + void messageReceived (const MemoryBlock& message) override + { + const ScopedLock sl (messagesLock); + receivedMessages.add (message); + messageEvent.signal(); + } + + bool waitForConnection (int timeoutMs = 5000) { return madeEvent.wait (timeoutMs); } + + bool waitForMessage (int timeoutMs = 5000) { return messageEvent.wait (timeoutMs); } + + bool waitForDisconnect (int timeoutMs = 5000) { return lostEvent.wait (timeoutMs); } + + std::atomic madeCount { 0 }; + std::atomic lostCount { 0 }; + WaitableEvent madeEvent, lostEvent, messageEvent; + + CriticalSection messagesLock; + Array receivedMessages; +}; + +class TestConnectionServer : public InterprocessConnectionServer +{ +public: + ~TestConnectionServer() override + { + stop(); + const ScopedLock sl (connectionsLock); + for (auto* conn : serverConnections) + { + conn->disconnect (2000); + delete conn; + } + serverConnections.clearQuick(); + } + + InterprocessConnection* createConnectionObject() override + { + auto* conn = new TestConnection(); + { + const ScopedLock sl (connectionsLock); + serverConnections.add (conn); + } + newConnectionEvent.signal(); + return conn; + } + + bool waitForServerConnection (int timeoutMs = 5000) + { + return newConnectionEvent.wait (timeoutMs); + } + + TestConnection* getLastConnection() + { + const ScopedLock sl (connectionsLock); + return serverConnections.isEmpty() ? nullptr : serverConnections.getLast(); + } + + WaitableEvent newConnectionEvent; + CriticalSection connectionsLock; + Array serverConnections; +}; + +} // namespace + +// ============================================================================= +// InterprocessConnection via named pipe +// ============================================================================= + +class InterprocessConnectionPipeTests : public ::testing::Test +{ +protected: + void SetUp() override + { + pipeName = "YUP_IPC_Test_" + String::toHexString (Random::getSystemRandom().nextInt()); + } + + String pipeName; +}; + +TEST_F (InterprocessConnectionPipeTests, CreateAndConnectPipe) +{ + TestConnection creator; + ASSERT_TRUE (creator.createPipe (pipeName, 2000, true)); + + TestConnection connector; + ASSERT_TRUE (connector.connectToPipe (pipeName, 2000)); + + EXPECT_TRUE (creator.waitForConnection()); + EXPECT_TRUE (connector.waitForConnection()); + + EXPECT_TRUE (creator.isConnected()); + EXPECT_TRUE (connector.isConnected()); + + EXPECT_EQ (creator.madeCount.load(), 1); + EXPECT_EQ (connector.madeCount.load(), 1); +} + +TEST_F (InterprocessConnectionPipeTests, SendMessageFromConnectorToCreator) +{ + TestConnection creator; + ASSERT_TRUE (creator.createPipe (pipeName, 2000, true)); + + TestConnection connector; + ASSERT_TRUE (connector.connectToPipe (pipeName, 2000)); + + ASSERT_TRUE (creator.waitForConnection()); + ASSERT_TRUE (connector.waitForConnection()); + + const char* text = "hello pipe"; + MemoryBlock message (text, strlen (text)); + EXPECT_TRUE (connector.sendMessage (message)); + + EXPECT_TRUE (creator.waitForMessage()); + + const ScopedLock sl (creator.messagesLock); + ASSERT_EQ (creator.receivedMessages.size(), 1); + EXPECT_EQ (creator.receivedMessages[0], message); +} + +TEST_F (InterprocessConnectionPipeTests, SendMessageFromCreatorToConnector) +{ + TestConnection creator; + ASSERT_TRUE (creator.createPipe (pipeName, 2000, true)); + + TestConnection connector; + ASSERT_TRUE (connector.connectToPipe (pipeName, 2000)); + + ASSERT_TRUE (creator.waitForConnection()); + ASSERT_TRUE (connector.waitForConnection()); + + const char* text = "reply pipe"; + MemoryBlock message (text, strlen (text)); + EXPECT_TRUE (creator.sendMessage (message)); + + EXPECT_TRUE (connector.waitForMessage()); + + const ScopedLock sl (connector.messagesLock); + ASSERT_EQ (connector.receivedMessages.size(), 1); + EXPECT_EQ (connector.receivedMessages[0], message); +} + +TEST_F (InterprocessConnectionPipeTests, DisconnectingConnectorNotifiesCreator) +{ + TestConnection creator; + ASSERT_TRUE (creator.createPipe (pipeName, 2000, true)); + + TestConnection connector; + ASSERT_TRUE (connector.connectToPipe (pipeName, 2000)); + + ASSERT_TRUE (creator.waitForConnection()); + ASSERT_TRUE (connector.waitForConnection()); + + connector.disconnect (2000); + + EXPECT_TRUE (creator.waitForDisconnect()); + EXPECT_EQ (creator.lostCount.load(), 1); +} + +TEST_F (InterprocessConnectionPipeTests, SendMessageIsNotConnectedWhenNoPipe) +{ + TestConnection connection; + EXPECT_FALSE (connection.isConnected()); + + MemoryBlock message ("data", 4); + EXPECT_FALSE (connection.sendMessage (message)); +} + +TEST_F (InterprocessConnectionPipeTests, CreatePipeWithMustNotExistFailsOnDuplicate) +{ + TestConnection first; + ASSERT_TRUE (first.createPipe (pipeName, 2000, true)); + + TestConnection second; + EXPECT_FALSE (second.createPipe (pipeName, 2000, true)); +} + +// ============================================================================= +// InterprocessConnection via socket server +// ============================================================================= + +class InterprocessConnectionSocketTests : public ::testing::Test +{ +protected: +}; + +TEST_F (InterprocessConnectionSocketTests, ServerListensOnValidPort) +{ + TestConnectionServer server; + ASSERT_TRUE (server.beginWaitingForSocket (0)); + EXPECT_NE (server.getBoundPort(), -1); + EXPECT_NE (server.getBoundPort(), 0); +} + +TEST_F (InterprocessConnectionSocketTests, ClientConnectsToServer) +{ + TestConnectionServer server; + ASSERT_TRUE (server.beginWaitingForSocket (0)); + const int port = server.getBoundPort(); + + TestConnection client; + ASSERT_TRUE (client.connectToSocket ("127.0.0.1", port, 3000)); + + EXPECT_TRUE (client.waitForConnection()); + EXPECT_TRUE (server.waitForServerConnection()); + + EXPECT_TRUE (client.isConnected()); + + auto* serverConn = server.getLastConnection(); + ASSERT_NE (serverConn, nullptr); + EXPECT_TRUE (serverConn->waitForConnection()); + EXPECT_TRUE (serverConn->isConnected()); +} + +TEST_F (InterprocessConnectionSocketTests, SendMessageClientToServerConnection) +{ + TestConnectionServer server; + ASSERT_TRUE (server.beginWaitingForSocket (0)); + const int port = server.getBoundPort(); + + TestConnection client; + ASSERT_TRUE (client.connectToSocket ("127.0.0.1", port, 3000)); + + ASSERT_TRUE (client.waitForConnection()); + ASSERT_TRUE (server.waitForServerConnection()); + + auto* serverConn = server.getLastConnection(); + ASSERT_NE (serverConn, nullptr); + ASSERT_TRUE (serverConn->waitForConnection()); + + const char* text = "hello socket"; + MemoryBlock message (text, strlen (text)); + EXPECT_TRUE (client.sendMessage (message)); + + EXPECT_TRUE (serverConn->waitForMessage()); + + const ScopedLock sl (serverConn->messagesLock); + ASSERT_EQ (serverConn->receivedMessages.size(), 1); + EXPECT_EQ (serverConn->receivedMessages[0], message); +} + +TEST_F (InterprocessConnectionSocketTests, SendMessageServerConnectionToClient) +{ + TestConnectionServer server; + ASSERT_TRUE (server.beginWaitingForSocket (0)); + const int port = server.getBoundPort(); + + TestConnection client; + ASSERT_TRUE (client.connectToSocket ("127.0.0.1", port, 3000)); + + ASSERT_TRUE (client.waitForConnection()); + ASSERT_TRUE (server.waitForServerConnection()); + + auto* serverConn = server.getLastConnection(); + ASSERT_NE (serverConn, nullptr); + ASSERT_TRUE (serverConn->waitForConnection()); + + const char* text = "reply socket"; + MemoryBlock message (text, strlen (text)); + EXPECT_TRUE (serverConn->sendMessage (message)); + + EXPECT_TRUE (client.waitForMessage()); + + const ScopedLock sl (client.messagesLock); + ASSERT_EQ (client.receivedMessages.size(), 1); + EXPECT_EQ (client.receivedMessages[0], message); +} + +TEST_F (InterprocessConnectionSocketTests, DisconnectingClientNotifiesServer) +{ + TestConnectionServer server; + ASSERT_TRUE (server.beginWaitingForSocket (0)); + const int port = server.getBoundPort(); + + TestConnection client; + ASSERT_TRUE (client.connectToSocket ("127.0.0.1", port, 3000)); + + ASSERT_TRUE (client.waitForConnection()); + ASSERT_TRUE (server.waitForServerConnection()); + + auto* serverConn = server.getLastConnection(); + ASSERT_NE (serverConn, nullptr); + ASSERT_TRUE (serverConn->waitForConnection()); + + client.disconnect (2000); + + EXPECT_TRUE (serverConn->waitForDisconnect()); + EXPECT_EQ (serverConn->lostCount.load(), 1); +} + +TEST_F (InterprocessConnectionSocketTests, ConnectToInvalidPortFails) +{ + TestConnection client; + // Port 1 is typically reserved and not listening + EXPECT_FALSE (client.connectToSocket ("127.0.0.1", 1, 500)); + EXPECT_FALSE (client.isConnected()); +} + +TEST_F (InterprocessConnectionSocketTests, GetConnectedHostName) +{ + TestConnectionServer server; + ASSERT_TRUE (server.beginWaitingForSocket (0)); + const int port = server.getBoundPort(); + + TestConnection client; + ASSERT_TRUE (client.connectToSocket ("127.0.0.1", port, 3000)); + ASSERT_TRUE (client.waitForConnection()); + + EXPECT_FALSE (client.getConnectedHostName().isEmpty()); +} + +#endif From b5067826e6ef6dab84f279e3e5fed7a2fe473275 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Tue, 2 Jun 2026 23:36:54 +0200 Subject: [PATCH 2/2] More tests --- .../buffers/yup_AudioSampleBuffer.h | 50 +++ .../yup_AudioSampleBuffer.cpp | 81 ++++ tests/yup_audio_formats.cpp | 2 + .../yup_AudioFormatReader.cpp | 153 ++++++++ .../yup_AudioFormatWriter.cpp | 235 +++++++++++ .../yup_audio_formats/yup_WaveAudioFormat.cpp | 6 + tests/yup_core.cpp | 1 + tests/yup_core/yup_BigInteger.cpp | 208 ++++++++++ tests/yup_core/yup_Expression.cpp | 212 ++++++++++ tests/yup_core/yup_PerformanceCounter.cpp | 367 ++++++++++++++++++ 10 files changed, 1315 insertions(+) create mode 100644 tests/yup_audio_formats/yup_AudioFormatReader.cpp create mode 100644 tests/yup_audio_formats/yup_AudioFormatWriter.cpp create mode 100644 tests/yup_core/yup_PerformanceCounter.cpp diff --git a/modules/yup_audio_basics/buffers/yup_AudioSampleBuffer.h b/modules/yup_audio_basics/buffers/yup_AudioSampleBuffer.h index 63f724afa..bf6ed5bf9 100644 --- a/modules/yup_audio_basics/buffers/yup_AudioSampleBuffer.h +++ b/modules/yup_audio_basics/buffers/yup_AudioSampleBuffer.h @@ -657,6 +657,56 @@ class AudioBuffer */ void setNotClear() noexcept { isClear = false; } + //============================================================================== + /** Fills all the samples in all channels by a value. + + This method will do nothing if the buffer has been marked as cleared (i.e. the + hasBeenCleared method returns true.) + */ + void fill (Type value) noexcept + { + for (int i = 0; i < numChannels; ++i) + FloatVectorOperations::fill (channels[i], value, size); + + isClear = false; + } + + /** Fills a specified region of all the channels. + + For speed, this doesn't check whether the channel and sample number + are in-range, so be careful! + */ + void fill (int startSample, int numSamples, Type value) noexcept + { + jassert (startSample >= 0 && startSample + numSamples <= size); + + if (numSamples <= 0) + return; + + for (int i = 0; i < numChannels; ++i) + FloatVectorOperations::fill (channels[i] + startSample, value, numSamples); + + isClear = false; + } + + /** Fills a specified region of just one channel. + + For speed, this doesn't check whether the channel and sample number + are in-range, so be careful! + */ + void fill (int channel, int startSample, int numSamples, Type value) noexcept + { + jassert (isPositiveAndBelow (channel, numChannels)); + jassert (startSample >= 0 && startSample + numSamples <= size); + + if (numSamples <= 0) + return; + + FloatVectorOperations::fill (channels[channel] + startSample, value, numSamples); + + isClear = false; + } + //============================================================================== /** Returns a sample from the buffer. diff --git a/tests/yup_audio_basics/yup_AudioSampleBuffer.cpp b/tests/yup_audio_basics/yup_AudioSampleBuffer.cpp index b0deac97d..08218c5d5 100644 --- a/tests/yup_audio_basics/yup_AudioSampleBuffer.cpp +++ b/tests/yup_audio_basics/yup_AudioSampleBuffer.cpp @@ -253,6 +253,87 @@ TYPED_TEST (AudioBufferTests, ClearAndHasBeenCleared) EXPECT_TRUE (approximatelyEqual (buffer.getSample (0, 0), static_cast (0))); } +// Test fill methods +TYPED_TEST (AudioBufferTests, FillAllChannels) +{ + using BufferType = typename TestFixture::BufferType; + + BufferType buffer (3, 5); + buffer.clear(); + EXPECT_TRUE (buffer.hasBeenCleared()); + + buffer.fill (static_cast (2.5)); + + EXPECT_FALSE (buffer.hasBeenCleared()); + + for (int ch = 0; ch < buffer.getNumChannels(); ++ch) + for (int i = 0; i < buffer.getNumSamples(); ++i) + EXPECT_TRUE (approximatelyEqual (buffer.getSample (ch, i), static_cast (2.5))); +} + +TYPED_TEST (AudioBufferTests, FillRegionInAllChannels) +{ + using BufferType = typename TestFixture::BufferType; + + BufferType buffer; + this->initializeBuffer (buffer, 2, 6); + + buffer.fill (2, 3, static_cast (-4.0)); + + for (int ch = 0; ch < buffer.getNumChannels(); ++ch) + { + EXPECT_TRUE (approximatelyEqual (buffer.getSample (ch, 0), static_cast (1.0))); + EXPECT_TRUE (approximatelyEqual (buffer.getSample (ch, 1), static_cast (2.0))); + EXPECT_TRUE (approximatelyEqual (buffer.getSample (ch, 2), static_cast (-4.0))); + EXPECT_TRUE (approximatelyEqual (buffer.getSample (ch, 3), static_cast (-4.0))); + EXPECT_TRUE (approximatelyEqual (buffer.getSample (ch, 4), static_cast (-4.0))); + EXPECT_TRUE (approximatelyEqual (buffer.getSample (ch, 5), static_cast (6.0))); + } + + EXPECT_FALSE (buffer.hasBeenCleared()); +} + +TYPED_TEST (AudioBufferTests, FillRegionInSingleChannel) +{ + using BufferType = typename TestFixture::BufferType; + + BufferType buffer; + this->initializeBuffer (buffer, 3, 5); + + buffer.fill (1, 1, 3, static_cast (9.0)); + + for (int ch = 0; ch < buffer.getNumChannels(); ++ch) + { + for (int i = 0; i < buffer.getNumSamples(); ++i) + { + const auto expected = (ch == 1 && i >= 1 && i <= 3) ? static_cast (9.0) + : static_cast (i + 1); + + EXPECT_TRUE (approximatelyEqual (buffer.getSample (ch, i), expected)); + } + } + + EXPECT_FALSE (buffer.hasBeenCleared()); +} + +TYPED_TEST (AudioBufferTests, FillWithZeroSamplesDoesNothing) +{ + using BufferType = typename TestFixture::BufferType; + + BufferType buffer (2, 4); + buffer.clear(); + EXPECT_TRUE (buffer.hasBeenCleared()); + + buffer.fill (1, 0, static_cast (3.0)); + buffer.fill (0, 1, 0, static_cast (4.0)); + + EXPECT_TRUE (buffer.hasBeenCleared()); + + for (int ch = 0; ch < buffer.getNumChannels(); ++ch) + for (int i = 0; i < buffer.getNumSamples(); ++i) + EXPECT_TRUE (approximatelyEqual (buffer.getSample (ch, i), static_cast (0))); +} + // Test getSample and setSample methods TYPED_TEST (AudioBufferTests, GetAndSetSample) { diff --git a/tests/yup_audio_formats.cpp b/tests/yup_audio_formats.cpp index 48304b6ca..755cc817c 100644 --- a/tests/yup_audio_formats.cpp +++ b/tests/yup_audio_formats.cpp @@ -32,6 +32,8 @@ #endif #include "yup_audio_formats/yup_AudioFormatManager.cpp" +#include "yup_audio_formats/yup_AudioFormatReader.cpp" +#include "yup_audio_formats/yup_AudioFormatWriter.cpp" #if YUP_MODULE_AVAILABLE_dr_libs && YUP_AUDIO_FORMAT_WAVE #include "yup_audio_formats/yup_WaveAudioFormat.cpp" diff --git a/tests/yup_audio_formats/yup_AudioFormatReader.cpp b/tests/yup_audio_formats/yup_AudioFormatReader.cpp new file mode 100644 index 000000000..7851533d1 --- /dev/null +++ b/tests/yup_audio_formats/yup_AudioFormatReader.cpp @@ -0,0 +1,153 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +#include +#include + +using namespace yup; + +namespace +{ +class MockAudioFormatReader : public AudioFormatReader +{ +public: + MockAudioFormatReader (int numChannelsToUse, int numSamples) + : AudioFormatReader (nullptr, "MockAudioFormatReader") + , buffer (numChannelsToUse, numSamples) + { + sampleRate = 48000.0; + bitsPerSample = 32; + lengthInSamples = numSamples; + numChannels = numChannelsToUse; + usesFloatingPointData = true; + } + + bool readSamples (float* const* destChannels, + int numDestChannels, + int startOffsetInDestBuffer, + int64 startSampleInFile, + int numSamples) override + { + if (startSampleInFile < 0 || startSampleInFile + numSamples > lengthInSamples) + return false; + + const auto channelsToCopy = jmin (numDestChannels, numChannels); + + for (int ch = 0; ch < channelsToCopy; ++ch) + { + if (destChannels[ch] == nullptr) + continue; + + const auto* source = buffer.getReadPointer (ch) + startSampleInFile; + auto* destination = destChannels[ch] + startOffsetInDestBuffer; + std::copy (source, source + numSamples, destination); + } + + return true; + } + + AudioBuffer buffer; +}; + +static void fillChannel (AudioBuffer& buffer, int channel, std::initializer_list values) +{ + auto* data = buffer.getWritePointer (channel); + + int index = 0; + for (float value : values) + data[index++] = value; +} +} // namespace + +TEST (AudioFormatReaderTests, ReadCopiesMonoSourceToAllDestinationChannels) +{ + MockAudioFormatReader reader (1, 4); + fillChannel (reader.buffer, 0, { 0.1f, -0.25f, 0.75f, 0.0f }); + + AudioBuffer destination (3, 4); + destination.clear(); + + EXPECT_TRUE (reader.read (&destination, 0, 4, 0, true, false)); + + for (int channel = 0; channel < destination.getNumChannels(); ++channel) + { + for (int sample = 0; sample < destination.getNumSamples(); ++sample) + EXPECT_FLOAT_EQ (reader.buffer.getSample (0, sample), destination.getSample (channel, sample)); + } +} + +TEST (AudioFormatReaderTests, ReadClearsDestinationWhenNoChannelsAreRequested) +{ + MockAudioFormatReader reader (2, 4); + fillChannel (reader.buffer, 0, { 0.1f, -0.25f, 0.75f, 0.0f }); + fillChannel (reader.buffer, 1, { -0.8f, 0.5f, 0.2f, -0.1f }); + + AudioBuffer destination (2, 4); + destination.fill (1.0f); + + EXPECT_TRUE (reader.read (&destination, 0, 4, 0, false, false)); + + for (int channel = 0; channel < destination.getNumChannels(); ++channel) + { + for (int sample = 0; sample < destination.getNumSamples(); ++sample) + EXPECT_FLOAT_EQ (0.0f, destination.getSample (channel, sample)); + } +} + +TEST (AudioFormatReaderTests, ReadMaxLevelsReturnsChannelExtremes) +{ + MockAudioFormatReader reader (2, 4); + fillChannel (reader.buffer, 0, { 0.1f, -0.4f, 0.25f, 0.05f }); + fillChannel (reader.buffer, 1, { -0.9f, 0.2f, 0.6f, -0.1f }); + + Range levels[2]; + reader.readMaxLevels (0, 4, levels, 2); + + EXPECT_NEAR (-0.4f, levels[0].getStart(), 1.0e-6f); + EXPECT_NEAR (0.25f, levels[0].getEnd(), 1.0e-6f); + EXPECT_NEAR (-0.9f, levels[1].getStart(), 1.0e-6f); + EXPECT_NEAR (0.6f, levels[1].getEnd(), 1.0e-6f); +} + +TEST (AudioFormatReaderTests, SearchForLevelFindsFirstMatchingRun) +{ + MockAudioFormatReader reader (2, 5); + fillChannel (reader.buffer, 0, { 0.1f, 0.55f, -0.6f, 0.2f, 0.1f }); + fillChannel (reader.buffer, 1, { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }); + + EXPECT_EQ (1, reader.searchForLevel (0, 5, 0.5, 0.7, 2)); + EXPECT_EQ (-1, reader.searchForLevel (0, 5, 0.8, 0.9, 1)); +} + +TEST (AudioFormatReaderTests, GetChannelLayoutMatchesChannelCount) +{ + MockAudioFormatReader monoReader (1, 1); + MockAudioFormatReader stereoReader (2, 1); + MockAudioFormatReader surroundReader (4, 1); + + EXPECT_EQ (AudioChannelSet::mono(), monoReader.getChannelLayout()); + EXPECT_EQ (AudioChannelSet::stereo(), stereoReader.getChannelLayout()); + EXPECT_EQ (AudioChannelSet::discreteChannels (4), surroundReader.getChannelLayout()); +} diff --git a/tests/yup_audio_formats/yup_AudioFormatWriter.cpp b/tests/yup_audio_formats/yup_AudioFormatWriter.cpp new file mode 100644 index 000000000..45f916943 --- /dev/null +++ b/tests/yup_audio_formats/yup_AudioFormatWriter.cpp @@ -0,0 +1,235 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +#include +#include +#include +#include + +using namespace yup; + +namespace +{ +class WriterTestAudioFormatReader : public AudioFormatReader +{ +public: + WriterTestAudioFormatReader (int numChannelsToUse, int numSamples) + : AudioFormatReader (nullptr, "MockAudioFormatReader") + , buffer (numChannelsToUse, numSamples) + { + sampleRate = 44100.0; + bitsPerSample = 32; + lengthInSamples = numSamples; + numChannels = numChannelsToUse; + usesFloatingPointData = true; + } + + bool readSamples (float* const* destChannels, + int numDestChannels, + int startOffsetInDestBuffer, + int64 startSampleInFile, + int numSamples) override + { + if (startSampleInFile < 0 || startSampleInFile + numSamples > lengthInSamples) + return false; + + const auto channelsToCopy = jmin (numDestChannels, numChannels); + + for (int ch = 0; ch < channelsToCopy; ++ch) + { + if (destChannels[ch] == nullptr) + continue; + + const auto* source = buffer.getReadPointer (ch) + startSampleInFile; + auto* destination = destChannels[ch] + startOffsetInDestBuffer; + std::copy (source, source + numSamples, destination); + } + + return true; + } + + AudioBuffer buffer; +}; + +class MockAudioFormatWriter : public AudioFormatWriter +{ +public: + MockAudioFormatWriter (int numChannelsToUse, int bitsPerSampleToUse, double sampleRateToUse = 44100.0) + : AudioFormatWriter (nullptr, "MockAudioFormatWriter", sampleRateToUse, numChannelsToUse, bitsPerSampleToUse) + { + } + + bool write (const float* const* samplesToWrite, int numSamples) override + { + if (writtenSamples.size() != static_cast (getNumChannels())) + writtenSamples.resize (static_cast (getNumChannels())); + + for (int channel = 0; channel < getNumChannels(); ++channel) + writtenSamples[static_cast (channel)].insert (writtenSamples[static_cast (channel)].end(), + samplesToWrite[channel], + samplesToWrite[channel] + numSamples); + + ++writeCalls; + lastNumSamples = numSamples; + return true; + } + + void reset() + { + writeCalls = 0; + lastNumSamples = 0; + writtenSamples.clear(); + } + + int writeCalls = 0; + int lastNumSamples = 0; + std::vector> writtenSamples; +}; + +static void fillWriterChannel (AudioBuffer& buffer, int channel, std::initializer_list values) +{ + auto* data = buffer.getWritePointer (channel); + + int index = 0; + for (float value : values) + data[index++] = value; +} + +static void expectFloatArrayEquals (const std::vector& actual, std::initializer_list expected) +{ + ASSERT_EQ (expected.size(), actual.size()); + + size_t index = 0; + for (float value : expected) + { + EXPECT_FLOAT_EQ (value, actual[index]); + ++index; + } +} +} // namespace + +TEST (AudioFormatWriterTests, WriteFromAudioSampleBufferCopiesAvailableChannels) +{ + MockAudioFormatWriter writer (3, 16); + + AudioBuffer source (2, 4); + fillWriterChannel (source, 0, { 0.1f, -0.2f, 0.3f, -0.4f }); + fillWriterChannel (source, 1, { 0.5f, 0.6f, -0.7f, 0.8f }); + + EXPECT_TRUE (writer.writeFromAudioSampleBuffer (source, 1, 10)); + + EXPECT_EQ (1, writer.writeCalls); + EXPECT_EQ (3, writer.lastNumSamples); + ASSERT_EQ (3u, writer.writtenSamples.size()); + + expectFloatArrayEquals (writer.writtenSamples[0], { -0.2f, 0.3f, -0.4f }); + expectFloatArrayEquals (writer.writtenSamples[1], { 0.6f, -0.7f, 0.8f }); + expectFloatArrayEquals (writer.writtenSamples[2], { 0.0f, 0.0f, 0.0f }); +} + +TEST (AudioFormatWriterTests, WriteFromAudioReaderUsesReaderSamples) +{ + WriterTestAudioFormatReader reader (2, 5); + fillWriterChannel (reader.buffer, 0, { 0.1f, -0.2f, 0.3f, -0.4f, 0.5f }); + fillWriterChannel (reader.buffer, 1, { -0.5f, 0.4f, -0.3f, 0.2f, -0.1f }); + + MockAudioFormatWriter writer (2, 16); + + EXPECT_TRUE (writer.writeFromAudioReader (reader, 1, 3)); + + EXPECT_EQ (1, writer.writeCalls); + EXPECT_EQ (3, writer.lastNumSamples); + ASSERT_EQ (2u, writer.writtenSamples.size()); + EXPECT_EQ (3u, writer.writtenSamples[0].size()); + EXPECT_EQ (3u, writer.writtenSamples[1].size()); +} + +TEST (AudioFormatWriterTests, WriteHelperEncodesIntegerAndFloatFormats) +{ + const std::array sourceValues { -1.0f, -0.5f, 0.0f, 1.0f }; + + { + std::array destination {}; + AudioFormatWriter::WriteHelper::writeInt8 (destination.data(), sourceValues.data(), static_cast (sourceValues.size())); + + EXPECT_EQ (1, destination[0]); + EXPECT_EQ (65, destination[1]); + EXPECT_EQ (128, destination[2]); + EXPECT_EQ (255, destination[3]); + } + + { + std::array destination {}; + const std::array values { 0.25f, 1.0f }; + + AudioFormatWriter::WriteHelper::writeInt16 (destination.data(), values.data(), static_cast (values.size()), true); + + EXPECT_EQ (ByteOrder::swapIfBigEndian (static_cast (static_cast (values[0] * 32767.0f))), destination[0]); + EXPECT_EQ (ByteOrder::swapIfBigEndian (static_cast (static_cast (values[1] * 32767.0f))), destination[1]); + } + + { + std::array destination {}; + const std::array values { 0.25f, 1.0f }; + + AudioFormatWriter::WriteHelper::writeInt24 (destination.data(), values.data(), static_cast (values.size()), true); + + EXPECT_EQ (0xFF, destination[0]); + EXPECT_EQ (0xFF, destination[1]); + EXPECT_EQ (0x1F, destination[2]); + EXPECT_EQ (0xFF, destination[3]); + EXPECT_EQ (0xFF, destination[4]); + EXPECT_EQ (0x7F, destination[5]); + } + + { + std::array destination {}; + const std::array values { 0.25f }; + + AudioFormatWriter::WriteHelper::writeInt32 (destination.data(), values.data(), static_cast (values.size()), true); + + EXPECT_EQ (ByteOrder::swapIfBigEndian (static_cast (static_cast (values[0] * 2147483647.0f))), destination[0]); + } + + { + std::array destination {}; + const std::array values { 0.125f, -0.75f }; + + AudioFormatWriter::WriteHelper::writeFloat32 (destination.data(), values.data(), static_cast (values.size()), true); + + EXPECT_EQ (std::bit_cast (ByteOrder::swapIfBigEndian (values[0])), std::bit_cast (destination[0])); + EXPECT_EQ (std::bit_cast (ByteOrder::swapIfBigEndian (values[1])), std::bit_cast (destination[1])); + } + + { + std::array destination {}; + const std::array values { 0.125, -0.75 }; + + AudioFormatWriter::WriteHelper::writeFloat64 (destination.data(), values.data(), static_cast (values.size()), true); + + EXPECT_EQ (std::bit_cast (ByteOrder::swapIfBigEndian (values[0])), std::bit_cast (destination[0])); + EXPECT_EQ (std::bit_cast (ByteOrder::swapIfBigEndian (values[1])), std::bit_cast (destination[1])); + } +} \ No newline at end of file diff --git a/tests/yup_audio_formats/yup_WaveAudioFormat.cpp b/tests/yup_audio_formats/yup_WaveAudioFormat.cpp index 991103863..cbbc8d519 100644 --- a/tests/yup_audio_formats/yup_WaveAudioFormat.cpp +++ b/tests/yup_audio_formats/yup_WaveAudioFormat.cpp @@ -148,6 +148,12 @@ TEST_F (WaveAudioFormatTests, IsNotCompressed) EXPECT_FALSE (format->isCompressed()); } +TEST_F (WaveAudioFormatTests, QualityOptionsAreEmpty) +{ + auto qualityOptions = format->getQualityOptions(); + EXPECT_TRUE (qualityOptions.isEmpty()); +} + TEST_F (WaveAudioFormatTests, CreateReaderForNullStream) { auto reader = format->createReaderFor (nullptr); diff --git a/tests/yup_core.cpp b/tests/yup_core.cpp index eebb22ea1..b9460d681 100644 --- a/tests/yup_core.cpp +++ b/tests/yup_core.cpp @@ -64,6 +64,7 @@ #include "yup_core/yup_NamedPipe.cpp" #include "yup_core/yup_NormalisableRange.cpp" #include "yup_core/yup_OwnedArray.cpp" +#include "yup_core/yup_PerformanceCounter.cpp" #include "yup_core/yup_PlatformDefs.cpp" #include "yup_core/yup_Process.cpp" #include "yup_core/yup_Profiler.cpp" diff --git a/tests/yup_core/yup_BigInteger.cpp b/tests/yup_core/yup_BigInteger.cpp index 2941e33b8..a8a787d43 100644 --- a/tests/yup_core/yup_BigInteger.cpp +++ b/tests/yup_core/yup_BigInteger.cpp @@ -180,6 +180,19 @@ TEST (BigIntegerTests, EnsureSizeWithExistingHeapAllocation) EXPECT_TRUE (big[300]); } +TEST (BigIntegerTests, ReallocationClearsNewStorage) +{ + BigInteger big; + + big.setBit (130); + big.setBit (900); + + EXPECT_TRUE (big[130]); + EXPECT_TRUE (big[900]); + EXPECT_FALSE (big[512]); + EXPECT_EQ (2, big.countNumberOfSetBits()); +} + TEST (BigIntegerTests, AdditionEdgeCases) { // Test adding to itself @@ -338,6 +351,31 @@ TEST (BigIntegerTests, BitwiseOrOperator) EXPECT_EQ (42, orWithZero.toInteger()); } +TEST (BigIntegerTests, BitwiseOrWithSelfIsUnchanged) +{ + BigInteger value; + value.setBit (4); + value.setBit (140); + + const auto original = value; + value |= value; + + EXPECT_TRUE (value == original); +} + +TEST (BigIntegerTests, BitwiseOrExtendsToOtherHighestBit) +{ + BigInteger value (1); + BigInteger other; + other.setBit (180); + + value |= other; + + EXPECT_TRUE (value[0]); + EXPECT_TRUE (value[180]); + EXPECT_EQ (180, value.getHighestBit()); +} + TEST (BigIntegerTests, BitwiseAndOperator) { BigInteger a (0b1010); @@ -351,6 +389,32 @@ TEST (BigIntegerTests, BitwiseAndOperator) EXPECT_TRUE (andWithZero.isZero()); } +TEST (BigIntegerTests, BitwiseAndWithSelfIsUnchanged) +{ + BigInteger value; + value.setBit (3); + value.setBit (170); + + const auto original = value; + value &= value; + + EXPECT_TRUE (value == original); +} + +TEST (BigIntegerTests, BitwiseAndClearsWordsAboveOtherAllocation) +{ + BigInteger value; + value.setBit (3); + value.setBit (220); + + BigInteger mask (0b1000); + value &= mask; + + EXPECT_EQ (0b1000, value.toInteger()); + EXPECT_FALSE (value[220]); + EXPECT_EQ (3, value.getHighestBit()); +} + TEST (BigIntegerTests, BitwiseXorOperator) { BigInteger a (0b1010); @@ -363,6 +427,30 @@ TEST (BigIntegerTests, BitwiseXorOperator) EXPECT_TRUE (xorWithItself.isZero()); } +TEST (BigIntegerTests, BitwiseXorWithSelfClearsValue) +{ + BigInteger value; + value.setBit (8); + value.setBit (160); + + value ^= value; + + EXPECT_TRUE (value.isZero()); +} + +TEST (BigIntegerTests, BitwiseXorExtendsToOtherHighestBit) +{ + BigInteger value (1); + BigInteger other; + other.setBit (190); + + value ^= other; + + EXPECT_TRUE (value[0]); + EXPECT_TRUE (value[190]); + EXPECT_EQ (190, value.getHighestBit()); +} + TEST (BigIntegerTests, CompareEqual) { BigInteger a (42); @@ -394,6 +482,39 @@ TEST (BigIntegerTests, ShiftRightWithStartBit) EXPECT_FALSE (value[12]); // Cleared } +TEST (BigIntegerTests, BitRangeAsIntCanCrossWordBoundary) +{ + BigInteger value; + value.setBitRangeAsInt (28, 8, 0xabu); + + EXPECT_EQ (0xabu, value.getBitRangeAsInt (28, 8)); +} + +TEST (BigIntegerTests, BitRangeAsIntClampsToThirtyTwoBits) +{ +#if YUP_ASSERTIONS_ENABLED + GTEST_SKIP() << "Skipping invalid bit range test in assertion-enabled builds"; +#else + BigInteger value; + value.setRange (0, 40, true); + + EXPECT_EQ (0xffffffffu, value.getBitRangeAsInt (0, 64)); +#endif +} + +TEST (BigIntegerTests, SetBitRangeAsIntClampsToThirtyTwoBits) +{ +#if YUP_ASSERTIONS_ENABLED + GTEST_SKIP() << "Skipping invalid bit range test in assertion-enabled builds"; +#else + BigInteger value; + value.setBitRangeAsInt (4, 40, 0xffffffffu); + + EXPECT_EQ (0xffffffffu, value.getBitRangeAsInt (4, 32)); + EXPECT_FALSE (value[36]); +#endif +} + TEST (BigIntegerTests, FindGreatestCommonDivisorComplex) { // Test the complex path that creates temp2 @@ -414,6 +535,16 @@ TEST (BigIntegerTests, FindGreatestCommonDivisorComplex) EXPECT_EQ (8, gcd2.toInteger()); } +TEST (BigIntegerTests, FindGreatestCommonDivisorWithZeroReturnsValue) +{ + BigInteger value (123456); + BigInteger zero; + + auto gcd = value.findGreatestCommonDivisor (zero); + + EXPECT_EQ (123456, gcd.toInteger()); +} + TEST (BigIntegerTests, ExponentModuloComplexPath) { // Test the else branch (Montgomery multiplication path) @@ -435,6 +566,30 @@ TEST (BigIntegerTests, ExponentModuloComplexPath) EXPECT_TRUE (base2.toInteger() < 17); } +TEST (BigIntegerTests, ExponentModuloUsesMontgomeryPathForLargeOddModulus) +{ + BigInteger base (1234567); + BigInteger exponent (13); + BigInteger modulus; + modulus.setBit (40); + modulus += 87; + + BigInteger expected (1); + const BigInteger multiplier (1234567); + + for (int i = 0; i < 13; ++i) + { + expected *= multiplier; + expected %= modulus; + } + + base.exponentModulo (exponent, modulus); + + EXPECT_TRUE (base == expected); + EXPECT_GE (base.compare (BigInteger()), 0); + EXPECT_LT (base.compare (modulus), 0); +} + TEST (BigIntegerTests, MontgomeryMultiplicationNegative) { BigInteger a (5); @@ -490,6 +645,50 @@ TEST (BigIntegerTests, ExtendedEuclideanSwapPath) EXPECT_TRUE (gcd2.compareAbsolute (diff1) == 0 || gcd2.compareAbsolute (diff2) == 0); } +TEST (BigIntegerTests, ExtendedEuclideanHandlesInputsRequiringCoefficientSwap) +{ + BigInteger a (13); + BigInteger b (17); + BigInteger x, y; + BigInteger gcd; + + gcd.extendedEuclidean (a, b, x, y); + + const auto check = (a * x) - (b * y); + + EXPECT_EQ (1, gcd.toInteger()); + EXPECT_EQ (0, gcd.compareAbsolute (check)); +} + +TEST (BigIntegerTests, InverseModuloClearsForInvalidModulus) +{ + BigInteger oneModulus (3); + oneModulus.inverseModulo (BigInteger (1)); + EXPECT_TRUE (oneModulus.isZero()); + + BigInteger negativeModulus (3); + negativeModulus.inverseModulo (BigInteger (-11)); + EXPECT_TRUE (negativeModulus.isZero()); +} + +TEST (BigIntegerTests, InverseModuloReducesLargeInput) +{ + BigInteger value (45); + + value.inverseModulo (BigInteger (13)); + + EXPECT_EQ (11, value.toInteger()); +} + +TEST (BigIntegerTests, InverseModuloClearsWhenNotInvertible) +{ + BigInteger value (6); + + value.inverseModulo (BigInteger (9)); + + EXPECT_TRUE (value.isZero()); +} + TEST (BigIntegerTests, OutputStreamOperator) { BigInteger value (12345); @@ -552,3 +751,12 @@ TEST (BigIntegerTests, ParseStringBase10) EXPECT_EQ (-6789, manualNegative.toInteger()); EXPECT_TRUE (manualNegative.isNegative()); } + +TEST (BigIntegerTests, ToStringReturnsEmptyForUnsupportedBase) +{ +#if YUP_ASSERTIONS_ENABLED + GTEST_SKIP() << "Skipping invalid base test in assertion-enabled builds"; +#else + EXPECT_TRUE (BigInteger (123).toString (3).isEmpty()); +#endif +} diff --git a/tests/yup_core/yup_Expression.cpp b/tests/yup_core/yup_Expression.cpp index af6bd2b3c..40b7b8205 100644 --- a/tests/yup_core/yup_Expression.cpp +++ b/tests/yup_core/yup_Expression.cpp @@ -59,6 +59,9 @@ class TestScope : public Expression::Scope if (functionName == "add" && numParams == 2) return parameters[0] + parameters[1]; + if (functionName == "zero" && numParams == 0) + return 123.0; + return Expression::Scope::evaluateFunction (functionName, parameters, numParams); } @@ -111,6 +114,23 @@ class OuterScope : public Expression::Scope } } }; + +class RecursiveScope : public Expression::Scope +{ +public: + String getScopeUID() const override + { + return "RecursiveScope"; + } + + Expression getSymbolValue (const String& symbol) const override + { + if (symbol == "self") + return Expression::symbol ("self"); + + return Expression::Scope::getSymbolValue (symbol); + } +}; } // namespace // ============================================================================== @@ -318,6 +338,29 @@ TEST (ExpressionTests, ParseWhitespaceOnly) EXPECT_EQ (0.0, e.evaluate()); } +TEST (ExpressionTests, GetInputFromConstantReturnsEmptyExpression) +{ +#if YUP_ASSERTIONS_ENABLED + GTEST_SKIP() << "Skipping invalid input access test in assertion-enabled builds"; +#else + Expression e (42.0); + auto input = e.getInput (0); + + EXPECT_EQ (0.0, input.evaluate()); +#endif +} + +TEST (ExpressionTests, GetSymbolOrFunctionForConstantReturnsEmptyString) +{ +#if YUP_ASSERTIONS_ENABLED + GTEST_SKIP() << "Skipping invalid name access test in assertion-enabled builds"; +#else + Expression e (42.0); + + EXPECT_TRUE (e.getSymbolOrFunction().isEmpty()); +#endif +} + // ============================================================================== // Arithmetic Operator Tests // ============================================================================== @@ -387,6 +430,41 @@ TEST (ExpressionTests, DivisionByZeroReturnsInfinity) EXPECT_TRUE (std::isinf (value)); } +TEST (ExpressionTests, ToStringWrapsRightAssociativeSubtraction) +{ + String error; + Expression e ("10 - (3 - 1)", error); + + EXPECT_TRUE (error.isEmpty()); + EXPECT_EQ ("10 - (3 - 1)", e.toString()); + EXPECT_EQ (8.0, e.evaluate()); +} + +TEST (ExpressionTests, NegatedSymbolExposesInputAndName) +{ + auto e = -Expression::symbol ("x"); + + EXPECT_EQ (Expression::operatorType, e.getType()); + EXPECT_EQ ("-", e.getSymbolOrFunction()); + EXPECT_EQ (1, e.getNumInputs()); + EXPECT_EQ ("x", e.getInput (0).toString()); + EXPECT_EQ ("-x", e.toString()); +} + +TEST (ExpressionTests, NegatedOperatorUsesParenthesesInString) +{ + auto e = -(Expression::symbol ("x") + Expression (1.0)); + + EXPECT_EQ ("-(x + 1)", e.toString()); +} + +TEST (ExpressionTests, DoubleNegatedSymbolReturnsOriginalSymbol) +{ + auto e = -(-Expression::symbol ("x")); + + EXPECT_EQ ("x", e.toString()); +} + // ============================================================================== // Built-in Function Tests // ============================================================================== @@ -493,6 +571,16 @@ TEST (ExpressionTests, ParseEmptyFunctionCall) EXPECT_TRUE (error.isEmpty()); } +TEST (ExpressionTests, EvaluateParameterlessFunctionWithCustomScope) +{ + String error; + Expression e ("zero()", error); + TestScope scope; + + EXPECT_TRUE (error.isEmpty()); + EXPECT_EQ (123.0, e.evaluate (scope)); +} + // ============================================================================== // Symbol Tests // ============================================================================== @@ -656,6 +744,20 @@ TEST (ExpressionTests, FunctionWithSymbolArguments) EXPECT_EQ (25.0, e.evaluate (scope)); // x=5, so square(5)=25 } +TEST (ExpressionTests, AdjustedFunctionAddsOuterConstant) +{ + Array params; + params.add (Expression (5.0)); + + auto e = Expression::function ("square", params); + TestScope scope; + + auto adjusted = e.adjustedToGiveNewResult (30.0, scope); + + EXPECT_EQ (30.0, adjusted.evaluate (scope)); + EXPECT_EQ ("square (5) + 5", adjusted.toString()); +} + // ============================================================================== // ToString Tests // ============================================================================== @@ -947,6 +1049,32 @@ TEST (ExpressionTests, AdjustedToGiveNewResultWithResolutionTarget) EXPECT_EQ (20.0, adjusted.evaluate (scope)); } +TEST (ExpressionTests, AdjustedToGiveNewResultThroughNestedParent) +{ + String error; + Expression e ("(x + @2) * 3", error); + TestScope scope; + + auto adjusted = e.adjustedToGiveNewResult (30.0, scope); + + EXPECT_TRUE (error.isEmpty()); + EXPECT_EQ (30.0, adjusted.evaluate (scope)); + EXPECT_EQ ("(x + @5) * 3", adjusted.toString()); +} + +TEST (ExpressionTests, AdjustedToGiveNewResultThroughNegation) +{ + String error; + Expression e ("-(x + @5)", error); + TestScope scope; + + auto adjusted = e.adjustedToGiveNewResult (-12.0, scope); + + EXPECT_TRUE (error.isEmpty()); + EXPECT_EQ (-12.0, adjusted.evaluate (scope)); + EXPECT_EQ ("-(x + @7)", adjusted.toString()); +} + // ============================================================================== // Dot Operator Tests // ============================================================================== @@ -989,6 +1117,79 @@ TEST (ExpressionTests, DotOperatorInComplexExpression) EXPECT_EQ (84.0, e.evaluate (scope)); } +TEST (ExpressionTests, DotOperatorReportsOperatorName) +{ + String error; + Expression e ("inner.value", error); + + EXPECT_TRUE (error.isEmpty()); + EXPECT_EQ (".", e.getSymbolOrFunction()); +} + +TEST (ExpressionTests, DotOperatorFindsSymbolsInRelativeScope) +{ + String error; + Expression e ("inner.value + 1", error); + OuterScope scope; + + Array symbols; + e.findReferencedSymbols (symbols, scope); + + EXPECT_TRUE (error.isEmpty()); + EXPECT_TRUE (symbols.contains (Expression::Symbol ("OuterScope", "inner"))); + EXPECT_TRUE (symbols.contains (Expression::Symbol ("NestedScope", "value"))); +} + +TEST (ExpressionTests, DotOperatorReferencesSymbolsInRelativeScope) +{ + String error; + Expression e ("inner.value + 1", error); + OuterScope scope; + + EXPECT_TRUE (error.isEmpty()); + EXPECT_TRUE (e.referencesSymbol (Expression::Symbol ("OuterScope", "inner"), scope)); + EXPECT_TRUE (e.referencesSymbol (Expression::Symbol ("NestedScope", "value"), scope)); + EXPECT_FALSE (e.referencesSymbol (Expression::Symbol ("OuterScope", "value"), scope)); +} + +TEST (ExpressionTests, DotOperatorIgnoresMissingRelativeScopeWhenFindingSymbols) +{ + String error; + Expression e ("missing.value", error); + OuterScope scope; + + Array symbols; + e.findReferencedSymbols (symbols, scope); + + EXPECT_TRUE (error.isEmpty()); + EXPECT_EQ (1, symbols.size()); + EXPECT_TRUE (symbols.contains (Expression::Symbol ("OuterScope", "missing"))); +} + +TEST (ExpressionTests, DotOperatorRenamesSymbolInRelativeScope) +{ + String error; + Expression e ("inner.value", error); + OuterScope scope; + + auto renamed = e.withRenamedSymbol (Expression::Symbol ("NestedScope", "value"), "renamed", scope); + + EXPECT_TRUE (error.isEmpty()); + EXPECT_EQ ("inner.renamed", renamed.toString()); +} + +TEST (ExpressionTests, DotOperatorRenamesOuterSymbolWhenRelativeScopeIsMissing) +{ + String error; + Expression e ("missing.value", error); + OuterScope scope; + + auto renamed = e.withRenamedSymbol (Expression::Symbol ("OuterScope", "missing"), "renamed", scope); + + EXPECT_TRUE (error.isEmpty()); + EXPECT_EQ ("renamed.value", renamed.toString()); +} + // ============================================================================== // Edge Cases and Error Handling // ============================================================================== @@ -1189,6 +1390,17 @@ TEST (ExpressionTests, ScopeThrowsOnUnknownRelativeScope) EXPECT_FALSE (error.isEmpty()); } +TEST (ExpressionTests, RecursiveSymbolReferencesReturnEvaluationError) +{ + RecursiveScope scope; + String error; + + auto e = Expression::symbol ("self"); + EXPECT_EQ (0.0, e.evaluate (scope, error)); + + EXPECT_FALSE (error.isEmpty()); +} + // ============================================================================== // Resolution Target Tests // ============================================================================== diff --git a/tests/yup_core/yup_PerformanceCounter.cpp b/tests/yup_core/yup_PerformanceCounter.cpp new file mode 100644 index 000000000..72a77e000 --- /dev/null +++ b/tests/yup_core/yup_PerformanceCounter.cpp @@ -0,0 +1,367 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +namespace +{ + +class PerfTestLogger : public Logger +{ +public: + void logMessage (const String& message) override + { + const ScopedLock sl (lock); + messages.add (message); + } + + StringArray getMessages() const + { + const ScopedLock sl (lock); + return messages; + } + +private: + mutable CriticalSection lock; + StringArray messages; +}; + +} // namespace + +// ============================================================================= +// PerformanceCounter::Statistics Tests +// ============================================================================= + +TEST (PerformanceCounterStatisticsTests, DefaultConstructionHasZeroValues) +{ + PerformanceCounter::Statistics stats; + EXPECT_EQ (stats.numRuns, 0); + EXPECT_EQ (stats.averageSeconds, 0.0); + EXPECT_EQ (stats.minimumSeconds, 0.0); + EXPECT_EQ (stats.maximumSeconds, 0.0); + EXPECT_EQ (stats.totalSeconds, 0.0); + EXPECT_TRUE (stats.name.isEmpty()); +} + +TEST (PerformanceCounterStatisticsTests, ClearResetsAllNumericValues) +{ + PerformanceCounter::Statistics stats; + stats.name = "keep"; + stats.addResult (0.1); + stats.addResult (0.2); + + stats.clear(); + + EXPECT_EQ (stats.numRuns, 0); + EXPECT_EQ (stats.averageSeconds, 0.0); + EXPECT_EQ (stats.minimumSeconds, 0.0); + EXPECT_EQ (stats.maximumSeconds, 0.0); + EXPECT_EQ (stats.totalSeconds, 0.0); +} + +TEST (PerformanceCounterStatisticsTests, AddFirstResultSetsBothMinAndMax) +{ + PerformanceCounter::Statistics stats; + stats.addResult (0.05); + + EXPECT_EQ (stats.numRuns, 1); + EXPECT_DOUBLE_EQ (stats.totalSeconds, 0.05); + EXPECT_DOUBLE_EQ (stats.minimumSeconds, 0.05); + EXPECT_DOUBLE_EQ (stats.maximumSeconds, 0.05); +} + +TEST (PerformanceCounterStatisticsTests, AddMultipleResultsTracksMinMaxAndTotal) +{ + PerformanceCounter::Statistics stats; + stats.addResult (0.3); + stats.addResult (0.1); + stats.addResult (0.5); + stats.addResult (0.2); + + EXPECT_EQ (stats.numRuns, 4); + EXPECT_DOUBLE_EQ (stats.minimumSeconds, 0.1); + EXPECT_DOUBLE_EQ (stats.maximumSeconds, 0.5); + EXPECT_DOUBLE_EQ (stats.totalSeconds, 1.1); +} + +TEST (PerformanceCounterStatisticsTests, AddResultIncrementsRunCount) +{ + PerformanceCounter::Statistics stats; + for (int i = 1; i <= 10; ++i) + { + stats.addResult (0.001 * i); + EXPECT_EQ (stats.numRuns, i); + } +} + +TEST (PerformanceCounterStatisticsTests, ToStringContainsNameAndRunCount) +{ + PerformanceCounter::Statistics stats; + stats.name = "MyBenchmark"; + stats.addResult (0.001); + + auto str = stats.toString(); + EXPECT_TRUE (str.contains ("MyBenchmark")); + EXPECT_TRUE (str.contains ("1")); +} + +TEST (PerformanceCounterStatisticsTests, ToStringContainsTimingKeywords) +{ + PerformanceCounter::Statistics stats; + stats.name = "bench"; + stats.addResult (0.001); + + auto str = stats.toString(); + EXPECT_TRUE (str.contains ("Average") || str.contains ("average")); + EXPECT_TRUE (str.contains ("minimum") || str.contains ("Minimum")); + EXPECT_TRUE (str.contains ("maximum") || str.contains ("Maximum")); +} + +// ============================================================================= +// PerformanceCounter Tests +// ============================================================================= + +class PerformanceCounterTests : public ::testing::Test +{ +protected: + void TearDown() override + { + Logger::setCurrentLogger (nullptr); + } +}; + +TEST_F (PerformanceCounterTests, StopReturnsFalseBeforeReachingRunsPerPrint) +{ + PerformanceCounter pc ("test", 5); + + for (int i = 0; i < 4; ++i) + { + pc.start(); + EXPECT_FALSE (pc.stop()); + } + + pc.getStatisticsAndReset(); +} + +TEST_F (PerformanceCounterTests, StopReturnsTrueWhenRunsPerPrintIsReached) +{ + PerformanceCounter pc ("test", 3); + + pc.start(); + EXPECT_FALSE (pc.stop()); + pc.start(); + EXPECT_FALSE (pc.stop()); + pc.start(); + EXPECT_TRUE (pc.stop()); +} + +TEST_F (PerformanceCounterTests, StopResetsCountAfterTriggeringPrint) +{ + PerformanceCounter pc ("test", 2); + + pc.start(); + pc.stop(); + pc.start(); + EXPECT_TRUE (pc.stop()); + + pc.start(); + EXPECT_FALSE (pc.stop()); + + pc.getStatisticsAndReset(); +} + +TEST_F (PerformanceCounterTests, GetStatisticsAndResetReturnsAccumulatedData) +{ + PerformanceCounter pc ("test", 100); + + for (int i = 0; i < 5; ++i) + { + pc.start(); + pc.stop(); + } + + auto stats = pc.getStatisticsAndReset(); + EXPECT_EQ (stats.numRuns, 5); + EXPECT_GE (stats.totalSeconds, 0.0); + EXPECT_GE (stats.minimumSeconds, 0.0); + EXPECT_GE (stats.maximumSeconds, stats.minimumSeconds); +} + +TEST_F (PerformanceCounterTests, GetStatisticsAndResetClearsInternalState) +{ + PerformanceCounter pc ("test", 100); + + pc.start(); + pc.stop(); + + pc.getStatisticsAndReset(); + + auto afterReset = pc.getStatisticsAndReset(); + EXPECT_EQ (afterReset.numRuns, 0); +} + +TEST_F (PerformanceCounterTests, AverageEqualsToTotalDividedByNumRuns) +{ + PerformanceCounter pc ("test", 100); + + for (int i = 0; i < 4; ++i) + { + pc.start(); + pc.stop(); + } + + auto stats = pc.getStatisticsAndReset(); + EXPECT_EQ (stats.numRuns, 4); + EXPECT_NEAR (stats.averageSeconds, stats.totalSeconds / 4.0, 1e-12); +} + +TEST_F (PerformanceCounterTests, MeasuredTimesAreNonNegative) +{ + PerformanceCounter pc ("test", 100); + + for (int i = 0; i < 10; ++i) + { + pc.start(); + pc.stop(); + } + + auto stats = pc.getStatisticsAndReset(); + EXPECT_GE (stats.minimumSeconds, 0.0); + EXPECT_GE (stats.maximumSeconds, 0.0); + EXPECT_GE (stats.totalSeconds, 0.0); + EXPECT_GE (stats.maximumSeconds, stats.minimumSeconds); + EXPECT_GE (stats.totalSeconds, stats.maximumSeconds); +} + +TEST_F (PerformanceCounterTests, PrintStatisticsLogsToCurrentLogger) +{ + PerfTestLogger logger; + Logger::setCurrentLogger (&logger); + + { + PerformanceCounter pc ("BenchmarkName", 100); + pc.start(); + pc.stop(); + pc.printStatistics(); + } + + Logger::setCurrentLogger (nullptr); + + auto messages = logger.getMessages(); + bool found = false; + for (int i = 0; i < messages.size(); ++i) + found = found || messages[i].contains ("BenchmarkName"); + + EXPECT_TRUE (found); +} + +TEST_F (PerformanceCounterTests, FileLoggingWritesStatsToFile) +{ + auto tempFile = File::getSpecialLocation (File::tempDirectory) + .getChildFile ("YUP_PerfTest_" + String::toHexString (Random::getSystemRandom().nextInt()) + ".log"); + + { + PerformanceCounter pc ("FileTarget", 2, tempFile); + pc.start(); + pc.stop(); + pc.start(); + pc.stop(); + } + + auto content = tempFile.loadFileAsString(); + EXPECT_TRUE (content.contains ("FileTarget")); + + tempFile.deleteFile(); +} + +// ============================================================================= +// ScopedTimeMeasurement Tests +// ============================================================================= + +TEST (ScopedTimeMeasurementTests, ConstructorInitializesResultToZero) +{ + double result = 99.0; + { + ScopedTimeMeasurement m (result); + EXPECT_EQ (result, 0.0); + } +} + +TEST (ScopedTimeMeasurementTests, DestructorPopulatesResultWithElapsedTime) +{ + double result = 0.0; + { + ScopedTimeMeasurement m (result); + } + + EXPECT_GE (result, 0.0); +} + +TEST (ScopedTimeMeasurementTests, ResultIsInSecondsRange) +{ + double result = 0.0; + { + ScopedTimeMeasurement m (result); + } + + // An empty scope should complete in well under 1 second + EXPECT_GE (result, 0.0); + EXPECT_LT (result, 1.0); +} + +TEST (ScopedTimeMeasurementTests, SequentialScopesDontInterfere) +{ + double result1 = -1.0; + double result2 = -1.0; + + { + ScopedTimeMeasurement m (result1); + } + + { + ScopedTimeMeasurement m (result2); + } + + EXPECT_GE (result1, 0.0); + EXPECT_GE (result2, 0.0); +} + +TEST (ScopedTimeMeasurementTests, SleepingScopeYieldsLargerResult) +{ + double shortResult = 0.0; + double longResult = 0.0; + + { + ScopedTimeMeasurement m (shortResult); + } + + { + ScopedTimeMeasurement m (longResult); + Thread::sleep (5); + } + + EXPECT_GT (longResult, shortResult); + EXPECT_GE (longResult, 0.003); +}