Skip to content

Commit 846b40c

Browse files
etrclaude
andcommitted
TASK-072: refactor unescape arena tests — helpers + tighter suite
Three improvements to http_request_unescape_arena_test.cpp: 1. decode_via_arena(const char*) helper: extracts the shared setup boilerplate (arena+impl+aa+build_request_args+teardown) from correctness tests (3)-(5). Each test body is now a single LT_CHECK_EQ assertion. (code-simplifier-iter1-3) 2. run_alloc_pin(unescaper_ptr, int warmup_rounds) helper: extracts the shared setup from the two headline zero-alloc-pin tests (1)/(2). The warmup_rounds parameter makes the reason for the different cold- call counts between the default-unescaper (1 warmup) and user- unescaper (2 warmups) paths explicit. (code-simplifier-iter1-6) 3. Suite comment: annotate the empty set_up/tear_down stubs as intentional (the per-test lifecycle lives in the helpers), removing the structural ambiguity. (code-simplifier-iter1-4) All 8 tests pass; 12 assertions (down from 16 because tests (3)-(5) are now single-assertion calls to decode_via_arena). Addresses: code-simplifier-iter1-3 (major), code-simplifier-iter1-4 (major), code-simplifier-iter1-6 (major) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 9cb548e commit 846b40c

1 file changed

Lines changed: 88 additions & 140 deletions

File tree

test/unit/http_request_unescape_arena_test.cpp

Lines changed: 88 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -154,193 +154,149 @@ void prepend_x_dash_unescaper(std::string& val) {
154154
// default unescaper does real work (decodes to '/').
155155
constexpr const char* kLongValueWithPct2F = "a%2Fbcdefghijklmnopqrstuvwxyz_padding_to_force_heap_allocation"; // NOLINT(whitespace/line_length)
156156

157-
} // namespace
158-
159-
LT_BEGIN_SUITE(http_request_unescape_arena_suite)
160-
void set_up() {
161-
}
162-
void tear_down() {
163-
}
164-
LT_END_SUITE(http_request_unescape_arena_suite)
165-
166-
// (1) Headline pin -- default unescaper. With a value strictly longer
167-
// than std::string's SSO threshold, the v1 code path copies into a
168-
// std::string temporary that is guaranteed to allocate on the global
169-
// heap. After the TASK-072 fix the unescape destination is materialised
170-
// in the per-connection arena and no global-heap allocation occurs in
171-
// the build_request_args call itself.
157+
// ---------------------------------------------------------------------------
158+
// Helpers shared across the correctness tests (3-6).
172159
//
173-
// The test wraps the build_request_args call window in a
174-
// `count_new_window` RAII guard that toggles a global operator-new
175-
// counter; the assertion is that during the warm window the counter
176-
// does not advance.
177-
LT_BEGIN_AUTO_TEST(http_request_unescape_arena_suite,
178-
warm_path_zero_global_allocs_default_unescape)
160+
// decode_via_arena: constructs a fresh arena-backed impl+accumulator,
161+
// calls build_request_args once for (key, raw_value) with no unescaper,
162+
// retrieves the stored value, deletes the impl, and returns the decoded
163+
// std::string. Each correctness test is a single LT_CHECK_EQ call.
164+
// (code-simplifier-iter1-3)
165+
// ---------------------------------------------------------------------------
166+
using httpserver::detail::http_request_impl;
167+
using httpserver::detail::arguments_accumulator;
168+
using impl_alloc_t = std::pmr::polymorphic_allocator<http_request_impl>;
169+
170+
std::string decode_via_arena(const char* raw_value) {
179171
alignas(std::max_align_t) std::array<std::byte, 8192> buf{};
180172
std::pmr::monotonic_buffer_resource arena(buf.data(), buf.size(),
181173
std::pmr::new_delete_resource());
182-
183-
using httpserver::detail::http_request_impl;
184-
using httpserver::detail::arguments_accumulator;
185-
using impl_alloc_t = std::pmr::polymorphic_allocator<http_request_impl>;
186-
187174
impl_alloc_t alloc(&arena);
188175
auto* p = alloc.new_object<http_request_impl>(nullptr, nullptr, alloc);
189176

190177
arguments_accumulator aa;
191178
aa.unescaper = nullptr;
192179
aa.arguments = &p->unescaped_args;
193180

194-
// Cold call: may populate any one-shot internal caches.
195181
http_request_impl::build_request_args(
196-
&aa, MHD_GET_ARGUMENT_KIND, "warmup", kLongValueWithPct2F);
182+
&aa, MHD_GET_ARGUMENT_KIND, "k", raw_value);
197183

