pytest: fix and improve reliability

Address issues listed in #19770:
- allow for ngttpx to successfully shut down on last attempt that might
  extend beyond the finish timestamp
- timeline checks: allos `time_starttransfer` to appear anywhere in
  the timeline as a slow client might seen response data before setting
  the other counters
- dump logs on test_05_02 as it was not reproduced locally

Fixes #19970
Closes #19783
diff --git a/tests/http/test_01_basic.py b/tests/http/test_01_basic.py
index 14a8dec..eb328ea 100644
--- a/tests/http/test_01_basic.py
+++ b/tests/http/test_01_basic.py
@@ -308,13 +308,14 @@
         url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo'
         r = curl.http_download(urls=[url], alpn_proto=proto, with_stats=True,
                                extra_args=['-X', method])
-        assert len(r.stats) == 1
         if proto == 'h2' or proto == 'h3':
-            r.check_response(http_status=0)
+            # h2+3 may close the connection for such invalid requests
             re_m = re.compile(r'.*\[:method: ([^\]]+)\].*')
             lines = [line for line in r.trace_lines if re_m.match(line)]
             assert len(lines) == 1, f'{r.dump_logs()}'
             m = re_m.match(lines[0])
             assert m.group(1) == method, f'{r.dump_logs()}'
         else:
+            # h1 should give us a real response
+            assert len(r.stats) == 1
             r.check_response(http_status=400)
diff --git a/tests/http/test_05_errors.py b/tests/http/test_05_errors.py
index 258b7f1..102b92e 100644
--- a/tests/http/test_05_errors.py
+++ b/tests/http/test_05_errors.py
@@ -73,8 +73,8 @@
         invalid_stats = []
         for idx, s in enumerate(r.stats):
             if 'exitcode' not in s or s['exitcode'] not in [18, 55, 56, 92, 95]:
-                invalid_stats.append(f'request {idx} exit with {s["exitcode"]}\n{s}')
-        assert len(invalid_stats) == 0, f'failed: {invalid_stats}'
+                invalid_stats.append(f'request {idx} exit with {s["exitcode"]}\n{r.dump_logs()}')
+        assert len(invalid_stats) == 0, f'failed: {invalid_stats}\n{r.dump_logs()}'
 
     # access a resource that, on h2, RST the stream with HTTP_1_1_REQUIRED
     @pytest.mark.skipif(condition=not Env.have_h2_curl(), reason="curl without h2")
diff --git a/tests/http/testenv/curl.py b/tests/http/testenv/curl.py
index e486715..f54170b 100644
--- a/tests/http/testenv/curl.py
+++ b/tests/http/testenv/curl.py
@@ -525,6 +525,7 @@
         }
         # stat keys where we expect a positive value
         ref_tl = []
+        somewhere_keys = []
         exact_match = True
         # redirects mess up the queue time, only count without
         if s['time_redirect'] == 0:
@@ -542,10 +543,13 @@
         # what kind of transfer was it?
         if s['size_upload'] == 0 and s['size_download'] > 0:
             # this is a download
-            dl_tl = ['time_pretransfer', 'time_starttransfer']
+            dl_tl = ['time_pretransfer']
             if s['size_request'] > 0:
                 dl_tl = ['time_posttransfer'] + dl_tl
             ref_tl += dl_tl
+            # the first byte of the response may arrive before we
+            # track the other times when the client is slow (CI).
+            somewhere_keys = ['time_starttransfer']
         elif s['size_upload'] > 0 and s['size_download'] == 0:
             # this is an upload
             ul_tl = ['time_pretransfer', 'time_posttransfer']
@@ -561,11 +565,14 @@
             self.check_stat_positive(s, idx, key)
         if exact_match:
             # assert all events not in reference timeline are 0
-            for key in [key for key in all_keys if key not in ref_tl]:
+            for key in [key for key in all_keys if key not in ref_tl and key not in somewhere_keys]:
                 self.check_stat_zero(s, key)
         # calculate the timeline that did happen
         seen_tl = sorted(ref_tl, key=lambda ts: s[ts])
         assert seen_tl == ref_tl, f'{[f"{ts}: {s[ts]}" for ts in seen_tl]}'
+        for key in somewhere_keys:
+            self.check_stat_positive(s, idx, key)
+            assert s[key] <= s['time_total']
 
     def dump_logs(self):
         lines = ['>>--stdout ----------------------------------------------\n']
diff --git a/tests/http/testenv/nghttpx.py b/tests/http/testenv/nghttpx.py
index 6db888b..106766f 100644
--- a/tests/http/testenv/nghttpx.py
+++ b/tests/http/testenv/nghttpx.py
@@ -138,7 +138,7 @@
                 except subprocess.TimeoutExpired:
                     log.warning(f'nghttpx({running.pid}), not shut down yet.')
                     os.kill(running.pid, signal.SIGQUIT)
-            if datetime.now() >= end_wait:
+            if running and datetime.now() >= end_wait:
                 log.error(f'nghttpx({running.pid}), terminate forcefully.')
                 os.kill(running.pid, signal.SIGKILL)
                 running.terminate()