utils API

utils

package

API reference for the utils package.

F
function

GetLocalIP

GetLocalIP returns the first non-loopback IPv4 address.

Returns

string
error
cmd/rfw/utils/network.go:17-30
func GetLocalIP() (string, error)

{
	addrs, err := net.InterfaceAddrs()
	if err != nil {
		return "", err
	}

	for _, addr := range addrs {
		if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() && ipNet.IP.To4() != nil {
			return ipNet.IP.String(), nil
		}
	}

	return "", fmt.Errorf("no local IP address found")
}
F
function

OpenBrowser

OpenBrowser opens url with the configured system browser.

Parameters

url
string

Returns

error
cmd/rfw/utils/network.go:33-41
func OpenBrowser(url string) error

{
	if configured := os.Getenv("BROWSER"); configured != "" {
		cmd := strings.Fields(configured)[0]
		if _, err := exec.LookPath(cmd); err != nil {
			return err
		}
	}
	return browser.OpenURL(url)
}
F
function

TestOpenBrowserError

Parameters

cmd/rfw/utils/network_test.go:7-13
func TestOpenBrowserError(t *testing.T)

{
	t.Setenv("BROWSER", "nonexistent-browser")

	if err := OpenBrowser("http://example.com"); err == nil {
		t.Fatalf("expected error when browser command is missing")
	}
}
F
function

ClearScreen

ClearScreen clears the terminal.

cmd/rfw/utils/output.go:29-32
func ClearScreen()

{
	fmt.Print("\033[H\033[2J")
	fmt.Println()
}
F
function

PrintStartupInfo

PrintStartupInfo prints local and network development URLs.

Parameters

port
string
httpsPort
string
localIP
string
host
bool
cmd/rfw/utils/output.go:35-48
func PrintStartupInfo(port, httpsPort, localIP string, host bool)

{
	fmt.Println(indent, boldRed("rfw"), faint(core.Version()))
	fmt.Println()
	fmt.Println(indent, red("➜ "), bold("Local:"), red(fmt.Sprintf("http://localhost:%s/ - https://localhost:%s/", port, httpsPort)))

	if host {
		fmt.Println(indent, red("➜ "), faint(bold("Network:")), white(fmt.Sprintf("http://%s:%s/ (https://%s:%s/)", localIP, port, localIP, httpsPort)))
	} else {
		fmt.Println(indent, red("➜ "), faint(bold("Network:")), white("--host"), faint("to expose"))
	}

	fmt.Println(indent, faintRed("➜ "), faint("Press"), bold("h + enter"), faint("to show help"))
	fmt.Println()
}
F
function

LogServeRequest

LogServeRequest logs an HTTP request path.

Parameters

cmd/rfw/utils/output.go:51-53
func LogServeRequest(r *http.Request)

{
	fmt.Printf("%s %s %s\n", faint(time.Now().Format("15:04:05")), boldYellow("serving"), faint(r.URL.Path))
}
F
function

Info

Info prints an informational CLI message.

Parameters

message
string
cmd/rfw/utils/output.go:56-58
func Info(message string)

{
	fmt.Println(boldRed("[rfw]"), message)
}
F
function

Fatal

Fatal logs a fatal CLI error and exits.

Parameters

message
string
err
error
cmd/rfw/utils/output.go:61-63
func Fatal(message string, err error)

{
	log.Fatalf(boldRed("[rfw] "), message, err)
}
F
function

Error

Error reports a failure without terminating the process, for loops that
must survive it (e.g. the dev watcher across compile errors).

Parameters

message
string
err
error
cmd/rfw/utils/output.go:67-69
func Error(message string, err error)

{
	log.Printf("%s%s%v", boldRed("[rfw] "), message, err)
}
F
function

EnableDebug

EnableDebug enables or disables debug output.

Parameters

d
bool
cmd/rfw/utils/output.go:72-72
func EnableDebug(d bool)

