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 package cliutil
import ( import (
"strings"
"github.com/rs/zerolog"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc" "github.com/urfave/cli/v2/altsrc"
@@ -57,3 +60,57 @@ func ConfigureLoggingFlags(shouldHide bool) []cli.Flag {
FlagLogOutput, 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) report := prechecks.Run(c.Context, c.String(cfdflags.CACert), cfg, log, dialers)
// Output the human-readable table to console // Output the human-readable table
fmt.Println(report.String()) cliutil.LogTable(log, report.String(), "CONNECTIVITY PRE-CHECKS")
// Also log structured results for log aggregation // Also log structured results for log aggregation
report.LogEvent(log) report.LogEvent(log)
+4 -28
View File
@@ -11,6 +11,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/flags" "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/connection"
) )
@@ -44,7 +45,7 @@ func RunQuickTunnel(sc *subcommandContext) error {
if err != nil { if err != nil {
return errors.Wrap(err, "failed to request quick Tunnel") 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 // This will read the entire response into memory so we can print it in case of error
rsp_body, err := io.ReadAll(resp.Body) rsp_body, err := io.ReadAll(resp.Body)
@@ -76,12 +77,10 @@ func RunQuickTunnel(sc *subcommandContext) error {
url = "https://" + url 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):", "Your quick Tunnel has been created! Visit it at (it may take some time to be reachable):",
url, url,
}, 2) { })
sc.log.Info().Msg(line)
}
if !sc.c.IsSet(flags.Protocol) { if !sc.c.IsSet(flags.Protocol) {
_ = sc.c.Set(flags.Protocol, "quic") _ = sc.c.Set(flags.Protocol, "quic")
@@ -116,26 +115,3 @@ type QuickTunnel struct {
AccountTag string `json:"account_tag"` AccountTag string `json:"account_tag"`
Secret []byte `json:"secret"` 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 Scope
----- -----
These tests verify the end-to-end behavior of cloudflared pre-checks: These tests verify the end-to-end behavior of cloudflared pre-checks:
- that the human-readable table written to stdout has the correct structure - that the human-readable table written to the log output has the correct
and content, structure and content,
- that structured JSON log lines are emitted with the expected fields, and - that structured JSON log lines are emitted with the expected fields, and
- that running the `diag` subcommand against a live tunnel instance produces a - that running the `diag` subcommand against a live tunnel instance produces a
zip archive that contains prechecks.json. 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; DNS failure and Management API failure cannot be triggered via CLI flags alone;
they require network-level intervention outside the component-test harness. they require network-level intervention outside the component-test harness.
stdout design stdout/stderr design
------------- --------------------
fmt.Println(report.String()) runs inside a goroutine that is started The pre-checks table is emitted via cliutil.LogTable, which wraps the content
concurrently with the tunnel. We poll a --logfile for the "precheck complete" in an ASCII box and logs each line at Info level through zerolog. zerolog
sentinel before leaving the `with` block, ensuring the goroutine has finished. writes to stderr, which the test harness merges into stdout (stderr=STDOUT in
We then call cfd.terminate(). After the `with` block exits, the process is Popen). We poll a --logfile for the "precheck complete" sentinel before
dead and all output has been captured by CloudflaredProcess's background reader leaving the `with` block, ensuring the goroutine has finished. We then call
thread (stderr is merged into stdout). We read the accumulated lines from cfd.terminate(). After the `with` block exits, the process is dead and all
cfd.stdout_lines. 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 import json
@@ -46,11 +55,13 @@ import zipfile as zipfilemod
from constants import METRICS_PORT from constants import METRICS_PORT
from util import LOGGER, start_cloudflared, wait_tunnel_ready from util import LOGGER, start_cloudflared, wait_tunnel_ready
# stdout table constants # ASCII box constants (cliutil.asciiBox, padding=2, title="CONNECTIVITY PRE-CHECKS")
TABLE_WIDTH = 80 BOX_TITLE = "CONNECTIVITY PRE-CHECKS"
HEADER_LINE = "--- CONNECTIVITY PRE-CHECKS " + "-" * (TABLE_WIDTH - len("--- CONNECTIVITY PRE-CHECKS ") - 1) BOX_BORDER_RE = re.compile(r"^\+(-+)\+$", re.MULTILINE) # matches +----...----+
COL_HEADER = "COMPONENT" # first token of the column-header row COL_HEADER = "COMPONENT" # first word of the column-header row
SEPARATOR = "-" * TABLE_WIDTH
# zerolog console format: "2006-01-02T15:04:05Z LVL <message>"
_LOG_PREFIX_RE = re.compile(r"^\S+ \w+ ")
# Component names (probes.go: componentXxx) # Component names (probes.go: componentXxx)
COMP_DNS = "DNS Resolution" 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})" 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]: def _parse_table(stdout: str) -> list[TableRow]:
""" """
Parse the data rows from a precheck table in stdout. Parse the data rows from a precheck table in stdout.
text/tabwriter uses padding=2, so columns are separated by two or more The table is now wrapped in an ASCII box by cliutil.LogTable. Each
spaces. We skip the column-header row and stop at blank lines, SUMMARY, content line has the form '| <content> |', optionally preceded by a
separator, ERROR, or WARNING lines. 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 = [] rows = []
in_data = False 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"): if line.startswith("COMPONENT"):
in_data = True in_data = True
continue continue
if not in_data: if not in_data:
continue 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")): or line.startswith("ERROR") or line.startswith("WARNING")):
in_data = False in_data = False
continue continue
@@ -261,14 +295,18 @@ class TestPrechecksHappyPath:
LOGGER.debug(f"[happy-path] stdout:\n{stdout}") LOGGER.debug(f"[happy-path] stdout:\n{stdout}")
LOGGER.debug(f"[happy-path] log_lines:\n{log_lines}") 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 ────────────────────────────────────────────────── # ── table structure ──────────────────────────────────────────────────
# stderr is merged into stdout so log lines precede the table. # zerolog writes to stderr which is merged into stdout by the harness.
assert HEADER_LINE in stdout, \ # The table is wrapped in an ASCII box by cliutil.LogTable.
f"Expected header line in output;\ngot:\n{stdout}" assert BOX_TITLE in messages, \
assert COL_HEADER in stdout, \ 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}" f"Expected column header row in output;\ngot:\n{stdout}"
assert SEPARATOR in stdout, \ assert BOX_BORDER_RE.search(messages), \
f"Expected closing separator in output;\ngot:\n{stdout}" f"Expected box border line (+---+) in output;\ngot:\n{stdout}"
# ── row content ────────────────────────────────────────────────────── # ── row content ──────────────────────────────────────────────────────
rows = _parse_table(stdout) 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]}" assert api_rows[0].details == DETAILS_API_OK, f"API row details wrong: {api_rows[0]}"
# ── no action lines ────────────────────────────────────────────────── # ── no action lines ──────────────────────────────────────────────────
assert PREFIX_ERROR not in stdout, f"Unexpected ERROR action:\n{stdout}" assert PREFIX_ERROR not in messages, f"Unexpected ERROR action:\n{stdout}"
assert PREFIX_WARNING not in stdout, f"Unexpected WARNING action:\n{stdout}" assert PREFIX_WARNING not in messages, f"Unexpected WARNING action:\n{stdout}"
# ── exact summary line ─────────────────────────────────────────────── # ── summary line ─────────────────────────────────────────────────────
assert SUMMARY_HEALTHY in stdout, \ assert SUMMARY_HEALTHY in messages, \
f"Expected healthy summary;\ngot:\n{stdout}" f"Expected healthy summary;\ngot:\n{stdout}"
# ── structured log ─────────────────────────────────────────────────── # ── structured log ───────────────────────────────────────────────────
@@ -366,14 +404,18 @@ class TestPrechecksHardFail:
LOGGER.debug(f"[hard-fail] stdout:\n{stdout}") LOGGER.debug(f"[hard-fail] stdout:\n{stdout}")
LOGGER.debug(f"[hard-fail] log_lines:\n{log_lines}") 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 ────────────────────────────────────────────────── # ── table structure ──────────────────────────────────────────────────
# stderr is merged into stdout so log lines precede the table. # zerolog writes to stderr which is merged into stdout by the harness.
assert HEADER_LINE in stdout, \ # The table is wrapped in an ASCII box by cliutil.LogTable.
f"Expected header line in output;\ngot:\n{stdout}" assert BOX_TITLE in messages, \
assert COL_HEADER in stdout, \ 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}" f"Expected column header row in output;\ngot:\n{stdout}"
assert SEPARATOR in stdout, \ assert BOX_BORDER_RE.search(messages), \
f"Expected closing separator in output;\ngot:\n{stdout}" f"Expected box border line (+---+) in output;\ngot:\n{stdout}"
# ── row content ────────────────────────────────────────────────────── # ── row content ──────────────────────────────────────────────────────
rows = _parse_table(stdout) 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].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 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}" 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}" 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}" f"Expected critical summary;\ngot:\n{stdout}"
_assert_precheck_summary_log(log_lines, hard_fail=True, suggested_protocol=None) _assert_precheck_summary_log(log_lines, hard_fail=True, suggested_protocol=None)
+14 -51
View File
@@ -10,19 +10,12 @@ import (
) )
const ( const (
// tableWidth is the total character width of the separator lines.
tableWidth = 80
// Status names. // Status names.
passStatus = "PASS" passStatus = "PASS"
failStatus = "FAIL" failStatus = "FAIL"
skipStatus = "SKIP" skipStatus = "SKIP"
unknownStatus = "UNKNOWN" unknownStatus = "UNKNOWN"
// Section separators.
sectionChar = "-"
headerTitle = "CONNECTIVITY PRE-CHECKS"
// Log message constants. // Log message constants.
logMsgPrecheck = "precheck" logMsgPrecheck = "precheck"
logMsgPrecheckComplete = "precheck complete" logMsgPrecheckComplete = "precheck complete"
@@ -35,8 +28,6 @@ const (
logFieldDetails = "details" logFieldDetails = "details"
logFieldHardFail = "hard_fail" logFieldHardFail = "hard_fail"
logFieldSuggestedProtocol = "suggested_protocol" logFieldSuggestedProtocol = "suggested_protocol"
sep = " "
) )
// statusLabel returns the display label for a given Status. // statusLabel returns the display label for a given Status.
@@ -58,21 +49,9 @@ func (s Status) logString() string {
return strings.ToLower(s.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 // renderTable uses text/tabwriter to format the results rows with
// automatically aligned columns, returning the rendered string. // automatically aligned columns, returning the rendered lines.
func renderTable(results []CheckResult) string { func renderTable(results []CheckResult) []string {
var buf bytes.Buffer var buf bytes.Buffer
// minwidth=0, tabwidth=8, padding=2, padchar=' ', flags=0 // minwidth=0, tabwidth=8, padding=2, padchar=' ', flags=0
w := tabwriter.NewWriter(&buf, 0, 8, 2, ' ', 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) _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", r.Component, r.Target, r.ProbeStatus.statusLabel(), r.Details)
} }
_ = w.Flush() _ = 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 // renderActions collects all non-empty Action strings from results and returns
// the formatted warning/error block that appears between the table and SUMMARY. // 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 // A Fail result is rendered as ERROR when the report is a hard fail, and as
// WARNING otherwise (degraded but tunnel can still run). // WARNING otherwise (degraded but tunnel can still run).
func renderActions(r Report) string { func renderActions(r Report) []string {
hardFail := r.hasHardFail() hardFail := r.hasHardFail()
var sb strings.Builder actions := make([]string, 0)
for _, res := range r.Results { for _, res := range r.Results {
if res.Action == "" || res.ProbeStatus != Fail { if res.Action == "" || res.ProbeStatus != Fail {
continue continue
} }
if hardFail { if hardFail {
_, _ = fmt.Fprintf(&sb, "ERROR: %s\n", res.Action) actions = append(actions, fmt.Sprintf("ERROR: %s", res.Action))
} else { } 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. // summaryLine builds the SUMMARY: line based on the Report state.
@@ -181,28 +160,12 @@ func (r Report) hasWarn() bool {
return (quicFail != http2Fail) || apiFail return (quicFail != http2Fail) || apiFail
} }
// String renders the Report as a human-readable table suitable for os.Stdout. // String renders the Report as human-readable table lines suitable for logging.
func (r Report) String() string { func (r Report) String() []string {
var sb strings.Builder lines := renderTable(r.Results)
lines = append(lines, renderActions(r)...)
sb.WriteString(header()) lines = append(lines, "", summaryLine(r))
sb.WriteString("\n") return lines
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()
} }
// LogEvent emits each CheckResult as a structured zerolog log line, followed by // 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) { func TestString_AllPass(t *testing.T) {
t.Parallel() t.Parallel()
want := "" + want := []string{
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" + "COMPONENT TARGET STATUS DETAILS",
"COMPONENT TARGET STATUS DETAILS\n" + "DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully",
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully\n" + "DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully",
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully\n" + "UDP Connectivity Port 7844 (QUIC) PASS QUIC connection successful",
"UDP Connectivity Port 7844 (QUIC) PASS QUIC connection successful\n" + "TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful",
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful\n" + "Cloudflare API api.cloudflare.com:443 PASS API is reachable",
"Cloudflare API api.cloudflare.com:443 PASS API is reachable\n" + "",
"\n" + "SUMMARY: Environment is healthy. cloudflared will use 'quic' as primary protocol.",
"SUMMARY: Environment is healthy. cloudflared will use 'quic' as primary protocol.\n" + }
"--------------------------------------------------------------------------------\n"
assert.Equal(t, want, allPassReport().String()) assert.Equal(t, want, allPassReport().String())
} }
func TestString_QuicBlocked(t *testing.T) { func TestString_QuicBlocked(t *testing.T) {
t.Parallel() t.Parallel()
want := "" + want := []string{
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" + "COMPONENT TARGET STATUS DETAILS",
"COMPONENT TARGET STATUS DETAILS\n" + "DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully",
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully\n" + "DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully",
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully\n" + "UDP Connectivity Port 7844 (QUIC) FAIL QUIC connection failed",
"UDP Connectivity Port 7844 (QUIC) FAIL QUIC connection failed\n" + "TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful",
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful\n" + "Cloudflare API api.cloudflare.com:443 PASS API is reachable",
"Cloudflare API api.cloudflare.com:443 PASS API is reachable\n" + "WARNING: Allow outbound QUIC traffic on port 7844 or use HTTP2.",
"WARNING: Allow outbound QUIC traffic on port 7844 or use HTTP2.\n" + "",
"\n" + "SUMMARY: Environment ready with degraded transport. cloudflared will proceed using 'http2'.",
"SUMMARY: Environment ready with degraded transport. cloudflared will proceed using 'http2'.\n" + }
"--------------------------------------------------------------------------------\n"
assert.Equal(t, want, quicBlockedReport().String()) assert.Equal(t, want, quicBlockedReport().String())
} }
func TestString_APIFail(t *testing.T) { func TestString_APIFail(t *testing.T) {
t.Parallel() t.Parallel()
want := "" + want := []string{
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" + "COMPONENT TARGET STATUS DETAILS",
"COMPONENT TARGET STATUS DETAILS\n" + "DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully",
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully\n" + "DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully",
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully\n" + "UDP Connectivity Port 7844 (QUIC) PASS QUIC connection successful",
"UDP Connectivity Port 7844 (QUIC) PASS QUIC connection successful\n" + "TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful",
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful\n" + "Cloudflare API api.cloudflare.com:443 FAIL Connection refused",
"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.",
"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'.",
"SUMMARY: Environment ready with degraded transport. cloudflared will proceed using 'quic'.\n" + }
"--------------------------------------------------------------------------------\n"
assert.Equal(t, want, apiFailReport().String()) assert.Equal(t, want, apiFailReport().String())
} }
func TestString_BothTransportsBlocked(t *testing.T) { func TestString_BothTransportsBlocked(t *testing.T) {
t.Parallel() t.Parallel()
want := "" + want := []string{
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" + "COMPONENT TARGET STATUS DETAILS",
"COMPONENT TARGET STATUS DETAILS\n" + "DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully",
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully\n" + "DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully",
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully\n" + "UDP Connectivity Port 7844 (QUIC) FAIL QUIC connection failed",
"UDP Connectivity Port 7844 (QUIC) FAIL QUIC connection failed\n" + "TCP Connectivity Port 7844 (HTTP/2) FAIL HTTP/2 connection is blocked or unreachable",
"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",
"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.",
"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.",
"SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel.\n" + }
"--------------------------------------------------------------------------------\n"
assert.Equal(t, want, bothTransportsBlockedReport().String()) assert.Equal(t, want, bothTransportsBlockedReport().String())
} }
func TestString_DNSFail(t *testing.T) { func TestString_DNSFail(t *testing.T) {
t.Parallel() t.Parallel()
want := "" + want := []string{
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" + "COMPONENT TARGET STATUS DETAILS",
"COMPONENT TARGET STATUS DETAILS\n" + "DNS Resolution region1.v2.argotunnel.com FAIL No addresses returned",
"DNS Resolution region1.v2.argotunnel.com FAIL No addresses returned\n" + "DNS Resolution region2.v2.argotunnel.com FAIL No addresses returned",
"DNS Resolution region2.v2.argotunnel.com FAIL No addresses returned\n" + "UDP Connectivity Port 7844 (QUIC) SKIP DNS prerequisite failed",
"UDP Connectivity Port 7844 (QUIC) SKIP DNS prerequisite failed\n" + "TCP Connectivity Port 7844 (HTTP/2) SKIP DNS prerequisite failed",
"TCP Connectivity Port 7844 (HTTP/2) SKIP DNS prerequisite failed\n" + "Cloudflare API api.cloudflare.com:443 FAIL Connection refused",
"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.",
"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.",
"SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel.\n" + }
"--------------------------------------------------------------------------------\n"
assert.Equal(t, want, dnsFailReport().String()) assert.Equal(t, want, dnsFailReport().String())
} }
@@ -221,9 +216,9 @@ func TestString_EmptyResults(t *testing.T) {
r := Report{RunID: fixedRunID, SuggestedProtocol: new(connection.QUIC)} r := Report{RunID: fixedRunID, SuggestedProtocol: new(connection.QUIC)}
out := r.String() out := r.String()
// Must not panic and must still emit a valid skeleton. // Must not panic and must still emit a valid skeleton.
assert.Contains(t, out, "CONNECTIVITY PRE-CHECKS") require.Len(t, out, 3)
assert.Contains(t, out, "SUMMARY:") assert.Contains(t, out[0], "COMPONENT")
assert.Contains(t, out, separator()) assert.Contains(t, out[2], "SUMMARY:")
} }
// LogEvent() / structured log renderer tests // LogEvent() / structured log renderer tests