198-
// Now measure a second call. The arena has already been grown by
199-
// the cold call; the warm call must consume only arena memory and
200-
// never touch the global heap.
201-
{
202-
count_new_window window;
203-
http_request_impl::build_request_args(
204-
&aa, MHD_GET_ARGUMENT_KIND, "key", kLongValueWithPct2F);
205-
LT_CHECK_EQ(window.count(), std::size_t{0});
184+
std::string result;
185+
auto it = p->unescaped_args.find(std::string_view("k"));
186+
if (it != p->unescaped_args.end() && !it->second.empty()) {
187+
result.assign(it->second[0].data(), it->second[0].size());
206188
}
207189

208190
alloc.delete_object(p);
209-
LT_END_AUTO_TEST(warm_path_zero_global_allocs_default_unescape)
191+
return result;
192+
}
210193

211-
// (2) Headline pin -- user-registered unescaper. Same invariant must
212-
// hold when the user-callback path is exercised. A per-thread scratch
213-
// std::string amortises its capacity across requests on the same
214-
// thread; the first cold call grows it once, the warm call after that
215-
// must consume no further global heap.
216-
LT_BEGIN_AUTO_TEST(http_request_unescape_arena_suite,
217-
warm_path_zero_global_allocs_user_unescape)
194+
// ---------------------------------------------------------------------------
195+
// Helper for the two headline alloc-pin tests (1) and (2).
196+
//
197+
// run_alloc_pin: constructs a fresh arena-backed impl+accumulator,
198+
// runs `warmup_rounds` cold calls to prime caches and grow any
199+
// thread_local buffers, then opens a count_new_window and runs one
200+
// measured call, returning the observed global-allocation count.
201+
// (code-simplifier-iter1-6)
202+
// ---------------------------------------------------------------------------
203+
std::size_t run_alloc_pin(httpserver::unescaper_ptr fn,
204+
int warmup_rounds) {
218205
alignas(std::max_align_t) std::array<std::byte, 8192> buf{};
219206
std::pmr::monotonic_buffer_resource arena(buf.data(), buf.size(),
220207
std::pmr::new_delete_resource());
221-
222-
using httpserver::detail::http_request_impl;
223-
using httpserver::detail::arguments_accumulator;
224-
using impl_alloc_t = std::pmr::polymorphic_allocator<http_request_impl>;
225-
226208
impl_alloc_t alloc(&arena);
227209
auto* p = alloc.new_object<http_request_impl>(nullptr, nullptr, alloc);
228210

229211
arguments_accumulator aa;
230-
aa.unescaper = &passthrough_unescaper;
212+
aa.unescaper = fn;
231213
aa.arguments = &p->unescaped_args;
232214

233-
// Two cold calls: warm any one-shot impl-internal caches AND grow
234-
// the per-thread user-scratch std::string capacity, so the warm
235-
// window below measures only steady-state behaviour.
236-
http_request_impl::build_request_args(
237-
&aa, MHD_GET_ARGUMENT_KIND, "warmup1", kLongValueWithPct2F);
238-
http_request_impl::build_request_args(
239-
&aa, MHD_GET_ARGUMENT_KIND, "warmup2", kLongValueWithPct2F);
215+
for (int i = 0; i < warmup_rounds; ++i) {
216+
// Use a unique key per warmup call so each inserts into a fresh
217+
// map slot; this avoids accumulating a large vector under one key.
218+
const std::string warmup_key = "warmup" + std::to_string(i);
219+
http_request_impl::build_request_args(
220+
&aa, MHD_GET_ARGUMENT_KIND, warmup_key.c_str(),
221+
kLongValueWithPct2F);
222+
}
240223

224+
std::size_t alloc_count = 0;
241225
{
242226
count_new_window window;
243227
http_request_impl::build_request_args(
244228
&aa, MHD_GET_ARGUMENT_KIND, "key", kLongValueWithPct2F);
245-
LT_CHECK_EQ(window.count(), std::size_t{0});
229+
alloc_count = window.count();
246230
}
247231

248232
alloc.delete_object(p);
249-
LT_END_AUTO_TEST(warm_path_zero_global_allocs_user_unescape)
250-
251-
// (3) Correctness: "%2F" decodes to '/' on the default-unescaper path.
252-
LT_BEGIN_AUTO_TEST(http_request_unescape_arena_suite,
253-
unescape_pct2f_decodes_to_slash)
254-
using httpserver::detail::http_request_impl;
255-
using httpserver::detail::arguments_accumulator;
256-
using impl_alloc_t = std::pmr::polymorphic_allocator<http_request_impl>;
233+
return alloc_count;
234+
}
257235

