Skip to main content

q: add some tests; tidy up the code

ID
415b8d2
date
2025-10-12 12:05:12+00:00
author
Alex Chan <alexc@tailscale.com>
parent
84465bb
message
q: add some tests; tidy up the code
changed files
3 files, 74 additions, 17 deletions

Changed files

.github/workflows/test.yml (0) → .github/workflows/test.yml (368)

diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..fe422d9
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,23 @@
+name: Go
+
+on: [push]
+
+jobs:
+  build:
+
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        go-version:
+          - "1.25"
+
+    steps:
+      - uses: actions/checkout@v5
+
+      - name: Setup Go ${{ matrix.go-version }}
+        uses: actions/setup-go@v5
+        with:
+          go-version: ${{ matrix.go-version }}
+
+      - name: Run tests
+        run: go test

q.go (2236) → q.go (2438)

diff --git a/q.go b/q.go
index 3300804..4b3964a 100644
--- a/q.go
+++ b/q.go
@@ -4,10 +4,29 @@ import (
 	"bufio"
 	"fmt"
 	"os"
+	"regexp"
 	"runtime"
 	"strings"
 )
 
+// Choose how to write the function name in the file.
+//
+// e.g. test functions can be long and have multiple parts, so we just
+// pull out the most meaningful part.
+func chooseDisplayName(functionName string) string {
+	parts := strings.Split(functionName, ".")
+
+	// If this is an anonymous function, the name will be something like
+	// "func1", which is unhelpful.
+	//
+	// Throw away that part and get the next part.
+	if _, err := regexp.MatchString("^func[0-9]+$", functionName); len(parts) > 1 && err == nil {
+		return parts[len(parts)-2]
+	}
+
+	return parts[len(parts)-1]
+}
+
 func getFunctionName() string {
 	pc, _, _, ok := runtime.Caller(2)
 	if !ok {
@@ -19,14 +38,7 @@ func getFunctionName() string {
 		return "<unknown>"
 	}
 
-	// The name of test functions can be long and have multiple parts,
-	// e.g. tailscale.com/wgengine/magicsock.TestNetworkDownSendErrors
-	//
-	// For brevity, just get the last part.
-	parts := strings.Split(fn.Name(), ".")
-	lastPart := parts[len(parts)-1]
-
-	return lastPart
+	return chooseDisplayName(fn.Name())
 }
 
 func getExpression() string {
@@ -79,7 +91,7 @@ func toString(value any, a ...any) string {
 	case fmt.Stringer:
 		return v.String()
 	default:
-		return fmt.Sprintf("%v", v) // fallback
+		return fmt.Sprintf("%+v", v) // fallback
 	}
 }
 
@@ -93,14 +105,7 @@ func Q(value any, a ...any) {
 	functionName := getFunctionName()
 	expression := getExpression()
 
-	var line string
-
-	switch value.(type) {
-	case string:
-		line = "\x1b[32m" + functionName + "\x1b[39m: " + toString(value, a...) + "\n\n"
-	default:
-		line = "\x1b[32m" + functionName + "\x1b[39m: " + expression + " = \x1b[36m" + toString(value, a...) + "\x1b[39m\n\n"
-	}
+	line := "\x1b[32m" + functionName + "\x1b[39m: " + expression + " = \x1b[36m" + toString(value, a...) + "\x1b[39m\n\n"
 
 	if _, err = f.WriteString(line); err != nil {
 		panic(err)

q_test.go (0) → q_test.go (705)

diff --git a/q_test.go b/q_test.go
new file mode 100644
index 0000000..ead6941
--- /dev/null
+++ b/q_test.go
@@ -0,0 +1,29 @@
+package q
+
+import "testing"
+
+func TestChooseDisplayName(t *testing.T) {
+	for _, tt := range []struct {
+		label        string
+		functionName string
+		displayName  string
+	}{
+		{
+			label:        "test-function",
+			functionName: "tailscale.com/wgengine/magicsock.TestNetworkDownSendErrors",
+			displayName:  "TestNetworkDownSendErrors",
+		},
+		{
+			label:        "test-function-with-parameters",
+			functionName: "tailscale.com/tstest/integration.TestOneNodeUpAuth.func1",
+			displayName:  "TestOneNodeUpAuth",
+		},
+	} {
+		t.Run(tt.label, func(t *testing.T) {
+			if got := chooseDisplayName(tt.functionName); got != tt.displayName {
+				t.Fatalf("wanted: %s, got: %s", tt.displayName, got)
+			}
+		})
+
+	}
+}