{ dbg = d }
F
function

IsDebug

IsDebug reports whether debug mode is enabled.

Returns

bool
cmd/rfw/utils/output.go:75-75
func IsDebug() bool

{ return dbg }
F
function

Debug

Debug prints a message when debug output is enabled.

Parameters

message
string
cmd/rfw/utils/output.go:78-82
func Debug(message string)

{
	if dbg {
		fmt.Println(boldRed("[rfw][debug]"), faint(message))
	}
}
F
function

PrintHelp

PrintHelp prints CLI shortcuts and flags.

cmd/rfw/utils/output.go:85-98
func PrintHelp()

{
	ClearScreen()
	fmt.Println()
	fmt.Println(indent, red("➜ "), bold("Help"))
	fmt.Println(indent, indent, yellow("➜ "), bold("Shortcuts"))
	fmt.Println(indent, indent, indent, faint("Press"), bold("c + enter"), faint("to stop the server"))
	fmt.Println(indent, indent, indent, faint("Press"), bold("o + enter"), faint("to open the browser"))
	fmt.Println(indent, indent, indent, faint("Press"), bold("u + enter"), faint("to show the startup info and clear logs"))
	fmt.Println(indent, indent, indent, faint("Press"), bold("h + enter"), faint("to show this help"))
	fmt.Println(indent, indent, yellow("➜ "), bold("Flags"))
	fmt.Println(indent, indent, indent, faint("Use"), bold("--host"), faint("to expose the server to the network"))
	fmt.Println(indent, indent, indent, faint("Use"), bold("--port=XXXX"), faint("to specify a port"))
	fmt.Println()
}
S
struct

githubRelease

cmd/rfw/utils/update.go:34-41
type githubRelease struct

Fields

Name Type Description
TagName string json:"tag_name"
Assets []struct { BrowserDownloadURL string `json:"browser_download_url"` Digest string `json:"digest"` Name string `json:"name"` } json:"assets"
F
function

fetchLatestRelease

Returns

cmd/rfw/utils/update.go:43-86
func fetchLatestRelease() (githubRelease, error)

{
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	req, err := safehttp.NewRequest(
		ctx,
		http.MethodGet,
		"https://api.github.com/repos/"+githubRepo+"/releases/latest",
	)
	if err != nil {
		return githubRelease{}, err
	}
	req.Header.Set("Accept", "application/vnd.github+json")

	resp, err := safehttp.NewClient().Do(req)
	if err != nil {
		return githubRelease{}, err
	}

	if resp.StatusCode != http.StatusOK {
		if closeErr := resp.Body.Close(); closeErr != nil {
			return githubRelease{}, closeErr
		}
		return githubRelease{}, fmt.Errorf("status %d", resp.StatusCode)
	}

	body, err := io.ReadAll(resp.Body)
	closeErr := resp.Body.Close()
	if err != nil {
		return githubRelease{}, err
	}
	if closeErr != nil {
		return githubRelease{}, closeErr
	}

	var release githubRelease
	if err := json.Unmarshal(body, &release); err != nil {
		return githubRelease{}, err
	}
	if release.TagName == "" {
		return githubRelease{}, fmt.Errorf("latest release has no tag")
	}
	return release, nil
}
F
function

shouldCheckUpdate

Returns

bool
cmd/rfw/utils/update.go:88-99
func shouldCheckUpdate() bool

{
	home, err := os.UserHomeDir()
	if err != nil {
		return true
	}
	path := home + "/" + checkFile
	info, err := os.Stat(path)
	if err != nil {
		return true
	}
	return time.Since(info.ModTime()) > checkInterval
}
F
function

markChecked

Returns

error
cmd/rfw/utils/update.go:101-108
func markChecked() error

{
	home, err := os.UserHomeDir()
	if err != nil {
		return err
	}
	path := home + "/" + checkFile
	return os.WriteFile(path, []byte(time.Now().Format(time.RFC3339)), 0o600)
}
S
struct

