@@ -154,193 +154,149 @@ void prepend_x_dash_unescaper(std::string& val) {
154154// default unescaper does real work (decodes to '/').
155155constexpr 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" ));
278282LT_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 '%').
282286LT_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" ));
307289LT_END_AUTO_TEST (unescape_double_percent_passthrough)
308290
309291// (5) Trailing percent: "abc%" stays as "abc%".
310292LT_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%" ));
335295LT_END_AUTO_TEST (unescape_trailing_percent_passthrough)
336296
337297// (6) Empty value: produces an empty arg without crashing.
338298LT_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.
369325LT_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.
411363LT_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