77These tests verify the end-to-end behavior of cloudflared pre-checks:
88- that the human-readable table written to the log output has the correct
99 structure and content,
10- - that structured JSON log lines are emitted with the expected fields , and
10+ - that the diagnostic endpoint exposes the completed report , and
1111- that running the `diag` subcommand against a live tunnel instance produces a
1212 zip archive that contains prechecks.json.
1313
3030The pre-checks table is emitted via cliutil.LogTable, which wraps the content
3131in an ASCII box and logs each line at Info level through zerolog. zerolog
3232writes to stderr, which the test harness merges into stdout (stderr=STDOUT in
33- Popen). We poll a --logfile for the "precheck complete" sentinel before
34- leaving the `with` block, ensuring the goroutine has finished. We then call
35- cfd.terminate(). After the `with` block exits, the process is dead and all
36- output has been captured by CloudflaredProcess's background reader thread. We
37- read the accumulated lines from cfd.stdout_lines.
33+ Popen). We poll /diag/precheck before leaving the `with` block, ensuring the
34+ goroutine has finished. We then call cfd.terminate(). After the `with` block
35+ exits, the process is dead and all output has been captured by
36+ CloudflaredProcess's background reader thread.
3837
3938Box format (cliutil.asciiBox with padding=2, title="CONNECTIVITY PRE-CHECKS"):
4039 +----...----+
5251import time
5352import zipfile as zipfilemod
5453
54+ import requests
55+
5556from constants import METRICS_PORT
5657from util import LOGGER , start_cloudflared , wait_tunnel_ready
5758
106107SUMMARY_HEALTHY = "SUMMARY: Environment is healthy. cloudflared will use 'quic' as primary protocol."
107108SUMMARY_CRITICAL = "SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel."
108109
109- # structured log constants (result.go)
110-
111- LOG_MSG_PRECHECK = "precheck"
112- LOG_MSG_PRECHECK_COMPLETE = "precheck complete"
113110STATUS_PASS_LOG = "pass"
114111
115112UNREACHABLE_EDGE = "192.0.2.1:7844"
120117
121118# ---------- helpers ----------
122119
123- def _poll_log_file_for_precheck_complete (log_file : str , timeout : float ) -> list [dict ]:
124- """
125- Poll a JSON log file until a 'precheck complete' line appears or timeout
126- expires. Returns all precheck-related log lines found.
127-
128- cloudflared's --logfile writes one JSON object per line. Polling keeps
129- the test fast on healthy networks and still tolerates slow CI hosts.
130-
131- We re-read from the beginning of the file on every poll because the file
132- is append-only, small, and tracking a byte offset would add complexity with
133- no meaningful performance benefit for a ~15 s total window.
134- """
120+ def _poll_precheck_endpoint (timeout : float ) -> dict :
121+ """Poll /diag/precheck until a completed report is available."""
122+ url = f"http://localhost:{ METRICS_PORT } /diag/precheck"
135123 deadline = time .monotonic () + timeout
136124 while time .monotonic () < deadline :
137- lines = _read_precheck_log_lines_from_file (log_file )
138- if any (l .get ("message" ) == LOG_MSG_PRECHECK_COMPLETE for l in lines ):
139- return lines
125+ try :
126+ response = requests .get (url , timeout = 1 )
127+ if response .status_code == 200 :
128+ return response .json ()
129+ assert response .status_code == 204 , \
130+ f"Expected { url } to return 200 or 204, got { response .status_code } "
131+ except requests .exceptions .RequestException :
132+ pass
140133 time .sleep (PRECHECK_POLL_INTERVAL_SECS )
141- return _read_precheck_log_lines_from_file (log_file )
142-
143-
144- def _read_precheck_log_lines_from_file (log_file : str ) -> list [dict ]:
145- """Parse all precheck-related JSON log lines from a --logfile path."""
146- result = []
147- try :
148- with open (log_file , "r" ) as f :
149- for raw_line in f :
150- raw_line = raw_line .strip ()
151- if not raw_line :
152- continue
153- try :
154- obj = json .loads (raw_line )
155- except json .JSONDecodeError :
156- continue
157- msg = obj .get ("message" ) or obj .get ("msg" , "" )
158- if msg in (LOG_MSG_PRECHECK , LOG_MSG_PRECHECK_COMPLETE ):
159- result .append (obj )
160- except FileNotFoundError :
161- pass
162- return result
134+ raise AssertionError (f"Timed out waiting for a completed precheck report from { url } " )
163135
164136
165137# stdout table parse
@@ -235,25 +207,21 @@ def _rows_for(rows: list[TableRow], component: str) -> list[TableRow]:
235207
236208# log assertions
237209
238- def _assert_precheck_summary_log (
239- log_lines : list [ dict ] ,
210+ def _assert_precheck_report (
211+ report : dict ,
240212 * ,
241213 hard_fail : bool ,
242214 suggested_protocol : str | None = None ,
243215):
244- """Assert the 'precheck complete' summary log line has the expected fields."""
245- summary_lines = [l for l in log_lines if l .get ("message" ) == LOG_MSG_PRECHECK_COMPLETE ]
246- assert len (summary_lines ) == 1 , \
247- f"Expected exactly one '{ LOG_MSG_PRECHECK_COMPLETE } ' log line; got { summary_lines } "
248- summary = summary_lines [0 ]
249-
250- assert summary .get ("hard_fail" ) is hard_fail , \
251- f"Expected hard_fail={ hard_fail } in summary log: { summary } "
216+ """Assert the diagnostic report has the expected summary fields."""
217+ assert report .get ("run_id" ), f"Expected a run_id in precheck report: { report } "
218+ assert report .get ("hard_fail" ) is hard_fail , \
219+ f"Expected hard_fail={ hard_fail } in precheck report: { report } "
252220
253221 if suggested_protocol is not None :
254- assert summary .get ("suggested_protocol" ) == suggested_protocol , \
222+ assert report .get ("suggested_protocol" ) == suggested_protocol , \
255223 (f"Expected suggested_protocol={ suggested_protocol !r} ; "
256- f"got { summary .get ('suggested_protocol' )!r} " )
224+ f"got { report .get ('suggested_protocol' )!r} " )
257225
258226
259227# ---------- Tests ----------
@@ -265,12 +233,11 @@ class TestPrechecksHappyPath:
265233 - every row's component, target, status, and details
266234 - no ERROR/WARNING action lines
267235 - the exact summary line
268- - the structured log summary (hard_fail=false, suggested_protocol=quic)
236+ - the diagnostic report summary (hard_fail=false, suggested_protocol=quic)
269237 """
270238
271239 def test_prechecks_pass_on_healthy_connection (self , tmp_path , component_tests_config ):
272- log_file = str (tmp_path / "cloudflared.log" )
273- config = component_tests_config ({"logfile" : log_file })
240+ config = component_tests_config ({})
274241
275242 with start_cloudflared (
276243 tmp_path ,
@@ -281,10 +248,7 @@ def test_prechecks_pass_on_healthy_connection(self, tmp_path, component_tests_co
281248 capture_output = True ,
282249 ) as cfd :
283250 wait_tunnel_ready (tunnel_url = config .get_url (), require_min_connections = 1 )
284- # Poll the log file for the sentinel before signalling the process.
285- log_lines = _poll_log_file_for_precheck_complete (
286- log_file , timeout = PRECHECK_POLL_TIMEOUT_SECS
287- )
251+ report = _poll_precheck_endpoint (timeout = PRECHECK_POLL_TIMEOUT_SECS )
288252 # Signal shutdown.
289253 cfd .terminate ()
290254
@@ -293,7 +257,7 @@ def test_prechecks_pass_on_healthy_connection(self, tmp_path, component_tests_co
293257 stdout = b"" .join (cfd .stdout_lines ).decode (errors = "replace" )
294258
295259 LOGGER .debug (f"[happy-path] stdout:\n { stdout } " )
296- LOGGER .debug (f"[happy-path] log_lines :\n { log_lines } " )
260+ LOGGER .debug (f"[happy-path] precheck report :\n { report } " )
297261
298262 # Strip zerolog console prefixes so pattern matching works on raw messages.
299263 messages = "\n " .join (_strip_log_prefix (l ) for l in stdout .splitlines ())
@@ -351,14 +315,12 @@ def test_prechecks_pass_on_healthy_connection(self, tmp_path, component_tests_co
351315 assert SUMMARY_HEALTHY in messages , \
352316 f"Expected healthy summary;\n got:\n { stdout } "
353317
354- # ── structured log ───────────────────────────────────────────────────
355- assert len (log_lines ) > 0 , \
356- "Expected at least one structured precheck log line in log file"
357- for line in log_lines :
358- if line .get ("message" ) == LOG_MSG_PRECHECK :
359- assert line .get ("status" ) == STATUS_PASS_LOG , \
360- f"Expected status=pass in precheck log line: { line } "
361- _assert_precheck_summary_log (log_lines , hard_fail = False , suggested_protocol = "quic" )
318+ # ── diagnostic report ────────────────────────────────────────────────
319+ assert len (report ["results" ]) == 7
320+ for result in report ["results" ]:
321+ assert result ["status" ] == STATUS_PASS_LOG , \
322+ f"Expected status=pass in precheck report: { result } "
323+ _assert_precheck_report (report , hard_fail = False , suggested_protocol = "quic" )
362324
363325
364326class TestPrechecksHardFail :
@@ -372,15 +334,14 @@ class TestPrechecksHardFail:
372334 - HTTP/2 row: FAIL with correct details + ERROR action
373335 - API row: PASS (api.cloudflare.com:443 is independently reachable)
374336 - the exact critical summary line
375- - the structured log summary (hard_fail=true)
337+ - the diagnostic report summary (hard_fail=true)
376338
377339 This test does NOT call wait_tunnel_ready because the tunnel will not
378340 connect to the unreachable address.
379341 """
380342
381343 def test_prechecks_hard_fail_when_edge_unreachable (self , tmp_path , component_tests_config ):
382- log_file = str (tmp_path / "cloudflared.log" )
383- config = component_tests_config ({"logfile" : log_file })
344+ config = component_tests_config ({})
384345
385346 with start_cloudflared (
386347 tmp_path ,
@@ -394,15 +355,13 @@ def test_prechecks_hard_fail_when_edge_unreachable(self, tmp_path, component_tes
394355 new_process = True ,
395356 capture_output = True ,
396357 ) as cfd :
397- log_lines = _poll_log_file_for_precheck_complete (
398- log_file , timeout = PRECHECK_POLL_TIMEOUT_SECS
399- )
358+ report = _poll_precheck_endpoint (timeout = PRECHECK_POLL_TIMEOUT_SECS )
400359 cfd .terminate ()
401360
402361 stdout = b"" .join (cfd .stdout_lines ).decode (errors = "replace" )
403362
404363 LOGGER .debug (f"[hard-fail] stdout:\n { stdout } " )
405- LOGGER .debug (f"[hard-fail] log_lines :\n { log_lines } " )
364+ LOGGER .debug (f"[hard-fail] precheck report :\n { report } " )
406365
407366 # Strip zerolog console prefixes so pattern matching works on raw messages.
408367 messages = "\n " .join (_strip_log_prefix (l ) for l in stdout .splitlines ())
@@ -454,7 +413,7 @@ def test_prechecks_hard_fail_when_edge_unreachable(self, tmp_path, component_tes
454413 assert SUMMARY_CRITICAL in messages , \
455414 f"Expected critical summary;\n got:\n { stdout } "
456415
457- _assert_precheck_summary_log ( log_lines , hard_fail = True , suggested_protocol = None )
416+ _assert_precheck_report ( report , hard_fail = True , suggested_protocol = None )
458417
459418
460419class TestPreChecksDiag :
0 commit comments