semanticVersion

cmd/rfw/utils/update.go:110-113
type semanticVersion struct

Fields

Name Type Description
core [3]string
prerelease []string
F
function

isNewer

Parameters

current
string
latest
string

Returns

bool
cmd/rfw/utils/update.go:115-132
func isNewer(current, latest string) bool

{
	currentVersion, currentOK := parseSemanticVersion(current)
	latestVersion, latestOK := parseSemanticVersion(latest)
	if !currentOK || !latestOK {
		return false
	}

	for i := 0; i < 3; i++ {
		comparison := compareNumericIdentifier(latestVersion.core[i], currentVersion.core[i])
		if comparison > 0 {
			return true
		}
		if comparison < 0 {
			return false
		}
	}
	return comparePrerelease(latestVersion.prerelease, currentVersion.prerelease) > 0
}
F
function

parseSemanticVersion

Parameters

raw
string

Returns

cmd/rfw/utils/update.go:134-167
func parseSemanticVersion(raw string) (semanticVersion, bool)

{
	value := strings.TrimPrefix(raw, "v")
	if value == "" {
		return semanticVersion{}, false
	}

	main, build, hasBuild := strings.Cut(value, "+")
	if hasBuild && !validIdentifierList(build, false) {
		return semanticVersion{}, false
	}

	core, prerelease, hasPrerelease := strings.Cut(main, "-")
	coreParts := strings.Split(core, ".")
	if len(coreParts) != 3 {
		return semanticVersion{}, false
	}

	var parsed semanticVersion
	for i, part := range coreParts {
		if !validNumericIdentifier(part) {
			return semanticVersion{}, false
		}
		parsed.core[i] = part
	}

	if hasPrerelease {
		if !validIdentifierList(prerelease, true) {
			return semanticVersion{}, false
		}
		parsed.prerelease = strings.Split(prerelease, ".")
	}

	return parsed, true
}
F
function

validIdentifierList

Parameters

value
string
rejectNumericLeadingZero
bool

Returns

bool
cmd/rfw/utils/update.go:169-193
func validIdentifierList(value string, rejectNumericLeadingZero bool) bool

{
	parts := strings.Split(value, ".")
	for _, part := range parts {
		if part == "" {
			return false
		}
		numeric := true
		for i := 0; i < len(part); i++ {
			character := part[i]
			if character < '0' || character > '9' {
				numeric = false
			}
			if (character < '0' || character > '9') &&
				(character < 'A' || character > 'Z') &&
				(character < 'a' || character > 'z') &&
				character != '-' {
				return false
			}
		}
		if rejectNumericLeadingZero && numeric && len(part) > 1 && part[0] == '0' {
			return false
		}
	}
	return true
}
F
function

validNumericIdentifier

Parameters

value
string

Returns

bool
cmd/rfw/utils/update.go:195-205
func validNumericIdentifier(value string) bool

{
	if value == "" || len(value) > 1 && value[0] == '0' {
		return false
	}
	for i := 0; i < len(value); i++ {
		if value[i] < '0' || value[i] > '9' {
			return false
		}
	}
	return true
}
F
function

compareNumericIdentifier

Parameters

left
string
right
string

Returns

int
cmd/rfw/utils/update.go:207-215
func compareNumericIdentifier(left, right string) int

{
	if len(left) != len(right) {
		if len(left) > len(right) {
			return 1
		}
		return -1
	}
	return strings.Compare(left, right)
}
F
function

comparePrerelease

Parameters

left
[]string
right
[]string

Returns

int
cmd/rfw/utils/update.go:217-257
func comparePrerelease(left, right []string) int

