diff --git a/.github/workflows/clang_format.yml b/.github/workflows/clang_format.yml_ similarity index 100% rename from .github/workflows/clang_format.yml rename to .github/workflows/clang_format.yml_ diff --git a/modules/yup_audio_basics/midi/yup_MidiFile.cpp b/modules/yup_audio_basics/midi/yup_MidiFile.cpp index 133ccb168..4452c348b 100644 --- a/modules/yup_audio_basics/midi/yup_MidiFile.cpp +++ b/modules/yup_audio_basics/midi/yup_MidiFile.cpp @@ -340,7 +340,22 @@ void MidiFile::setTicksPerQuarterNote (int ticks) noexcept void MidiFile::setSmpteTimeFormat (int framesPerSecond, int subframeResolution) noexcept { - timeFormat = (short) (((-framesPerSecond) << 8) | subframeResolution); + switch (framesPerSecond) + { + case 24: + case 25: + case 29: + case 30: + break; + + default: + framesPerSecond = 25; + break; + } + + const int8 smpteByte = static_cast (-framesPerSecond); + + timeFormat = static_cast ((static_cast (smpteByte) << 8) | static_cast (subframeResolution)); } //============================================================================== diff --git a/modules/yup_audio_devices/native/yup_CoreAudio_mac.cpp b/modules/yup_audio_devices/native/yup_CoreAudio_mac.cpp index d5f29c16d..1569ad0bd 100644 --- a/modules/yup_audio_devices/native/yup_CoreAudio_mac.cpp +++ b/modules/yup_audio_devices/native/yup_CoreAudio_mac.cpp @@ -1055,7 +1055,7 @@ struct CoreAudioClasses CoreAudioIODevice& owner; int bitDepth = 32; - int xruns = 0; + std::atomic xruns { 0 }; Array sampleRates; Array bufferSizes; AudioDeviceID deviceID; @@ -1158,12 +1158,12 @@ struct CoreAudioClasses { auto& intern = *static_cast (inClientData); - const auto xruns = std::count_if (pa, pa + numAddresses, [] (const AudioObjectPropertyAddress& x) + const auto xruns = (int) std::count_if (pa, pa + numAddresses, [] (const AudioObjectPropertyAddress& x) { return x.mSelector == kAudioDeviceProcessorOverload; }); - intern.xruns += xruns; + intern.xruns.fetch_add (xruns); const auto detailsChanged = std::any_of (pa, pa + numAddresses, [] (const AudioObjectPropertyAddress& x) { @@ -1318,7 +1318,7 @@ struct CoreAudioClasses int getCurrentBufferSizeSamples() override { return internal->getBufferSize(); } - int getXRunCount() const noexcept override { return internal->xruns; } + int getXRunCount() const noexcept override { return internal->xruns.load (std::memory_order_relaxed); } int getIndexOfDevice (bool asInput) const { return deviceType->getDeviceNames (asInput).indexOf (getName()); } @@ -1341,7 +1341,7 @@ struct CoreAudioClasses int bufferSizeSamples) override { isOpen_ = true; - internal->xruns = 0; + internal->xruns.store (0); inputChannelsRequested = inputChannels; outputChannelsRequested = outputChannels; @@ -1650,10 +1650,12 @@ struct CoreAudioClasses d->close(); } - void restart (AudioIODeviceCallback* cb) + void restart() { const ScopedLock sl (closeLock); + AudioIODeviceCallback* cb = previousCallback; + close(); auto newSampleRate = sampleRateRequested; @@ -1763,7 +1765,7 @@ struct CoreAudioClasses int getXRunCount() const noexcept override { - return xruns.load(); + return xruns.load (std::memory_order_relaxed); } private: @@ -1771,6 +1773,7 @@ struct CoreAudioClasses WeakReference owner; CriticalSection callbackLock; + CriticalSection closeLock; AudioIODeviceCallback* callback = nullptr; AudioIODeviceCallback* previousCallback = nullptr; double currentSampleRate = 0; @@ -1778,7 +1781,6 @@ struct CoreAudioClasses bool active = false; String lastError; AudioSampleBuffer fifo, scratchBuffer; - CriticalSection closeLock; int targetLatency = 0; std::atomic xruns { -1 }; std::atomic lastValidReadPosition { invalidSampleTime }; @@ -1791,7 +1793,7 @@ struct CoreAudioClasses { stopTimer(); - restart (previousCallback); + restart(); } void shutdown (const String& error) @@ -1965,7 +1967,7 @@ struct CoreAudioClasses for (auto& d : getDeviceWrappers()) d->sampleTime.store (invalidSampleTime); - ++xruns; + xruns.fetch_add (1); } void handleAudioDeviceAboutToStart (AudioIODevice* device) @@ -2132,8 +2134,7 @@ struct CoreAudioClasses }; /* If the current AudioIODeviceCombiner::callback is nullptr, it sets itself as the callback - and forwards error related callbacks to the provided callback - */ + and forwards error related callbacks to the provided callback. */ class ScopedErrorForwarder final : public AudioIODeviceCallback { public: diff --git a/modules/yup_audio_devices/yup_audio_devices.cpp b/modules/yup_audio_devices/yup_audio_devices.cpp index 3f40ddb8b..e6d2cc294 100644 --- a/modules/yup_audio_devices/yup_audio_devices.cpp +++ b/modules/yup_audio_devices/yup_audio_devices.cpp @@ -164,7 +164,7 @@ YUP_END_IGNORE_WARNINGS_MSVC #include #include "native/yup_ASIO_windows.cpp" #endif -// clang-format oon +// clang-format on //============================================================================== #elif YUP_LINUX || YUP_BSD diff --git a/modules/yup_audio_gui/yup_audio_gui.cpp b/modules/yup_audio_gui/yup_audio_gui.cpp index c4f0e944e..80178fcd0 100644 --- a/modules/yup_audio_gui/yup_audio_gui.cpp +++ b/modules/yup_audio_gui/yup_audio_gui.cpp @@ -21,10 +21,10 @@ #ifdef YUP_AUDIO_GUI_H_INCLUDED /* When you add this cpp file to your project, you mustn't include it in a file where you've - already included any other headers - just put it inside a file on its own, possibly with your config - flags preceding it, but don't include anything else. That also includes avoiding any automatic prefix - header files that the compiler may be using. - */ + already included any other headers - just put it inside a file on its own, possibly with your config + flags preceding it, but don't include anything else. That also includes avoiding any automatic prefix + header files that the compiler may be using. +*/ #error "Incorrect use of YUP cpp file" #endif diff --git a/modules/yup_core/containers/yup_FixedSizeFunction.h b/modules/yup_core/containers/yup_FixedSizeFunction.h index 92da4cc4d..e7fa6cbf4 100644 --- a/modules/yup_core/containers/yup_FixedSizeFunction.h +++ b/modules/yup_core/containers/yup_FixedSizeFunction.h @@ -117,7 +117,9 @@ template class FixedSizeFunction { private: - using Storage = std::aligned_storage_t; + using Storage = struct { + alignas (alignof (std::max_align_t)) std::byte data[len]; + }; template using Decay = std::decay_t; diff --git a/modules/yup_core/text/yup_StringPairArray.cpp b/modules/yup_core/text/yup_StringPairArray.cpp index 4c97eb882..f7d18f2ab 100644 --- a/modules/yup_core/text/yup_StringPairArray.cpp +++ b/modules/yup_core/text/yup_StringPairArray.cpp @@ -45,6 +45,25 @@ StringPairArray::StringPairArray (bool shouldIgnoreCase) { } +StringPairArray::StringPairArray (const std::initializer_list& stringPairs) +{ + for (const auto& item : stringPairs) + { + keys.add (item.key); + values.add (item.value); + } +} + +StringPairArray::StringPairArray (bool shouldIgnoreCase, const std::initializer_list& stringPairs) + : ignoreCase (shouldIgnoreCase) +{ + for (const auto& item : stringPairs) + { + keys.add (item.key); + values.add (item.value); + } +} + StringPairArray::StringPairArray (const StringPairArray& other) : keys (other.keys) , values (other.values) diff --git a/modules/yup_core/text/yup_StringPairArray.h b/modules/yup_core/text/yup_StringPairArray.h index c289624dc..5bef36911 100644 --- a/modules/yup_core/text/yup_StringPairArray.h +++ b/modules/yup_core/text/yup_StringPairArray.h @@ -43,8 +43,45 @@ namespace yup //============================================================================== /** A container for holding a set of strings which are keyed by another string. - - @see StringArray + + This class provides a map-like container that associates string keys with string values. + It offers both case-sensitive and case-insensitive key comparison modes, and maintains + insertion order of key-value pairs. + + Key features: + - Case-sensitive or case-insensitive key matching + - Maintains insertion order of pairs + - Range-based for loop support via iterators + - Convenient initializer list construction + - Integration with standard library maps + - Memory-efficient storage with StringArray backing + + Usage examples: + @code + // Basic usage + StringPairArray config; + config.set("host", "localhost"); + config.set("port", "8080"); + + // Initializer list construction + StringPairArray headers { + { "Content-Type", "application/json" }, + { "User-Agent", "YUP/1.0" } + }; + + // Range-based iteration + for (const auto& pair : config) + { + std::cout << pair.key << " = " << pair.value << std::endl; + } + + // Case sensitivity control + StringPairArray caseSensitive(false); + caseSensitive.set("Key", "value1"); + caseSensitive.set("key", "value2"); // Different from "Key" + @endcode + + @see StringArray, KeyValuePair, Iterator @tags{Core} */ @@ -52,107 +89,330 @@ class YUP_API StringPairArray { public: //============================================================================== - /** Creates an empty array */ - StringPairArray (bool ignoreCaseWhenComparingKeys = true); + /** A key-value pair structure for iterator support. + + This structure provides access to both the key and value when iterating + over a StringPairArray using range-based for loops. + + @see Iterator, begin(), end() + */ + struct KeyValuePair + { + StringRef key; /**< Reference to the key string */ + StringRef value; /**< Reference to the value string */ + + /** Constructs a key-value pair. + + @param k The key string reference + @param v The value string reference + */ + KeyValuePair (StringRef k, StringRef v) + : key (k) + , value (v) + { + } + }; + + //============================================================================== + /** Creates an empty array with default case-insensitive key comparison. */ + StringPairArray() = default; + + /** Creates an empty array with specified case sensitivity. - /** Creates a copy of another array */ + @param ignoreCaseWhenComparingKeys If true, key comparisons will be case-insensitive + */ + StringPairArray (bool ignoreCaseWhenComparingKeys); + + /** Creates an array from an initializer list with default case-insensitive key comparison. + + This constructor allows convenient initialization using brace syntax: + + @code + StringPairArray spa { + { "key1", "value1" }, + { "key2", "value2" } + }; + @endcode + + @param stringPairs Initializer list of key-value pairs + */ + StringPairArray (const std::initializer_list& stringPairs); + + /** Creates an array from an initializer list with specified case sensitivity. + + This constructor allows convenient initialization with custom case sensitivity: + + @code + StringPairArray spa (false, { + { "Key", "value1" }, + { "key", "value2" } // Will be treated as different from "Key" + }); + @endcode + + @param ignoreCaseWhenComparingKeys If true, key comparisons will be case-insensitive + @param stringPairs Initializer list of key-value pairs + */ + StringPairArray (bool ignoreCaseWhenComparingKeys, const std::initializer_list& stringPairs); + + /** Creates a copy of another array. + + @param other The array to copy from + */ StringPairArray (const StringPairArray& other); - /** Move constructs from another array */ + /** Move constructs from another array. + + @param other The array to move from + */ StringPairArray (StringPairArray&& other); /** Destructor. */ ~StringPairArray() = default; - /** Copies the contents of another string array into this one */ + /** Copies the contents of another string array into this one. + + @param other The array to copy from + + @returns A reference to this array + */ StringPairArray& operator= (const StringPairArray& other); - /** Moves the contents of another string array into this one */ + /** Moves the contents of another string array into this one. + + @param other The array to move from + + @returns A reference to this array + */ StringPairArray& operator= (StringPairArray&& other); //============================================================================== - /** Compares two arrays. - Comparisons are case-sensitive. - @returns true only if the other array contains exactly the same strings with the same keys + /** Compares two arrays for equality. + + Comparisons are case-sensitive regardless of the ignoreCase setting. + The arrays are considered equal if they contain the same key-value pairs, + though not necessarily in the same order. + + @param other The array to compare with + + @returns true only if the other array contains exactly the same strings with the same keys */ bool operator== (const StringPairArray& other) const; - /** Compares two arrays. - Comparisons are case-sensitive. - @returns false if the other array contains exactly the same strings with the same keys + /** Compares two arrays for inequality. + + @param other The array to compare with + + @returns true if the arrays are not equal + + @see operator== */ bool operator!= (const StringPairArray& other) const; //============================================================================== /** Finds the value corresponding to a key string. - If no such key is found, this will just return an empty string. To check whether - a given key actually exists (because it might actually be paired with an empty string), use - the getAllKeys() method to obtain a list. + If no such key is found, this will just return an empty string. To check whether a given key actually exists + (because it might actually be paired with an empty string), use the containsKey() method or getAllKeys() + method. Obviously the reference returned shouldn't be stored for later use, as the string it refers to may disappear when the array changes. - @see getValue + @param key The key to search for + + @returns A reference to the value string, or an empty string if not found + + @see getValue, containsKey */ const String& operator[] (StringRef key) const; - /** Finds the value corresponding to a key string. - If no such key is found, this will just return the value provided as a default. - @see operator[] + /** Finds the value corresponding to a key string with a default fallback. + + This is safer than operator[] when you need to distinguish between a missing key and a key with an empty value. + + @param key The key to search for + @param defaultReturnValue The value to return if the key is not found + + @returns The value associated with the key, or the default value + + @see operator[], containsKey */ - String getValue (StringRef, const String& defaultReturnValue) const; + String getValue (StringRef key, const String& defaultReturnValue) const; + + /** Checks if a key exists in the array. + + This method respects the case sensitivity setting of the array. + + @param key The key to search for - /** Returns true if the given key exists. */ + @returns true if the key exists, false otherwise + */ bool containsKey (StringRef key) const noexcept; - /** Returns a list of all keys in the array. */ + /** Returns a list of all keys in the array. + + The keys are returned in the order they were added to the array. + + @returns A reference to the internal StringArray containing all keys + */ const StringArray& getAllKeys() const noexcept { return keys; } - /** Returns a list of all values in the array. */ + /** Returns a list of all values in the array. + + The values are returned in the same order as their corresponding keys. + + @returns A reference to the internal StringArray containing all values + + @see getAllKeys + */ const StringArray& getAllValues() const noexcept { return values; } - /** Returns the number of strings in the array */ + /** Returns the number of key-value pairs in the array. + + @returns The number of pairs stored in the array + */ inline int size() const noexcept { return keys.size(); } + //============================================================================== + /** Iterator class for range-based for loop support. + + This iterator allows you to iterate over key-value pairs using range-based for loops: + + @code + StringPairArray spa; + spa.set("key1", "value1"); + spa.set("key2", "value2"); + + for (const auto& pair : spa) + YUP_DBG (pair.key << " = " << pair.value); + @endcode + + @see KeyValuePair, begin(), end() + */ + class Iterator + { + public: + /** Constructs an iterator. + + @param array The StringPairArray to iterate over + @param index The starting index position + */ + Iterator (const StringPairArray& array, int index) + : spa (array) + , idx (index) + { + } + + /** Dereferences the iterator to get the current key-value pair. + + @returns A KeyValuePair containing references to the current key and value + */ + KeyValuePair operator*() const { return KeyValuePair (spa.keys[idx], spa.values[idx]); } + + /** Pre-increments the iterator to the next position. + + @returns A reference to this iterator + */ + Iterator& operator++() + { + ++idx; + return *this; + } + + /** Compares two iterators for inequality. + @param other The iterator to compare with + @returns true if the iterators point to different positions + */ + bool operator!= (const Iterator& other) const { return idx != other.idx; } + + private: + const StringPairArray& spa; + int idx; + }; + + /** Returns an iterator pointing to the beginning of the array. + + @returns An iterator positioned at the first key-value pair + + @see end(), Iterator + */ + Iterator begin() const noexcept { return Iterator (*this, 0); } + + /** Returns an iterator pointing to the end of the array. + + @returns An iterator positioned one past the last key-value pair + + @see begin(), Iterator + */ + Iterator end() const noexcept { return Iterator (*this, size()); } + //============================================================================== /** Adds or amends a key/value pair. + If a value already exists with this key, its value will be overwritten, - otherwise the key/value pair will be added to the array. + otherwise the key/value pair will be added to the array. Key comparison + respects the case sensitivity setting of this array. + + @param key The key string + @param value The value string to associate with the key */ void set (const String& key, const String& value); /** Adds the items from another array to this one. + This is equivalent to using set() to add each of the pairs from the other array. + Existing keys will be overwritten with values from the other array. + + @param other The StringPairArray whose pairs should be added */ void addArray (const StringPairArray& other); //============================================================================== - /** Removes all elements from the array. */ + /** Removes all key-value pairs from the array. + + After calling this method, the array will be empty. + */ void clear(); - /** Removes a string from the array based on its key. + /** Removes a key-value pair from the array based on its key. + + Key comparison respects the case sensitivity setting of this array. If the key isn't found, nothing will happen. + + @param key The key of the pair to remove */ void remove (StringRef key); - /** Removes a string from the array based on its index. + /** Removes a key-value pair from the array based on its index. + If the index is out-of-range, no action will be taken. + + @param index The zero-based index of the pair to remove */ void remove (int index); //============================================================================== - /** Indicates whether to use a case-insensitive search when looking up a key string. + /** Sets whether to use case-insensitive search when looking up keys. + + This affects all key operations including lookup, containsKey, set, and remove. + + @param shouldIgnoreCase If true, key comparisons will be case-insensitive */ void setIgnoresCase (bool shouldIgnoreCase); - /** Indicates whether a case-insensitive search is used when looking up a key string. + /** Returns whether case-insensitive search is used when looking up keys. + + @returns true if key comparisons are case-insensitive, false if case-sensitive */ bool getIgnoresCase() const noexcept; //============================================================================== - /** Returns a descriptive string containing the items. - This is handy for dumping the contents of an array. + /** Returns a descriptive string containing all key-value pairs. + + This creates a human-readable representation of the array contents, + which is handy for debugging or logging the array state. + + @returns A string representation of all key-value pairs */ String getDescription() const; @@ -162,14 +422,30 @@ class YUP_API StringPairArray Arrays typically allocate slightly more storage than they need, and after removing elements, they may have quite a lot of unused space allocated. This method will reduce the amount of allocated storage to a minimum. + + This is useful for optimizing memory usage after many removals. */ void minimiseStorageOverheads(); //============================================================================== - /** Adds the contents of a map to this StringPairArray. */ + /** Adds the contents of a std::map to this StringPairArray. + + This method efficiently adds key-value pairs from a standard map. + Existing keys will be overwritten. The case sensitivity setting of + this array affects how duplicate keys are handled. + + @param mapToAdd The map whose contents should be added + */ void addMap (const std::map& mapToAdd); - /** Adds the contents of an unordered map to this StringPairArray. */ + /** Adds the contents of a std::unordered_map to this StringPairArray. + + This method efficiently adds key-value pairs from a standard unordered map. + Existing keys will be overwritten. The case sensitivity setting of + this array affects how duplicate keys are handled. + + @param mapToAdd The unordered map whose contents should be added + */ void addUnorderedMap (const std::unordered_map& mapToAdd); private: @@ -178,7 +454,7 @@ class YUP_API StringPairArray void addMapImpl (const Map& mapToAdd); StringArray keys, values; - bool ignoreCase; + bool ignoreCase = true; YUP_LEAK_DETECTOR (StringPairArray) }; diff --git a/modules/yup_core/threads/yup_CriticalSection.h b/modules/yup_core/threads/yup_CriticalSection.h index 81482341b..266beecc9 100644 --- a/modules/yup_core/threads/yup_CriticalSection.h +++ b/modules/yup_core/threads/yup_CriticalSection.h @@ -121,9 +121,9 @@ class YUP_API CriticalSection // a block of memory here that's big enough to be used internally as a windows // CRITICAL_SECTION structure. #if YUP_64BIT - std::aligned_storage_t<44, 8> lock; + alignas(8) std::byte lock[44]; #else - std::aligned_storage_t<24, 8> lock; + alignas(8) std::byte lock[24]; #endif #else mutable pthread_mutex_t lock; diff --git a/modules/yup_data_model/undo/yup_UndoManager.h b/modules/yup_data_model/undo/yup_UndoManager.h index c2d80d96a..b6a8635e7 100644 --- a/modules/yup_data_model/undo/yup_UndoManager.h +++ b/modules/yup_data_model/undo/yup_UndoManager.h @@ -265,7 +265,7 @@ class YUP_API UndoManager : object (object) , function (std::move (function)) { - jassert (function != nullptr); + jassert (this->function != nullptr); } bool perform (UndoableActionState stateToPerform) override diff --git a/modules/yup_data_model/yup_data_model.cpp b/modules/yup_data_model/yup_data_model.cpp index b92595cb0..485f57b0c 100644 --- a/modules/yup_data_model/yup_data_model.cpp +++ b/modules/yup_data_model/yup_data_model.cpp @@ -19,16 +19,14 @@ ============================================================================== */ -// clang-format off #ifdef YUP_AUDIO_PROCESSORS_H_INCLUDED /* When you add this cpp file to your project, you mustn't include it in a file where you've - already included any other headers - just put it inside a file on its own, possibly with your config - flags preceding it, but don't include anything else. That also includes avoiding any automatic prefix - header files that the compiler may be using. - */ + already included any other headers - just put it inside a file on its own, possibly with your config + flags preceding it, but don't include anything else. That also includes avoiding any automatic prefix + header files that the compiler may be using. +*/ #error "Incorrect use of YUP cpp file" #endif -// clang-format on #include "yup_data_model.h" diff --git a/modules/yup_events/native/yup_Messaging_linux.cpp b/modules/yup_events/native/yup_Messaging_linux.cpp index 638f894cd..c3c2da42a 100644 --- a/modules/yup_events/native/yup_Messaging_linux.cpp +++ b/modules/yup_events/native/yup_Messaging_linux.cpp @@ -389,12 +389,14 @@ void MessageManager::broadcastMessage (const String&) void LinuxEventLoop::registerFdCallback (int fd, std::function readCallback, short eventMask) { if (auto* runLoop = InternalRunLoop::getInstanceWithoutCreating()) + { runLoop->registerFdCallback ( fd, [cb = std::move (readCallback), fd] { cb (fd); }, eventMask); + } } void LinuxEventLoop::unregisterFdCallback (int fd) diff --git a/modules/yup_events/timers/yup_Timer.cpp b/modules/yup_events/timers/yup_Timer.cpp index 3937173e0..e179c83a9 100644 --- a/modules/yup_events/timers/yup_Timer.cpp +++ b/modules/yup_events/timers/yup_Timer.cpp @@ -134,7 +134,7 @@ class Timer::TimerThread final : private Thread break; auto* timer = first.timer; - first.countdownMs = timer->timerPeriodMs; + first.countdownMs = timer->getTimerInterval(); shuffleTimerBackInQueue (0); notify(); @@ -175,7 +175,7 @@ class Timer::TimerThread final : private Thread auto pos = timers.size(); - timers.push_back ({ t, t->timerPeriodMs }); + timers.push_back ({ t, t->getTimerInterval() }); t->positionInQueue = pos; shuffleTimerForwardInQueue (pos); notify(); @@ -210,7 +210,7 @@ class Timer::TimerThread final : private Thread jassert (timers[pos].timer == t); auto lastCountdown = timers[pos].countdownMs; - auto newCountdown = t->timerPeriodMs; + auto newCountdown = t->getTimerInterval(); if (newCountdown != lastCountdown) { @@ -350,10 +350,7 @@ void Timer::startTimer (int interval) noexcept if (auto* instance = TimerThread::getInstance()) { - bool wasStopped = (timerPeriodMs == 0); - timerPeriodMs = jmax (1, interval); - - if (wasStopped) + if (timerPeriodMs.exchange (jmax (1, interval)) == 0) instance->addTimer (this); else instance->resetTimerCounter (this); @@ -370,12 +367,10 @@ void Timer::startTimerHz (int timerFrequencyHz) noexcept void Timer::stopTimer() noexcept { - if (timerPeriodMs > 0) + if (timerPeriodMs.exchange (0, std::memory_order_relaxed) > 0) { if (auto* instance = TimerThread::getInstanceWithoutCreating()) instance->removeTimer (this); - - timerPeriodMs = 0; } } diff --git a/modules/yup_events/timers/yup_Timer.h b/modules/yup_events/timers/yup_Timer.h index 17f0f6a6c..47f9ea32e 100644 --- a/modules/yup_events/timers/yup_Timer.h +++ b/modules/yup_events/timers/yup_Timer.h @@ -125,12 +125,13 @@ class YUP_API Timer //============================================================================== /** Returns true if the timer is currently running. */ - bool isTimerRunning() const noexcept { return timerPeriodMs > 0; } + bool isTimerRunning() const noexcept { return getTimerInterval() > 0; } /** Returns the timer's interval. + @returns the timer's interval in milliseconds if it's running, or 0 if it's not. */ - int getTimerInterval() const noexcept { return timerPeriodMs; } + int getTimerInterval() const noexcept { return timerPeriodMs.load (std::memory_order_relaxed); } //============================================================================== /** Invokes a lambda after a given number of milliseconds. */ @@ -143,7 +144,7 @@ class YUP_API Timer private: class TimerThread; size_t positionInQueue = (size_t) -1; - int timerPeriodMs = 0; + std::atomic timerPeriodMs = 0; Timer& operator= (const Timer&) = delete; }; diff --git a/modules/yup_gui/widgets/yup_Slider.h b/modules/yup_gui/widgets/yup_Slider.h index eea526c2d..3eebeee4d 100644 --- a/modules/yup_gui/widgets/yup_Slider.h +++ b/modules/yup_gui/widgets/yup_Slider.h @@ -350,10 +350,10 @@ class YUP_API Slider : public Component SliderType sliderType = LinearHorizontal; NormalisableRange range { 0.0, 1.0 }; + double defaultValue = 0.0; double currentValue = 0.0; double minValue = 0.0; double maxValue = 1.0; - double defaultValue = 0.0; int numDecimalPlaces = 7; double mouseDragSensitivity = 1.0; diff --git a/modules/yup_python/bindings/yup_YupCore_bindings.h b/modules/yup_python/bindings/yup_YupCore_bindings.h index f13747a08..6b7996834 100644 --- a/modules/yup_python/bindings/yup_YupCore_bindings.h +++ b/modules/yup_python/bindings/yup_YupCore_bindings.h @@ -34,8 +34,8 @@ #include #include #include -#include #include +#include #include namespace PYBIND11_NAMESPACE @@ -361,7 +361,7 @@ void registerArray (pybind11::module_& m) //============================================================================== -struct YUP_API PyThreadID +struct PyThreadID { explicit PyThreadID (Thread::ThreadID value) noexcept : value (value) @@ -691,7 +691,7 @@ struct PyFileFilter : Base //============================================================================== -struct YUP_API PyURLDownloadTaskListener : public URL::DownloadTaskListener +struct PyURLDownloadTaskListener : public URL::DownloadTaskListener { void finished (URL::DownloadTask* task, bool success) override { @@ -706,7 +706,7 @@ struct YUP_API PyURLDownloadTaskListener : public URL::DownloadTaskListener //============================================================================== -struct YUP_API PyXmlElementComparator +struct PyXmlElementComparator { PyXmlElementComparator() = default; @@ -725,7 +725,7 @@ struct YUP_API PyXmlElementComparator } }; -struct YUP_API PyXmlElementCallableComparator +struct PyXmlElementCallableComparator { explicit PyXmlElementCallableComparator (pybind11::function f) : fn (std::move (f)) @@ -752,7 +752,7 @@ struct YUP_API PyXmlElementCallableComparator //============================================================================== -struct YUP_API PyHighResolutionTimer : public HighResolutionTimer +struct PyHighResolutionTimer : public HighResolutionTimer { void hiResTimerCallback() override { @@ -910,7 +910,7 @@ struct PyThread : Base //============================================================================== -struct YUP_API PyThreadListener : Thread::Listener +struct PyThreadListener : Thread::Listener { using Thread::Listener::Listener; @@ -922,7 +922,7 @@ struct YUP_API PyThreadListener : Thread::Listener //============================================================================== -struct YUP_API PyThreadPoolJob : ThreadPoolJob +struct PyThreadPoolJob : ThreadPoolJob { using ThreadPoolJob::ThreadPoolJob; @@ -934,7 +934,7 @@ struct YUP_API PyThreadPoolJob : ThreadPoolJob //============================================================================== -struct YUP_API PyThreadPoolJobSelector : ThreadPool::JobSelector +struct PyThreadPoolJobSelector : ThreadPool::JobSelector { using ThreadPool::JobSelector::JobSelector; @@ -946,7 +946,7 @@ struct YUP_API PyThreadPoolJobSelector : ThreadPool::JobSelector //============================================================================== -struct YUP_API PyTimeSliceClient : TimeSliceClient +struct PyTimeSliceClient : TimeSliceClient { using TimeSliceClient::TimeSliceClient; diff --git a/modules/yup_python/bindings/yup_YupEvents_bindings.h b/modules/yup_python/bindings/yup_YupEvents_bindings.h index f01fb837f..9f52f2e85 100644 --- a/modules/yup_python/bindings/yup_YupEvents_bindings.h +++ b/modules/yup_python/bindings/yup_YupEvents_bindings.h @@ -42,7 +42,7 @@ void registerYupEventsBindings (pybind11::module_& m); // ================================================================================================= -struct YUP_API PyActionListener : public yup::ActionListener +struct PyActionListener : public yup::ActionListener { void actionListenerCallback (const yup::String& message) override { @@ -52,7 +52,7 @@ struct YUP_API PyActionListener : public yup::ActionListener // ================================================================================================= -struct YUP_API PyAsyncUpdater : public yup::AsyncUpdater +struct PyAsyncUpdater : public yup::AsyncUpdater { void handleAsyncUpdate() override { @@ -98,7 +98,7 @@ struct PyCallbackMessage : public PyMessageBase // ================================================================================================= -struct YUP_API PyMessageListener : public yup::MessageListener +struct PyMessageListener : public yup::MessageListener { void handleMessage (const yup::Message& message) override { @@ -108,7 +108,7 @@ struct YUP_API PyMessageListener : public yup::MessageListener // ================================================================================================= -struct YUP_API PyMessageManagerLock +struct PyMessageManagerLock { explicit PyMessageManagerLock (yup::Thread* thread) : thread (thread) @@ -127,7 +127,7 @@ struct YUP_API PyMessageManagerLock // ================================================================================================= -struct YUP_API PyTimer : public yup::Timer +struct PyTimer : public yup::Timer { using yup::Timer::Timer; @@ -139,7 +139,7 @@ struct YUP_API PyTimer : public yup::Timer // ================================================================================================= -struct YUP_API PyMultiTimer : public yup::MultiTimer +struct PyMultiTimer : public yup::MultiTimer { using yup::MultiTimer::MultiTimer; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6724c3589..e3fca4389 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -104,6 +104,7 @@ yup_standalone_app ( TARGET_CONSOLE ${target_console} DEFINITIONS YUP_USE_CURL=0 + YUP_MODAL_LOOPS_PERMITTED=1 MODULES ${target_modules} ${target_gtest_modules}) diff --git a/tests/main.cpp b/tests/main.cpp index 9dcccef00..37d5d7637 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -257,7 +257,7 @@ struct TestApplication : yup::YUPApplication std::cout << "\n========================================\n"; std::cout << "*** FAILURES (" << failedTests.size() << "):\n"; for (const auto& fail : failedTests) - std::cout << "\n--- " << fail.name << "\n" + std::cout << "\n*** " << fail.name << "\n" << fail.failureDetails << "\n"; } @@ -268,6 +268,8 @@ struct TestApplication : yup::YUPApplication << std::chrono::duration_cast (totalElapsed).count() << " ms\n"; + std::cout.flush(); + setApplicationReturnValue (failedTests.empty() ? 0 : 1); quit(); @@ -326,6 +328,12 @@ struct TestApplication : yup::YUPApplication testStart = std::chrono::steady_clock::now(); failureStream.str (""); failureStream.clear(); + + std::ostringstream line; + line << (std::string (info.test_suite_name()) + "." + info.name()); + + std::cout << "--- " << line.str() << " "; + std::cout.flush(); } void OnTestPartResult (const testing::TestPartResult& result) override @@ -333,7 +341,7 @@ struct TestApplication : yup::YUPApplication if (result.failed()) { failureStream << result.file_name() << ":" << result.line_number() << ": " - << result.summary() << "\n"; + << result.summary() << '\n'; } } @@ -352,17 +360,19 @@ struct TestApplication : yup::YUPApplication if (testPassed) { - std::cout << "--- PASS - " << line.str() << " (" << elapsedMs << " ms)\n"; + std::cout << "--- PASS (" << elapsedMs << " ms)" << '\n'; owner.passedTests++; } else { - std::cout << "*** FAIL - " << line.str() << " (" << elapsedMs << " ms)\n"; - owner.failedTests.push_back ( - { std::string (info.test_suite_name()) + "." + info.name(), - failureStream.str() }); + std::cout << "*** FAIL (" << elapsedMs << " ms)" << '\n'; + std::cout << failureStream.str() << '\n'; + + owner.failedTests.push_back ({ line.str(), failureStream.str() }); } + std::cout.flush(); + if (owner.currentSuite) { TestCaseResult testCase; diff --git a/tests/yup_audio_basics/yup_MidiFile.cpp b/tests/yup_audio_basics/yup_MidiFile.cpp index 932edda92..2e8048109 100644 --- a/tests/yup_audio_basics/yup_MidiFile.cpp +++ b/tests/yup_audio_basics/yup_MidiFile.cpp @@ -214,11 +214,14 @@ TEST_F (MidiFileTest, TimeFormatGetSet) EXPECT_EQ (file.getTimeFormat(), 96); // Test SMPTE format - file.setSmpteTimeFormat (24, 8); - //EXPECT_EQ(file.getTimeFormat(), (short)(((-24) << 8) | 8)); + file.setSmpteTimeFormat (24, 4); + EXPECT_EQ (file.getTimeFormat(), -6140); + + file.setSmpteTimeFormat (25, 40); + EXPECT_EQ (file.getTimeFormat(), -6360); - file.setSmpteTimeFormat (30, 10); - //EXPECT_EQ(file.getTimeFormat(), (short)(((-30) << 8) | 10)); + file.setSmpteTimeFormat (26, 40); + EXPECT_EQ (file.getTimeFormat(), -6360); } TEST_F (MidiFileTest, ReadFromValidStream) diff --git a/tests/yup_audio_devices/yup_AudioDeviceManager.cpp b/tests/yup_audio_devices/yup_AudioDeviceManager.cpp index 310ad944a..ee4e5cdcb 100644 --- a/tests/yup_audio_devices/yup_AudioDeviceManager.cpp +++ b/tests/yup_audio_devices/yup_AudioDeviceManager.cpp @@ -617,3 +617,28 @@ TEST_F (AudioDeviceManagerTests, AudioDeviceManagerUpdatesSettingsBeforeNotifyin ptr->restartDevices (newSr, newBs); EXPECT_EQ (numCalls, 1); } + +TEST_F (AudioDeviceManagerTests, DISABLED_DataRace) +{ + // This is disable but can be enabled with TSAN to recreate + // potential threading issues with multiple combined devices + for (int i = 0; i < 42; ++i) + { + AudioDeviceManager adm; + adm.initialise (1, 2, nullptr, true); + + AudioDeviceManager::AudioDeviceSetup setup; + setup.bufferSize = 512; + setup.sampleRate = 48000; + setup.inputChannels = 0b1; + setup.outputChannels = 0b11; + setup.inputDeviceName = "BlackHole 2ch"; + setup.outputDeviceName = "MacBook Pro Speakers"; + + adm.setAudioDeviceSetup (setup, true); + + setup.sampleRate = 44100; + + adm.setAudioDeviceSetup (setup, true); + } +} diff --git a/tests/yup_core/yup_StringPairArray.cpp b/tests/yup_core/yup_StringPairArray.cpp index 314b40485..6db625c47 100644 --- a/tests/yup_core/yup_StringPairArray.cpp +++ b/tests/yup_core/yup_StringPairArray.cpp @@ -75,6 +75,46 @@ TEST_F (StringPairArrayTests, ParameterizedConstructorCaseSensitivity) EXPECT_TRUE (caseInsensitive.getIgnoresCase()); } +TEST_F (StringPairArrayTests, InitializerListConstructor) +{ + StringPairArray spa { + { "key1", "value1" }, + { "key2", "value2" }, + { "key3", "value3" } + }; + + EXPECT_EQ (spa.size(), 3); + EXPECT_EQ (spa["key1"], "value1"); + EXPECT_EQ (spa["key2"], "value2"); + EXPECT_EQ (spa["key3"], "value3"); + EXPECT_TRUE (spa.getIgnoresCase()); // Default should ignore case +} + +TEST_F (StringPairArrayTests, InitializerListConstructorWithCaseSensitivity) +{ + StringPairArray caseSensitive (false, { { "Key", "value1" }, { "key", "value2" } }); + + EXPECT_EQ (caseSensitive.size(), 2); + EXPECT_EQ (caseSensitive["Key"], "value1"); + EXPECT_EQ (caseSensitive["key"], "value2"); + EXPECT_FALSE (caseSensitive.getIgnoresCase()); + + StringPairArray caseInsensitive (true, { { "Key", "value1" }, { "key", "value2" } }); + + EXPECT_EQ (caseInsensitive.size(), 2); + EXPECT_EQ (caseInsensitive["KEY"], "value1"); + EXPECT_EQ (caseInsensitive["Key"], "value1"); + EXPECT_TRUE (caseInsensitive.getIgnoresCase()); +} + +TEST_F (StringPairArrayTests, EmptyInitializerListConstructor) +{ + StringPairArray spa {}; + + EXPECT_EQ (spa.size(), 0); + EXPECT_TRUE (spa.getIgnoresCase()); // Default should ignore case +} + TEST_F (StringPairArrayTests, CopyConstructor) { StringPairArray original; @@ -293,3 +333,80 @@ TEST_F (StringPairArrayTests, AddMapHasEquivalentBehaviourToAddArray) EXPECT_EQ (withAddMap, withAddArray); } + +TEST_F (StringPairArrayTests, RangeBasedForLoopIteration) +{ + StringPairArray spa; + addDefaultPairs (spa); + + StringArray keysFound, valuesFound; + for (const auto& pair : spa) + { + keysFound.add (pair.key); + valuesFound.add (pair.value); + } + + EXPECT_EQ (keysFound.size(), 3); + EXPECT_EQ (valuesFound.size(), 3); + EXPECT_TRUE (keysFound.contains ("key1")); + EXPECT_TRUE (keysFound.contains ("key2")); + EXPECT_TRUE (keysFound.contains ("key3")); + EXPECT_TRUE (valuesFound.contains ("value1")); + EXPECT_TRUE (valuesFound.contains ("value2")); + EXPECT_TRUE (valuesFound.contains ("value3")); +} + +TEST_F (StringPairArrayTests, RangeBasedForLoopEmpty) +{ + StringPairArray spa; + int count = 0; + + // This should never execute + for (const auto& pair : spa) + ++count; + + EXPECT_EQ (count, 0); +} + +TEST_F (StringPairArrayTests, RangeBasedForLoopKeyValueAccess) +{ + StringPairArray spa; + spa.set ("testKey", "testValue"); + spa.set ("anotherKey", "anotherValue"); + + for (const auto& pair : spa) + { + if (pair.key == StringRef ("testKey")) + EXPECT_EQ (pair.value, StringRef ("testValue")); + else if (pair.key == StringRef ("anotherKey")) + EXPECT_EQ (pair.value, StringRef ("anotherValue")); + } +} + +TEST_F (StringPairArrayTests, IteratorComparison) +{ + StringPairArray spa; + addDefaultPairs (spa); + + auto it1 = spa.begin(); + auto it2 = spa.begin(); + auto end = spa.end(); + + EXPECT_FALSE (it1 != it2); // Both should point to same position + EXPECT_TRUE (it1 != end); // Begin should not equal end +} + +TEST_F (StringPairArrayTests, IteratorIncrement) +{ + StringPairArray spa; + spa.set ("first", "1"); + spa.set ("second", "2"); + + auto it = spa.begin(); + auto firstPair = *it; + ++it; + auto secondPair = *it; + + EXPECT_NE (firstPair.key, secondPair.key); + EXPECT_NE (firstPair.value, secondPair.value); +} diff --git a/tests/yup_core/yup_WebInputStream.cpp b/tests/yup_core/yup_WebInputStream.cpp new file mode 100644 index 000000000..de1536625 --- /dev/null +++ b/tests/yup_core/yup_WebInputStream.cpp @@ -0,0 +1,421 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - 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; + +#if YUP_MAC || YUP_LINUX || YUP_WINDOWS + +namespace +{ + +class SimpleHttpServer : public Thread +{ +public: + SimpleHttpServer() + : Thread ("HttpTestServer") + { + serverSocket = std::make_unique(); + } + + ~SimpleHttpServer() override + { + stop(); + } + + bool start (int port = 9876) + { + if (! serverSocket->createListener (port, "127.0.0.1")) + return false; + + serverPort = serverSocket->getPort(); + startThread(); + return true; + } + + void stop() + { + signalThreadShouldExit(); + + if (serverSocket != nullptr) + serverSocket->close(); + + stopThread (1000); + } + + int getPort() const { return serverPort; } + + String getBaseUrl() const + { + return "http://127.0.0.1:" + String (serverPort); + } + + void run() override + { + while (! threadShouldExit()) + { + std::unique_ptr clientSocket (serverSocket->waitForNextConnection()); + + if (clientSocket != nullptr && ! threadShouldExit()) + handleRequest (clientSocket.get()); + } + } + +private: + void handleRequest (StreamingSocket* socket) + { + auto connectionStatus = socket->waitUntilReady (true, 1000); + if (connectionStatus == -1) + return; + + auto readHttpPayload = [] (StreamingSocket& connection) -> MemoryBlock + { + MemoryBlock payload; + uint8 data[1024] = { 0 }; + + while (true) + { + auto numBytesRead = connection.read (data, numElementsInArray (data), false); + if (numBytesRead <= 0) + break; + + payload.append (data, static_cast (numBytesRead)); + } + + return payload; + }; + + String request = readHttpPayload (*socket).toString(); + String response; + + if (request.startsWith ("GET / ")) + { + response = createHttpResponse (200, "text/html", "Test Page" + "