258-
alignas(std::max_align_t) std::array<std::byte, 8192> buf{};
259-
std::pmr::monotonic_buffer_resource arena(buf.data(), buf.size(),
260-
std::pmr::new_delete_resource());
261-
impl_alloc_t alloc(&arena);
262-
auto* p = alloc.new_object<http_request_impl>(nullptr, nullptr, alloc);
236+
} // namespace
263237

264-
arguments_accumulator aa;
265-
aa.unescaper = nullptr;
266-
aa.arguments = &p->unescaped_args;
238+
// The suite has no shared fixture state: each test constructs its own
239+
// arena+impl via the helpers above. set_up/tear_down are present
240+
// (required by the littletest template) but empty; the comments
241+
// signal this is intentional, not an oversight.
242+
// (code-simplifier-iter1-4)
243+
LT_BEGIN_SUITE(http_request_unescape_arena_suite)
244+
void set_up() {} // per-test setup is in the helpers above
245+
void tear_down() {} // per-test teardown is in the helpers above
246+
LT_END_SUITE(http_request_unescape_arena_suite)
267247

268-
http_request_impl::build_request_args(
269-
&aa, MHD_GET_ARGUMENT_KIND, "k", "a%2Fb");
248+
// (1) Headline pin -- default unescaper. With a value strictly longer
249+
// than std::string's SSO threshold, the v1 code path copies into a
250+
// std::string temporary that is guaranteed to allocate on the global
251+
// heap. After the TASK-072 fix the unescape destination is materialised
252+
// in the per-connection arena and no global-heap allocation occurs in
253+
// the build_request_args call itself.
254+
//
255+
// One cold warmup call primes the impl-internal caches. The warm call
256+
// must consume only arena memory.
257+
LT_BEGIN_AUTO_TEST(http_request_unescape_arena_suite,
258+
warm_path_zero_global_allocs_default_unescape)
259+
// 1 warmup: primes impl-internal caches; no thread_local to grow on
260+
// the default-unescaper path.
261+
LT_CHECK_EQ(run_alloc_pin(nullptr, 1), std::size_t{0});
262+
LT_END_AUTO_TEST(warm_path_zero_global_allocs_default_unescape)
270263

271-
auto it = p->unescaped_args.find(std::string_view("k"));
272-
LT_CHECK(it != p->unescaped_args.end());
273-
LT_CHECK_EQ(it->second.size(), std::size_t{1});
274-
LT_CHECK_EQ(std::string(it->second[0].data(), it->second[0].size()),
275-
std::string("a/b"));
264+
// (2) Headline pin -- user-registered unescaper. Same invariant must
265+
// hold when the user-callback path is exercised. A per-thread scratch
266+
// std::string (thread_value in unescape_in_arena) amortises its capacity
267+
// across requests on the same thread.
268+
//
269+
// Two cold warmup calls are required: the first grows the thread_local
270+
// buffer to the peak value length; the second confirms steady-state. The
271+
// warm call after that must consume no further global heap.
272+
LT_BEGIN_AUTO_TEST(http_request_unescape_arena_suite,
273+
warm_path_zero_global_allocs_user_unescape)
274+
// 2 warmups: first grows thread_value capacity; second confirms it.
275+
LT_CHECK_EQ(run_alloc_pin(&passthrough_unescaper, 2), std::size_t{0});
276+
LT_END_AUTO_TEST(warm_path_zero_global_allocs_user_unescape)
276277

277-
alloc.delete_object(p);
278+
// (3) Correctness: "%2F" decodes to '/' on the default-unescaper path.
279+
LT_BEGIN_AUTO_TEST(http_request_unescape_arena_suite,
280+
unescape_pct2f_decodes_to_slash)
281+
LT_CHECK_EQ(decode_via_arena("a%2Fb"), std::string("a/b"));
278282
LT_END_AUTO_TEST(unescape_pct2f_decodes_to_slash)
279283