{
	if len(left) == 0 || len(right) == 0 {
		switch {
		case len(left) == 0 && len(right) == 0:
			return 0
		case len(left) == 0:
			return 1
		default:
			return -1
		}
	}

	sharedLength := min(len(left), len(right))
	for i := 0; i < sharedLength; i++ {
		leftNumeric := validNumericIdentifier(left[i])
		rightNumeric := validNumericIdentifier(right[i])
		switch {
		case leftNumeric && rightNumeric:
			if comparison := compareNumericIdentifier(left[i], right[i]); comparison != 0 {
				return comparison
			}
		case leftNumeric:
			return -1
		case rightNumeric:
			return 1
		default:
			if comparison := strings.Compare(left[i], right[i]); comparison != 0 {
				return comparison
			}
		}
	}

	switch {
	case len(left) > len(right):
		return 1
	case len(left) < len(right):
		return -1
	default:
		return 0
	}
}
F
function

downloadAndReplace

Parameters

assetURL
string
digest
string

Returns

err
error
cmd/rfw/utils/update.go:259-329
func downloadAndReplace(assetURL, digest string) (err error)

{
	expectedDigest, err := parseSHA256Digest(digest)
	if err != nil {
		return err
	}

	exePath, err := os.Executable()
	if err != nil {
		return err
	}
	tmp, err := os.CreateTemp(filepath.Dir(exePath), ".rfw-update-*")
	if err != nil {
		return err
	}
	tmpName := tmp.Name()
	defer func() {
		if tmp != nil {
			if closeErr := tmp.Close(); err == nil {
				err = closeErr
			}
		}
		if removeErr := os.Remove(tmpName); err == nil && removeErr != nil && !errors.Is(removeErr, os.ErrNotExist) {
			err = removeErr
		}
	}()

	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
	defer cancel()

	req, err := safehttp.NewRequest(ctx, http.MethodGet, assetURL)
	if err != nil {
		return err
	}

	resp, err := safehttp.NewClient().Do(req)
	if err != nil {
		return err
	}

	if resp.StatusCode != http.StatusOK {
		if closeErr := resp.Body.Close(); closeErr != nil {
			return closeErr
		}
		return fmt.Errorf("download failed: status %d", resp.StatusCode)
	}

	copyErr := copyWithSHA256(tmp, resp.Body, expectedDigest)
	closeErr := resp.Body.Close()
	if copyErr != nil {
		return copyErr
	}
	if closeErr != nil {
		return closeErr
	}
	if err := tmp.Chmod(0o700); err != nil {
		return err
	}
	if err := tmp.Sync(); err != nil {
		return err
	}
	if err := tmp.Close(); err != nil {
		return err
	}
	tmp = nil

	if err := replaceExecutable(tmpName, exePath); err != nil {
		return err
	}

	return nil
}
F
function

parseSHA256Digest

Parameters

digest
string

Returns

[sha256.Size]byte
error
cmd/rfw/utils/update.go:331-343
func parseSHA256Digest(digest string) ([sha256.Size]byte, error)

{
	var expected [sha256.Size]byte
	encoded, found := strings.CutPrefix(digest, "sha256:")
	if !found {
		return expected, fmt.Errorf("release asset has no SHA-256 digest")
	}
	decoded, err := hex.DecodeString(encoded)
	if err != nil || len(decoded) != sha256.Size {
		return expected, fmt.Errorf("release asset has an invalid SHA-256 digest")
	}
	copy(expected[:], decoded)
	return expected, nil
}
F
function

copyWithSHA256

Parameters

destination
source
expected
[sha256.Size]byte

Returns

error
cmd/rfw/utils/update.go:345-354
func copyWithSHA256(destination io.Writer, source io.Reader, expected [sha256.Size]byte) error

{
	hasher := sha256.New()
	if _, err := io.Copy(io.MultiWriter(destination, hasher), source); err != nil {
		return err
	}
	if subtle.ConstantTimeCompare(hasher.Sum(nil), expected[:]) != 1 {
		return fmt.Errorf("downloaded asset checksum does not match the release")
	}
	return nil
}
F
function

replaceExecutable

Parameters

source
string
target
string

Returns

