From 15e900821671e2e592b94808ac071bb63b26f203 Mon Sep 17 00:00:00 2001 From: Pablo Andres Fuente Date: Tue, 6 Aug 2024 22:41:53 -0300 Subject: [PATCH] Add a unit test for HTTPRequest Adding cpp_mock header only mocking library to making unit test that requires mocks easier to implement. Adding some HTTPRequest unit tests that makes use of cpp_mock to assess how useful could be to testing Godot. --- tests/core/io/test_http_client_manual_mock.h | 173 +++++ tests/core/io/test_http_client_mock.h | 85 +++ tests/scene/test_http_request.h | 493 +++++++++++++ tests/scene/test_http_request_manual_mock.h | 455 ++++++++++++ tests/test_http_client_mock.cpp | 38 ++ tests/test_macros.h | 15 + tests/test_main.cpp | 2 + thirdparty/README.md | 13 + thirdparty/cpp_mock/LICENSE | 21 + thirdparty/cpp_mock/cpp_mock.h | 684 +++++++++++++++++++ 10 files changed, 1979 insertions(+) create mode 100644 tests/core/io/test_http_client_manual_mock.h create mode 100644 tests/core/io/test_http_client_mock.h create mode 100644 tests/scene/test_http_request.h create mode 100644 tests/scene/test_http_request_manual_mock.h create mode 100644 tests/test_http_client_mock.cpp create mode 100644 thirdparty/cpp_mock/LICENSE create mode 100644 thirdparty/cpp_mock/cpp_mock.h diff --git a/tests/core/io/test_http_client_manual_mock.h b/tests/core/io/test_http_client_manual_mock.h new file mode 100644 index 00000000000..5b2d860a690 --- /dev/null +++ b/tests/core/io/test_http_client_manual_mock.h @@ -0,0 +1,173 @@ +/**************************************************************************/ +/* test_http_client_manual_mock.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef TEST_HTTP_CLIENT_MANUAL_MOCK_H +#define TEST_HTTP_CLIENT_MANUAL_MOCK_H + +#include "core/io/http_client.h" + +#include "thirdparty/doctest/doctest.h" + +class HTTPClientManualMock : public HTTPClient { +public: + static HTTPClientManualMock *current_instance; + + static HTTPClient *_create_func(bool p_notify_postinitialize = true) { + current_instance = static_cast(ClassDB::creator(p_notify_postinitialize)); + return current_instance; + } + + static HTTPClient *(*_old_create)(bool); + static void make_current() { + _old_create = HTTPClient::_create; + HTTPClient::_create = _create_func; + } + static void reset_current() { + if (_old_create) { + HTTPClient::_create = _old_create; + } + } + + Vector get_status_return; + + int set_read_chunk_size_p_size_parameter = 0; + int set_read_chunk_size_call_count = 0; + + String connect_to_host_p_host_parameter; + int connect_to_host_p_port_parameter = 0; + Ref connect_to_host_p_tls_options_parameter = nullptr; + Error connect_to_host_return = Error::OK; + int connect_to_host_call_count = 0; + + int close_call_count = 0; + + int get_response_code_return = 0; + + bool has_response_return = false; + + Method request_p_method_parameter = Method::METHOD_GET; + String request_p_url_parameter; + Vector request_p_headers_parameter; + uint8_t *request_p_body_parameter = nullptr; + int request_p_body_size_parameter = 0; + int request_call_count = 0; + Error request_return = Error::OK; + + List get_response_headers_r_response_parameter; + Error get_response_headers_return = Error::OK; + + int64_t get_response_body_length_return; + + PackedByteArray read_response_body_chunk_return; +#ifdef THREADS_ENABLED + Semaphore *read_response_body_chunk_semaphore = nullptr; +#endif // THREADS_ENABLED + + Error poll_return; + + Error request(Method p_method, const String &p_url, const Vector &p_headers, const uint8_t *p_body, int p_body_size) override { + request_p_method_parameter = p_method; + request_p_url_parameter = p_url; + request_p_headers_parameter = p_headers; + request_p_body_parameter = const_cast(p_body); + request_p_body_size_parameter = p_body_size; + request_call_count++; + return request_return; + } + Error connect_to_host(const String &p_host, int p_port = -1, Ref p_tls_options = Ref()) override { + connect_to_host_p_host_parameter = p_host; + connect_to_host_p_port_parameter = p_port; + connect_to_host_p_tls_options_parameter = p_tls_options; + connect_to_host_call_count++; + return connect_to_host_return; + } + + void set_connection(const Ref &p_connection) override {} + Ref get_connection() const override { return Ref(); } + + void close() override { + close_call_count++; + } + + Status get_status() const override { + if (get_status_return.size() == 0) { + FAIL("Call to HTTPClient::get_status not set. Please set a return value."); + return Status(); + } + + Status status = get_status_return[get_status_return_current]; + if (get_status_return_current + 1 < get_status_return.size()) { + get_status_return_current++; + } + return status; + } + + bool has_response() const override { return has_response_return; } + bool is_response_chunked() const override { return true; } + int get_response_code() const override { return get_response_code_return; } + Error get_response_headers(List *r_response) override { + *r_response = get_response_headers_r_response_parameter; + (void)r_response; + return get_response_headers_return; + } + int64_t get_response_body_length() const override { + return get_response_body_length_return; + } + + PackedByteArray read_response_body_chunk() override { +#ifdef THREADS_ENABLED + if (read_response_body_chunk_semaphore != nullptr) { + read_response_body_chunk_semaphore->post(); + } +#endif // THREADS_ENABLED + return read_response_body_chunk_return; + } + + void set_blocking_mode(bool p_enable) override {} + bool is_blocking_mode_enabled() const override { return true; } + + void set_read_chunk_size(int p_size) override { + set_read_chunk_size_p_size_parameter = p_size; + set_read_chunk_size_call_count++; + } + int get_read_chunk_size() const override { return 0; } + + Error poll() override { + return poll_return; + } + + HTTPClientManualMock() {} + +private: + // This MUST be mutable because I need to update its value from a const method (the mock method) + mutable Vector::Size get_status_return_current = 0; +}; + +#endif // TEST_HTTP_CLIENT_MANUAL_MOCK_H diff --git a/tests/core/io/test_http_client_mock.h b/tests/core/io/test_http_client_mock.h new file mode 100644 index 00000000000..ac0dac5f655 --- /dev/null +++ b/tests/core/io/test_http_client_mock.h @@ -0,0 +1,85 @@ +/**************************************************************************/ +/* test_http_client_mock.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef TEST_HTTP_CLIENT_MOCK_H +#define TEST_HTTP_CLIENT_MOCK_H + +#include "core/io/http_client.h" + +#include "thirdparty/cpp_mock/cpp_mock.h" + +class HTTPClientMock : public HTTPClient { +public: + static HTTPClientMock *current_instance; + + static HTTPClient *_create_func(bool p_notify_postinitialize = true) { + current_instance = static_cast(ClassDB::creator(p_notify_postinitialize)); + return current_instance; + } + + static HTTPClient *(*_old_create)(bool); + static void make_current() { + _old_create = HTTPClient::_create; + HTTPClient::_create = _create_func; + } + static void reset_current() { + if (_old_create) { + HTTPClient::_create = _old_create; + } + } + + MockMethod(Error, request, (Method, const String &, const Vector &, const uint8_t *, int)); + MockMethod(Error, connect_to_host, (const String &, int, Ref)); + + MockMethod(void, set_connection, (const Ref &)); + MockConstMethod(Ref, get_connection, ()); + + MockMethod(void, close, ()); + + MockConstMethod(Status, get_status, ()); + + MockConstMethod(bool, has_response, ()); + MockConstMethod(bool, is_response_chunked, ()); + MockConstMethod(int, get_response_code, ()); + MockMethod(Error, get_response_headers, (List *)); + MockConstMethod(int64_t, get_response_body_length, ()); + + MockMethod(PackedByteArray, read_response_body_chunk, ()); + + MockMethod(void, set_blocking_mode, (bool)); + MockConstMethod(bool, is_blocking_mode_enabled, ()); + + MockMethod(void, set_read_chunk_size, (int)); + MockConstMethod(int, get_read_chunk_size, ()); + + MockMethod(Error, poll, ()); +}; + +#endif // TEST_HTTP_CLIENT_MOCK_H diff --git a/tests/scene/test_http_request.h b/tests/scene/test_http_request.h new file mode 100644 index 00000000000..5f98f9b12ca --- /dev/null +++ b/tests/scene/test_http_request.h @@ -0,0 +1,493 @@ +/**************************************************************************/ +/* test_http_request.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef TEST_HTTP_REQUEST_H +#define TEST_HTTP_REQUEST_H + +#include "scene/main/http_request.h" +#include "tests/core/io/test_http_client_mock.h" +#include "tests/test_macros.h" + +#include "thirdparty/cpp_mock/cpp_mock.h" + +namespace TestHTTPRequest { + +static inline Array build_array() { + return Array(); +} +template +static inline Array build_array(Variant item, Targs... Fargs) { + Array a = build_array(Fargs...); + a.push_front(item); + return a; +} + +static inline PackedStringArray build_headers() { + return PackedStringArray(); +} +template +static inline PackedStringArray build_headers(Variant item, Targs... Fargs) { + PackedStringArray psa = build_headers(Fargs...); + psa.push_back(item); + return psa; +} + +TEST_CASE("[Network][HTTPRequest] Download chunk size is set when HTTP client is disconnected") { + HTTPClientMock::make_current(); + HTTPRequest *http_request = memnew(HTTPRequest); + HTTPClientMock *http_client = HTTPClientMock::current_instance; + int expected_value = 42; + + When(http_client->get_status).Return(HTTPClient::STATUS_DISCONNECTED); + + http_request->set_download_chunk_size(expected_value); + + Verify(http_client->set_read_chunk_size).With(expected_value); + + memdelete(http_request); + HTTPClientMock::reset_current(); +} + +TEST_CASE("[Network][HTTPRequest] Download chunk size is not set when HTTP client is not disconnected") { + HTTPClientMock::make_current(); + HTTPRequest *http_request = memnew(HTTPRequest); + HTTPClientMock *http_client = HTTPClientMock::current_instance; + int expected_value = 42; + + When(http_client->get_status).Return(HTTPClient::STATUS_CONNECTED); + + ERR_PRINT_OFF; + http_request->set_download_chunk_size(expected_value); + ERR_PRINT_ON; + + Verify(http_client->set_read_chunk_size).With(expected_value).Times(0); + + memdelete(http_request); + HTTPClientMock::reset_current(); +} + +TEST_CASE("[Network][HTTPRequest] ERR_UNCONFIGURED when HTTPRequest is not inside tree") { + HTTPClientMock::make_current(); + HTTPRequest *http_request = memnew(HTTPRequest); + + String url = "http://foo.com"; + ERR_PRINT_OFF; + Error error = http_request->request(url); + ERR_PRINT_ON; + + CHECK(error == Error::ERR_UNCONFIGURED); + + memdelete(http_request); + HTTPClientMock::reset_current(); +} + +TEST_CASE("[Network][HTTPRequest][SceneTree] Request when disconnected") { + HTTPClientMock::make_current(); + HTTPRequest *http_request = memnew(HTTPRequest); + SceneTree::get_singleton()->get_root()->add_child(http_request); + HTTPClientMock *http_client = HTTPClientMock::current_instance; + + When(http_client->get_status).Return(HTTPClient::STATUS_DISCONNECTED); + SIGNAL_WATCH(http_request, "request_completed"); + + String url = "http://foo.com"; + Error error = http_request->request(url); + + SceneTree::get_singleton()->process(0); + + CHECK(http_request->is_processing_internal() == false); + CHECK(error == Error::OK); + SIGNAL_CHECK("request_completed", build_array(build_array(HTTPRequest::Result::RESULT_CANT_CONNECT, 0, PackedStringArray(), PackedByteArray()))); + + SIGNAL_UNWATCH(http_request, "request_completed"); + memdelete(http_request); + HTTPClientMock::reset_current(); +} + +TEST_CASE("[Network][HTTPRequest][SceneTree] Parse URL errors") { + HTTPClientMock::make_current(); + HTTPRequest *http_request = memnew(HTTPRequest); + SceneTree::get_singleton()->get_root()->add_child(http_request); + + SIGNAL_WATCH(http_request, "request_completed"); + + SUBCASE("URL is invalid") { + String url = "http://foo.com:8080:433"; + ERR_PRINT_OFF; + Error error = http_request->request(url); + ERR_PRINT_ON; + + CHECK(error == Error::ERR_INVALID_PARAMETER); + SIGNAL_CHECK_FALSE("request_completed"); + } + + SUBCASE("URL schema is invalid") { + String url = "ftp://foo.com"; + ERR_PRINT_OFF; + Error error = http_request->request(url); + ERR_PRINT_ON; + + CHECK(error == Error::ERR_INVALID_PARAMETER); + SIGNAL_CHECK_FALSE("request_completed"); + } + + SIGNAL_UNWATCH(http_request, "request_completed"); + memdelete(http_request); + HTTPClientMock::reset_current(); +} + +TEST_CASE("[Network][HTTPRequest][SceneTree] Port") { + HTTPClientMock::make_current(); + + SUBCASE("URLs are parse to get the port") { + HTTPRequest *http_request = memnew(HTTPRequest); + SceneTree::get_singleton()->get_root()->add_child(http_request); + HTTPClientMock *http_client = HTTPClientMock::current_instance; + int port = 8080; + String host = "foo.com"; + String url = "http://" + host + ":" + itos(port); + + Error error = http_request->request(url); + + Verify(http_client->connect_to_host).With(host, port, (Ref)(nullptr)).Times(1); + CHECK(http_request->is_processing_internal()); + CHECK(error == Error::OK); + + memdelete(http_request); + } + + SUBCASE("HTTP URLs default port") { + HTTPRequest *http_request = memnew(HTTPRequest); + SceneTree::get_singleton()->get_root()->add_child(http_request); + HTTPClientMock *http_client = HTTPClientMock::current_instance; + String host = "foo.com"; + String url = "http://" + host; + + Error error = http_request->request(url); + + Verify(http_client->connect_to_host).With(host, 80, (Ref)(nullptr)).Times(1); + CHECK(http_request->is_processing_internal()); + CHECK(error == Error::OK); + + memdelete(http_request); + } + + SUBCASE("HTTPS URLs default port") { + HTTPRequest *http_request = memnew(HTTPRequest); + SceneTree::get_singleton()->get_root()->add_child(http_request); + HTTPClientMock *http_client = HTTPClientMock::current_instance; + Ref tls_options = TLSOptions::client(); + String host = "foo.com"; + String url = "https://" + host; + + http_request->set_tls_options(tls_options); + Error error = http_request->request(url); + + Verify(http_client->connect_to_host).With(host, 443, tls_options).Times(1); + CHECK(http_request->is_processing_internal()); + CHECK(error == Error::OK); + + memdelete(http_request); + } + + HTTPClientMock::reset_current(); +} + +TEST_CASE("[Network][HTTPRequest][SceneTree] Requests") { + HTTPClientMock::make_current(); + String url = "http://foo.com"; + + SUBCASE("Just one at the same time") { + HTTPRequest *http_request = memnew(HTTPRequest); + SceneTree::get_singleton()->get_root()->add_child(http_request); + + Error error = http_request->request(url); + CHECK(error == Error::OK); + + ERR_PRINT_OFF; + error = http_request->request(url); + ERR_PRINT_ON; + CHECK(error == Error::ERR_BUSY); + + memdelete(http_request); + } + + SUBCASE("Can be cancelled") { + HTTPRequest *http_request = memnew(HTTPRequest); + HTTPClientMock *http_client = HTTPClientMock::current_instance; + SceneTree::get_singleton()->get_root()->add_child(http_request); + + Error error = http_request->request(url); + CHECK(error == Error::OK); + + http_request->cancel_request(); + CHECK_FALSE(http_request->is_processing_internal()); + + error = http_request->request(url); + CHECK(error == Error::OK); + Verify(http_client->close).Times(1); + + memdelete(http_request); + } + + SUBCASE("Are cancelled when HTTPRequest node is removed from SceneTree") { + HTTPRequest *http_request = memnew(HTTPRequest); + HTTPClientMock *http_client = HTTPClientMock::current_instance; + SceneTree::get_singleton()->get_root()->add_child(http_request); + + Error error = http_request->request(url); + CHECK(error == Error::OK); + + ERR_PRINT_OFF; + error = http_request->request(url); + ERR_PRINT_ON; + CHECK(error == Error::ERR_BUSY); + + // This will cancel the request. + SceneTree::get_singleton()->get_root()->remove_child(http_request); + CHECK_FALSE(http_request->is_processing_internal()); + + // This is needed to create a new request. + SceneTree::get_singleton()->get_root()->add_child(http_request); + error = http_request->request(url); + CHECK(error == Error::OK); + Verify(http_client->close).Times(1); + + memdelete(http_request); + } + + HTTPClientMock::reset_current(); +} + +TEST_CASE("[Network][HTTPRequest][SceneTree] Timeout") { + HTTPClientMock::make_current(); + HTTPRequest *http_request = memnew(HTTPRequest); + SceneTree::get_singleton()->get_root()->add_child(http_request); + HTTPClientMock *http_client = HTTPClientMock::current_instance; + + When(http_client->get_status).Return({ HTTPClient::STATUS_RESOLVING }); + When(http_client->poll).Return({ Error::OK }); + SIGNAL_WATCH(http_request, "request_completed"); + + http_request->set_timeout(1); + String url = "http://foo.com"; + Error error = http_request->request(url); + + // Call process with time greater than timeout. + SceneTree::get_singleton()->process(2); + + Verify(http_client->request).Times(0); + SIGNAL_CHECK("request_completed", build_array(build_array(HTTPRequest::Result::RESULT_TIMEOUT, 0, PackedStringArray(), PackedByteArray()))); + CHECK_FALSE(http_request->is_processing_internal()); + CHECK(error == Error::OK); + + SIGNAL_UNWATCH(http_request, "request_completed"); + memdelete(http_request); + HTTPClientMock::reset_current(); +} + +TEST_CASE("[Network][HTTPRequest][SceneTree] GET Request") { + HTTPClientMock::make_current(); + HTTPRequest *http_request = memnew(HTTPRequest); + SceneTree::get_singleton()->get_root()->add_child(http_request); + HTTPClientMock *http_client = HTTPClientMock::current_instance; + + When(http_client->get_status).Return({ HTTPClient::STATUS_RESOLVING, HTTPClient::STATUS_CONNECTING, + // First STATUS_CONNECTED is to send the request, second STATUS_CONNECTED is to receive request. + HTTPClient::STATUS_CONNECTED, HTTPClient::STATUS_CONNECTED }); + When(http_client->get_response_code).Return(HTTPClient::ResponseCode::RESPONSE_OK); + When(http_client->has_response).Return(true); + SIGNAL_WATCH(http_request, "request_completed"); + + String url = "http://foo.com"; + Error error = http_request->request(url); + + // Call process for each status. + for (int i = 0; i < 4; i++) { + SceneTree::get_singleton()->process(0); + } + + Verify(http_client->request).With(HTTPClient::Method::METHOD_GET, String("/"), build_headers("Accept-Encoding: gzip, deflate"), (uint8_t *)nullptr, 0).Times(1); + SIGNAL_CHECK("request_completed", build_array(build_array(HTTPRequest::Result::RESULT_SUCCESS, HTTPClient::ResponseCode::RESPONSE_OK, PackedStringArray(), PackedByteArray()))); + CHECK_FALSE(http_request->is_processing_internal()); + CHECK(error == Error::OK); + + SIGNAL_UNWATCH(http_request, "request_completed"); + memdelete(http_request); + HTTPClientMock::reset_current(); +} + +TEST_CASE("[Network][HTTPRequest][SceneTree] GET Request with body and headers") { + HTTPClientMock::make_current(); + HTTPRequest *http_request = memnew(HTTPRequest); + SceneTree::get_singleton()->get_root()->add_child(http_request); + HTTPClientMock *http_client = HTTPClientMock::current_instance; + PackedByteArray body = String("Godot Rules!!!").to_utf8_buffer(); + + When(http_client->get_status).Return({ HTTPClient::STATUS_RESOLVING, HTTPClient::STATUS_CONNECTING, HTTPClient::STATUS_CONNECTED, HTTPClient::STATUS_BODY }); + When(http_client->get_response_code).Return(HTTPClient::ResponseCode::RESPONSE_OK); + When(http_client->has_response).Return(true); + When(http_client->get_response_headers).Do([](List *r_response) -> Error { + r_response->push_front("Server: Mock"); + return Error::OK; + }); + When(http_client->get_response_body_length).Return(body.size()); + When(http_client->read_response_body_chunk).Return(body); + SIGNAL_WATCH(http_request, "request_completed"); + + String url = "http://foo.com"; + Error error = http_request->request(url); + + // Call process for each status. + for (int i = 0; i < 4; i++) { + SceneTree::get_singleton()->process(0); + } + + Verify(http_client->request).With(HTTPClient::Method::METHOD_GET, String("/"), build_headers("Accept-Encoding: gzip, deflate"), (uint8_t *)nullptr, 0).Times(1); + SIGNAL_CHECK("request_completed", + build_array(build_array(HTTPRequest::Result::RESULT_SUCCESS, HTTPClient::ResponseCode::RESPONSE_OK, build_headers("Server: Mock"), body))); + CHECK_FALSE(http_request->is_processing_internal()); + CHECK(error == Error::OK); + + SIGNAL_UNWATCH(http_request, "request_completed"); + memdelete(http_request); + HTTPClientMock::reset_current(); +} + +TEST_CASE("[Network][HTTPRequest][SceneTree] POST Request with body and headers") { + HTTPClientMock::make_current(); + HTTPRequest *http_request = memnew(HTTPRequest); + SceneTree::get_singleton()->get_root()->add_child(http_request); + HTTPClientMock *http_client = HTTPClientMock::current_instance; + String body("Godot Rules!!!"); + + When(http_client->get_status).Return({ HTTPClient::STATUS_RESOLVING, HTTPClient::STATUS_CONNECTING, + // First STATUS_CONNECTED is to send the request, second STATUS_CONNECTED is to receive request. + HTTPClient::STATUS_CONNECTED, HTTPClient::STATUS_CONNECTED }); + When(http_client->get_response_code).Return(HTTPClient::ResponseCode::RESPONSE_CREATED); + When(http_client->has_response).Return(true); + SIGNAL_WATCH(http_request, "request_completed"); + + String url = "http://foo.com"; + Error error = http_request->request(url, build_headers("Accept: text/json"), HTTPClient::Method::METHOD_POST, body); + + // Call process for each status. + for (int i = 0; i < 4; i++) { + SceneTree::get_singleton()->process(0); + } + + Verify(http_client->request).With(HTTPClient::Method::METHOD_POST, String("/"), build_headers("Accept-Encoding: gzip, deflate", "Accept: text/json"), cpp_mock::matching::any_matcher(), body.size() - 1).Times(1); + SIGNAL_CHECK("request_completed", build_array(build_array(HTTPRequest::Result::RESULT_SUCCESS, HTTPClient::ResponseCode::RESPONSE_CREATED, PackedStringArray(), PackedByteArray()))); + CHECK_FALSE(http_request->is_processing_internal()); + CHECK(error == Error::OK); + + SIGNAL_UNWATCH(http_request, "request_completed"); + memdelete(http_request); + HTTPClientMock::reset_current(); +} + +#ifdef THREADS_ENABLED + +TEST_CASE("[Network][HTTPRequest][SceneTree][Threads] GET Request with body") { + HTTPClientMock::make_current(); + HTTPRequest *http_request = memnew(HTTPRequest); + SceneTree::get_singleton()->get_root()->add_child(http_request); + HTTPClientMock *http_client = HTTPClientMock::current_instance; + PackedByteArray body = String("Godot Rules!!!").to_utf8_buffer(); + Semaphore s; + + // HTTPClient::STATUS_DISCONNECTED is needed by HTTPRequest::set_use_threads. + When(http_client->get_status).Return({ HTTPClient::STATUS_DISCONNECTED, HTTPClient::STATUS_RESOLVING, HTTPClient::STATUS_CONNECTING, HTTPClient::STATUS_CONNECTED, HTTPClient::STATUS_BODY }); + When(http_client->get_response_code).Return(HTTPClient::ResponseCode::RESPONSE_OK); + When(http_client->has_response).Return(true); + When(http_client->get_response_body_length).Return(body.size()); + When(http_client->read_response_body_chunk).Do([&s, body]() -> PackedByteArray { + s.post(); + return body; + }); + SIGNAL_WATCH(http_request, "request_completed"); + + http_request->set_use_threads(true); + String url = "http://foo.com"; + Error error = http_request->request(url); + + // Let the thread do its job. + s.wait(); + + // This is needed to get defer calls processed. + SceneTree::get_singleton()->process(0); + + Verify(http_client->request).With(HTTPClient::Method::METHOD_GET, String("/"), build_headers("Accept-Encoding: gzip, deflate"), (uint8_t *)nullptr, 0).Times(1); + SIGNAL_CHECK("request_completed", build_array(build_array(HTTPRequest::Result::RESULT_SUCCESS, HTTPClient::ResponseCode::RESPONSE_OK, PackedStringArray(), body))); + CHECK_FALSE(http_request->is_processing_internal()); + CHECK(error == Error::OK); + + SIGNAL_UNWATCH(http_request, "request_completed"); + memdelete(http_request); + HTTPClientMock::reset_current(); +} + +TEST_CASE("[Network][HTTPRequest][SceneTree][Threads] Timeout") { + HTTPClientMock::make_current(); + HTTPRequest *http_request = memnew(HTTPRequest); + SceneTree::get_singleton()->get_root()->add_child(http_request); + HTTPClientMock *http_client = HTTPClientMock::current_instance; + + // HTTPClient::STATUS_DISCONNECTED is needed by HTTPRequest::set_use_threads. + When(http_client->get_status).Return({ HTTPClient::STATUS_DISCONNECTED, HTTPClient::STATUS_RESOLVING }); + When(http_client->poll).Return({ Error::OK }); + SIGNAL_WATCH(http_request, "request_completed"); + + http_request->set_use_threads(true); + http_request->set_timeout(1); + String url = "http://foo.com"; + Error error = http_request->request(url); + + // Call process with time greater than timeout. + SceneTree::get_singleton()->process(2); + + // Let the thread do its job. + OS::get_singleton()->delay_usec(250000); + + Verify(http_client->request).Times(0); + SIGNAL_CHECK("request_completed", build_array(build_array(HTTPRequest::Result::RESULT_TIMEOUT, 0, PackedStringArray(), PackedByteArray()))); + CHECK_FALSE(http_request->is_processing_internal()); + CHECK(error == Error::OK); + + SIGNAL_UNWATCH(http_request, "request_completed"); + memdelete(http_request); + HTTPClientMock::reset_current(); +} + +#endif // THREADS_ENABLED + +} // namespace TestHTTPRequest + +#endif // TEST_HTTP_REQUEST_H diff --git a/tests/scene/test_http_request_manual_mock.h b/tests/scene/test_http_request_manual_mock.h new file mode 100644 index 00000000000..fb646ad68ae --- /dev/null +++ b/tests/scene/test_http_request_manual_mock.h @@ -0,0 +1,455 @@ +/**************************************************************************/ +/* test_http_request_manual_mock.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef TEST_HTTP_REQUEST_MANUAL_MOCK_H +#define TEST_HTTP_REQUEST_MANUAL_MOCK_H + +#include "scene/main/http_request.h" +#include "tests/core/io/test_http_client_manual_mock.h" +#include "tests/test_macros.h" + +namespace TestHTTPRequestManualMock { + +static inline Array build_array() { + return Array(); +} +template +static inline Array build_array(Variant item, Targs... Fargs) { + Array a = build_array(Fargs...); + a.push_front(item); + return a; +} + +static inline PackedStringArray build_headers() { + return PackedStringArray(); +} +template +static inline PackedStringArray build_headers(Variant item, Targs... Fargs) { + PackedStringArray psa = build_headers(Fargs...); + psa.push_back(item); + return psa; +} + +TEST_CASE("[Network][HTTPRequest][ManualMock] Download chunk size is set when HTTP client is disconnected") { + HTTPClientManualMock::make_current(); + HTTPRequest *http_request = memnew(HTTPRequest); + HTTPClientManualMock *http_client = HTTPClientManualMock::current_instance; + int expected_value = 42; + + http_client->get_status_return = Vector({ HTTPClient::STATUS_DISCONNECTED }); + + http_request->set_download_chunk_size(expected_value); + + CHECK_EQ(http_client->set_read_chunk_size_p_size_parameter, expected_value); + + memdelete(http_request); + HTTPClientManualMock::reset_current(); +} + +TEST_CASE("[Network][HTTPRequest][ManualMock] Download chunk size is not set when HTTP client is not disconnected") { + HTTPClientManualMock::make_current(); + HTTPRequest *http_request = memnew(HTTPRequest); + HTTPClientManualMock *http_client = HTTPClientManualMock::current_instance; + int expected_value = 42; + + http_client->get_status_return = Vector({ HTTPClient::STATUS_CONNECTED }); + + ERR_PRINT_OFF; + http_request->set_download_chunk_size(expected_value); + ERR_PRINT_ON; + + CHECK_EQ(http_client->set_read_chunk_size_call_count, 0); + + memdelete(http_request); + HTTPClientManualMock::reset_current(); +} + +TEST_CASE("[Network][HTTPRequest][SceneTree][ManualMock] Request when disconnected") { + HTTPClientManualMock::make_current(); + HTTPRequest *http_request = memnew(HTTPRequest); + SceneTree::get_singleton()->get_root()->add_child(http_request); + HTTPClientManualMock *http_client = HTTPClientManualMock::current_instance; + + http_client->get_status_return = Vector({ HTTPClient::STATUS_DISCONNECTED }); + SIGNAL_WATCH(http_request, "request_completed"); + + String url = "http://foo.com"; + Error error = http_request->request(url); + + SceneTree::get_singleton()->process(0); + + CHECK(http_request->is_processing_internal() == false); + CHECK(error == Error::OK); + SIGNAL_CHECK("request_completed", build_array(build_array(HTTPRequest::Result::RESULT_CANT_CONNECT, 0, PackedStringArray(), PackedByteArray()))); + + SIGNAL_UNWATCH(http_request, "request_completed"); + memdelete(http_request); + HTTPClientManualMock::reset_current(); +} + +TEST_CASE("[Network][HTTPRequest][SceneTree][ManualMock] Port") { + HTTPClientManualMock::make_current(); + + SUBCASE("URLs are parse to get the port") { + HTTPRequest *http_request = memnew(HTTPRequest); + SceneTree::get_singleton()->get_root()->add_child(http_request); + HTTPClientManualMock *http_client = HTTPClientManualMock::current_instance; + int port = 8080; + String host = "foo.com"; + String url = "http://" + host + ":" + itos(port); + + Error error = http_request->request(url); + + CHECK_EQ(http_client->connect_to_host_p_host_parameter, host); + CHECK_EQ(http_client->connect_to_host_p_port_parameter, port); + CHECK_EQ(http_client->connect_to_host_p_tls_options_parameter, (Ref)(nullptr)); + CHECK_EQ(http_client->connect_to_host_call_count, 1); + CHECK(http_request->is_processing_internal()); + CHECK(error == Error::OK); + + memdelete(http_request); + } + + SUBCASE("HTTP URLs default port") { + HTTPRequest *http_request = memnew(HTTPRequest); + SceneTree::get_singleton()->get_root()->add_child(http_request); + HTTPClientManualMock *http_client = HTTPClientManualMock::current_instance; + String host = "foo.com"; + String url = "http://" + host; + + Error error = http_request->request(url); + + CHECK_EQ(http_client->connect_to_host_p_host_parameter, host); + CHECK_EQ(http_client->connect_to_host_p_port_parameter, 80); + CHECK_EQ(http_client->connect_to_host_p_tls_options_parameter, (Ref)(nullptr)); + CHECK_EQ(http_client->connect_to_host_call_count, 1); + CHECK(http_request->is_processing_internal()); + CHECK(error == Error::OK); + + memdelete(http_request); + } + + SUBCASE("HTTPS URLs default port") { + HTTPRequest *http_request = memnew(HTTPRequest); + SceneTree::get_singleton()->get_root()->add_child(http_request); + HTTPClientManualMock *http_client = HTTPClientManualMock::current_instance; + Ref tls_options = TLSOptions::client(); + String host = "foo.com"; + String url = "https://" + host; + + http_request->set_tls_options(tls_options); + Error error = http_request->request(url); + + CHECK_EQ(http_client->connect_to_host_p_host_parameter, host); + CHECK_EQ(http_client->connect_to_host_p_port_parameter, 443); + CHECK_EQ(http_client->connect_to_host_p_tls_options_parameter, tls_options); + CHECK_EQ(http_client->connect_to_host_call_count, 1); + CHECK(http_request->is_processing_internal()); + CHECK(error == Error::OK); + + memdelete(http_request); + } + + HTTPClientManualMock::reset_current(); +} + +TEST_CASE("[Network][HTTPRequest][SceneTree][ManualMock] Requests") { + HTTPClientManualMock::make_current(); + String url = "http://foo.com"; + + SUBCASE("Can be cancelled") { + HTTPRequest *http_request = memnew(HTTPRequest); + HTTPClientManualMock *http_client = HTTPClientManualMock::current_instance; + SceneTree::get_singleton()->get_root()->add_child(http_request); + + Error error = http_request->request(url); + CHECK(error == Error::OK); + + http_request->cancel_request(); + CHECK_FALSE(http_request->is_processing_internal()); + + error = http_request->request(url); + CHECK(error == Error::OK); + CHECK_EQ(http_client->close_call_count, 1); + + memdelete(http_request); + } + + SUBCASE("Are cancelled when HTTPRequest node is removed from SceneTree") { + HTTPRequest *http_request = memnew(HTTPRequest); + HTTPClientManualMock *http_client = HTTPClientManualMock::current_instance; + SceneTree::get_singleton()->get_root()->add_child(http_request); + + Error error = http_request->request(url); + CHECK(error == Error::OK); + + ERR_PRINT_OFF; + error = http_request->request(url); + ERR_PRINT_ON; + CHECK(error == Error::ERR_BUSY); + + // This will cancel the request. + SceneTree::get_singleton()->get_root()->remove_child(http_request); + CHECK_FALSE(http_request->is_processing_internal()); + + // This is needed to create a new request. + SceneTree::get_singleton()->get_root()->add_child(http_request); + error = http_request->request(url); + CHECK(error == Error::OK); + CHECK_EQ(http_client->close_call_count, 1); + + memdelete(http_request); + } + + HTTPClientManualMock::reset_current(); +} + +TEST_CASE("[Network][HTTPRequest][SceneTree][ManualMock] Timeout") { + HTTPClientManualMock::make_current(); + HTTPRequest *http_request = memnew(HTTPRequest); + SceneTree::get_singleton()->get_root()->add_child(http_request); + HTTPClientManualMock *http_client = HTTPClientManualMock::current_instance; + + http_client->get_status_return = Vector({ HTTPClient::STATUS_RESOLVING }); + http_client->poll_return = Error::OK; + SIGNAL_WATCH(http_request, "request_completed"); + + http_request->set_timeout(1); + String url = "http://foo.com"; + Error error = http_request->request(url); + + // Call process with time greater than timeout. + SceneTree::get_singleton()->process(2); + + CHECK_EQ(http_client->request_call_count, 0); + SIGNAL_CHECK("request_completed", build_array(build_array(HTTPRequest::Result::RESULT_TIMEOUT, 0, PackedStringArray(), PackedByteArray()))); + CHECK_FALSE(http_request->is_processing_internal()); + CHECK(error == Error::OK); + + SIGNAL_UNWATCH(http_request, "request_completed"); + memdelete(http_request); + HTTPClientManualMock::reset_current(); +} + +TEST_CASE("[Network][HTTPRequest][SceneTree][ManualMock] GET Request") { + HTTPClientManualMock::make_current(); + HTTPRequest *http_request = memnew(HTTPRequest); + SceneTree::get_singleton()->get_root()->add_child(http_request); + HTTPClientManualMock *http_client = HTTPClientManualMock::current_instance; + + http_client->get_status_return = Vector({ HTTPClient::STATUS_RESOLVING, HTTPClient::STATUS_CONNECTING, + // First STATUS_CONNECTED is to send the request, second STATUS_CONNECTED is to receive request. + HTTPClient::STATUS_CONNECTED, HTTPClient::STATUS_CONNECTED }); + http_client->get_response_code_return = HTTPClient::ResponseCode::RESPONSE_OK; + http_client->has_response_return = true; + SIGNAL_WATCH(http_request, "request_completed"); + + String url = "http://foo.com"; + Error error = http_request->request(url); + + // Call process for each status. + for (int i = 0; i < 4; i++) { + SceneTree::get_singleton()->process(0); + } + + CHECK_EQ(http_client->request_p_method_parameter, HTTPClient::Method::METHOD_GET); + CHECK_EQ(http_client->request_p_url_parameter, String("/")); + CHECK_EQ(http_client->request_p_headers_parameter, build_headers("Accept-Encoding: gzip, deflate")); + CHECK_EQ(http_client->request_p_body_parameter, (uint8_t *)nullptr); + CHECK_EQ(http_client->request_p_body_size_parameter, 0); + CHECK_EQ(http_client->request_call_count, 1); + SIGNAL_CHECK("request_completed", build_array(build_array(HTTPRequest::Result::RESULT_SUCCESS, HTTPClient::ResponseCode::RESPONSE_OK, PackedStringArray(), PackedByteArray()))); + CHECK_FALSE(http_request->is_processing_internal()); + CHECK(error == Error::OK); + + SIGNAL_UNWATCH(http_request, "request_completed"); + memdelete(http_request); + HTTPClientManualMock::reset_current(); +} + +TEST_CASE("[Network][HTTPRequest][SceneTree][ManualMock] GET Request with body and headers") { + HTTPClientManualMock::make_current(); + HTTPRequest *http_request = memnew(HTTPRequest); + SceneTree::get_singleton()->get_root()->add_child(http_request); + HTTPClientManualMock *http_client = HTTPClientManualMock::current_instance; + PackedByteArray body = String("Godot Rules!!!").to_utf8_buffer(); + + http_client->get_status_return = Vector({ HTTPClient::STATUS_RESOLVING, HTTPClient::STATUS_CONNECTING, HTTPClient::STATUS_CONNECTED, HTTPClient::STATUS_BODY, HTTPClient::STATUS_BODY }); + http_client->get_response_code_return = HTTPClient::ResponseCode::RESPONSE_OK; + http_client->has_response_return = true; + List headers; + headers.push_front("Server: Mock"); + http_client->get_response_headers_r_response_parameter = headers; + http_client->get_response_headers_return = Error::OK; + http_client->get_response_body_length_return = body.size(); + http_client->read_response_body_chunk_return = body; + SIGNAL_WATCH(http_request, "request_completed"); + + String url = "http://foo.com"; + Error error = http_request->request(url); + + // Call process for each status. + for (int i = 0; i < 4; i++) { + SceneTree::get_singleton()->process(0); + } + + CHECK_EQ(http_client->request_p_method_parameter, HTTPClient::Method::METHOD_GET); + CHECK_EQ(http_client->request_p_url_parameter, String("/")); + CHECK_EQ(http_client->request_p_headers_parameter, build_headers("Accept-Encoding: gzip, deflate")); + CHECK_EQ(http_client->request_p_body_parameter, (uint8_t *)nullptr); + CHECK_EQ(http_client->request_p_body_size_parameter, 0); + CHECK_EQ(http_client->request_call_count, 1); + SIGNAL_CHECK("request_completed", + build_array(build_array(HTTPRequest::Result::RESULT_SUCCESS, HTTPClient::ResponseCode::RESPONSE_OK, build_headers("Server: Mock"), body))); + CHECK_FALSE(http_request->is_processing_internal()); + CHECK(error == Error::OK); + + SIGNAL_UNWATCH(http_request, "request_completed"); + memdelete(http_request); + HTTPClientManualMock::reset_current(); +} + +TEST_CASE("[Network][HTTPRequest][SceneTree][ManualMock] POST Request with body and headers") { + HTTPClientManualMock::make_current(); + HTTPRequest *http_request = memnew(HTTPRequest); + SceneTree::get_singleton()->get_root()->add_child(http_request); + HTTPClientManualMock *http_client = HTTPClientManualMock::current_instance; + String body("Godot Rules!!!"); + + http_client->get_status_return = Vector({ HTTPClient::STATUS_RESOLVING, HTTPClient::STATUS_CONNECTING, + // First STATUS_CONNECTED is to send the request, second STATUS_CONNECTED is to receive request. + HTTPClient::STATUS_CONNECTED, HTTPClient::STATUS_CONNECTED }); + http_client->get_response_code_return = HTTPClient::ResponseCode::RESPONSE_CREATED; + http_client->has_response_return = true; + SIGNAL_WATCH(http_request, "request_completed"); + + String url = "http://foo.com"; + Error error = http_request->request(url, build_headers("Accept: text/json"), HTTPClient::Method::METHOD_POST, body); + + // Call process for each status. + for (int i = 0; i < 4; i++) { + SceneTree::get_singleton()->process(0); + } + + CHECK_EQ(http_client->request_p_method_parameter, HTTPClient::Method::METHOD_POST); + CHECK_EQ(http_client->request_p_url_parameter, String("/")); + CHECK_EQ(http_client->request_p_headers_parameter, build_headers("Accept-Encoding: gzip, deflate", "Accept: text/json")); + CHECK_EQ(http_client->request_p_body_size_parameter, body.size() - 1); + CHECK_EQ(http_client->request_call_count, 1); + SIGNAL_CHECK("request_completed", build_array(build_array(HTTPRequest::Result::RESULT_SUCCESS, HTTPClient::ResponseCode::RESPONSE_CREATED, PackedStringArray(), PackedByteArray()))); + CHECK_FALSE(http_request->is_processing_internal()); + CHECK(error == Error::OK); + + SIGNAL_UNWATCH(http_request, "request_completed"); + memdelete(http_request); + HTTPClientManualMock::reset_current(); +} + +#ifdef THREADS_ENABLED + +TEST_CASE("[Network][HTTPRequest][SceneTree][Threads][ManualMock] GET Request with body") { + HTTPClientManualMock::make_current(); + HTTPRequest *http_request = memnew(HTTPRequest); + SceneTree::get_singleton()->get_root()->add_child(http_request); + HTTPClientManualMock *http_client = HTTPClientManualMock::current_instance; + PackedByteArray body = String("Godot Rules!!!").to_utf8_buffer(); + Semaphore *semaphore = new Semaphore(); + + // HTTPClient::STATUS_DISCONNECTED is needed by HTTPRequest::set_use_threads. + http_client->get_status_return = Vector({ HTTPClient::STATUS_DISCONNECTED, HTTPClient::STATUS_RESOLVING, HTTPClient::STATUS_CONNECTING, HTTPClient::STATUS_CONNECTED, HTTPClient::STATUS_BODY, HTTPClient::STATUS_BODY }); + http_client->get_response_code_return = HTTPClient::ResponseCode::RESPONSE_OK; + http_client->has_response_return = true; + http_client->get_response_headers_return = Error::OK; + http_client->get_response_body_length_return = body.size(); + http_client->read_response_body_chunk_return = body; + http_client->read_response_body_chunk_semaphore = semaphore; + SIGNAL_WATCH(http_request, "request_completed"); + + http_request->set_use_threads(true); + String url = "http://foo.com"; + Error error = http_request->request(url); + + // Let the thread do its job. + semaphore->wait(); + + // This is needed to get defer calls processed. + SceneTree::get_singleton()->process(0); + + CHECK_EQ(http_client->request_p_method_parameter, HTTPClient::Method::METHOD_GET); + CHECK_EQ(http_client->request_p_url_parameter, String("/")); + CHECK_EQ(http_client->request_p_headers_parameter, build_headers("Accept-Encoding: gzip, deflate")); + CHECK_EQ(http_client->request_p_body_parameter, (uint8_t *)nullptr); + CHECK_EQ(http_client->request_p_body_size_parameter, 0); + CHECK_EQ(http_client->request_call_count, 1); + SIGNAL_CHECK("request_completed", build_array(build_array(HTTPRequest::Result::RESULT_SUCCESS, HTTPClient::ResponseCode::RESPONSE_OK, PackedStringArray(), body))); + CHECK_FALSE(http_request->is_processing_internal()); + CHECK(error == Error::OK); + + SIGNAL_UNWATCH(http_request, "request_completed"); + http_client->read_response_body_chunk_semaphore = nullptr; + delete semaphore; + memdelete(http_request); + HTTPClientManualMock::reset_current(); +} + +TEST_CASE("[Network][HTTPRequest][SceneTree][Threads][ManualMock] Timeout") { + HTTPClientManualMock::make_current(); + HTTPRequest *http_request = memnew(HTTPRequest); + SceneTree::get_singleton()->get_root()->add_child(http_request); + HTTPClientManualMock *http_client = HTTPClientManualMock::current_instance; + + // HTTPClient::STATUS_DISCONNECTED is needed by HTTPRequest::set_use_threads. + http_client->get_status_return = Vector({ HTTPClient::STATUS_DISCONNECTED, HTTPClient::STATUS_RESOLVING }); + http_client->poll_return = Error::OK; + SIGNAL_WATCH(http_request, "request_completed"); + + http_request->set_use_threads(true); + http_request->set_timeout(1); + String url = "http://foo.com"; + Error error = http_request->request(url); + + // Call process with time greater than timeout. + SceneTree::get_singleton()->process(2); + + CHECK_EQ(http_client->request_call_count, 0); + SIGNAL_CHECK("request_completed", build_array(build_array(HTTPRequest::Result::RESULT_TIMEOUT, 0, PackedStringArray(), PackedByteArray()))); + CHECK_FALSE(http_request->is_processing_internal()); + CHECK(error == Error::OK); + + SIGNAL_UNWATCH(http_request, "request_completed"); + memdelete(http_request); + HTTPClientManualMock::reset_current(); +} + +#endif // THREADS_ENABLED + +} // namespace TestHTTPRequestManualMock + +#endif // TEST_HTTP_REQUEST_MANUAL_MOCK_H diff --git a/tests/test_http_client_mock.cpp b/tests/test_http_client_mock.cpp new file mode 100644 index 00000000000..1b4080abb0b --- /dev/null +++ b/tests/test_http_client_mock.cpp @@ -0,0 +1,38 @@ +/**************************************************************************/ +/* test_http_client_mock.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "tests/core/io/test_http_client_mock.h" +#include "tests/core/io/test_http_client_manual_mock.h" + +HTTPClient *(*HTTPClientMock::_old_create)(bool) = nullptr; +HTTPClientMock *HTTPClientMock::current_instance = nullptr; + +HTTPClient *(*HTTPClientManualMock::_old_create)(bool) = nullptr; +HTTPClientManualMock *HTTPClientManualMock::current_instance = nullptr; diff --git a/tests/test_macros.h b/tests/test_macros.h index 10f4c59a908..110315184ac 100644 --- a/tests/test_macros.h +++ b/tests/test_macros.h @@ -294,6 +294,15 @@ private: _add_signal_entry(args, p_name); } + void _signal_callback_four(Variant p_arg1, Variant p_arg2, Variant p_arg3, Variant p_arg4, const String &p_name) { + Array args; + args.push_back(p_arg1); + args.push_back(p_arg2); + args.push_back(p_arg3); + args.push_back(p_arg4); + _add_signal_entry(args, p_name); + } + public: static SignalWatcher *get_singleton() { return singleton; } @@ -313,6 +322,9 @@ public: case 3: { p_object->connect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_three).bind(p_signal)); } break; + case 4: { + p_object->connect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_four).bind(p_signal)); + } break; default: { MESSAGE("Signal ", p_signal, " arg count not supported."); } break; @@ -335,6 +347,9 @@ public: case 3: { p_object->disconnect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_three)); } break; + case 4: { + p_object->disconnect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_four)); + } break; default: { MESSAGE("Signal ", p_signal, " arg count not supported."); } break; diff --git a/tests/test_main.cpp b/tests/test_main.cpp index 12ff3ad4bc9..e69d2f36409 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -113,6 +113,8 @@ #include "tests/scene/test_curve_3d.h" #include "tests/scene/test_gradient.h" #include "tests/scene/test_gradient_texture.h" +#include "tests/scene/test_http_request.h" +#include "tests/scene/test_http_request_manual_mock.h" #include "tests/scene/test_image_texture.h" #include "tests/scene/test_image_texture_3d.h" #include "tests/scene/test_instance_placeholder.h" diff --git a/thirdparty/README.md b/thirdparty/README.md index a219839afca..a324d7ebb0e 100644 --- a/thirdparty/README.md +++ b/thirdparty/README.md @@ -117,6 +117,18 @@ Apply the patches in the `patches/` folder when syncing on newer upstream commits. +## cpp_mock + +- Upstream: https://github.com/samcragg/cpp_mock +- Version: 1.0.2 (e50d268a105a5c945599b9c184b0bb1dcb1c1f10, 2020) +- License: MIT + +Files extracted from upstream source: + +- `cpp_mock.h` +- `LICENSE` + + ## cvtt - Upstream: https://github.com/elasota/ConvectionKernels @@ -241,6 +253,7 @@ Files extracted from upstream source: ``` - `AUTHORS.txt` and `LICENSE.txt` + ## fonts - `DroidSans*.woff2`: diff --git a/thirdparty/cpp_mock/LICENSE b/thirdparty/cpp_mock/LICENSE new file mode 100644 index 00000000000..49d57a6d5d9 --- /dev/null +++ b/thirdparty/cpp_mock/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Samuel Cragg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/thirdparty/cpp_mock/cpp_mock.h b/thirdparty/cpp_mock/cpp_mock.h new file mode 100644 index 00000000000..bbdf593b39b --- /dev/null +++ b/thirdparty/cpp_mock/cpp_mock.h @@ -0,0 +1,684 @@ +#ifndef CPP_MOCK_H +#define CPP_MOCK_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "thirdparty/doctest/doctest.h" + +namespace cpp_mock +{ + namespace details + { +#if __cplusplus <= 201703L + template + struct remove_cvref + { + using type = typename std::remove_cv< + typename std::remove_reference::type>::type; + }; +#else + using std::remove_cvref; +#endif + +#if __cplusplus < 201703L + template + struct is_invocable_r : std::is_constructible< + std::function, + std::reference_wrapper::type>> + { + }; +#else + using std::is_invocable_r; +#endif + + template + struct index_sequence + { + using type = index_sequence; + }; + + template + struct make_index_sequence + { + }; + + template<> struct make_index_sequence<0> : index_sequence<> { }; + template<> struct make_index_sequence<1> : index_sequence<0> { }; + template<> struct make_index_sequence<2> : index_sequence<0, 1> { }; + template<> struct make_index_sequence<3> : index_sequence<0, 1, 2> { }; + template<> struct make_index_sequence<4> : index_sequence<0, 1, 2, 3> { }; + template<> struct make_index_sequence<5> : index_sequence<0, 1, 2, 3, 4> { }; + template<> struct make_index_sequence<6> : index_sequence<0, 1, 2, 3, 4, 5> { }; + template<> struct make_index_sequence<7> : index_sequence<0, 1, 2, 3, 4, 5, 6> { }; + template<> struct make_index_sequence<8> : index_sequence<0, 1, 2, 3, 4, 5, 6, 7> { }; + template<> struct make_index_sequence<9> : index_sequence<0, 1, 2, 3, 4, 5, 6, 7, 8> { }; + template<> struct make_index_sequence<10> : index_sequence<0, 1, 2, 3, 4, 5, 6, 7, 8, 9> { }; + } + + namespace invoking + { + using namespace ::cpp_mock::details; + + template + struct return_values + { + using storage_type = std::vector; + + template + T operator()(Args&&...) + { + if (index == values.size()) + { + return values.back(); + } + else + { + return values[index++]; + } + } + + storage_type values; + std::size_t index; + }; + + struct ignore_arg + { + explicit ignore_arg(const void*) + { + } + + template + void save(const T&) + { + } + }; + + template + struct save_arg + { + explicit save_arg(T* arg) : + _arg(arg) + { + } + + void save(const T& value) + { + *_arg = value; + } + + T* _arg; + }; + + template + struct sync_arg_save + { + using type = ignore_arg; + }; + + template + struct sync_arg_save::value>::type> + { + using type = save_arg; + }; + + template + class sync_arg + { + public: + using element_type = typename std::tuple_element::type; + + sync_arg(Args& args, ArgSetters& setters) : + _args(&args), + _setters(&setters) + { + } + + ~sync_arg() + { + std::get(*_setters).save(get()); + } + + element_type& get() + { + return std::get(*_args); + } + private: + Args* _args; + ArgSetters* _setters; + }; + + template + static R invoke_indexes(const F& func, Args& args, ArgSetters& setters, index_sequence) + { + return func(sync_arg(args, setters).get()...); + } + + template + static R invoke(F&& func, Args& args, ArgSetters& setters) + { + return invoke_indexes(func, args, setters, typename make_index_sequence::value>::type{}); + } + } + + namespace matching + { + using namespace ::cpp_mock::details; + + struct any_matcher + { + template + bool operator()(const T&) const { return true; } + }; + + template + class equals_matcher + { + public: + equals_matcher(T&& value) : + _expected(std::move(value)) + { + } + + equals_matcher(const T& value) : + _expected(value) + { + } + + bool operator()(const T& actual) const + { + return actual == _expected; + } + private: + T _expected; + }; + + template + struct method_arguments_matcher + { + virtual bool matches(const Tuple& args) = 0; + virtual ~method_arguments_matcher() {} + }; + + template + struct any_method_arguments_matcher : method_arguments_matcher + { + bool matches(const Tuple&) override + { + return true; + } + }; + + template + class match_arguments_wrapper : public method_arguments_matcher + { + public: + explicit match_arguments_wrapper(MatcherTuple&& predicates) : + _predicates(std::move(predicates)) + { + } + + bool matches(const Tuple& args) override + { + return match_arguments(_predicates, args); + } + private: + MatcherTuple _predicates; + }; + +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" +#elif defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-function" +#elif defined(_MSC_VER) +#pragma warning( push ) +#pragma warning( disable : 4505 ) +#endif + template + static bool and_together(Head value) + { + return value; + } +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic pop +#elif defined(__clang__) +#pragma clang diagnostic pop +#elif defined(_MSC_VER) +#pragma warning( pop ) +#endif + + template + static bool and_together(Head head, Tail... tail) + { + return head && and_together(tail...); + } + + template + static bool match_argument_indexes(const P& predicates, const T& args, index_sequence) + { + return and_together(std::get(predicates)(std::get(args))...); + } + + template + static bool match_arguments(const P& predicates, const T& args) + { + static_assert( + std::tuple_size

::value == std::tuple_size::value, + "The number of predicates must match the number of arguments"); + + return match_argument_indexes(predicates, args, typename make_index_sequence::value>::type{}); + } + } + + template + struct method_action + { + std::shared_ptr>> matcher; + std::function action; + }; + + // static matching::any_matcher _; + + namespace mocking + { + using namespace ::cpp_mock::details; + + template + struct argument_matcher_wrapper : + std::conditional::value, Fn, matching::equals_matcher> + { + }; + + template + struct argument_wrapper + { + template + struct match_with + { + using type = std::tuple< + typename argument_matcher_wrapper::type...>; + }; + }; + + template + class method_action_builder + { + public: + explicit method_action_builder(method_action* action) : + _action(action) + { + } + + method_action_builder(const method_action_builder&) = delete; + method_action_builder(method_action_builder&&) = default; + + ~method_action_builder() + { + if (_action->action == nullptr) + { + if (_values.empty()) + { + _values.push_back(R()); + } + + _action->action = invoking::return_values { std::move(_values), 0u }; + } + + if (_action->matcher == nullptr) + { + _action->matcher = std::make_shared< + matching::any_method_arguments_matcher>>(); + } + } + + method_action_builder& operator=(const method_action_builder&) = delete; + method_action_builder& operator=(method_action_builder&&) = default; + + template + method_action_builder& With(MatchArgs&&... args) + { + static_assert( + sizeof...(MatchArgs) == sizeof...(Args), + "The number of matchers must match the number of arguments"); + + using matcher_tuple = typename argument_wrapper + ::template match_with::type; + + _action->matcher = std::make_shared< + matching::match_arguments_wrapper, matcher_tuple>>( + matcher_tuple(std::forward(args)...)); + + return *this; + } + + method_action_builder& Do(std::function function) + { + _action->action = std::move(function); + return *this; + } + + method_action_builder& Return(const R& value) + { + _values.push_back(value); + return *this; + } + + method_action_builder& Return(std::initializer_list values) + { + for (auto& value : values) + { + _values.push_back(value); + } + + return *this; + } + + private: + method_action* _action; + typename invoking::return_values::storage_type _values; + }; + + template + class method_action_builder + { + public: + explicit method_action_builder(method_action* action) : + _action(action) + { + } + + method_action_builder(const method_action_builder&) = delete; + method_action_builder(method_action_builder&&) = default; + + ~method_action_builder() + { + if (_action->action == nullptr) + { + _action->action = [](const Args& ...) { }; + } + + if (_action->matcher == nullptr) + { + _action->matcher = std::make_shared< + matching::any_method_arguments_matcher>>(); + } + } + + method_action_builder& operator=(const method_action_builder&) = delete; + method_action_builder& operator=(method_action_builder&&) = default; + + template + method_action_builder& With(MatchArgs&&... args) + { + static_assert( + sizeof...(MatchArgs) == sizeof...(Args), + "The number of matchers must match the number of arguments"); + + using matcher_tuple = typename argument_wrapper + ::template match_with::type; + + _action->matcher = std::make_shared< + matching::match_arguments_wrapper, matcher_tuple>>( + matcher_tuple(std::forward(args)...)); + + return *this; + } + + method_action_builder& Do(std::function function) + { + _action->action = std::move(function); + return *this; + } + + private: + method_action* _action; + }; + + template + class method_verify_builder + { + public: + method_verify_builder( + const char* method, + const char* file, + std::size_t line, + const std::vector>* calls + ) : + _calls(calls), + _count(std::numeric_limits::max()), + _matched_count(calls->size()), + _method(method), + _file(file), + _line(line) + { + } + + method_verify_builder(const method_verify_builder&) = delete; + method_verify_builder(method_verify_builder&&) = default; + + ~method_verify_builder() noexcept(false) + { + if (_count == std::numeric_limits::max()) + { + if (_matched_count == 0) + { + std::ostringstream stream; + write_location(stream) << "Expecting a call to " + << _method << " but none were received."; + FAIL(stream.str()); + } + } + else if (_count != _matched_count) + { + std::ostringstream stream; + write_location(stream) << "Expecting a call to " << _method << ' '; + write_times(stream, _count) << ", but it was invoked "; + write_times(stream, _matched_count) << '.'; + FAIL(stream.str()); + } + } + + method_verify_builder& operator=(const method_verify_builder&) = delete; + method_verify_builder& operator=(method_verify_builder&&) = default; + + template + method_verify_builder& With(MatchArgs&&... matchers) + { + static_assert( + sizeof...(MatchArgs) == sizeof...(Args), + "The number of matchers must match the number of arguments"); + + using argument_tuple = std::tuple; + using matcher_tuple = typename argument_wrapper + ::template match_with::type; + + matcher_tuple matcher(std::forward(matchers)...); + _matched_count = std::count_if( + _calls->begin(), + _calls->end(), + [&](const argument_tuple& args) { return match_arguments(matcher, args); }); + + return *this; + } + + method_verify_builder& Times(std::size_t count) + { + _count = count; + return *this; + } + private: + std::ostream& write_location(std::ostream& stream) + { + return stream << _file << ':' << _line << ' '; + } + + std::ostream& write_times(std::ostream& stream, std::size_t count) + { + return stream << count << " time" << ((count == 1) ? "" : "s"); + } + + const std::vector>* _calls; + std::size_t _count; + std::size_t _matched_count; + const char* _method; + const char* _file; + std::size_t _line; + }; + + template + class mock_method_types; + + template + class mock_method_types + { + public: + using method_action_t = method_action::type ...>; + using tuple_t = typename std::tuple::type ...>; + + using action_t = typename std::vector; + using record_t = typename std::vector; + }; + + template + static method_action_builder add_action(std::vector>& actions) + { + actions.emplace_back(method_action()); + return method_action_builder(&actions.back()); + } + + template + static method_verify_builder check_action( + const char* method, + const char* file, + std::size_t line, + const std::vector>& invocations) + { + return method_verify_builder(method, file, line, &invocations); + } + + template + static T return_default() + { + return T(); + } + } +} + +// Required for VC++ to expand variadic macro arguments correctly +#define EXPAND_MACRO(macro, ...) macro + +#define MAKE_FORWARD(arg) std::forward(arg) +#define MAKE_SETTER(arg) ::cpp_mock::invoking::sync_arg_save::type(&arg) + +#define MOCK_METHOD_IMPL(Ret, Name, Args, Specs, NameArgs, Transform, ...) \ + Ret Name(NameArgs Args) Specs \ + { \ + Name ## _Invocations.emplace_back(std::make_tuple( \ + EXPAND_MACRO(Transform(MAKE_FORWARD, __VA_ARGS__)))); \ + auto& args = Name ## _Invocations.back(); \ + for (auto it = Name ## _Actions.rbegin(); it != Name ## _Actions.rend(); ++it) \ + { \ + if (it->matcher->matches(args)) \ + { \ + auto setters = std::make_tuple( \ + EXPAND_MACRO(Transform(MAKE_SETTER, __VA_ARGS__))); \ + return ::cpp_mock::invoking::invoke(it->action, args, setters); \ + } \ + } \ + return ::cpp_mock::mocking::return_default(); \ + } \ + ::cpp_mock::mocking::mock_method_types::action_t Name ## _Actions; \ + mutable ::cpp_mock::mocking::mock_method_types::record_t Name ## _Invocations; + +#define NAME_ARGS1(type) type a +#define NAME_ARGS2(type, ...) type b, EXPAND_MACRO(NAME_ARGS1(__VA_ARGS__)) +#define NAME_ARGS3(type, ...) type c, EXPAND_MACRO(NAME_ARGS2(__VA_ARGS__)) +#define NAME_ARGS4(type, ...) type d, EXPAND_MACRO(NAME_ARGS3(__VA_ARGS__)) +#define NAME_ARGS5(type, ...) type e, EXPAND_MACRO(NAME_ARGS4(__VA_ARGS__)) +#define NAME_ARGS6(type, ...) type f, EXPAND_MACRO(NAME_ARGS5(__VA_ARGS__)) +#define NAME_ARGS7(type, ...) type g, EXPAND_MACRO(NAME_ARGS6(__VA_ARGS__)) +#define NAME_ARGS8(type, ...) type h, EXPAND_MACRO(NAME_ARGS7(__VA_ARGS__)) +#define NAME_ARGS9(type, ...) type i, EXPAND_MACRO(NAME_ARGS8(__VA_ARGS__)) +#define NAME_ARGS10(type, ...) type j, EXPAND_MACRO(NAME_ARGS9(__VA_ARGS__)) + +#define TRANSFORM1(Call, x) Call(x) +#define TRANSFORM2(Call, x, ...) Call(x), EXPAND_MACRO(TRANSFORM1(Call, __VA_ARGS__)) +#define TRANSFORM3(Call, x, ...) Call(x), EXPAND_MACRO(TRANSFORM2(Call, __VA_ARGS__)) +#define TRANSFORM4(Call, x, ...) Call(x), EXPAND_MACRO(TRANSFORM3(Call, __VA_ARGS__)) +#define TRANSFORM5(Call, x, ...) Call(x), EXPAND_MACRO(TRANSFORM4(Call, __VA_ARGS__)) +#define TRANSFORM6(Call, x, ...) Call(x), EXPAND_MACRO(TRANSFORM5(Call, __VA_ARGS__)) +#define TRANSFORM7(Call, x, ...) Call(x), EXPAND_MACRO(TRANSFORM6(Call, __VA_ARGS__)) +#define TRANSFORM8(Call, x, ...) Call(x), EXPAND_MACRO(TRANSFORM7(Call, __VA_ARGS__)) +#define TRANSFORM9(Call, x, ...) Call(x), EXPAND_MACRO(TRANSFORM8(Call, __VA_ARGS__)) +#define TRANSFORM10(Call, x, ...) Call(x), EXPAND_MACRO(TRANSFORM9(Call, __VA_ARGS__)) + +// We need to handle () and (int) differently, however, when they get passed in +// via __VA_ARG__ then it looks like we received a single parameter for both +// cases. Use the fact that we're always appending an 'a' to the parameter to +// detect the difference between 'type a' and 'a' +#define CAT(a, b) a ## b +#define GET_SECOND_ARG(a, b, ...) b +#define DEFINE_EXISTS(...) EXPAND_MACRO(GET_SECOND_ARG(__VA_ARGS__, TRUE)) +#define IS_TYPE_MISSING(x) DEFINE_EXISTS(CAT(TOKEN_IS_EMPTY_, x)) +#define TOKEN_IS_EMPTY_a ignored, FALSE +#define GET_METHOD(method, suffix) CAT(method, suffix) +#define NOOP(...) +#define MOCK_METHOD_IS_SINGLE_FALSE(Ret, Name, Args, Specs) MOCK_METHOD_IMPL(Ret, Name, Args, Specs, NOOP, NOOP) +#define MOCK_METHOD_IS_SINGLE_TRUE(Ret, Name, Args, Specs) MOCK_METHOD_IMPL(Ret, Name, Args, Specs, NAME_ARGS1, TRANSFORM1, a) +#define HANDLE_EMPTY_TYPE(...) GET_METHOD(MOCK_METHOD_IS_SINGLE_, IS_TYPE_MISSING(__VA_ARGS__ a)) + +#define MOCK_METHOD_1(Ret, Name, Args, Specs) HANDLE_EMPTY_TYPE Args (Ret, Name, Args, Specs) +#define MOCK_METHOD_2(Ret, Name, Args, Specs) MOCK_METHOD_IMPL(Ret, Name, Args, Specs, NAME_ARGS2, TRANSFORM2, b, a) +#define MOCK_METHOD_3(Ret, Name, Args, Specs) MOCK_METHOD_IMPL(Ret, Name, Args, Specs, NAME_ARGS3, TRANSFORM3, c, b, a) +#define MOCK_METHOD_4(Ret, Name, Args, Specs) MOCK_METHOD_IMPL(Ret, Name, Args, Specs, NAME_ARGS4, TRANSFORM4, d, c, b, a) +#define MOCK_METHOD_5(Ret, Name, Args, Specs) MOCK_METHOD_IMPL(Ret, Name, Args, Specs, NAME_ARGS5, TRANSFORM5, e, d, c, b, a) +#define MOCK_METHOD_6(Ret, Name, Args, Specs) MOCK_METHOD_IMPL(Ret, Name, Args, Specs, NAME_ARGS6, TRANSFORM6, f, e, d, c, b, a) +#define MOCK_METHOD_7(Ret, Name, Args, Specs) MOCK_METHOD_IMPL(Ret, Name, Args, Specs, NAME_ARGS7, TRANSFORM7, g, f, e, d, c, b, a) +#define MOCK_METHOD_8(Ret, Name, Args, Specs) MOCK_METHOD_IMPL(Ret, Name, Args, Specs, NAME_ARGS8, TRANSFORM8, h, g, f, e, d, c, b, a) +#define MOCK_METHOD_9(Ret, Name, Args, Specs) MOCK_METHOD_IMPL(Ret, Name, Args, Specs, NAME_ARGS9, TRANSFORM9, i, h, g, f, e, d, c, b, a) +#define MOCK_METHOD_10(Ret, Name, Args, Specs) MOCK_METHOD_IMPL(Ret, Name, Args, Specs, NAME_ARGS10, TRANSFORM10, j, i, h, g, f, e, d, c, b, a) + +#define GET_NTH_ARG(a, b, c, d, e, f, g, h, i, j, N, ...) N + +#define GET_MOCK_METHOD(...) EXPAND_MACRO(GET_NTH_ARG(__VA_ARGS__, \ + MOCK_METHOD_10, \ + MOCK_METHOD_9, \ + MOCK_METHOD_8, \ + MOCK_METHOD_7, \ + MOCK_METHOD_6, \ + MOCK_METHOD_5, \ + MOCK_METHOD_4, \ + MOCK_METHOD_3, \ + MOCK_METHOD_2, \ + MOCK_METHOD_1)) + +#define INVALID_METHOD(...) static_assert(false, "Invalid usage. Call with return type, name, argument types and, optionally, specifiers."); +#define MOCK_METHOD_SPEC(Ret, Name, Args, Spec) GET_MOCK_METHOD Args (Ret, Name, Args, Spec) +#define MOCK_METHOD(Ret, Name, Args) MOCK_METHOD_SPEC(Ret, Name, Args, override) + +#define MockMethod(...) EXPAND_MACRO(EXPAND_MACRO(GET_NTH_ARG(__VA_ARGS__, \ + INVALID_METHOD, \ + INVALID_METHOD, \ + INVALID_METHOD, \ + INVALID_METHOD, \ + INVALID_METHOD, \ + INVALID_METHOD, \ + MOCK_METHOD_SPEC, \ + MOCK_METHOD, \ + INVALID_METHOD, \ + INVALID_METHOD))(__VA_ARGS__)) + +#define MockConstMethod(Ret, Name, Args) MockMethod(Ret, Name, Args, const override) +#define When(call_method) ::cpp_mock::mocking::add_action(call_method ## _Actions) +#define Verify(call_method) ::cpp_mock::mocking::check_action(#call_method, __FILE__, __LINE__, call_method ## _Invocations) + +#endif