Hello World

This is a test page.

"); + } + else if (request.startsWith ("GET /api/test ")) + { + response = createHttpResponse (200, "application/json", "{\"message\":\"Hello from API\",\"status\":\"success\"}"); + } + else if (request.startsWith ("POST /api/echo ")) + { + auto bodyStart = request.indexOf ("\r\n\r\n"); + String body = bodyStart >= 0 ? request.substring (bodyStart + 4) : "{}"; + + response = createHttpResponse (200, "application/json", "{\"echo\":" + body.quoted() + ",\"method\":\"POST\"}"); + } + else if (request.startsWith ("GET /headers ")) + { + String customHeaders = "X-Test-Header: TestValue\r\n" + "X-Custom: CustomValue\r\n"; + response = createHttpResponse (200, "text/plain", "Headers test", customHeaders); + } + else if (request.startsWith ("GET /large ")) + { + String largeContent; + for (int i = 0; i < 1000; ++i) + largeContent += "This is line " + String (i) + " of the large response.\n"; + + response = createHttpResponse (200, "text/plain", largeContent); + } + else if (request.startsWith ("GET /slow ")) + { + Thread::sleep (100); + response = createHttpResponse (200, "text/plain", "This response was delayed"); + } + else + { + response = createHttpResponse (404, "text/plain", "Not Found"); + } + + socket->write (response.toRawUTF8(), (int) response.getNumBytesAsUTF8()); + } + + String createHttpResponse (int statusCode, const String& contentType, const String& content, const String& extraHeaders = {}) + { + String statusText = (statusCode == 200) ? "OK" : (statusCode == 404) ? "Not Found" + : "Error"; + + String response = "HTTP/1.1 " + String (statusCode) + " " + statusText + "\r\n"; + response += "Content-Type: " + contentType + "\r\n"; + response += "Content-Length: " + String (content.getNumBytesAsUTF8()) + "\r\n"; + response += "Connection: close\r\n"; + + if (extraHeaders.isNotEmpty()) + response += extraHeaders; + + response += "\r\n"; + response += content; + + return response; + } + + std::unique_ptr serverSocket; + int serverPort = 0; + std::atomic_bool isReady = false; +}; +} // namespace + +class WebInputStreamTests : public ::testing::Test +{ +protected: + static void SetUpTestSuite() + { + server = std::make_unique(); + ASSERT_TRUE (server != nullptr && server->start()) << "Failed to start test HTTP server"; + + while (! server->isThreadRunning()) + Thread::sleep (10); + } + + static void TearDownTestSuite() + { + server.reset(); + } + + int defaultTimeoutMs() const + { + return yup_isRunningUnderDebugger() ? -1 : 5000; + } + + static std::unique_ptr server; +}; + +std::unique_ptr WebInputStreamTests::server; + +TEST_F (WebInputStreamTests, CanReadHtmlContent) +{ + URL url (server->getBaseUrl()); + WebInputStream stream (url, false); + stream.withConnectionTimeout (defaultTimeoutMs()); + EXPECT_TRUE (stream.isError()); + + auto content = stream.readEntireStreamAsString(); + ASSERT_FALSE (stream.isError()); + EXPECT_EQ (200, stream.getStatusCode()); + EXPECT_TRUE (content.containsIgnoreCase ("getBaseUrl()); + WebInputStream stream (url, false); + stream.withConnectionTimeout (defaultTimeoutMs()); + + auto headers = stream.getResponseHeaders(); + ASSERT_FALSE (stream.isError()); + EXPECT_GT (headers.size(), 0); + + bool hasContentType = false; + bool hasContentLength = false; + + for (const auto& headerName : headers.getAllKeys()) + { + if (headerName.equalsIgnoreCase ("content-type")) + { + hasContentType = true; + EXPECT_TRUE (headers.getValue (headerName, "").containsIgnoreCase ("text/html")); + } + + if (headerName.equalsIgnoreCase ("content-length")) + hasContentLength = true; + } + + EXPECT_TRUE (hasContentType); + EXPECT_TRUE (hasContentLength); +} + +TEST_F (WebInputStreamTests, CustomHeadersInResponse) +{ + URL url (server->getBaseUrl() + "/headers"); + WebInputStream stream (url, false); + stream.withConnectionTimeout (defaultTimeoutMs()); + + auto headers = stream.getResponseHeaders(); + ASSERT_FALSE (stream.isError()); + + bool hasTestHeader = false; + bool hasCustomHeader = false; + + for (const auto& headerName : headers.getAllKeys()) + { + if (headerName.equalsIgnoreCase ("X-Test-Header")) + { + hasTestHeader = true; + EXPECT_EQ ("TestValue", headers.getValue (headerName, "")); + } + + if (headerName.equalsIgnoreCase ("X-Custom")) + { + hasCustomHeader = true; + EXPECT_EQ ("CustomValue", headers.getValue (headerName, "")); + } + } + + EXPECT_TRUE (hasTestHeader); + EXPECT_TRUE (hasCustomHeader); +} + +TEST_F (WebInputStreamTests, JsonApiEndpoint) +{ + URL url (server->getBaseUrl() + "/api/test"); + WebInputStream stream (url, false); + stream.withConnectionTimeout (defaultTimeoutMs()); + + String jsonResponse = stream.readEntireStreamAsString(); + ASSERT_FALSE (stream.isError()); + EXPECT_EQ (200, stream.getStatusCode()); + EXPECT_TRUE (jsonResponse.contains ("\"message\"")); + EXPECT_TRUE (jsonResponse.contains ("Hello from API")); + EXPECT_TRUE (jsonResponse.contains ("\"status\":\"success\"")); +} + +TEST_F (WebInputStreamTests, DISABLED_PostRequestWithData) +{ + URL url (server->getBaseUrl() + "/api/echo"); + url = url.withPOSTData ("{\"test\":\"Hello POST\"}"); + + WebInputStream stream (url, false); + stream.withConnectionTimeout (defaultTimeoutMs()); + stream.withExtraHeaders ("Content-Type: application/json\r\n"); + + String response = stream.readEntireStreamAsString(); + ASSERT_FALSE (stream.isError()); + EXPECT_EQ (200, stream.getStatusCode()); + EXPECT_TRUE (response.contains ("\"echo\"")); + EXPECT_TRUE (response.contains ("Hello POST")); + EXPECT_TRUE (response.contains ("\"method\":\"POST\"")); +} + +TEST_F (WebInputStreamTests, HandlesNotFoundUrl) +{ + URL url (server->getBaseUrl() + "/nonexistent"); + WebInputStream stream (url, false); + stream.withConnectionTimeout (defaultTimeoutMs()); + + String response = stream.readEntireStreamAsString(); + EXPECT_FALSE (stream.isError()); + EXPECT_EQ (404, stream.getStatusCode()); + EXPECT_TRUE (response.contains ("Not Found")); +} + +TEST_F (WebInputStreamTests, HandlesInvalidUrl) +{ + URL url ("http://127.0.0.1:99999"); + WebInputStream stream (url, false); + stream.withConnectionTimeout (1000); + + EXPECT_TRUE (stream.isError()); + stream.readEntireStreamAsString(); + EXPECT_TRUE (stream.isError()); +} + +TEST_F (WebInputStreamTests, CanGetContentLength) +{ + URL url (server->getBaseUrl()); + WebInputStream stream (url, false); + stream.withConnectionTimeout (defaultTimeoutMs()); + + auto contentLength = stream.getTotalLength(); + ASSERT_FALSE (stream.isError()); + EXPECT_GT (contentLength, 0); + + String content = stream.readEntireStreamAsString(); + EXPECT_EQ (contentLength, content.getNumBytesAsUTF8()); +} + +TEST_F (WebInputStreamTests, StreamPositionWorks) +{ + URL url (server->getBaseUrl()); + WebInputStream stream (url, false); + stream.withConnectionTimeout (defaultTimeoutMs()); + + EXPECT_EQ (0, stream.getPosition()); + + char buffer[100]; + auto bytesRead = stream.read (buffer, sizeof (buffer)); + ASSERT_FALSE (stream.isError()); + EXPECT_GT (bytesRead, 0); + EXPECT_EQ (bytesRead, stream.getPosition()); +} + +TEST_F (WebInputStreamTests, MultipleReadsWork) +{ + URL url (server->getBaseUrl()); + WebInputStream stream (url, false); + stream.withConnectionTimeout (defaultTimeoutMs()); + + char buffer1[50]; + char buffer2[50]; + + auto bytesRead1 = stream.read (buffer1, sizeof (buffer1)); + ASSERT_FALSE (stream.isError()); + auto bytesRead2 = stream.read (buffer2, sizeof (buffer2)); + ASSERT_FALSE (stream.isError()); + + EXPECT_GT (bytesRead1, 0); + EXPECT_GE (bytesRead2, 0); + + EXPECT_EQ (bytesRead1 + bytesRead2, stream.getPosition()); + + if (bytesRead1 > 0 && bytesRead2 > 0) + { + String content1 (buffer1, bytesRead1); + String content2 (buffer2, bytesRead2); + EXPECT_NE (content1, content2); + } +} + +TEST_F (WebInputStreamTests, LargeContentHandling) +{ + URL url (server->getBaseUrl() + "/large"); + WebInputStream stream (url, false); + stream.withConnectionTimeout (defaultTimeoutMs()); + + String content = stream.readEntireStreamAsString(); + ASSERT_FALSE (stream.isError()); + EXPECT_GT (content.length(), 10000); + EXPECT_TRUE (content.contains ("This is line 0")); + EXPECT_TRUE (content.contains ("This is line 999")); +} + +TEST_F (WebInputStreamTests, SlowResponseHandling) +{ + URL url (server->getBaseUrl() + "/slow"); + + auto startTime = Time::getMillisecondCounter(); + WebInputStream stream (url, false); + stream.withConnectionTimeout (defaultTimeoutMs()); + + String content = stream.readEntireStreamAsString(); + auto endTime = Time::getMillisecondCounter(); + + ASSERT_FALSE (stream.isError()); + EXPECT_TRUE (content.contains ("delayed")); + EXPECT_GE (endTime - startTime, 100); // Should take at least 100ms due to server delay +} + +#endif // YUP_MAC diff --git a/tests/yup_events/yup_MessageManager.cpp b/tests/yup_events/yup_MessageManager.cpp new file mode 100644 index 000000000..912fce9c9 --- /dev/null +++ b/tests/yup_events/yup_MessageManager.cpp @@ -0,0 +1,179 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - 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; + +class MessageManagerTests : public ::testing::Test +{ +protected: + void SetUp() override + { + mm = MessageManager::getInstance(); + jassert (mm != nullptr); + } + + void TearDown() override + { + } + + void runDispatchLoopUntil (int millisecondsToRunFor = 10) + { +#if YUP_MODAL_LOOPS_PERMITTED + mm->runDispatchLoopUntil (millisecondsToRunFor); +#endif + } + + MessageManager* mm = nullptr; +}; + +#if 0 + +TEST_F (MessageManagerTests, Existence) +{ + EXPECT_NE (MessageManager::getInstanceWithoutCreating(), nullptr); + EXPECT_NE (MessageManager::getInstance(), nullptr); + + EXPECT_FALSE (mm->hasStopMessageBeenSent()); + + EXPECT_TRUE (mm->isThisTheMessageThread()); + EXPECT_TRUE (MessageManager::existsAndIsCurrentThread()); + EXPECT_TRUE (mm->currentThreadHasLockedMessageManager()); + EXPECT_TRUE (MessageManager::existsAndIsLockedByCurrentThread()); + +#if ! YUP_EMSCRIPTEN + auto messageThreadId = mm->getCurrentMessageThread(); + + auto t = std::thread ([messageThreadId] + { + auto mmt = MessageManager::getInstance(); + EXPECT_EQ (messageThreadId, mmt->getCurrentMessageThread()); + + EXPECT_FALSE (mmt->isThisTheMessageThread()); + EXPECT_FALSE (MessageManager::existsAndIsCurrentThread()); + EXPECT_FALSE (mmt->currentThreadHasLockedMessageManager()); + EXPECT_FALSE (MessageManager::existsAndIsLockedByCurrentThread()); + }); + + if (t.joinable()) + t.join(); +#endif +} + +#if YUP_MODAL_LOOPS_PERMITTED +TEST_F (MessageManagerTests, CallAsync) +{ + bool called = false; + mm->callAsync ([&] + { + called = true; + }); + + runDispatchLoopUntil(); + + EXPECT_TRUE (called); +} + +#if ! YUP_EMSCRIPTEN +TEST_F (MessageManagerTests, DISABLED_CallFunctionOnMessageThread) +{ + int called = 0; + + auto t = std::thread ([&] + { + // clang-format off + auto result = mm->callFunctionOnMessageThread (+[] (void* data) -> void* + { + *reinterpret_cast (data) = 42; + return nullptr; + }, (void*) &called); + // clang-format on + + EXPECT_EQ (result, nullptr); + }); + + runDispatchLoopUntil(); + + if (t.joinable()) + t.join(); + + EXPECT_EQ (called, 42); +} +#endif + +TEST_F (MessageManagerTests, BroadcastMessage) +{ + struct Listener : ActionListener + { + String valueCalled; + + void actionListenerCallback (const String& message) override + { + valueCalled = message; + } + } listener; + + mm->registerBroadcastListener (&listener); + mm->deliverBroadcastMessage ("xyz"); + EXPECT_TRUE (listener.valueCalled.isEmpty()); + + runDispatchLoopUntil(); + EXPECT_EQ (listener.valueCalled, "xyz"); + + mm->deregisterBroadcastListener (&listener); + mm->deliverBroadcastMessage ("123"); + + runDispatchLoopUntil(); + EXPECT_EQ (listener.valueCalled, "xyz"); +} + +TEST_F (MessageManagerTests, PostMessage) +{ + String valueCalled; + + struct Message : MessageManager::MessageBase + { + String& data; + + Message (String& data) + : data (data) + { + } + + void messageCallback() override + { + data = "xyz"; + } + }; + + (new Message (valueCalled))->post(); + + runDispatchLoopUntil(); + + EXPECT_EQ (valueCalled, "xyz"); +} + +#endif + +#endif diff --git a/tests/yup_events/yup_Timer.cpp b/tests/yup_events/yup_Timer.cpp new file mode 100644 index 000000000..27059df78 --- /dev/null +++ b/tests/yup_events/yup_Timer.cpp @@ -0,0 +1,70 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - 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; + +class TimerTests : public ::testing::Test +{ +protected: + void SetUp() override + { + mm = MessageManager::getInstance(); + jassert (mm != nullptr); + } + + void TearDown() override + { + } + + void runDispatchLoopUntil (int millisecondsToRunFor = 10) + { +#if YUP_MODAL_LOOPS_PERMITTED + mm->runDispatchLoopUntil (millisecondsToRunFor); +#endif + } + + MessageManager* mm = nullptr; +}; + +TEST_F (TimerTests, DISABLED_SimpleTimerSingleCall) +{ + struct TestTimer : Timer + { + int calledCount = 0; + + void timerCallback() override + { + calledCount = 1; + + stopTimer(); + } + } testTimer; + + testTimer.startTimer (1); + + EXPECT_EQ (testTimer.calledCount, 0); + runDispatchLoopUntil (200); + EXPECT_EQ (testTimer.calledCount, 1); +}