error
cmd/rfw/utils/update.go:356-375
func replaceExecutable(source, target string) error

{
	if runtime.GOOS != "windows" {
		return os.Rename(source, target)
	}

	backup := target + ".old"
	if err := os.Remove(backup); err != nil && !errors.Is(err, os.ErrNotExist) {
		return err
	}
	if err := os.Rename(target, backup); err != nil {
		return err
	}
	if err := os.Rename(source, target); err != nil {
		if rollbackErr := os.Rename(backup, target); rollbackErr != nil {
			return errors.Join(err, rollbackErr)
		}
		return err
	}
	return nil
}
F
function

getAssetName

Returns

string
cmd/rfw/utils/update.go:377-385
func getAssetName() string

{
	goos := runtime.GOOS
	goarch := runtime.GOARCH
	ext := ""
	if goos == "windows" {
		ext = ".exe"
	}
	return fmt.Sprintf("rfw-%s-%s%s", goos, goarch, ext)
}
F
function

shouldSkipUpdateCheck

shouldSkipUpdateCheck reports whether the update check must not run at all:
explicit opt-out via RFW_NO_UPDATE_CHECK, or a non-interactive session (CI,
scripts, pipes) where a network call and an update prompt would get in the
way.

Parameters

noUpdateEnv
string
stdinTTY
bool
stdoutTTY
bool

Returns

bool
cmd/rfw/utils/update.go:391-396
func shouldSkipUpdateCheck(noUpdateEnv string, stdinTTY, stdoutTTY bool) bool

{
	if noUpdateEnv != "" {
		return true
	}
	return !stdinTTY || !stdoutTTY
}
F
function

isTerminal

isTerminal reports whether f is attached to a character device.

Parameters

f

Returns

bool
cmd/rfw/utils/update.go:399-405
func isTerminal(f *os.File) bool

{
	info, err := f.Stat()
	if err != nil {
		return false
	}
	return info.Mode()&os.ModeCharDevice != 0
}
F
function

CheckForUpdate

CheckForUpdate checks GitHub for a newer CLI release in interactive sessions.

cmd/rfw/utils/update.go:408-465
func CheckForUpdate()

{
	if shouldSkipUpdateCheck(os.Getenv("RFW_NO_UPDATE_CHECK"), isTerminal(os.Stdin), isTerminal(os.Stdout)) {
		return
	}
	if !shouldCheckUpdate() {
		return
	}

	release, err := fetchLatestRelease()
	if err != nil {
		return
	}
	if err := markChecked(); err != nil {
		Debug(fmt.Sprintf("failed to record update check: %v", err))
	}
	latest := release.TagName

	if !isNewer(core.Version(), release.TagName) {
		return
	}

	assetName := getAssetName()
	fmt.Println()
	Info(fmt.Sprintf("Update available: %s → %s", faint(core.Version()), boldCyan(latest)))

	fmt.Print(indent, red("➜ "), bold("Update now? [y/N] "))
	var answer string
	if _, err := fmt.Scanln(&answer); err != nil {
		return
	}
	answer = strings.TrimSpace(strings.ToLower(answer))
	if answer != "y" && answer != "yes" {
		fmt.Println(indent, faint("Skipped."))
		return
	}

	assetURL := ""
	assetDigest := ""
	for _, a := range release.Assets {
		if a.Name == assetName {
			assetURL = a.BrowserDownloadURL
			assetDigest = a.Digest
			break
		}
	}
	if assetURL == "" {
		Info(fmt.Sprintf("No binary found for %s/%s", runtime.GOOS, runtime.GOARCH))
		return
	}

	Info("Downloading...")
	if err := downloadAndReplace(assetURL, assetDigest); err != nil {
		Info(fmt.Sprintf("Update failed: %v", err))
		return
	}

	Info(fmt.Sprintf("Updated to %s!", latest))
}
F
function

TestShouldSkipUpdateCheck