280284
// (4) Invalid hex passthrough: "%%" stays as "%%" (consistent with
281285
// http_unescape's fall-through behavior on non-hex digits after '%').
282286
LT_BEGIN_AUTO_TEST(http_request_unescape_arena_suite,
283287
unescape_double_percent_passthrough)
284-
using httpserver::detail::http_request_impl;
285-
using httpserver::detail::arguments_accumulator;
286-
using impl_alloc_t = std::pmr::polymorphic_allocator<http_request_impl>;
287-
288-
alignas(std::max_align_t) std::array<std::byte, 8192> buf{};
289-
std::pmr::monotonic_buffer_resource arena(buf.data(), buf.size(),
290-
std::pmr::new_delete_resource());
291-
impl_alloc_t alloc(&arena);
292-
auto* p = alloc.new_object<http_request_impl>(nullptr, nullptr, alloc);
293-
294-
arguments_accumulator aa;
295-
aa.unescaper = nullptr;
296-
aa.arguments = &p->unescaped_args;
297-
298-
http_request_impl::build_request_args(
299-
&aa, MHD_GET_ARGUMENT_KIND, "k", "a%%b");
300-
301-
auto it = p->unescaped_args.find(std::string_view("k"));
302-
LT_CHECK(it != p->unescaped_args.end());
303-
LT_CHECK_EQ(std::string(it->second[0].data(), it->second[0].size()),
304-
std::string("a%%b"));
305-
306-
alloc.delete_object(p);
288+
LT_CHECK_EQ(decode_via_arena("a%%b"), std::string("a%%b"));
307289
LT_END_AUTO_TEST(unescape_double_percent_passthrough)
308290

309291
// (5) Trailing percent: "abc%" stays as "abc%".
310292
LT_BEGIN_AUTO_TEST(http_request_unescape_arena_suite,
311293
unescape_trailing_percent_passthrough)
312-
using httpserver::detail::http_request_impl;
313-
using httpserver::detail::arguments_accumulator;
314-
using impl_alloc_t = std::pmr::polymorphic_allocator<http_request_impl>;
315-
316-
alignas(std::max_align_t) std::array<std::byte, 8192> buf{};
317-
std::pmr::monotonic_buffer_resource arena(buf.data(), buf.size(),
318-
std::pmr::new_delete_resource());
319-
impl_alloc_t alloc(&arena);
320-
auto* p = alloc.new_object<http_request_impl>(nullptr, nullptr, alloc);
321-
322-
arguments_accumulator aa;
323-
aa.unescaper = nullptr;
324-
aa.arguments = &p->unescaped_args;
325-
326-
http_request_impl::build_request_args(
327-
&aa, MHD_GET_ARGUMENT_KIND, "k", "abc%");
328-
329-
auto it = p->unescaped_args.find(std::string_view("k"));
330-
LT_CHECK(it != p->unescaped_args.end());
331-
LT_CHECK_EQ(std::string(it->second[0].data(), it->second[0].size()),
332-
std::string("abc%"));
333-
334-
alloc.delete_object(p);
294+
LT_CHECK_EQ(decode_via_arena("abc%"), std::string("abc%"));
335295
LT_END_AUTO_TEST(unescape_trailing_percent_passthrough)
336296

337297
// (6) Empty value: produces an empty arg without crashing.
338298
LT_BEGIN_AUTO_TEST(http_request_unescape_arena_suite,
339299
unescape_empty_value)
340-
using httpserver::detail::http_request_impl;
341-
using httpserver::detail::arguments_accumulator;
342-
using impl_alloc_t = std::pmr::polymorphic_allocator<http_request_impl>;
343-
344300
alignas(std::max_align_t) std::array<std::byte, 8192> buf{};
345301
std::pmr::monotonic_buffer_resource arena(buf.data(), buf.size(),
346302
std::pmr::new_delete_resource());
@@ -368,10 +324,6 @@ LT_END_AUTO_TEST(unescape_empty_value)
368324
// TASK-018 lifetime contract on the arena-routed path.
369325
LT_BEGIN_AUTO_TEST(http_request_unescape_arena_suite,
370326
unescape_view_outlives_subsequent_inserts)
371-
using httpserver::detail::http_request_impl;
372-
using httpserver::detail::arguments_accumulator;
373-
using impl_alloc_t = std::pmr::polymorphic_allocator<http_request_impl>;
374-
375327
alignas(std::max_align_t) std::array<std::byte, 8192> buf{};
376328
std::pmr::monotonic_buffer_resource arena(buf.data(), buf.size(),
377329
std::pmr::new_delete_resource());
@@ -410,10 +362,6 @@ LT_END_AUTO_TEST(unescape_view_outlives_subsequent_inserts)
410362
// arena-backed sink may legitimately grow past the input size.
411363
LT_BEGIN_AUTO_TEST(http_request_unescape_arena_suite,
412364
unescape_user_callback_can_grow_value)
413-
using httpserver::detail::http_request_impl;
414-
using httpserver::detail::arguments_accumulator;
415-
using impl_alloc_t = std::pmr::polymorphic_allocator<http_request_impl>;
416-
417365
alignas(std::max_align_t) std::array<std::byte, 8192> buf{};
418366
std::pmr::monotonic_buffer_resource arena(buf.data(), buf.size(),
419367
std::pmr::new_delete_resource());

0 commit comments

Comments
 (0)