|
| 1 | +/* |
| 2 | + This file is part of libhttpserver |
| 3 | + Copyright (C) 2011-2026 Sebastiano Merlino |
| 4 | +
|
| 5 | + This library is free software; you can redistribute it and/or |
| 6 | + modify it under the terms of the GNU Lesser General Public |
| 7 | + License as published by the Free Software Foundation; either |
| 8 | + version 2.1 of the License, or (at your option) any later version. |
| 9 | +
|
| 10 | + This library is distributed in the hope that it will be useful, |
| 11 | + but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 12 | + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| 13 | + Lesser General Public License for more details. |
| 14 | +
|
| 15 | + You should have received a copy of the GNU Lesser General Public |
| 16 | + License along with this library; if not, write to the Free Software |
| 17 | + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 |
| 18 | + USA |
| 19 | +*/ |
| 20 | + |
| 21 | +// TASK-023: smart-pointer register_resource overloads. |
| 22 | +// |
| 23 | +// This TU pins both the compile-time signature contract for the new |
| 24 | +// register_resource overloads and the runtime ownership semantics: |
| 25 | +// - unique_ptr overload: webserver takes ownership; resource dtor |
| 26 | +// runs when the webserver is destroyed (acceptance criterion). |
| 27 | +// - shared_ptr overload: caller retains a reference; resource lives |
| 28 | +// as long as either the webserver or the caller holds it. |
| 29 | +// - Null smart-pointer arguments throw std::invalid_argument. |
| 30 | +// - Duplicate registrations throw std::invalid_argument (replacing |
| 31 | +// the v1 "return false" silent-fail behavior, which is gone with |
| 32 | +// the new void return type). |
| 33 | + |
| 34 | +#include <curl/curl.h> |
| 35 | + |
| 36 | +#include <atomic> |
| 37 | +#include <memory> |
| 38 | +#include <stdexcept> |
| 39 | +#include <string> |
| 40 | +#include <type_traits> |
| 41 | + |
| 42 | +#include "./httpserver.hpp" |
| 43 | +#include "./littletest.hpp" |
| 44 | + |
| 45 | +using httpserver::create_webserver; |
| 46 | +using httpserver::http_request; |
| 47 | +using httpserver::http_resource; |
| 48 | +using httpserver::http_response; |
| 49 | +using httpserver::webserver; |
| 50 | + |
| 51 | +#define PORT 8080 |
| 52 | + |
| 53 | +namespace { |
| 54 | + |
| 55 | +size_t writefunc(void* ptr, size_t size, size_t nmemb, std::string* s) { |
| 56 | + s->append(reinterpret_cast<char*>(ptr), size * nmemb); |
| 57 | + return size * nmemb; |
| 58 | +} |
| 59 | + |
| 60 | +class ok_resource : public http_resource { |
| 61 | + public: |
| 62 | + std::shared_ptr<http_response> render_get(const http_request&) override { |
| 63 | + return std::make_shared<http_response>(http_response::string("OK")); |
| 64 | + } |
| 65 | +}; |
| 66 | + |
| 67 | +// Resource whose destructor increments a static counter, so tests can |
| 68 | +// observe ownership-driven destruction. |
| 69 | +class counted_resource : public http_resource { |
| 70 | + public: |
| 71 | + static std::atomic<int> dtor_count; |
| 72 | + |
| 73 | + counted_resource() = default; |
| 74 | + ~counted_resource() override { ++dtor_count; } |
| 75 | + |
| 76 | + std::shared_ptr<http_response> render_get(const http_request&) override { |
| 77 | + return std::make_shared<http_response>(http_response::string("OK")); |
| 78 | + } |
| 79 | +}; |
| 80 | + |
| 81 | +std::atomic<int> counted_resource::dtor_count{0}; |
| 82 | + |
| 83 | +} // namespace |
| 84 | + |
| 85 | +// ---- Compile-time signature contract ----------------------------------- |
| 86 | +// |
| 87 | +// Probing overloaded members through `decltype` of a call expression: a |
| 88 | +// well-formed call means the overload exists with the given argument |
| 89 | +// shape; an ill-formed expression would fail SFINAE inside the |
| 90 | +// surrounding decltype. We exercise each overload through its natural |
| 91 | +// call syntax. |
| 92 | + |
| 93 | +// (1) unique_ptr overload exists and returns void. |
| 94 | +static_assert(std::is_same_v< |
| 95 | + decltype(std::declval<webserver&>().register_resource( |
| 96 | + std::declval<const std::string&>(), |
| 97 | + std::declval<std::unique_ptr<http_resource>>())), |
| 98 | + void>, |
| 99 | + "register_resource(const string&, unique_ptr<http_resource>) " |
| 100 | + "must exist and return void"); |
| 101 | + |
| 102 | +// (2) shared_ptr overload exists and returns void. |
| 103 | +static_assert(std::is_same_v< |
| 104 | + decltype(std::declval<webserver&>().register_resource( |
| 105 | + std::declval<const std::string&>(), |
| 106 | + std::declval<std::shared_ptr<http_resource>>())), |
| 107 | + void>, |
| 108 | + "register_resource(const string&, shared_ptr<http_resource>) " |
| 109 | + "must exist and return void"); |
| 110 | + |
| 111 | +// (3) The bool-family parameter remains on both overloads so TASK-024 |
| 112 | +// can do the register_path/register_prefix split in one go without |
| 113 | +// re-touching every call site twice. |
| 114 | +static_assert(std::is_same_v< |
| 115 | + decltype(std::declval<webserver&>().register_resource( |
| 116 | + std::declval<const std::string&>(), |
| 117 | + std::declval<std::unique_ptr<http_resource>>(), |
| 118 | + true)), |
| 119 | + void>, |
| 120 | + "unique_ptr overload must accept a trailing bool family arg"); |
| 121 | +static_assert(std::is_same_v< |
| 122 | + decltype(std::declval<webserver&>().register_resource( |
| 123 | + std::declval<const std::string&>(), |
| 124 | + std::declval<std::shared_ptr<http_resource>>(), |
| 125 | + true)), |
| 126 | + void>, |
| 127 | + "shared_ptr overload must accept a trailing bool family arg"); |
| 128 | + |
| 129 | +// (4) Negative: the raw-pointer overload must be gone. Use a SFINAE |
| 130 | +// helper so this is observable as a compile-time bool. |
| 131 | +template <typename = void> |
| 132 | +struct has_raw_register_resource : std::false_type {}; |
| 133 | + |
| 134 | +template <> |
| 135 | +struct has_raw_register_resource<std::void_t< |
| 136 | + decltype(std::declval<webserver&>().register_resource( |
| 137 | + std::declval<const std::string&>(), |
| 138 | + std::declval<http_resource*>()))>> : std::true_type {}; |
| 139 | + |
| 140 | +static_assert(!has_raw_register_resource<>::value, |
| 141 | + "the raw-pointer register_resource overload must be removed"); |
| 142 | + |
| 143 | +// ---- Runtime ownership tests ------------------------------------------ |
| 144 | + |
| 145 | +LT_BEGIN_SUITE(webserver_register_smartptr_suite) |
| 146 | + void set_up() { |
| 147 | + counted_resource::dtor_count = 0; |
| 148 | + } |
| 149 | + |
| 150 | + void tear_down() {} |
| 151 | +LT_END_SUITE(webserver_register_smartptr_suite) |
| 152 | + |
| 153 | +// Acceptance criterion (verbatim from TASK-023 spec): |
| 154 | +// "auto r = std::make_unique<my_resource>(); |
| 155 | +// ws.register_resource('/foo', std::move(r)); compiles and serves." |
| 156 | +LT_BEGIN_AUTO_TEST(webserver_register_smartptr_suite, |
| 157 | + unique_ptr_overload_compiles_and_serves) |
| 158 | + webserver ws = create_webserver(PORT); |
| 159 | + auto r = std::make_unique<ok_resource>(); |
| 160 | + ws.register_resource("/foo", std::move(r)); |
| 161 | + ws.start(false); |
| 162 | + |
| 163 | + curl_global_init(CURL_GLOBAL_ALL); |
| 164 | + std::string s; |
| 165 | + CURL* curl = curl_easy_init(); |
| 166 | + curl_easy_setopt(curl, CURLOPT_URL, "localhost:8080/foo"); |
| 167 | + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); |
| 168 | + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); |
| 169 | + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); |
| 170 | + CURLcode res = curl_easy_perform(curl); |
| 171 | + LT_ASSERT_EQ(res, 0); |
| 172 | + LT_CHECK_EQ(s, std::string("OK")); |
| 173 | + curl_easy_cleanup(curl); |
| 174 | + ws.stop(); |
| 175 | +LT_END_AUTO_TEST(unique_ptr_overload_compiles_and_serves) |
| 176 | + |
| 177 | +// Acceptance criterion: "A test verifies the resource destructor runs |
| 178 | +// when the webserver is destroyed." |
| 179 | +LT_BEGIN_AUTO_TEST(webserver_register_smartptr_suite, |
| 180 | + unique_ptr_dtor_runs_on_webserver_destruction) |
| 181 | + LT_CHECK_EQ(counted_resource::dtor_count.load(), 0); |
| 182 | + { |
| 183 | + webserver ws = create_webserver(PORT + 1); |
| 184 | + ws.register_resource("/x", std::make_unique<counted_resource>()); |
| 185 | + // No start/stop needed — registration alone must transfer |
| 186 | + // ownership; webserver destruction must run the dtor. |
| 187 | + } |
| 188 | + LT_CHECK_EQ(counted_resource::dtor_count.load(), 1); |
| 189 | +LT_END_AUTO_TEST(unique_ptr_dtor_runs_on_webserver_destruction) |
| 190 | + |
| 191 | +// shared_ptr semantics: caller's shared_ptr keeps the resource alive |
| 192 | +// past webserver destruction. The dtor runs only once both refs drop. |
| 193 | +LT_BEGIN_AUTO_TEST(webserver_register_smartptr_suite, |
| 194 | + shared_ptr_caller_keeps_resource_alive) |
| 195 | + LT_CHECK_EQ(counted_resource::dtor_count.load(), 0); |
| 196 | + auto sp = std::make_shared<counted_resource>(); |
| 197 | + { |
| 198 | + webserver ws = create_webserver(PORT + 2); |
| 199 | + ws.register_resource("/x", sp); |
| 200 | + } |
| 201 | + // Webserver destroyed; caller still holds a ref. |
| 202 | + LT_CHECK_EQ(counted_resource::dtor_count.load(), 0); |
| 203 | + sp.reset(); |
| 204 | + LT_CHECK_EQ(counted_resource::dtor_count.load(), 1); |
| 205 | +LT_END_AUTO_TEST(shared_ptr_caller_keeps_resource_alive) |
| 206 | + |
| 207 | +LT_BEGIN_AUTO_TEST(webserver_register_smartptr_suite, |
| 208 | + null_unique_ptr_throws) |
| 209 | + webserver ws = create_webserver(PORT + 3); |
| 210 | + bool threw = false; |
| 211 | + try { |
| 212 | + ws.register_resource("/x", std::unique_ptr<http_resource>{}); |
| 213 | + } catch (const std::invalid_argument&) { |
| 214 | + threw = true; |
| 215 | + } |
| 216 | + LT_CHECK_EQ(threw, true); |
| 217 | +LT_END_AUTO_TEST(null_unique_ptr_throws) |
| 218 | + |
| 219 | +LT_BEGIN_AUTO_TEST(webserver_register_smartptr_suite, |
| 220 | + null_shared_ptr_throws) |
| 221 | + webserver ws = create_webserver(PORT + 4); |
| 222 | + bool threw = false; |
| 223 | + try { |
| 224 | + ws.register_resource("/x", std::shared_ptr<http_resource>{}); |
| 225 | + } catch (const std::invalid_argument&) { |
| 226 | + threw = true; |
| 227 | + } |
| 228 | + LT_CHECK_EQ(threw, true); |
| 229 | +LT_END_AUTO_TEST(null_shared_ptr_throws) |
| 230 | + |
| 231 | +// New void-returning API replaces v1's silent `return false` on |
| 232 | +// duplicate registration with a throw, matching the existing |
| 233 | +// throw-on-null behavior. |
| 234 | +LT_BEGIN_AUTO_TEST(webserver_register_smartptr_suite, |
| 235 | + duplicate_registration_throws) |
| 236 | + webserver ws = create_webserver(PORT + 5); |
| 237 | + ws.register_resource("/dup", std::make_shared<ok_resource>()); |
| 238 | + bool threw = false; |
| 239 | + try { |
| 240 | + ws.register_resource("/dup", std::make_shared<ok_resource>()); |
| 241 | + } catch (const std::invalid_argument&) { |
| 242 | + threw = true; |
| 243 | + } |
| 244 | + LT_CHECK_EQ(threw, true); |
| 245 | +LT_END_AUTO_TEST(duplicate_registration_throws) |
| 246 | + |
| 247 | +LT_BEGIN_AUTO_TEST_ENV() |
| 248 | + AUTORUN_TESTS() |
| 249 | +LT_END_AUTO_TEST_ENV() |
0 commit comments