// Copyright (c) Facebook, Inc. and its affiliates. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #pragma once #include #include #include #include #include #include "yarpl/single/Single.h" #include "yarpl/single/SingleObserver.h" #include "yarpl/single/SingleSubscriptions.h" namespace yarpl { namespace single { /** * A utility class for unit testing or experimenting with Single. * * Example usage: * * auto single = ... * auto to = SingleTestObserver::create(); * single->subscribe(to); * ts->awaitTerminalEvent(); * ts->assert... * * If you have a SingleObserver impl with specific logic you want used, * you can pass it into the SingleTestObserver and the on* events will be * delegated to your implementation. * * For example: * * auto to = SingleTestObserver::create(std::make_shared()); * single->subscribe(to); * * Now when 'single' is subscribed to, the SingleTestObserver behavior * will be used, but 'MyObserver' on* methods will also be invoked. * * @tparam T */ template class SingleTestObserver : public yarpl::single::SingleObserver { public: /** * Create a SingleTestObserver that will subscribe and store the value it * receives. * * @return */ static std::shared_ptr> create() { return std::make_shared>(); } /** * Create a SingleTestObserver that will delegate all on* method calls * to the provided SingleObserver. * * This will store the value it receives to allow assertions. * @return */ static std::shared_ptr> create( std::shared_ptr> delegate) { return std::make_shared>(std::move(delegate)); } SingleTestObserver() : delegate_(nullptr) {} // Note on thread safety => // Generally an observer assumes single threaded emission // but this class is intended for use in unit tests // when it will generally receive events on one thread // and then access them for verification/assertion // on the unit test main thread. explicit SingleTestObserver(std::shared_ptr> delegate) : delegate_(std::move(delegate)) {} void onSubscribe(std::shared_ptr subscription) override { if (delegate_) { delegateSubscription_->setDelegate(subscription); // copy delegate_->onSubscribe(std::move(subscription)); } else { delegateSubscription_->setDelegate(std::move(subscription)); } } void onSuccess(T t) override { { // take lock with local scope so we can emit without holding the lock std::lock_guard g(m_); if (delegate_) { value_ = t; // take copy // do not emit here, but later without lock } else { value_ = std::move(t); } delegateSubscription_ = nullptr; terminated_ = true; } // after lock is released we emit if (delegate_) { // Do NOT hold the mutex while emitting delegate_->onSuccess(std::move(t)); } // then we notify that we're completed terminalEventCV_.notify_all(); } void onError(folly::exception_wrapper ex) override { if (delegate_) { // Do NOT hold the mutex while emitting delegate_->onError(ex); } { std::lock_guard g(m_); e_ = std::move(ex); terminated_ = true; } terminalEventCV_.notify_all(); } /** * Block the current thread until either onSuccess or onError is called. */ void awaitTerminalEvent() { // now block this thread std::unique_lock lk(m_); // if shutdown gets implemented this would then be released by it terminalEventCV_.wait(lk, [this] { return terminated_; }); } /** * Assert no onSuccess or onError events were received */ void assertNoTerminalEvent() { std::lock_guard g(m_); if (terminated_) { throw std::runtime_error("An unexpected terminal event was received."); } } /** * If an onSuccess call was not received throw a runtime_error */ void assertSuccess() { std::lock_guard g(m_); if (!terminated_) { throw std::runtime_error("Did not receive terminal event."); } if (e_) { std::stringstream ss; ss << "Received onError instead of onSuccess"; ss << " (error was " << e_ << ")"; throw std::runtime_error(ss.str()); } } void assertOnSuccessValue(T t) { assertSuccess(); std::lock_guard g(m_); if (value_ != t) { std::stringstream ss; ss << "value == " << value_ << ", but expected " << t; throw std::runtime_error(ss.str()); } } /** * Get a reference to the received value if onSuccess was called. */ T& getOnSuccessValue() { std::lock_guard g(m_); return value_; } /** * Get the error received from onError if it was called. */ folly::exception_wrapper getError() { std::lock_guard g(m_); if (!terminated_) { throw std::logic_error{"Must call getError() on a terminated observer"}; } return e_; } /** * If the onError exception_wrapper points to an error containing * the given msg, complete successfully, otherwise throw a runtime_error */ void assertOnErrorMessage(std::string msg) { std::lock_guard g(m_); if (!e_ || e_.get_exception()->what() != msg) { std::stringstream ss; ss << "Error is: " << e_ << " but expected: " << msg; throw std::runtime_error(ss.str()); } } folly::exception_wrapper getException() const { return e_; } /** * Submit SingleSubscription->cancel(); */ void cancel() { // do NOT hold a lock while invoking the normal signals delegateSubscription_->cancel(); } private: std::mutex m_; std::condition_variable terminalEventCV_; std::shared_ptr> delegate_; // The following variables must be protected by mutex m_ T value_; folly::exception_wrapper e_; bool terminated_{false}; // allows thread-safe cancellation against a delegate // regardless of when it is received std::shared_ptr delegateSubscription_{ std::make_shared()}; }; } // namespace single } // namespace yarpl