From 726641b1170e56fd2d24354eb6656c08b46aa3a4 Mon Sep 17 00:00:00 2001 From: Maxime David Date: Tue, 26 May 2026 14:41:14 +0000 Subject: [PATCH 01/10] Use thread local curl handle for multi-concurrency support Replace the instance member CURL* with a thread_local static CURL* at namespace scope. This allows the runtime to be safely used from multiple threads, each getting their own curl handle without needing separate runtime instances. --- include/aws/lambda-runtime/runtime.h | 1 - src/runtime.cpp | 86 ++++++++++++++-------------- 2 files changed, 43 insertions(+), 44 deletions(-) diff --git a/include/aws/lambda-runtime/runtime.h b/include/aws/lambda-runtime/runtime.h index be3961c..0b2b90e 100644 --- a/include/aws/lambda-runtime/runtime.h +++ b/include/aws/lambda-runtime/runtime.h @@ -174,7 +174,6 @@ class runtime { invocation_response const& handler_response); std::string const m_user_agent_header; std::array const m_endpoints; - CURL* const m_curl_handle; }; inline std::chrono::milliseconds invocation_request::get_time_remaining() const diff --git a/src/runtime.cpp b/src/runtime.cpp index efbb697..735885c 100644 --- a/src/runtime.cpp +++ b/src/runtime.cpp @@ -42,6 +42,7 @@ static constexpr auto COGNITO_IDENTITY_HEADER = "lambda-runtime-cognito-identity static constexpr auto DEADLINE_MS_HEADER = "lambda-runtime-deadline-ms"; static constexpr auto FUNCTION_ARN_HEADER = "lambda-runtime-invoked-function-arn"; static constexpr auto TENANT_ID_HEADER = "lambda-runtime-aws-tenant-id"; +thread_local static CURL* m_curl_handle = curl_easy_init(); enum Endpoints { INIT, @@ -168,63 +169,62 @@ runtime::runtime(std::string const& endpoint, std::string const& user_agent) m_endpoints{ {endpoint + "/2018-06-01/runtime/init/error", endpoint + "/2018-06-01/runtime/invocation/next", - endpoint + "/2018-06-01/runtime/invocation/"}}, - m_curl_handle(curl_easy_init()) + endpoint + "/2018-06-01/runtime/invocation/"}} { - if (!m_curl_handle) { + if (!lambda_runtime::m_curl_handle) { logging::log_error(LOG_TAG, "Failed to acquire curl easy handle for next."); } } runtime::~runtime() { - curl_easy_cleanup(m_curl_handle); + curl_easy_cleanup(lambda_runtime::m_curl_handle); } void runtime::set_curl_next_options() { // lambda freezes the container when no further tasks are available. The freezing period could be longer than the // request timeout, which causes the following get_next request to fail with a timeout error. - curl_easy_reset(m_curl_handle); - curl_easy_setopt(m_curl_handle, CURLOPT_TIMEOUT, 0L); - curl_easy_setopt(m_curl_handle, CURLOPT_CONNECTTIMEOUT, 1L); - curl_easy_setopt(m_curl_handle, CURLOPT_NOSIGNAL, 1L); - curl_easy_setopt(m_curl_handle, CURLOPT_TCP_NODELAY, 1L); - curl_easy_setopt(m_curl_handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_easy_reset(lambda_runtime::m_curl_handle); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_TIMEOUT, 0L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_CONNECTTIMEOUT, 1L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_TCP_NODELAY, 1L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); - curl_easy_setopt(m_curl_handle, CURLOPT_HTTPGET, 1L); - curl_easy_setopt(m_curl_handle, CURLOPT_URL, m_endpoints[Endpoints::NEXT].c_str()); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_URL, m_endpoints[Endpoints::NEXT].c_str()); - curl_easy_setopt(m_curl_handle, CURLOPT_WRITEFUNCTION, write_data); - curl_easy_setopt(m_curl_handle, CURLOPT_HEADERFUNCTION, write_header); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_WRITEFUNCTION, write_data); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HEADERFUNCTION, write_header); - curl_easy_setopt(m_curl_handle, CURLOPT_PROXY, ""); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_PROXY, ""); #ifndef NDEBUG - curl_easy_setopt(m_curl_handle, CURLOPT_VERBOSE, 1); - curl_easy_setopt(m_curl_handle, CURLOPT_DEBUGFUNCTION, rt_curl_debug_callback); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_VERBOSE, 1); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_DEBUGFUNCTION, rt_curl_debug_callback); #endif } void runtime::set_curl_post_result_options() { - curl_easy_reset(m_curl_handle); - curl_easy_setopt(m_curl_handle, CURLOPT_TIMEOUT, 0L); - curl_easy_setopt(m_curl_handle, CURLOPT_CONNECTTIMEOUT, 1L); - curl_easy_setopt(m_curl_handle, CURLOPT_NOSIGNAL, 1L); - curl_easy_setopt(m_curl_handle, CURLOPT_TCP_NODELAY, 1L); - curl_easy_setopt(m_curl_handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_easy_reset(lambda_runtime::m_curl_handle); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_TIMEOUT, 0L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_CONNECTTIMEOUT, 1L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_TCP_NODELAY, 1L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); - curl_easy_setopt(m_curl_handle, CURLOPT_POST, 1L); - curl_easy_setopt(m_curl_handle, CURLOPT_READFUNCTION, read_data); - curl_easy_setopt(m_curl_handle, CURLOPT_WRITEFUNCTION, write_data); - curl_easy_setopt(m_curl_handle, CURLOPT_HEADERFUNCTION, write_header); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_POST, 1L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_READFUNCTION, read_data); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_WRITEFUNCTION, write_data); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HEADERFUNCTION, write_header); - curl_easy_setopt(m_curl_handle, CURLOPT_PROXY, ""); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_PROXY, ""); #ifndef NDEBUG - curl_easy_setopt(m_curl_handle, CURLOPT_VERBOSE, 1); - curl_easy_setopt(m_curl_handle, CURLOPT_DEBUGFUNCTION, rt_curl_debug_callback); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_VERBOSE, 1); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_DEBUGFUNCTION, rt_curl_debug_callback); #endif } @@ -232,15 +232,15 @@ runtime::next_outcome runtime::get_next() { http::response resp; set_curl_next_options(); - curl_easy_setopt(m_curl_handle, CURLOPT_WRITEDATA, &resp); - curl_easy_setopt(m_curl_handle, CURLOPT_HEADERDATA, &resp); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_WRITEDATA, &resp); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HEADERDATA, &resp); curl_slist* headers = nullptr; headers = curl_slist_append(headers, m_user_agent_header.c_str()); - curl_easy_setopt(m_curl_handle, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HTTPHEADER, headers); logging::log_debug(LOG_TAG, "Making request to %s", m_endpoints[Endpoints::NEXT].c_str()); - CURLcode curl_code = curl_easy_perform(m_curl_handle); + CURLcode curl_code = curl_easy_perform(lambda_runtime::m_curl_handle); logging::log_debug(LOG_TAG, "Completed request to %s", m_endpoints[Endpoints::NEXT].c_str()); curl_slist_free_all(headers); @@ -255,13 +255,13 @@ runtime::next_outcome runtime::get_next() { long resp_code; - curl_easy_getinfo(m_curl_handle, CURLINFO_RESPONSE_CODE, &resp_code); + curl_easy_getinfo(lambda_runtime::m_curl_handle, CURLINFO_RESPONSE_CODE, &resp_code); resp.set_response_code(static_cast(resp_code)); } { char* content_type = nullptr; - curl_easy_getinfo(m_curl_handle, CURLINFO_CONTENT_TYPE, &content_type); + curl_easy_getinfo(lambda_runtime::m_curl_handle, CURLINFO_CONTENT_TYPE, &content_type); resp.set_content_type(content_type); } @@ -343,7 +343,7 @@ runtime::post_outcome runtime::do_post( invocation_response const& handler_response) { set_curl_post_result_options(); - curl_easy_setopt(m_curl_handle, CURLOPT_URL, url.c_str()); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_URL, url.c_str()); logging::log_info(LOG_TAG, "Making request to %s", url.c_str()); curl_slist* headers = nullptr; @@ -364,11 +364,11 @@ runtime::post_outcome runtime::do_post( std::pair ctx{payload, 0}; aws::http::response resp; - curl_easy_setopt(m_curl_handle, CURLOPT_WRITEDATA, &resp); - curl_easy_setopt(m_curl_handle, CURLOPT_HEADERDATA, &resp); - curl_easy_setopt(m_curl_handle, CURLOPT_READDATA, &ctx); - curl_easy_setopt(m_curl_handle, CURLOPT_HTTPHEADER, headers); - CURLcode curl_code = curl_easy_perform(m_curl_handle); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_WRITEDATA, &resp); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HEADERDATA, &resp); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_READDATA, &ctx); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HTTPHEADER, headers); + CURLcode curl_code = curl_easy_perform(lambda_runtime::m_curl_handle); curl_slist_free_all(headers); if (curl_code != CURLE_OK) { @@ -382,7 +382,7 @@ runtime::post_outcome runtime::do_post( } long http_response_code; - curl_easy_getinfo(m_curl_handle, CURLINFO_RESPONSE_CODE, &http_response_code); + curl_easy_getinfo(lambda_runtime::m_curl_handle, CURLINFO_RESPONSE_CODE, &http_response_code); if (!is_success(aws::http::response_code(http_response_code))) { logging::log_error( From 2c9df5c559528c2e03dd996b0ae8f295ec908e3c Mon Sep 17 00:00:00 2001 From: Maxime David Date: Tue, 26 May 2026 14:47:03 +0000 Subject: [PATCH 02/10] Add unit tests for thread-local curl multi-concurrency Tests cover: - Runtime construction with thread-local curl handle - Multiple sequential runtime instances on the same thread - Concurrent runtime instances on different threads - Sequential requests on a single runtime instance Also fixes a bug from the original change: the destructor must not call curl_easy_cleanup on the thread_local handle, since the handle outlives any single runtime instance and is shared across all instances on the same thread. --- src/runtime.cpp | 3 +- tests/CMakeLists.txt | 20 ++++++++- tests/unit/main.cpp | 7 +++ tests/unit/thread_local_curl_test.cpp | 62 +++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 tests/unit/main.cpp create mode 100644 tests/unit/thread_local_curl_test.cpp diff --git a/src/runtime.cpp b/src/runtime.cpp index 735885c..2d5eae5 100644 --- a/src/runtime.cpp +++ b/src/runtime.cpp @@ -178,7 +178,8 @@ runtime::runtime(std::string const& endpoint, std::string const& user_agent) runtime::~runtime() { - curl_easy_cleanup(lambda_runtime::m_curl_handle); + // The curl handle is thread_local and outlives any single runtime instance. + // Cleanup happens automatically when the thread exits. } void runtime::set_curl_next_options() diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7406096..2e093a4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,7 +15,8 @@ if(DEFINED ENV{GITHUB_ACTIONS}) FetchContent_MakeAvailable(gtest) add_executable(unit_tests - unit/no_op_test.cpp) + unit/no_op_test.cpp + unit/thread_local_curl_test.cpp) target_link_libraries(unit_tests PRIVATE gtest_main aws-lambda-runtime) # Register unit tests @@ -25,7 +26,22 @@ if(DEFINED ENV{GITHUB_ACTIONS}) LABELS "unit" DISCOVERY_TIMEOUT 10) else() - message(STATUS "Unit tests skipped: Not in GitHub Actions environment") + # Build unit tests using the bundled gtest + add_executable(unit_tests + unit/no_op_test.cpp + unit/thread_local_curl_test.cpp + gtest/gtest-all.cc) + target_include_directories(unit_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(unit_tests PRIVATE aws-lambda-runtime pthread) + + # Provide a main() for gtest + target_sources(unit_tests PRIVATE unit/main.cpp) + + include(GoogleTest) + gtest_discover_tests(unit_tests + PROPERTIES + LABELS "unit" + DISCOVERY_TIMEOUT 10) endif() diff --git a/tests/unit/main.cpp b/tests/unit/main.cpp new file mode 100644 index 0000000..08fb839 --- /dev/null +++ b/tests/unit/main.cpp @@ -0,0 +1,7 @@ +#include "gtest/gtest.h" + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/unit/thread_local_curl_test.cpp b/tests/unit/thread_local_curl_test.cpp new file mode 100644 index 0000000..bf0f6e6 --- /dev/null +++ b/tests/unit/thread_local_curl_test.cpp @@ -0,0 +1,62 @@ +#include +#include +#include +#include +#include + +using namespace aws::lambda_runtime; + +TEST(ThreadLocalCurl, runtime_construction_succeeds) +{ + runtime rt("http://127.0.0.1:9001"); + auto outcome = rt.get_next(); + // We expect a connection failure since there's no server, but the runtime itself should construct fine + ASSERT_FALSE(outcome.is_success()); +} + +TEST(ThreadLocalCurl, multiple_runtimes_on_same_thread_do_not_crash) +{ + { + runtime rt1("http://127.0.0.1:9001"); + auto o1 = rt1.get_next(); + ASSERT_FALSE(o1.is_success()); + } + { + runtime rt2("http://127.0.0.1:9001"); + auto o2 = rt2.get_next(); + ASSERT_FALSE(o2.is_success()); + } +} + +TEST(ThreadLocalCurl, concurrent_runtimes_on_different_threads) +{ + constexpr int num_threads = 4; + std::vector threads; + std::atomic successes{0}; + + for (int i = 0; i < num_threads; ++i) { + threads.emplace_back([&successes]() { + runtime rt("http://127.0.0.1:9001"); + auto outcome = rt.get_next(); + // Connection will fail but runtime should not crash or corrupt state + if (!outcome.is_success()) { + successes.fetch_add(1); + } + }); + } + + for (auto& t : threads) { + t.join(); + } + + ASSERT_EQ(successes.load(), num_threads); +} + +TEST(ThreadLocalCurl, sequential_requests_on_same_runtime) +{ + runtime rt("http://127.0.0.1:9001"); + for (int i = 0; i < 5; ++i) { + auto outcome = rt.get_next(); + ASSERT_FALSE(outcome.is_success()); + } +} From e09e6bf3b8987a2a83fc164ee0634ed54aea0c9c Mon Sep 17 00:00:00 2001 From: Maxime David Date: Tue, 26 May 2026 15:24:48 +0000 Subject: [PATCH 03/10] Add error response body to post failure log and comprehensive unit tests Include the HTTP response body in the error log when posting the handler response fails, aiding debugging of Lambda runtime API errors. Add unit tests covering invocation_response, http::response, outcome, invocation_request, and version APIs. --- src/runtime.cpp | 5 +- tests/CMakeLists.txt | 4 +- tests/unit/unit_tests.cpp | 270 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 277 insertions(+), 2 deletions(-) create mode 100644 tests/unit/unit_tests.cpp diff --git a/src/runtime.cpp b/src/runtime.cpp index 2d5eae5..894c718 100644 --- a/src/runtime.cpp +++ b/src/runtime.cpp @@ -387,7 +387,10 @@ runtime::post_outcome runtime::do_post( if (!is_success(aws::http::response_code(http_response_code))) { logging::log_error( - LOG_TAG, "Failed to post handler success response. Http response code: %ld.", http_response_code); + LOG_TAG, + "Failed to post handler success response. Http response code: %ld. %s", + http_response_code, + resp.get_body().c_str()); return aws::http::response_code(http_response_code); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2e093a4..1304787 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -16,7 +16,8 @@ if(DEFINED ENV{GITHUB_ACTIONS}) add_executable(unit_tests unit/no_op_test.cpp - unit/thread_local_curl_test.cpp) + unit/thread_local_curl_test.cpp + unit/unit_tests.cpp) target_link_libraries(unit_tests PRIVATE gtest_main aws-lambda-runtime) # Register unit tests @@ -30,6 +31,7 @@ else() add_executable(unit_tests unit/no_op_test.cpp unit/thread_local_curl_test.cpp + unit/unit_tests.cpp gtest/gtest-all.cc) target_include_directories(unit_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(unit_tests PRIVATE aws-lambda-runtime pthread) diff --git a/tests/unit/unit_tests.cpp b/tests/unit/unit_tests.cpp new file mode 100644 index 0000000..3c6e718 --- /dev/null +++ b/tests/unit/unit_tests.cpp @@ -0,0 +1,270 @@ +#include +#include +#include +#include +#include "gtest/gtest.h" + +using namespace aws::lambda_runtime; +using namespace aws::http; + +// --- invocation_response tests --- + +TEST(InvocationResponseTest, success_response_has_correct_payload_and_content_type) +{ + auto resp = invocation_response::success("hello world", "text/plain"); + EXPECT_TRUE(resp.is_success()); + EXPECT_EQ("hello world", resp.get_payload()); + EXPECT_EQ("text/plain", resp.get_content_type()); +} + +TEST(InvocationResponseTest, success_response_with_json) +{ + auto resp = invocation_response::success(R"({"key":"value"})", "application/json"); + EXPECT_TRUE(resp.is_success()); + EXPECT_EQ(R"({"key":"value"})", resp.get_payload()); + EXPECT_EQ("application/json", resp.get_content_type()); +} + +TEST(InvocationResponseTest, success_response_with_empty_payload) +{ + auto resp = invocation_response::success("", "application/json"); + EXPECT_TRUE(resp.is_success()); + EXPECT_EQ("", resp.get_payload()); +} + +TEST(InvocationResponseTest, failure_response_is_not_success) +{ + auto resp = invocation_response::failure("something broke", "RuntimeError", ""); + EXPECT_FALSE(resp.is_success()); + EXPECT_EQ("application/json", resp.get_content_type()); +} + +TEST(InvocationResponseTest, failure_response_contains_error_message) +{ + auto resp = invocation_response::failure("something broke", "RuntimeError", ""); + auto const& payload = resp.get_payload(); + EXPECT_NE(std::string::npos, payload.find("something broke")); + EXPECT_NE(std::string::npos, payload.find("RuntimeError")); +} + +TEST(InvocationResponseTest, failure_response_json_escapes_quotes) +{ + auto resp = invocation_response::failure(R"(error with "quotes")", "TestError", ""); + auto const& payload = resp.get_payload(); + EXPECT_NE(std::string::npos, payload.find(R"(error with \"quotes\")")); + EXPECT_EQ(std::string::npos, payload.find(R"(error with "quotes")")); +} + +TEST(InvocationResponseTest, failure_response_json_escapes_backslash) +{ + auto resp = invocation_response::failure(R"(path\to\file)", "TestError", ""); + auto const& payload = resp.get_payload(); + EXPECT_NE(std::string::npos, payload.find(R"(path\\to\\file)")); +} + +TEST(InvocationResponseTest, failure_response_json_escapes_newlines) +{ + auto resp = invocation_response::failure("line1\nline2\r\n", "TestError", ""); + auto const& payload = resp.get_payload(); + EXPECT_NE(std::string::npos, payload.find(R"(line1\nline2\r\n)")); +} + +TEST(InvocationResponseTest, failure_response_json_escapes_tabs) +{ + auto resp = invocation_response::failure("col1\tcol2", "TestError", ""); + auto const& payload = resp.get_payload(); + EXPECT_NE(std::string::npos, payload.find(R"(col1\tcol2)")); +} + +TEST(InvocationResponseTest, failure_response_json_escapes_control_characters) +{ + std::string msg = "null\x00 byte"; + msg.push_back('\x01'); + auto resp = invocation_response::failure(msg, "TestError", ""); + auto const& payload = resp.get_payload(); + EXPECT_NE(std::string::npos, payload.find("\\u0001")); +} + +TEST(InvocationResponseTest, failure_response_preserves_xray_response) +{ + auto resp = invocation_response::failure("err", "Type", "xray-data-here"); + EXPECT_EQ("xray-data-here", resp.get_xray_response()); +} + +TEST(InvocationResponseTest, success_response_with_binary_content_type) +{ + std::string binary_data(256, '\0'); + for (int i = 0; i < 256; ++i) { + binary_data[static_cast(i)] = static_cast(i); + } + auto resp = invocation_response::success(binary_data, "application/octet-stream"); + EXPECT_TRUE(resp.is_success()); + EXPECT_EQ(256u, resp.get_payload().size()); +} + +TEST(InvocationResponseTest, constructor_based_failure) +{ + auto resp = invocation_response(R"({"custom":"error"})", "application/json", false, "xray"); + EXPECT_FALSE(resp.is_success()); + EXPECT_EQ(R"({"custom":"error"})", resp.get_payload()); + EXPECT_EQ("xray", resp.get_xray_response()); +} + +// --- http::response tests --- + +TEST(HttpResponseTest, add_and_retrieve_header) +{ + response resp; + resp.add_header("Content-Type", "application/json"); + EXPECT_TRUE(resp.has_header("content-type")); + EXPECT_EQ("application/json", resp.get_header("content-type")); +} + +TEST(HttpResponseTest, headers_are_lowercased) +{ + response resp; + resp.add_header("X-Custom-Header", "some-value"); + EXPECT_TRUE(resp.has_header("x-custom-header")); + EXPECT_FALSE(resp.has_header("X-Custom-Header")); +} + +TEST(HttpResponseTest, has_header_returns_false_for_missing) +{ + response resp; + resp.add_header("Content-Type", "text/plain"); + EXPECT_FALSE(resp.has_header("x-missing")); +} + +TEST(HttpResponseTest, append_body_accumulates) +{ + response resp; + resp.append_body("hello", 5); + resp.append_body(" world", 6); + EXPECT_EQ("hello world", resp.get_body()); +} + +TEST(HttpResponseTest, append_body_empty) +{ + response resp; + EXPECT_EQ("", resp.get_body()); +} + +TEST(HttpResponseTest, set_response_code) +{ + response resp; + resp.set_response_code(response_code::OK); + EXPECT_EQ(response_code::OK, resp.get_response_code()); +} + +TEST(HttpResponseTest, multiple_headers) +{ + response resp; + resp.add_header("lambda-runtime-aws-request-id", "req-123"); + resp.add_header("lambda-runtime-trace-id", "trace-456"); + resp.add_header("lambda-runtime-deadline-ms", "1234567890"); + EXPECT_EQ("req-123", resp.get_header("lambda-runtime-aws-request-id")); + EXPECT_EQ("trace-456", resp.get_header("lambda-runtime-trace-id")); + EXPECT_EQ("1234567890", resp.get_header("lambda-runtime-deadline-ms")); +} + +// --- outcome tests --- + +TEST(OutcomeTest, success_outcome) +{ + outcome o(std::string("result")); + EXPECT_TRUE(o.is_success()); + EXPECT_EQ("result", o.get_result()); +} + +TEST(OutcomeTest, failure_outcome) +{ + outcome o(42); + EXPECT_FALSE(o.is_success()); + EXPECT_EQ(42, o.get_failure()); +} + +TEST(OutcomeTest, move_success) +{ + outcome o1(std::string("moved")); + outcome o2(std::move(o1)); + EXPECT_TRUE(o2.is_success()); + EXPECT_EQ("moved", o2.get_result()); +} + +TEST(OutcomeTest, move_failure) +{ + outcome o1(99); + outcome o2(std::move(o1)); + EXPECT_FALSE(o2.is_success()); + EXPECT_EQ(99, o2.get_failure()); +} + +TEST(OutcomeTest, with_response_code) +{ + using test_outcome = outcome; + test_outcome success(no_result{}); + EXPECT_TRUE(success.is_success()); + + test_outcome failure(response_code::INTERNAL_SERVER_ERROR); + EXPECT_FALSE(failure.is_success()); + EXPECT_EQ(response_code::INTERNAL_SERVER_ERROR, failure.get_failure()); +} + +// --- invocation_request tests --- + +TEST(InvocationRequestTest, get_time_remaining_future_deadline) +{ + invocation_request req; + req.deadline = std::chrono::system_clock::now() + std::chrono::seconds(30); + auto remaining = req.get_time_remaining(); + EXPECT_GT(remaining.count(), 29000); + EXPECT_LE(remaining.count(), 30000); +} + +TEST(InvocationRequestTest, get_time_remaining_past_deadline) +{ + invocation_request req; + req.deadline = std::chrono::system_clock::now() - std::chrono::seconds(5); + auto remaining = req.get_time_remaining(); + EXPECT_LT(remaining.count(), 0); +} + +TEST(InvocationRequestTest, default_fields_are_empty) +{ + invocation_request req; + EXPECT_TRUE(req.payload.empty()); + EXPECT_TRUE(req.request_id.empty()); + EXPECT_TRUE(req.xray_trace_id.empty()); + EXPECT_TRUE(req.client_context.empty()); + EXPECT_TRUE(req.cognito_identity.empty()); + EXPECT_TRUE(req.function_arn.empty()); + EXPECT_TRUE(req.tenant_id.empty()); +} + +// --- runtime_response tests --- + +TEST(RuntimeResponseTest, constructor_sets_all_fields) +{ + runtime_response resp("payload", "application/json", "xray"); + EXPECT_EQ("payload", resp.get_payload()); + EXPECT_EQ("application/json", resp.get_content_type()); + EXPECT_EQ("xray", resp.get_xray_response()); +} + +// --- version tests (no AWS SDK needed) --- + +TEST(VersionTest, version_string_not_empty) +{ + EXPECT_NE(nullptr, get_version()); + EXPECT_GT(strlen(get_version()), 0u); +} + +TEST(VersionTest, version_format) +{ + std::string v = get_version(); + int dots = 0; + for (char c : v) { + if (c == '.') dots++; + } + EXPECT_EQ(2, dots); +} From 6a9536e7802b4e9c1af1cf54a894b7d105a22bf2 Mon Sep 17 00:00:00 2001 From: Maxime David Date: Tue, 26 May 2026 15:28:42 +0000 Subject: [PATCH 04/10] Revert logging change in do_post error path Remove response body from the post failure log message to avoid potential breaking changes in log output format. --- src/runtime.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/runtime.cpp b/src/runtime.cpp index 894c718..2d5eae5 100644 --- a/src/runtime.cpp +++ b/src/runtime.cpp @@ -387,10 +387,7 @@ runtime::post_outcome runtime::do_post( if (!is_success(aws::http::response_code(http_response_code))) { logging::log_error( - LOG_TAG, - "Failed to post handler success response. Http response code: %ld. %s", - http_response_code, - resp.get_body().c_str()); + LOG_TAG, "Failed to post handler success response. Http response code: %ld.", http_response_code); return aws::http::response_code(http_response_code); } From 12eff27a66b60e5019afe9951e5d2a0b260d9fc8 Mon Sep 17 00:00:00 2001 From: Maxime David Date: Tue, 26 May 2026 15:37:14 +0000 Subject: [PATCH 05/10] fix: format --- src/runtime.cpp | 9 ++++----- tests/unit/unit_tests.cpp | 3 ++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/runtime.cpp b/src/runtime.cpp index 2d5eae5..be6bd80 100644 --- a/src/runtime.cpp +++ b/src/runtime.cpp @@ -165,11 +165,10 @@ static int rt_curl_debug_callback(CURL* handle, curl_infotype type, char* data, runtime::runtime(std::string const& endpoint) : runtime(endpoint, "AWS_Lambda_Cpp/" + std::string(get_version())) {} runtime::runtime(std::string const& endpoint, std::string const& user_agent) - : m_user_agent_header("User-Agent: " + user_agent), - m_endpoints{ - {endpoint + "/2018-06-01/runtime/init/error", - endpoint + "/2018-06-01/runtime/invocation/next", - endpoint + "/2018-06-01/runtime/invocation/"}} + : m_user_agent_header("User-Agent: " + user_agent), m_endpoints{ + {endpoint + "/2018-06-01/runtime/init/error", + endpoint + "/2018-06-01/runtime/invocation/next", + endpoint + "/2018-06-01/runtime/invocation/"}} { if (!lambda_runtime::m_curl_handle) { logging::log_error(LOG_TAG, "Failed to acquire curl easy handle for next."); diff --git a/tests/unit/unit_tests.cpp b/tests/unit/unit_tests.cpp index 3c6e718..315840b 100644 --- a/tests/unit/unit_tests.cpp +++ b/tests/unit/unit_tests.cpp @@ -264,7 +264,8 @@ TEST(VersionTest, version_format) std::string v = get_version(); int dots = 0; for (char c : v) { - if (c == '.') dots++; + if (c == '.') + dots++; } EXPECT_EQ(2, dots); } From 3a731044d552d87b84a896a889c64253d16bcf01 Mon Sep 17 00:00:00 2001 From: Maxime David Date: Tue, 26 May 2026 15:42:56 +0000 Subject: [PATCH 06/10] Fix unit test compilation errors and remove unsupported clang-tidy key Align unit tests with actual API signatures: failure() takes 2 args, invocation_response constructor takes 3, get_header() returns outcome. Remove ExcludeHeaderFilter from .clang-tidy as it's unsupported by CI. --- .clang-tidy | 1 - tests/unit/unit_tests.cpp | 43 +++++++++++++-------------------------- 2 files changed, 14 insertions(+), 30 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 616d471..d42139e 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -4,7 +4,6 @@ Checks: -modernize-use-trailing-return-type,-bugprone-easily-swappable-parameters,-readability-identifier-length' WarningsAsErrors: '*' HeaderFilterRegex: 'include/aws/.*\.h$' -ExcludeHeaderFilter: 'build/_deps/gtest-src.*' FormatStyle: 'none' CheckOptions: - key: modernize-pass-by-value.ValuesOnly diff --git a/tests/unit/unit_tests.cpp b/tests/unit/unit_tests.cpp index 315840b..acfef45 100644 --- a/tests/unit/unit_tests.cpp +++ b/tests/unit/unit_tests.cpp @@ -34,14 +34,14 @@ TEST(InvocationResponseTest, success_response_with_empty_payload) TEST(InvocationResponseTest, failure_response_is_not_success) { - auto resp = invocation_response::failure("something broke", "RuntimeError", ""); + auto resp = invocation_response::failure("something broke", "RuntimeError"); EXPECT_FALSE(resp.is_success()); EXPECT_EQ("application/json", resp.get_content_type()); } TEST(InvocationResponseTest, failure_response_contains_error_message) { - auto resp = invocation_response::failure("something broke", "RuntimeError", ""); + auto resp = invocation_response::failure("something broke", "RuntimeError"); auto const& payload = resp.get_payload(); EXPECT_NE(std::string::npos, payload.find("something broke")); EXPECT_NE(std::string::npos, payload.find("RuntimeError")); @@ -49,7 +49,7 @@ TEST(InvocationResponseTest, failure_response_contains_error_message) TEST(InvocationResponseTest, failure_response_json_escapes_quotes) { - auto resp = invocation_response::failure(R"(error with "quotes")", "TestError", ""); + auto resp = invocation_response::failure(R"(error with "quotes")", "TestError"); auto const& payload = resp.get_payload(); EXPECT_NE(std::string::npos, payload.find(R"(error with \"quotes\")")); EXPECT_EQ(std::string::npos, payload.find(R"(error with "quotes")")); @@ -57,21 +57,21 @@ TEST(InvocationResponseTest, failure_response_json_escapes_quotes) TEST(InvocationResponseTest, failure_response_json_escapes_backslash) { - auto resp = invocation_response::failure(R"(path\to\file)", "TestError", ""); + auto resp = invocation_response::failure(R"(path\to\file)", "TestError"); auto const& payload = resp.get_payload(); EXPECT_NE(std::string::npos, payload.find(R"(path\\to\\file)")); } TEST(InvocationResponseTest, failure_response_json_escapes_newlines) { - auto resp = invocation_response::failure("line1\nline2\r\n", "TestError", ""); + auto resp = invocation_response::failure("line1\nline2\r\n", "TestError"); auto const& payload = resp.get_payload(); EXPECT_NE(std::string::npos, payload.find(R"(line1\nline2\r\n)")); } TEST(InvocationResponseTest, failure_response_json_escapes_tabs) { - auto resp = invocation_response::failure("col1\tcol2", "TestError", ""); + auto resp = invocation_response::failure("col1\tcol2", "TestError"); auto const& payload = resp.get_payload(); EXPECT_NE(std::string::npos, payload.find(R"(col1\tcol2)")); } @@ -80,17 +80,11 @@ TEST(InvocationResponseTest, failure_response_json_escapes_control_characters) { std::string msg = "null\x00 byte"; msg.push_back('\x01'); - auto resp = invocation_response::failure(msg, "TestError", ""); + auto resp = invocation_response::failure(msg, "TestError"); auto const& payload = resp.get_payload(); EXPECT_NE(std::string::npos, payload.find("\\u0001")); } -TEST(InvocationResponseTest, failure_response_preserves_xray_response) -{ - auto resp = invocation_response::failure("err", "Type", "xray-data-here"); - EXPECT_EQ("xray-data-here", resp.get_xray_response()); -} - TEST(InvocationResponseTest, success_response_with_binary_content_type) { std::string binary_data(256, '\0'); @@ -104,10 +98,9 @@ TEST(InvocationResponseTest, success_response_with_binary_content_type) TEST(InvocationResponseTest, constructor_based_failure) { - auto resp = invocation_response(R"({"custom":"error"})", "application/json", false, "xray"); + auto resp = invocation_response(R"({"custom":"error"})", "application/json", false); EXPECT_FALSE(resp.is_success()); EXPECT_EQ(R"({"custom":"error"})", resp.get_payload()); - EXPECT_EQ("xray", resp.get_xray_response()); } // --- http::response tests --- @@ -117,7 +110,9 @@ TEST(HttpResponseTest, add_and_retrieve_header) response resp; resp.add_header("Content-Type", "application/json"); EXPECT_TRUE(resp.has_header("content-type")); - EXPECT_EQ("application/json", resp.get_header("content-type")); + auto header = resp.get_header("content-type"); + EXPECT_TRUE(header.is_success()); + EXPECT_EQ("application/json", header.get_result()); } TEST(HttpResponseTest, headers_are_lowercased) @@ -162,9 +157,9 @@ TEST(HttpResponseTest, multiple_headers) resp.add_header("lambda-runtime-aws-request-id", "req-123"); resp.add_header("lambda-runtime-trace-id", "trace-456"); resp.add_header("lambda-runtime-deadline-ms", "1234567890"); - EXPECT_EQ("req-123", resp.get_header("lambda-runtime-aws-request-id")); - EXPECT_EQ("trace-456", resp.get_header("lambda-runtime-trace-id")); - EXPECT_EQ("1234567890", resp.get_header("lambda-runtime-deadline-ms")); + EXPECT_EQ("req-123", resp.get_header("lambda-runtime-aws-request-id").get_result()); + EXPECT_EQ("trace-456", resp.get_header("lambda-runtime-trace-id").get_result()); + EXPECT_EQ("1234567890", resp.get_header("lambda-runtime-deadline-ms").get_result()); } // --- outcome tests --- @@ -241,16 +236,6 @@ TEST(InvocationRequestTest, default_fields_are_empty) EXPECT_TRUE(req.tenant_id.empty()); } -// --- runtime_response tests --- - -TEST(RuntimeResponseTest, constructor_sets_all_fields) -{ - runtime_response resp("payload", "application/json", "xray"); - EXPECT_EQ("payload", resp.get_payload()); - EXPECT_EQ("application/json", resp.get_content_type()); - EXPECT_EQ("xray", resp.get_xray_response()); -} - // --- version tests (no AWS SDK needed) --- TEST(VersionTest, version_string_not_empty) From f2ad9052c3d83ed98f9f876c665078ac6a25cd6d Mon Sep 17 00:00:00 2001 From: Maxime David Date: Tue, 26 May 2026 15:45:15 +0000 Subject: [PATCH 07/10] Suppress clang-tidy checks unsupported by older versions Disable performance-enum-size, misc-use-anonymous-namespace, and misc-include-cleaner which trigger on pre-existing code with newer clang-tidy versions in CI. --- .clang-tidy | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.clang-tidy b/.clang-tidy index d42139e..af532d3 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,7 +1,8 @@ --- Checks: 'clang-diagnostic-*,clang-analyzer-*,performance-*,readability-*,modernize-*,bugprone-*,misc-*, --modernize-use-trailing-return-type,-bugprone-easily-swappable-parameters,-readability-identifier-length' +-modernize-use-trailing-return-type,-bugprone-easily-swappable-parameters,-readability-identifier-length, +-performance-enum-size,-misc-use-anonymous-namespace,-misc-include-cleaner' WarningsAsErrors: '*' HeaderFilterRegex: 'include/aws/.*\.h$' FormatStyle: 'none' From 9f31e9dfe99e0c6bfd56ba2db5057b13582630b4 Mon Sep 17 00:00:00 2001 From: Maxime David Date: Tue, 26 May 2026 15:49:44 +0000 Subject: [PATCH 08/10] Fix clang-tidy warnings in runtime and response code - Remove redundant inline specifier on in-class method - Add const to variables that are never modified - Use = default for trivial destructor - Make set_curl_post_result_options static (no instance members used) --- include/aws/http/response.h | 2 +- include/aws/lambda-runtime/runtime.h | 2 +- src/runtime.cpp | 18 +++++++----------- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/include/aws/http/response.h b/include/aws/http/response.h index 3740c22..f34659d 100644 --- a/include/aws/http/response.h +++ b/include/aws/http/response.h @@ -34,7 +34,7 @@ class response { inline void append_body(const char* p, size_t sz); inline bool has_header(char const* header) const; inline lambda_runtime::outcome get_header(char const* header) const; - inline response_code get_response_code() const { return m_response_code; } + response_code get_response_code() const { return m_response_code; } inline void set_response_code(aws::http::response_code c); inline void set_content_type(char const* ct); inline std::string const& get_body() const; diff --git a/include/aws/lambda-runtime/runtime.h b/include/aws/lambda-runtime/runtime.h index 0b2b90e..44244ad 100644 --- a/include/aws/lambda-runtime/runtime.h +++ b/include/aws/lambda-runtime/runtime.h @@ -167,7 +167,7 @@ class runtime { private: void set_curl_next_options(); - void set_curl_post_result_options(); + static void set_curl_post_result_options(); post_outcome do_post( std::string const& url, std::string const& request_id, diff --git a/src/runtime.cpp b/src/runtime.cpp index be6bd80..0bdf472 100644 --- a/src/runtime.cpp +++ b/src/runtime.cpp @@ -156,7 +156,7 @@ static int rt_curl_debug_callback(CURL* handle, curl_infotype type, char* data, (void)handle; (void)type; (void)userdata; - std::string s(data, size); + const std::string s(data, size); logging::log_debug(LOG_TAG, "CURL DBG: %s", s.c_str()); return 0; } @@ -175,11 +175,7 @@ runtime::runtime(std::string const& endpoint, std::string const& user_agent) } } -runtime::~runtime() -{ - // The curl handle is thread_local and outlives any single runtime instance. - // Cleanup happens automatically when the thread exits. -} +runtime::~runtime() = default; void runtime::set_curl_next_options() { @@ -240,7 +236,7 @@ runtime::next_outcome runtime::get_next() curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HTTPHEADER, headers); logging::log_debug(LOG_TAG, "Making request to %s", m_endpoints[Endpoints::NEXT].c_str()); - CURLcode curl_code = curl_easy_perform(lambda_runtime::m_curl_handle); + const CURLcode curl_code = curl_easy_perform(lambda_runtime::m_curl_handle); logging::log_debug(LOG_TAG, "Completed request to %s", m_endpoints[Endpoints::NEXT].c_str()); curl_slist_free_all(headers); @@ -312,7 +308,7 @@ runtime::next_outcome runtime::get_next() if (out.is_success()) { auto const& deadline_string = std::move(out).get_result(); constexpr int base = 10; - unsigned long ms = strtoul(deadline_string.c_str(), nullptr, base); + const unsigned long ms = strtoul(deadline_string.c_str(), nullptr, base); assert(ms > 0); assert(ms < ULONG_MAX); req.deadline += std::chrono::milliseconds(ms); @@ -368,7 +364,7 @@ runtime::post_outcome runtime::do_post( curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HEADERDATA, &resp); curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_READDATA, &ctx); curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HTTPHEADER, headers); - CURLcode curl_code = curl_easy_perform(lambda_runtime::m_curl_handle); + const CURLcode curl_code = curl_easy_perform(lambda_runtime::m_curl_handle); curl_slist_free_all(headers); if (curl_code != CURLE_OK) { @@ -448,7 +444,7 @@ void run_handler(std::function c auto const req = std::move(next_outcome).get_result(); logging::log_info(LOG_TAG, "Invoking user handler"); - invocation_response res = handler(req); + const invocation_response res = handler(req); logging::log_info(LOG_TAG, "Invoking user handler completed."); if (res.is_success()) { @@ -476,7 +472,7 @@ static std::string json_escape(std::string const& in) constexpr char last_non_printable_character = 31; std::string out; out.reserve(in.length()); // most strings will end up identical - for (char ch : in) { + for (const char ch : in) { if (ch > last_non_printable_character && ch != '\"' && ch != '\\') { out.append(1, ch); } From 14d6f702f16cb71a5a1eebe0c0a2867c70c33082 Mon Sep 17 00:00:00 2001 From: Maxime David Date: Tue, 26 May 2026 15:51:54 +0000 Subject: [PATCH 09/10] Fix all clang-tidy warnings and revert .clang-tidy to master - Add std::uint8_t base type to verbosity enum (performance-enum-size) - Move get_prefix to anonymous namespace (misc-use-anonymous-namespace) - Add direct include in logging.cpp (misc-include-cleaner) - Remove redundant inline on in-class method (readability-redundant-inline-specifier) - Add const qualifiers to unmodified variables (misc-const-correctness) - Use = default for trivial destructor (modernize-use-equals-default) - Make set_curl_post_result_options static (readability-convert-member-functions-to-static) --- .clang-tidy | 4 ++-- include/aws/logging/logging.h | 3 ++- src/logging.cpp | 6 +++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index af532d3..616d471 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,10 +1,10 @@ --- Checks: 'clang-diagnostic-*,clang-analyzer-*,performance-*,readability-*,modernize-*,bugprone-*,misc-*, --modernize-use-trailing-return-type,-bugprone-easily-swappable-parameters,-readability-identifier-length, --performance-enum-size,-misc-use-anonymous-namespace,-misc-include-cleaner' +-modernize-use-trailing-return-type,-bugprone-easily-swappable-parameters,-readability-identifier-length' WarningsAsErrors: '*' HeaderFilterRegex: 'include/aws/.*\.h$' +ExcludeHeaderFilter: 'build/_deps/gtest-src.*' FormatStyle: 'none' CheckOptions: - key: modernize-pass-by-value.ValuesOnly diff --git a/include/aws/logging/logging.h b/include/aws/logging/logging.h index 0b5d0ef..31e1308 100644 --- a/include/aws/logging/logging.h +++ b/include/aws/logging/logging.h @@ -15,11 +15,12 @@ */ #include +#include namespace aws { namespace logging { -enum class verbosity { +enum class verbosity : std::uint8_t { error, info, debug, diff --git a/src/logging.cpp b/src/logging.cpp index e68aafa..d04bb5d 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -14,6 +14,7 @@ */ #include "aws/logging/logging.h" #include +#include #include #include @@ -21,8 +22,9 @@ namespace aws { namespace logging { +namespace { -static inline char const* get_prefix(verbosity v) +inline char const* get_prefix(verbosity v) { switch (v) { case verbosity::error: @@ -36,6 +38,8 @@ static inline char const* get_prefix(verbosity v) } } +} // namespace + LAMBDA_RUNTIME_API void log(verbosity v, char const* tag, char const* msg, va_list args) { From 1415a024bcbb0cd748dc0ee318a2a624da4a22ca Mon Sep 17 00:00:00 2001 From: Maxime David Date: Tue, 26 May 2026 15:57:12 +0000 Subject: [PATCH 10/10] Remove no-op placeholder test No longer needed now that real unit tests are in place. --- tests/CMakeLists.txt | 2 -- tests/unit/no_op_test.cpp | 6 ------ 2 files changed, 8 deletions(-) delete mode 100644 tests/unit/no_op_test.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1304787..2774aa2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,7 +15,6 @@ if(DEFINED ENV{GITHUB_ACTIONS}) FetchContent_MakeAvailable(gtest) add_executable(unit_tests - unit/no_op_test.cpp unit/thread_local_curl_test.cpp unit/unit_tests.cpp) target_link_libraries(unit_tests PRIVATE gtest_main aws-lambda-runtime) @@ -29,7 +28,6 @@ if(DEFINED ENV{GITHUB_ACTIONS}) else() # Build unit tests using the bundled gtest add_executable(unit_tests - unit/no_op_test.cpp unit/thread_local_curl_test.cpp unit/unit_tests.cpp gtest/gtest-all.cc) diff --git a/tests/unit/no_op_test.cpp b/tests/unit/no_op_test.cpp deleted file mode 100644 index c9a3b7d..0000000 --- a/tests/unit/no_op_test.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include - -TEST(noop, dummy_test) -{ - ASSERT_EQ(0, 0); -}