The update check must stay out of the way in CI, scripts and pipes, and
honour the explicit RFW_NO_UPDATE_CHECK opt-out.

Parameters

cmd/rfw/utils/update_test.go:9-28
func TestShouldSkipUpdateCheck(t *testing.T)

{
	cases := []struct {
		name      string
		env       string
		stdinTTY  bool
		stdoutTTY bool
		wantSkip  bool
	}{
		{"interactive", "", true, true, false},
		{"env opt-out", "1", true, true, true},
		{"stdin piped", "", false, true, true},
		{"stdout piped", "", true, false, true},
		{"fully non-interactive", "", false, false, true},
	}
	for _, tc := range cases {
		if got := shouldSkipUpdateCheck(tc.env, tc.stdinTTY, tc.stdoutTTY); got != tc.wantSkip {
			t.Errorf("%s: got %v, want %v", tc.name, got, tc.wantSkip)
		}
	}
}
F
function

TestIsNewer

Parameters

cmd/rfw/utils/utils_test.go:16-40
func TestIsNewer(t *testing.T)

{
	tests := []struct {
		name    string
		current string
		latest  string
		want    bool
	}{
		{name: "minor with two digits", current: "v2.9.0", latest: "v2.10.0", want: true},
		{name: "stable after prerelease", current: "v2.1.0-beta.19", latest: "v2.1.0", want: true},
		{name: "prerelease before stable", current: "v2.1.0", latest: "v2.1.0-beta.19"},
		{name: "numeric prerelease", current: "v2.1.0-beta.9", latest: "v2.1.0-beta.10", want: true},
		{name: "build metadata ignored", current: "v2.1.0+one", latest: "v2.1.0+two"},
		{name: "older release", current: "v3.0.0", latest: "v2.99.99"},
		{name: "invalid current", current: "development", latest: "v2.1.0"},
		{name: "invalid latest", current: "v2.1.0", latest: "v2.01.0"},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if got := isNewer(test.current, test.latest); got != test.want {
				t.Fatalf("isNewer(%q, %q) = %v, want %v", test.current, test.latest, got, test.want)
			}
		})
	}
}
F
function

TestCopyWithSHA256

Parameters

cmd/rfw/utils/utils_test.go:42-59
func TestCopyWithSHA256(t *testing.T)

{
	content := []byte("verified update")
	expected := sha256.Sum256(content)
	var destination bytes.Buffer

	if err := copyWithSHA256(&destination, bytes.NewReader(content), expected); err != nil {
		t.Fatalf("copy verified content: %v", err)
	}
	if !bytes.Equal(destination.Bytes(), content) {
		t.Fatalf("copied %q, want %q", destination.Bytes(), content)
	}

	destination.Reset()
	wrong := sha256.Sum256([]byte("different"))
	if err := copyWithSHA256(&destination, bytes.NewReader(content), wrong); err == nil {
		t.Fatal("expected checksum mismatch")
	}
}
F
function

TestParseSHA256Digest

Parameters

cmd/rfw/utils/utils_test.go:61-74
func TestParseSHA256Digest(t *testing.T)

{
	content := sha256.Sum256([]byte("release"))
	digest := "sha256:" + fmt.Sprintf("%x", content)
	got, err := parseSHA256Digest(digest)
	if err != nil {
		t.Fatalf("parse digest: %v", err)
	}
	if got != content {
		t.Fatalf("parsed digest %x, want %x", got, content)
	}
	if _, err := parseSHA256Digest(""); err == nil {
		t.Fatal("expected missing digest error")
	}
}
F
function

captureOutput

captureOutput redirects stdout for the duration of f and returns what was
written to it.

Parameters

f
func()

Returns

string
cmd/rfw/utils/utils_test.go:78-99
func captureOutput(t *testing.T, f func()) string

