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.
This commit is contained in:
Pablo Andres Fuente 2024-08-06 22:41:53 -03:00
parent e3213aaef5
commit 15e9008216
10 changed files with 1979 additions and 0 deletions

View File

@ -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<HTTPClientManualMock *>(ClassDB::creator<HTTPClientManualMock>(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<Status> 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<TLSOptions> 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<String> 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<String> 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<String> &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<uint8_t *>(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<TLSOptions> p_tls_options = Ref<TLSOptions>()) 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<StreamPeer> &p_connection) override {}
Ref<StreamPeer> get_connection() const override { return Ref<StreamPeer>(); }
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<String> *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<Status>::Size get_status_return_current = 0;
};
#endif // TEST_HTTP_CLIENT_MANUAL_MOCK_H

View File

@ -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<HTTPClientMock *>(ClassDB::creator<HTTPClientMock>(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<String> &, const uint8_t *, int));
MockMethod(Error, connect_to_host, (const String &, int, Ref<TLSOptions>));
MockMethod(void, set_connection, (const Ref<StreamPeer> &));
MockConstMethod(Ref<StreamPeer>, 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<String> *));
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

View File

@ -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 <typename... Targs>
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 <typename... Targs>
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<TLSOptions>)(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<TLSOptions>)(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<TLSOptions> 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<String> *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

View File

@ -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 <typename... Targs>
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 <typename... Targs>
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>({ 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>({ 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>({ 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<TLSOptions>)(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<TLSOptions>)(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<TLSOptions> 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>({ 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>({ 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>({ 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<String> 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>({ 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>({ 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>({ 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

View File

@ -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;

View File

@ -294,6 +294,15 @@ private:
_add_signal_entry(args, p_name); _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: public:
static SignalWatcher *get_singleton() { return singleton; } static SignalWatcher *get_singleton() { return singleton; }
@ -313,6 +322,9 @@ public:
case 3: { case 3: {
p_object->connect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_three).bind(p_signal)); p_object->connect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_three).bind(p_signal));
} break; } break;
case 4: {
p_object->connect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_four).bind(p_signal));
} break;
default: { default: {
MESSAGE("Signal ", p_signal, " arg count not supported."); MESSAGE("Signal ", p_signal, " arg count not supported.");
} break; } break;
@ -335,6 +347,9 @@ public:
case 3: { case 3: {
p_object->disconnect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_three)); p_object->disconnect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_three));
} break; } break;
case 4: {
p_object->disconnect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_four));
} break;
default: { default: {
MESSAGE("Signal ", p_signal, " arg count not supported."); MESSAGE("Signal ", p_signal, " arg count not supported.");
} break; } break;

View File

@ -113,6 +113,8 @@
#include "tests/scene/test_curve_3d.h" #include "tests/scene/test_curve_3d.h"
#include "tests/scene/test_gradient.h" #include "tests/scene/test_gradient.h"
#include "tests/scene/test_gradient_texture.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.h"
#include "tests/scene/test_image_texture_3d.h" #include "tests/scene/test_image_texture_3d.h"
#include "tests/scene/test_instance_placeholder.h" #include "tests/scene/test_instance_placeholder.h"

13
thirdparty/README.md vendored
View File

@ -117,6 +117,18 @@ Apply the patches in the `patches/` folder when syncing on newer upstream
commits. 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 ## cvtt
- Upstream: https://github.com/elasota/ConvectionKernels - Upstream: https://github.com/elasota/ConvectionKernels
@ -241,6 +253,7 @@ Files extracted from upstream source:
``` ```
- `AUTHORS.txt` and `LICENSE.txt` - `AUTHORS.txt` and `LICENSE.txt`
## fonts ## fonts
- `DroidSans*.woff2`: - `DroidSans*.woff2`:

21
thirdparty/cpp_mock/LICENSE vendored Normal file
View File

@ -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.

684
thirdparty/cpp_mock/cpp_mock.h vendored Normal file
View File

@ -0,0 +1,684 @@
#ifndef CPP_MOCK_H
#define CPP_MOCK_H
#include <algorithm>
#include <initializer_list>
#include <limits>
#include <memory>
#include <sstream>
#include <stdexcept>
#include <tuple>
#include <type_traits>
#include <vector>
#include <functional>
#include "thirdparty/doctest/doctest.h"
namespace cpp_mock
{
namespace details
{
#if __cplusplus <= 201703L
template <class T>
struct remove_cvref
{
using type = typename std::remove_cv<
typename std::remove_reference<T>::type>::type;
};
#else
using std::remove_cvref;
#endif
#if __cplusplus < 201703L
template <class R, class Fn, class Arg>
struct is_invocable_r : std::is_constructible<
std::function<R(Arg)>,
std::reference_wrapper<typename std::remove_reference<Fn>::type>>
{
};
#else
using std::is_invocable_r;
#endif
template <std::size_t... I>
struct index_sequence
{
using type = index_sequence;
};
template <size_t N>
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 <class T>
struct return_values
{
using storage_type = std::vector<T>;
template <class... Args>
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 <class T>
void save(const T&)
{
}
};
template <class T>
struct save_arg
{
explicit save_arg(T* arg) :
_arg(arg)
{
}
void save(const T& value)
{
*_arg = value;
}
T* _arg;
};
template <class T, typename = void>
struct sync_arg_save
{
using type = ignore_arg;
};
template <class T>
struct sync_arg_save<T&, typename std::enable_if<!std::is_const<T>::value>::type>
{
using type = save_arg<T>;
};
template <std::size_t I, class Args, class ArgSetters>
class sync_arg
{
public:
using element_type = typename std::tuple_element<I, Args>::type;
sync_arg(Args& args, ArgSetters& setters) :
_args(&args),
_setters(&setters)
{
}
~sync_arg()
{
std::get<I>(*_setters).save(get());
}
element_type& get()
{
return std::get<I>(*_args);
}
private:
Args* _args;
ArgSetters* _setters;
};
template <class R, class F, class Args, class ArgSetters, std::size_t... I>
static R invoke_indexes(const F& func, Args& args, ArgSetters& setters, index_sequence<I...>)
{
return func(sync_arg<I, Args, ArgSetters>(args, setters).get()...);
}
template <class R, class F, class Args, class ArgSetters>
static R invoke(F&& func, Args& args, ArgSetters& setters)
{
return invoke_indexes<R>(func, args, setters, typename make_index_sequence<std::tuple_size<Args>::value>::type{});
}
}
namespace matching
{
using namespace ::cpp_mock::details;
struct any_matcher
{
template <class T>
bool operator()(const T&) const { return true; }
};
template <class T>
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 <class Tuple>
struct method_arguments_matcher
{
virtual bool matches(const Tuple& args) = 0;
virtual ~method_arguments_matcher() {}
};
template <class Tuple>
struct any_method_arguments_matcher : method_arguments_matcher<Tuple>
{
bool matches(const Tuple&) override
{
return true;
}
};
template <class Tuple, class MatcherTuple>
class match_arguments_wrapper : public method_arguments_matcher<Tuple>
{
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 <class Head>
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 <class Head, class... Tail>
static bool and_together(Head head, Tail... tail)
{
return head && and_together(tail...);
}
template <class P, class T, std::size_t... I>
static bool match_argument_indexes(const P& predicates, const T& args, index_sequence<I...>)
{
return and_together(std::get<I>(predicates)(std::get<I>(args))...);
}
template <class P, class T>
static bool match_arguments(const P& predicates, const T& args)
{
static_assert(
std::tuple_size<P>::value == std::tuple_size<T>::value,
"The number of predicates must match the number of arguments");
return match_argument_indexes(predicates, args, typename make_index_sequence<std::tuple_size<T>::value>::type{});
}
}
template <class R, class... Args>
struct method_action
{
std::shared_ptr<matching::method_arguments_matcher<std::tuple<Args...>>> matcher;
std::function<R(Args& ...)> action;
};
// static matching::any_matcher _;
namespace mocking
{
using namespace ::cpp_mock::details;
template <class Fn, class Arg>
struct argument_matcher_wrapper :
std::conditional<is_invocable_r<bool, Fn, Arg>::value, Fn, matching::equals_matcher<Arg>>
{
};
template <class... TupleArgs>
struct argument_wrapper
{
template <class... MatchArgs>
struct match_with
{
using type = std::tuple<
typename argument_matcher_wrapper<MatchArgs, TupleArgs>::type...>;
};
};
template <class R, class... Args>
class method_action_builder
{
public:
explicit method_action_builder(method_action<R, Args...>* 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<R> { std::move(_values), 0u };
}
if (_action->matcher == nullptr)
{
_action->matcher = std::make_shared<
matching::any_method_arguments_matcher<std::tuple<Args...>>>();
}
}
method_action_builder& operator=(const method_action_builder&) = delete;
method_action_builder& operator=(method_action_builder&&) = default;
template <class... MatchArgs>
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<Args...>
::template match_with<MatchArgs...>::type;
_action->matcher = std::make_shared<
matching::match_arguments_wrapper<std::tuple<Args...>, matcher_tuple>>(
matcher_tuple(std::forward<MatchArgs>(args)...));
return *this;
}
method_action_builder& Do(std::function<R(Args& ...)> 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<R> values)
{
for (auto& value : values)
{
_values.push_back(value);
}
return *this;
}
private:
method_action<R, Args...>* _action;
typename invoking::return_values<R>::storage_type _values;
};
template <class... Args>
class method_action_builder<void, Args...>
{
public:
explicit method_action_builder(method_action<void, Args...>* 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<std::tuple<Args...>>>();
}
}
method_action_builder& operator=(const method_action_builder&) = delete;
method_action_builder& operator=(method_action_builder&&) = default;
template <class... MatchArgs>
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<Args...>
::template match_with<MatchArgs...>::type;
_action->matcher = std::make_shared<
matching::match_arguments_wrapper<std::tuple<Args...>, matcher_tuple>>(
matcher_tuple(std::forward<MatchArgs>(args)...));
return *this;
}
method_action_builder& Do(std::function<void(Args & ...)> function)
{
_action->action = std::move(function);
return *this;
}
private:
method_action<void, Args...>* _action;
};
template <class... Args>
class method_verify_builder
{
public:
method_verify_builder(
const char* method,
const char* file,
std::size_t line,
const std::vector<std::tuple<Args...>>* calls
) :
_calls(calls),
_count(std::numeric_limits<std::size_t>::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<std::size_t>::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 <class... MatchArgs>
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<Args...>;
using matcher_tuple = typename argument_wrapper<Args...>
::template match_with<MatchArgs...>::type;
matcher_tuple matcher(std::forward<MatchArgs>(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<std::tuple<Args...>>* _calls;
std::size_t _count;
std::size_t _matched_count;
const char* _method;
const char* _file;
std::size_t _line;
};
template <class>
class mock_method_types;
template <class R, class... Args>
class mock_method_types<R(Args...)>
{
public:
using method_action_t = method_action<R, typename remove_cvref<Args>::type ...>;
using tuple_t = typename std::tuple<typename remove_cvref<Args>::type ...>;
using action_t = typename std::vector<method_action_t>;
using record_t = typename std::vector<tuple_t>;
};
template <class R, class... Args>
static method_action_builder<R, Args...> add_action(std::vector<method_action<R, Args...>>& actions)
{
actions.emplace_back(method_action<R, Args...>());
return method_action_builder<R, Args...>(&actions.back());
}
template <class... Args>
static method_verify_builder<Args...> check_action(
const char* method,
const char* file,
std::size_t line,
const std::vector<std::tuple<Args...>>& invocations)
{
return method_verify_builder<Args...>(method, file, line, &invocations);
}
template <class T>
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<decltype(arg)>(arg)
#define MAKE_SETTER(arg) ::cpp_mock::invoking::sync_arg_save<decltype(arg)>::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<Ret>(it->action, args, setters); \
} \
} \
return ::cpp_mock::mocking::return_default<Ret>(); \
} \
::cpp_mock::mocking::mock_method_types<Ret Args>::action_t Name ## _Actions; \
mutable ::cpp_mock::mocking::mock_method_types<Ret Args>::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