TUN-10391: Avoid using fmt.Println

Avoid using fmt.Println and instead switch to logging pre-checks with the provided logger.
This commit is contained in:
Miguel da Costa Martins Marcelino
2026-05-26 22:04:54 +00:00
parent f6f60e1059
commit 4177dd6936
7 changed files with 276 additions and 183 deletions
+57
View File
@@ -1,6 +1,9 @@
package cliutil
import (
"strings"
"github.com/rs/zerolog"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
@@ -57,3 +60,57 @@ func ConfigureLoggingFlags(shouldHide bool) []cli.Flag {
FlagLogOutput,
}
}
// LogTable renders lines inside an ASCII table and logs each rendered row.
func LogTable(log *zerolog.Logger, lines []string, title ...string) {
tableTitle := ""
if len(title) > 0 {
tableTitle = title[0]
}
for _, line := range asciiBox(lines, tableTitle, 2) {
if line != "" {
log.Info().Msg(line)
}
}
}
// asciiBox wraps lines in a bordered ASCII box with an optional title row.
func asciiBox(lines []string, title string, padding int) (box []string) {
maxLen := maxLen(lines, title)
spacer := strings.Repeat(" ", padding)
border := "+" + strings.Repeat("-", maxLen+(padding*2)) + "+"
box = append(box, border)
if title != "" {
box = append(box, renderBoxLine(centerLine(title, maxLen), maxLen, spacer))
box = append(box, border)
}
for _, line := range lines {
box = append(box, renderBoxLine(line, maxLen, spacer))
}
box = append(box, border)
return
}
// renderBoxLine pads a single line so it fills the box width.
func renderBoxLine(line string, maxLen int, spacer string) string {
return "|" + spacer + line + strings.Repeat(" ", maxLen-len(line)) + spacer + "|"
}
// centerLine pads line evenly so it is centered within width.
func centerLine(line string, width int) string {
padding := width - len(line)
leftPadding := padding / 2
rightPadding := padding - leftPadding
return strings.Repeat(" ", leftPadding) + line + strings.Repeat(" ", rightPadding)
}
// maxLen returns the longest visible line length including the title.
func maxLen(lines []string, title string) int {
max := len(title)
for _, line := range lines {
if len(line) > max {
max = len(line)
}
}
return max
}
+60
View File
@@ -0,0 +1,60 @@
package cliutil
import (
"bytes"
"encoding/json"
"testing"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLogTableWithoutTitle(t *testing.T) {
t.Parallel()
lines := captureTableLogs(t, []string{"first", "second"})
assert.Equal(t, []string{
"+----------+",
"| first |",
"| second |",
"+----------+",
}, lines)
}
func TestLogTableWithTitle(t *testing.T) {
t.Parallel()
lines := captureTableLogs(t, []string{"first", "second"}, "TT")
assert.Equal(t, []string{
"+----------+",
"| TT |",
"+----------+",
"| first |",
"| second |",
"+----------+",
}, lines)
}
func captureTableLogs(t *testing.T, lines []string, title ...string) []string {
t.Helper()
var buf bytes.Buffer
logger := zerolog.New(&buf)
LogTable(&logger, lines, title...)
// nolint: prealloc
var messages []string
for _, line := range bytes.Split(bytes.TrimSpace(buf.Bytes()), []byte("\n")) {
var entry struct {
Message string `json:"message"`
}
require.NoError(t, json.Unmarshal(line, &entry))
messages = append(messages, entry.Message)
}
return messages
}
+2 -2
View File
@@ -551,8 +551,8 @@ func runPrechecks(c *cli.Context, log *zerolog.Logger, region string) {
report := prechecks.Run(c.Context, c.String(cfdflags.CACert), cfg, log, dialers)
// Output the human-readable table to console
fmt.Println(report.String())
// Output the human-readable table
cliutil.LogTable(log, report.String(), "CONNECTIVITY PRE-CHECKS")
// Also log structured results for log aggregation
report.LogEvent(log)
+4 -28
View File
@@ -11,6 +11,7 @@ import (
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/connection"
)
@@ -44,7 +45,7 @@ func RunQuickTunnel(sc *subcommandContext) error {
if err != nil {
return errors.Wrap(err, "failed to request quick Tunnel")
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()
// This will read the entire response into memory so we can print it in case of error
rsp_body, err := io.ReadAll(resp.Body)
@@ -76,12 +77,10 @@ func RunQuickTunnel(sc *subcommandContext) error {
url = "https://" + url
}
for _, line := range AsciiBox([]string{
cliutil.LogTable(sc.log, []string{
"Your quick Tunnel has been created! Visit it at (it may take some time to be reachable):",
url,
}, 2) {
sc.log.Info().Msg(line)
}
})
if !sc.c.IsSet(flags.Protocol) {
_ = sc.c.Set(flags.Protocol, "quic")
@@ -116,26 +115,3 @@ type QuickTunnel struct {
AccountTag string `json:"account_tag"`
Secret []byte `json:"secret"`
}
// Print out the given lines in a nice ASCII box.
func AsciiBox(lines []string, padding int) (box []string) {
maxLen := maxLen(lines)
spacer := strings.Repeat(" ", padding)
border := "+" + strings.Repeat("-", maxLen+(padding*2)) + "+"
box = append(box, border)
for _, line := range lines {
box = append(box, "|"+spacer+line+strings.Repeat(" ", maxLen-len(line))+spacer+"|")
}
box = append(box, border)
return
}
func maxLen(lines []string) int {
max := 0
for _, line := range lines {
if len(line) > max {
max = len(line)
}
}
return max
}
+82 -40
View File
@@ -5,8 +5,8 @@ Integration tests for cloudflared connectivity pre-checks (TUN-10391).
Scope
-----
These tests verify the end-to-end behavior of cloudflared pre-checks:
- that the human-readable table written to stdout has the correct structure
and content,
- that the human-readable table written to the log output has the correct
structure and content,
- that structured JSON log lines are emitted with the expected fields, and
- that running the `diag` subcommand against a live tunnel instance produces a
zip archive that contains prechecks.json.
@@ -25,15 +25,24 @@ without real firewall intervention is:
DNS failure and Management API failure cannot be triggered via CLI flags alone;
they require network-level intervention outside the component-test harness.
stdout design
-------------
fmt.Println(report.String()) runs inside a goroutine that is started
concurrently with the tunnel. We poll a --logfile for the "precheck complete"
sentinel before leaving the `with` block, ensuring the goroutine has finished.
We then call cfd.terminate(). After the `with` block exits, the process is
dead and all output has been captured by CloudflaredProcess's background reader
thread (stderr is merged into stdout). We read the accumulated lines from
cfd.stdout_lines.
stdout/stderr design
--------------------
The pre-checks table is emitted via cliutil.LogTable, which wraps the content
in an ASCII box and logs each line at Info level through zerolog. zerolog
writes to stderr, which the test harness merges into stdout (stderr=STDOUT in
Popen). We poll a --logfile for the "precheck complete" sentinel before
leaving the `with` block, ensuring the goroutine has finished. We then call
cfd.terminate(). After the `with` block exits, the process is dead and all
output has been captured by CloudflaredProcess's background reader thread. We
read the accumulated lines from cfd.stdout_lines.
Box format (cliutil.asciiBox with padding=2, title="CONNECTIVITY PRE-CHECKS"):
+----...----+
| CONNECTIVITY PRE-CHECKS | (centered title)
+----...----+
| COMPONENT TARGET ... | (content rows)
...
+----...----+
"""
import json
@@ -46,11 +55,13 @@ import zipfile as zipfilemod
from constants import METRICS_PORT
from util import LOGGER, start_cloudflared, wait_tunnel_ready
# stdout table constants
TABLE_WIDTH = 80
HEADER_LINE = "--- CONNECTIVITY PRE-CHECKS " + "-" * (TABLE_WIDTH - len("--- CONNECTIVITY PRE-CHECKS ") - 1)
COL_HEADER = "COMPONENT" # first token of the column-header row
SEPARATOR = "-" * TABLE_WIDTH
# ASCII box constants (cliutil.asciiBox, padding=2, title="CONNECTIVITY PRE-CHECKS")
BOX_TITLE = "CONNECTIVITY PRE-CHECKS"
BOX_BORDER_RE = re.compile(r"^\+(-+)\+$", re.MULTILINE) # matches +----...----+
COL_HEADER = "COMPONENT" # first word of the column-header row
# zerolog console format: "2006-01-02T15:04:05Z LVL <message>"
_LOG_PREFIX_RE = re.compile(r"^\S+ \w+ ")
# Component names (probes.go: componentXxx)
COMP_DNS = "DNS Resolution"
@@ -164,23 +175,46 @@ class TableRow:
return f"TableRow({self.component!r}, {self.target!r}, {self.status!r}, {self.details!r})"
def _strip_log_prefix(line: str) -> str:
"""Remove the zerolog console prefix ('2006-01-02T15:04:05Z LVL ') if present."""
return _LOG_PREFIX_RE.sub("", line, count=1)
def _unbox_line(line: str) -> str:
"""Strip the box border padding from a content line: '| text |' -> 'text'.
Accepts lines that may still carry a zerolog console prefix; the prefix is
removed before the box delimiters are stripped.
"""
msg = _strip_log_prefix(line)
if msg.startswith("|") and msg.endswith("|"):
return msg[1:-1].strip()
return msg.strip()
def _parse_table(stdout: str) -> list[TableRow]:
"""
Parse the data rows from a precheck table in stdout.
text/tabwriter uses padding=2, so columns are separated by two or more
spaces. We skip the column-header row and stop at blank lines, SUMMARY,
separator, ERROR, or WARNING lines.
The table is now wrapped in an ASCII box by cliutil.LogTable. Each
content line has the form '| <content> |', optionally preceded by a
zerolog console prefix. We strip both the prefix and the box borders
before splitting on two-or-more spaces (text/tabwriter padding=2).
We skip the column-header row and stop at blank lines, SUMMARY, box
border lines, ERROR, or WARNING lines.
"""
rows = []
in_data = False
for line in stdout.splitlines():
for raw_line in stdout.splitlines():
msg = _strip_log_prefix(raw_line)
line = _unbox_line(raw_line)
if line.startswith("COMPONENT"):
in_data = True
continue
if not in_data:
continue
if (line == "" or line.startswith("SUMMARY") or line.startswith("---")
if (line == "" or line.startswith("SUMMARY") or BOX_BORDER_RE.match(msg)
or line.startswith("ERROR") or line.startswith("WARNING")):
in_data = False
continue
@@ -261,14 +295,18 @@ class TestPrechecksHappyPath:
LOGGER.debug(f"[happy-path] stdout:\n{stdout}")
LOGGER.debug(f"[happy-path] log_lines:\n{log_lines}")
# Strip zerolog console prefixes so pattern matching works on raw messages.
messages = "\n".join(_strip_log_prefix(l) for l in stdout.splitlines())
# ── table structure ──────────────────────────────────────────────────
# stderr is merged into stdout so log lines precede the table.
assert HEADER_LINE in stdout, \
f"Expected header line in output;\ngot:\n{stdout}"
assert COL_HEADER in stdout, \
# zerolog writes to stderr which is merged into stdout by the harness.
# The table is wrapped in an ASCII box by cliutil.LogTable.
assert BOX_TITLE in messages, \
f"Expected box title '{BOX_TITLE}' in output;\ngot:\n{stdout}"
assert COL_HEADER in messages, \
f"Expected column header row in output;\ngot:\n{stdout}"
assert SEPARATOR in stdout, \
f"Expected closing separator in output;\ngot:\n{stdout}"
assert BOX_BORDER_RE.search(messages), \
f"Expected box border line (+---+) in output;\ngot:\n{stdout}"
# ── row content ──────────────────────────────────────────────────────
rows = _parse_table(stdout)
@@ -306,11 +344,11 @@ class TestPrechecksHappyPath:
assert api_rows[0].details == DETAILS_API_OK, f"API row details wrong: {api_rows[0]}"
# ── no action lines ──────────────────────────────────────────────────
assert PREFIX_ERROR not in stdout, f"Unexpected ERROR action:\n{stdout}"
assert PREFIX_WARNING not in stdout, f"Unexpected WARNING action:\n{stdout}"
assert PREFIX_ERROR not in messages, f"Unexpected ERROR action:\n{stdout}"
assert PREFIX_WARNING not in messages, f"Unexpected WARNING action:\n{stdout}"
# ── exact summary line ───────────────────────────────────────────────
assert SUMMARY_HEALTHY in stdout, \
# ── summary line ─────────────────────────────────────────────────────
assert SUMMARY_HEALTHY in messages, \
f"Expected healthy summary;\ngot:\n{stdout}"
# ── structured log ───────────────────────────────────────────────────
@@ -366,14 +404,18 @@ class TestPrechecksHardFail:
LOGGER.debug(f"[hard-fail] stdout:\n{stdout}")
LOGGER.debug(f"[hard-fail] log_lines:\n{log_lines}")
# Strip zerolog console prefixes so pattern matching works on raw messages.
messages = "\n".join(_strip_log_prefix(l) for l in stdout.splitlines())
# ── table structure ──────────────────────────────────────────────────
# stderr is merged into stdout so log lines precede the table.
assert HEADER_LINE in stdout, \
f"Expected header line in output;\ngot:\n{stdout}"
assert COL_HEADER in stdout, \
# zerolog writes to stderr which is merged into stdout by the harness.
# The table is wrapped in an ASCII box by cliutil.LogTable.
assert BOX_TITLE in messages, \
f"Expected box title '{BOX_TITLE}' in output;\ngot:\n{stdout}"
assert COL_HEADER in messages, \
f"Expected column header row in output;\ngot:\n{stdout}"
assert SEPARATOR in stdout, \
f"Expected closing separator in output;\ngot:\n{stdout}"
assert BOX_BORDER_RE.search(messages), \
f"Expected box border line (+---+) in output;\ngot:\n{stdout}"
# ── row content ──────────────────────────────────────────────────────
rows = _parse_table(stdout)
@@ -404,12 +446,12 @@ class TestPrechecksHardFail:
assert api_rows[0].status == PASS, f"API row not PASS: {api_rows[0]}"
assert api_rows[0].details == DETAILS_API_OK, f"API row details wrong: {api_rows[0]}"
assert f"{PREFIX_ERROR}{ACTION_QUIC_BLOCKED}" in stdout, \
assert f"{PREFIX_ERROR}{ACTION_QUIC_BLOCKED}" in messages, \
f"Expected QUIC ERROR action;\ngot:\n{stdout}"
assert f"{PREFIX_ERROR}{ACTION_HTTP2_BLOCKED}" in stdout, \
assert f"{PREFIX_ERROR}{ACTION_HTTP2_BLOCKED}" in messages, \
f"Expected HTTP/2 ERROR action;\ngot:\n{stdout}"
assert SUMMARY_CRITICAL in stdout, \
assert SUMMARY_CRITICAL in messages, \
f"Expected critical summary;\ngot:\n{stdout}"
_assert_precheck_summary_log(log_lines, hard_fail=True, suggested_protocol=None)
+14 -51
View File
@@ -10,19 +10,12 @@ import (
)
const (
// tableWidth is the total character width of the separator lines.
tableWidth = 80
// Status names.
passStatus = "PASS"
failStatus = "FAIL"
skipStatus = "SKIP"
unknownStatus = "UNKNOWN"
// Section separators.
sectionChar = "-"
headerTitle = "CONNECTIVITY PRE-CHECKS"
// Log message constants.
logMsgPrecheck = "precheck"
logMsgPrecheckComplete = "precheck complete"
@@ -35,8 +28,6 @@ const (
logFieldDetails = "details"
logFieldHardFail = "hard_fail"
logFieldSuggestedProtocol = "suggested_protocol"
sep = " "
)
// statusLabel returns the display label for a given Status.
@@ -58,21 +49,9 @@ func (s Status) logString() string {
return strings.ToLower(s.String())
}
// separator returns a full-width horizontal line.
func separator() string {
return strings.Repeat(sectionChar, tableWidth)
}
// header returns the top section title line.
func header() string {
leftDashes := strings.Repeat(sectionChar, 3)
rightLen := tableWidth - len(leftDashes) - len(headerTitle) - len(sep)*2
return leftDashes + sep + headerTitle + sep + strings.Repeat(sectionChar, rightLen)
}
// renderTable uses text/tabwriter to format the results rows with
// automatically aligned columns, returning the rendered string.
func renderTable(results []CheckResult) string {
// automatically aligned columns, returning the rendered lines.
func renderTable(results []CheckResult) []string {
var buf bytes.Buffer
// minwidth=0, tabwidth=8, padding=2, padchar=' ', flags=0
w := tabwriter.NewWriter(&buf, 0, 8, 2, ' ', 0)
@@ -81,27 +60,27 @@ func renderTable(results []CheckResult) string {
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", r.Component, r.Target, r.ProbeStatus.statusLabel(), r.Details)
}
_ = w.Flush()
return buf.String()
return strings.Split(strings.TrimSuffix(buf.String(), "\n"), "\n")
}
// renderActions collects all non-empty Action strings from results and returns
// the formatted warning/error block that appears between the table and SUMMARY.
// A Fail result is rendered as ERROR when the report is a hard fail, and as
// WARNING otherwise (degraded but tunnel can still run).
func renderActions(r Report) string {
func renderActions(r Report) []string {
hardFail := r.hasHardFail()
var sb strings.Builder
actions := make([]string, 0)
for _, res := range r.Results {
if res.Action == "" || res.ProbeStatus != Fail {
continue
}
if hardFail {
_, _ = fmt.Fprintf(&sb, "ERROR: %s\n", res.Action)
actions = append(actions, fmt.Sprintf("ERROR: %s", res.Action))
} else {
_, _ = fmt.Fprintf(&sb, "WARNING: %s\n", res.Action)
actions = append(actions, fmt.Sprintf("WARNING: %s", res.Action))
}
}
return sb.String()
return actions
}
// summaryLine builds the SUMMARY: line based on the Report state.
@@ -181,28 +160,12 @@ func (r Report) hasWarn() bool {
return (quicFail != http2Fail) || apiFail
}
// String renders the Report as a human-readable table suitable for os.Stdout.
func (r Report) String() string {
var sb strings.Builder
sb.WriteString(header())
sb.WriteString("\n")
sb.WriteString(renderTable(r.Results))
actions := renderActions(r)
if actions != "" {
sb.WriteString(actions)
}
sb.WriteString("\n")
sb.WriteString(summaryLine(r))
sb.WriteString("\n")
sb.WriteString(separator())
sb.WriteString("\n")
return sb.String()
// String renders the Report as human-readable table lines suitable for logging.
func (r Report) String() []string {
lines := renderTable(r.Results)
lines = append(lines, renderActions(r)...)
lines = append(lines, "", summaryLine(r))
return lines
}
// LogEvent emits each CheckResult as a structured zerolog log line, followed by
+57 -62
View File
@@ -134,85 +134,80 @@ func dnsFailReport() Report {
func TestString_AllPass(t *testing.T) {
t.Parallel()
want := "" +
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
"COMPONENT TARGET STATUS DETAILS\n" +
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully\n" +
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully\n" +
"UDP Connectivity Port 7844 (QUIC) PASS QUIC connection successful\n" +
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful\n" +
"Cloudflare API api.cloudflare.com:443 PASS API is reachable\n" +
"\n" +
"SUMMARY: Environment is healthy. cloudflared will use 'quic' as primary protocol.\n" +
"--------------------------------------------------------------------------------\n"
want := []string{
"COMPONENT TARGET STATUS DETAILS",
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully",
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully",
"UDP Connectivity Port 7844 (QUIC) PASS QUIC connection successful",
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful",
"Cloudflare API api.cloudflare.com:443 PASS API is reachable",
"",
"SUMMARY: Environment is healthy. cloudflared will use 'quic' as primary protocol.",
}
assert.Equal(t, want, allPassReport().String())
}
func TestString_QuicBlocked(t *testing.T) {
t.Parallel()
want := "" +
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
"COMPONENT TARGET STATUS DETAILS\n" +
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully\n" +
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully\n" +
"UDP Connectivity Port 7844 (QUIC) FAIL QUIC connection failed\n" +
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful\n" +
"Cloudflare API api.cloudflare.com:443 PASS API is reachable\n" +
"WARNING: Allow outbound QUIC traffic on port 7844 or use HTTP2.\n" +
"\n" +
"SUMMARY: Environment ready with degraded transport. cloudflared will proceed using 'http2'.\n" +
"--------------------------------------------------------------------------------\n"
want := []string{
"COMPONENT TARGET STATUS DETAILS",
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully",
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully",
"UDP Connectivity Port 7844 (QUIC) FAIL QUIC connection failed",
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful",
"Cloudflare API api.cloudflare.com:443 PASS API is reachable",
"WARNING: Allow outbound QUIC traffic on port 7844 or use HTTP2.",
"",
"SUMMARY: Environment ready with degraded transport. cloudflared will proceed using 'http2'.",
}
assert.Equal(t, want, quicBlockedReport().String())
}
func TestString_APIFail(t *testing.T) {
t.Parallel()
want := "" +
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
"COMPONENT TARGET STATUS DETAILS\n" +
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully\n" +
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully\n" +
"UDP Connectivity Port 7844 (QUIC) PASS QUIC connection successful\n" +
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful\n" +
"Cloudflare API api.cloudflare.com:443 FAIL Connection refused\n" +
"WARNING: cloudflared will still run, but automatic software updates are unavailable. Ensure port 443 TCP to api.cloudflare.com is open if you want auto-updates.\n" +
"\n" +
"SUMMARY: Environment ready with degraded transport. cloudflared will proceed using 'quic'.\n" +
"--------------------------------------------------------------------------------\n"
want := []string{
"COMPONENT TARGET STATUS DETAILS",
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully",
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully",
"UDP Connectivity Port 7844 (QUIC) PASS QUIC connection successful",
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful",
"Cloudflare API api.cloudflare.com:443 FAIL Connection refused",
"WARNING: cloudflared will still run, but automatic software updates are unavailable. Ensure port 443 TCP to api.cloudflare.com is open if you want auto-updates.",
"",
"SUMMARY: Environment ready with degraded transport. cloudflared will proceed using 'quic'.",
}
assert.Equal(t, want, apiFailReport().String())
}
func TestString_BothTransportsBlocked(t *testing.T) {
t.Parallel()
want := "" +
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
"COMPONENT TARGET STATUS DETAILS\n" +
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully\n" +
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully\n" +
"UDP Connectivity Port 7844 (QUIC) FAIL QUIC connection failed\n" +
"TCP Connectivity Port 7844 (HTTP/2) FAIL HTTP/2 connection is blocked or unreachable\n" +
"Cloudflare API api.cloudflare.com:443 PASS API is reachable\n" +
"ERROR: Allow outbound QUIC and/or TCP on port 7844 to the Cloudflare edge.\n" +
"\n" +
"SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel.\n" +
"--------------------------------------------------------------------------------\n"
want := []string{
"COMPONENT TARGET STATUS DETAILS",
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully",
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully",
"UDP Connectivity Port 7844 (QUIC) FAIL QUIC connection failed",
"TCP Connectivity Port 7844 (HTTP/2) FAIL HTTP/2 connection is blocked or unreachable",
"Cloudflare API api.cloudflare.com:443 PASS API is reachable",
"ERROR: Allow outbound QUIC and/or TCP on port 7844 to the Cloudflare edge.",
"",
"SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel.",
}
assert.Equal(t, want, bothTransportsBlockedReport().String())
}
func TestString_DNSFail(t *testing.T) {
t.Parallel()
want := "" +
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
"COMPONENT TARGET STATUS DETAILS\n" +
"DNS Resolution region1.v2.argotunnel.com FAIL No addresses returned\n" +
"DNS Resolution region2.v2.argotunnel.com FAIL No addresses returned\n" +
"UDP Connectivity Port 7844 (QUIC) SKIP DNS prerequisite failed\n" +
"TCP Connectivity Port 7844 (HTTP/2) SKIP DNS prerequisite failed\n" +
"Cloudflare API api.cloudflare.com:443 FAIL Connection refused\n" +
"ERROR: Ensure your DNS resolver can resolve 'region1.v2.argotunnel.com'. Run: dig A region1.v2.argotunnel.com @1.1.1.1. If that fails, contact your network administrator.\n" +
"\n" +
"SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel.\n" +
"--------------------------------------------------------------------------------\n"
want := []string{
"COMPONENT TARGET STATUS DETAILS",
"DNS Resolution region1.v2.argotunnel.com FAIL No addresses returned",
"DNS Resolution region2.v2.argotunnel.com FAIL No addresses returned",
"UDP Connectivity Port 7844 (QUIC) SKIP DNS prerequisite failed",
"TCP Connectivity Port 7844 (HTTP/2) SKIP DNS prerequisite failed",
"Cloudflare API api.cloudflare.com:443 FAIL Connection refused",
"ERROR: Ensure your DNS resolver can resolve 'region1.v2.argotunnel.com'. Run: dig A region1.v2.argotunnel.com @1.1.1.1. If that fails, contact your network administrator.",
"",
"SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel.",
}
assert.Equal(t, want, dnsFailReport().String())
}
@@ -221,9 +216,9 @@ func TestString_EmptyResults(t *testing.T) {
r := Report{RunID: fixedRunID, SuggestedProtocol: new(connection.QUIC)}
out := r.String()
// Must not panic and must still emit a valid skeleton.
assert.Contains(t, out, "CONNECTIVITY PRE-CHECKS")
assert.Contains(t, out, "SUMMARY:")
assert.Contains(t, out, separator())
require.Len(t, out, 3)
assert.Contains(t, out[0], "COMPONENT")
assert.Contains(t, out[2], "SUMMARY:")
}
// LogEvent() / structured log renderer tests