{
	t.Helper()
	orig := os.Stdout
	r, w, err := os.Pipe()
	if err != nil {
		t.Fatalf("pipe: %v", err)
	}
	os.Stdout = w
	f()
	if err := w.Close(); err != nil {
		t.Fatalf("close writer: %v", err)
	}
	os.Stdout = orig
	defer func() {
		if err := r.Close(); err != nil {
			t.Errorf("close reader: %v", err)
		}
	}()
	var buf bytes.Buffer
	_, _ = io.Copy(&buf, r)
	return buf.String()
}
F
function

TestDebug

Parameters

cmd/rfw/utils/utils_test.go:101-113
func TestDebug(t *testing.T)

{
	EnableDebug(true)
	out := captureOutput(t, func() { Debug("hello") })
	if !strings.Contains(out, "[rfw][debug]") {
		t.Fatalf("expected debug output, got %q", out)
	}

	EnableDebug(false)
	out = captureOutput(t, func() { Debug("no output") })
	if out != "" {
		t.Fatalf("expected no output, got %q", out)
	}
}
F
function

TestIsDebug

Parameters

cmd/rfw/utils/utils_test.go:115-124
func TestIsDebug(t *testing.T)

{
	EnableDebug(true)
	if !IsDebug() {
		t.Fatalf("expected true in debug mode")
	}
	EnableDebug(false)
	if IsDebug() {
		t.Fatalf("expected false when debug disabled")
	}
}
F
function

TestPrintStartupInfo

Parameters

cmd/rfw/utils/utils_test.go:126-138
func TestPrintStartupInfo(t *testing.T)

{
	out := captureOutput(t, func() { PrintStartupInfo("8080", "8443", "192.168.0.1", true) })
	if !strings.Contains(out, "http://localhost:8080/") {
		t.Fatalf("expected local URL in output, got %q", out)
	}
	if !strings.Contains(out, "http://192.168.0.1:8080/") {
		t.Fatalf("expected network URL, got %q", out)
	}
	out = captureOutput(t, func() { PrintStartupInfo("8080", "8443", "", false) })
	if !strings.Contains(out, "--host") {
		t.Fatalf("expected hint about --host, got %q", out)
	}
}
F
function

TestPrintHelp

Parameters

cmd/rfw/utils/utils_test.go:140-145
func TestPrintHelp(t *testing.T)

{
	out := captureOutput(t, PrintHelp)
	if !strings.Contains(out, "Shortcuts") || !strings.Contains(out, "Flags") {
		t.Fatalf("missing help sections, got %q", out)
	}
}
F
function

TestLogServeRequest

Parameters

cmd/rfw/utils/utils_test.go:147-153
func TestLogServeRequest(t *testing.T)

{
	req := httptest.NewRequest("GET", "/foo", nil)
	out := captureOutput(t, func() { LogServeRequest(req) })
	if !strings.Contains(out, "/foo") {
		t.Fatalf("expected path in output, got %q", out)
	}
}
F
function

TestReplaceExecutable

Parameters

cmd/rfw/utils/utils_test.go:155-188
func TestReplaceExecutable(t *testing.T)

{
	dir := t.TempDir()
	source := dir + "/source"
	target := dir + "/target"
	root, err := os.OpenRoot(dir)
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() {
		if err := root.Close(); err != nil {
			t.Errorf("close test root: %v", err)
		}
	})
	if err := root.WriteFile("source", []byte("new"), 0o600); err != nil {
		t.Fatal(err)
	}
	if err := root.WriteFile("target", []byte("old"), 0o600); err != nil {
		t.Fatal(err)
	}

	if err := replaceExecutable(source, target); err != nil {
		t.Fatalf("replace executable: %v", err)
	}
	content, err := root.ReadFile("target")
	if err != nil {
		t.Fatal(err)
	}
	if string(content) != "new" {
		t.Fatalf("target contains %q, want %q", content, "new")
	}
	if _, err := os.Stat(source); !os.IsNotExist(err) {
		t.Fatalf("source still exists: %v", err)
	}
}