// 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 "yarpl/Observable.h" namespace yarpl { namespace observable { /** * A utility class for unit testing or experimenting with Observable. * * Example usage: * * auto observable = ... * auto ts = std::make_shared>(); * observable->subscribe(ts->unique_observer()); * ts->awaitTerminalEvent(); * ts->assert... * * If you have an Observer impl with specific logic you want used, * you can pass it into the TestObserver and the on* events will be * delegated to your implementation. * * For example: * * auto ts = * std::make_shared>(std::make_unique()); * observable->subscribe(ts->unique_observer()); * * Now when 'observable' is subscribed to, the TestObserver behavior * will be used, but 'MyObserver' on* methods will also be invoked. * * @tparam T */ template class TestObserver : public yarpl::observable::Observer, public std::enable_shared_from_this> { using Subscription = yarpl::observable::Subscription; using Observer = yarpl::observable::Observer; public: TestObserver(); explicit TestObserver(std::unique_ptr delegate); void onSubscribe(std::shared_ptr s) override; void onNext(T t) override; void onComplete() override; void onError(folly::exception_wrapper ex) override; /** * Get a unique Observer that can be passed into the Observable.subscribe * method which requires a unique_ptr. * * This decouples the lifetime of TestObserver from what is passed into * the Observable.subscribe method so that the testing code can retain * a reference to TestObserver to use it beyond the lifecycle * of Observable.subscribe. * * @return */ std::unique_ptr> unique_observer(); /** * Block the current thread until either onComplete or onError is called. */ void awaitTerminalEvent( std::chrono::milliseconds ms = std::chrono::seconds{1}); /** * If the onNext values received does not match the given count, * throw a runtime_error */ void assertValueCount(size_t count); /** * The number of onNext values received. * @return */ int64_t getValueCount(); /** * Get a reference to a stored value at a given index position. * * The values are stored in the order received from onNext. */ T& getValueAt(size_t index); /** * 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); /** * Submit Subscription->cancel(); */ void cancel(); bool isComplete() const { return complete_; } bool isError() const { return error_; } private: std::unique_ptr delegate_; std::vector values_; folly::exception_wrapper e_; bool terminated_{false}; bool complete_{false}; bool error_{false}; std::mutex m_; std::condition_variable terminalEventCV_; std::shared_ptr subscription_; }; template TestObserver::TestObserver() : delegate_(nullptr){}; template TestObserver::TestObserver(std::unique_ptr delegate) : delegate_(std::move(delegate)){}; template void TestObserver::onSubscribe(std::shared_ptr s) { subscription_ = s; if (delegate_) { delegate_->onSubscribe(s); } } template void TestObserver::onNext(T t) { if (delegate_) { // std::cout << "TestObserver onNext& => copy then delegate" << // std::endl; values_.push_back(t); delegate_->onNext(t); } else { // std::cout << "TestObserver onNext& => copy" << std::endl; values_.push_back(t); } } template void TestObserver::onComplete() { if (delegate_) { delegate_->onComplete(); } terminated_ = true; complete_ = true; terminalEventCV_.notify_all(); } template void TestObserver::onError(folly::exception_wrapper ex) { if (delegate_) { delegate_->onError(ex); } e_ = std::move(ex); terminated_ = true; error_ = true; terminalEventCV_.notify_all(); } template void TestObserver::awaitTerminalEvent(std::chrono::milliseconds ms) { // now block this thread std::unique_lock lk(m_); // if shutdown gets implemented this would then be released by it if (!terminalEventCV_.wait_for(lk, ms, [this] { return terminated_; })) { throw std::runtime_error("timeout in awaitTerminalEvent"); } } template void TestObserver::cancel() { subscription_->cancel(); } template std::unique_ptr> TestObserver::unique_observer() { class UObserver : public yarpl::observable::Observer { public: UObserver(std::shared_ptr> ts) : ts_(std::move(ts)) {} void onSubscribe(yarpl::observable::Subscription* s) override { ts_->onSubscribe(s); } void onNext(const T& t) override { ts_->onNext(t); } void onError(folly::exception_wrapper e) override { ts_->onError(std::move(e)); } void onComplete() override { ts_->onComplete(); } private: std::shared_ptr> ts_; }; return std::make_unique(this->shared_from_this()); } template void TestObserver::assertValueCount(size_t count) { if (values_.size() != count) { std::stringstream ss; ss << "Value count " << values_.size() << " does not match " << count; throw std::runtime_error(ss.str()); } } template int64_t TestObserver::getValueCount() { return values_.size(); } template T& TestObserver::getValueAt(size_t index) { return values_[index]; } template void TestObserver::assertOnErrorMessage(std::string msg) { if (!e_ || e_.get_exception()->what() != msg) { std::stringstream ss; ss << "Error is: " << e_ << " but expected: " << msg; throw std::runtime_error(ss.str()); } } } // namespace observable } // namespace yarpl