aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Majewsky <majewsky@gmx.net>2026-08-18 13:40:20 +0200
committerStefan Majewsky <majewsky@gmx.net>2026-08-18 13:40:20 +0200
commita3ccdd9f491c20771cfda343ebebd4b5d0ea6602 (patch)
treef0ad530dd072c55c6f46dfea23af993fb31333d4
parent5d17264c8ab8042042162f3b968557aabc4976b3 (diff)
downloadgo-gg-a3ccdd9f491c20771cfda343ebebd4b5d0ea6602.tar.gz
test coverage for microprom
-rw-r--r--assert/panic.go2
-rw-r--r--assert/panic_test.go2
-rw-r--r--internal/accept/accept.go7
-rw-r--r--internal/accept/accept_test.go13
-rw-r--r--microprom/handler_test.go142
-rw-r--r--microprom/labels.go2
-rw-r--r--microprom/microprom.go2
-rw-r--r--testing/microprom/handler_test.go6
8 files changed, 165 insertions, 11 deletions
diff --git a/assert/panic.go b/assert/panic.go
index 69828a4..e19b815 100644
--- a/assert/panic.go
+++ b/assert/panic.go
@@ -26,7 +26,7 @@ func PanicsWith[T any](t TestingTB, action func()) T {
value, ok := result.Panic.(T)
if !ok {
var zero T
- t.Fatalf("panicked with incorrect type: expected %T, but got %#v", zero, result.Panic)
+ t.Fatalf("panicked with incorrect type: expected %T, but got %T: %#v", zero, result.Panic, result.Panic)
}
return value
}
diff --git a/assert/panic_test.go b/assert/panic_test.go
index 135ce18..a3d6877 100644
--- a/assert/panic_test.go
+++ b/assert/panic_test.go
@@ -30,6 +30,6 @@ func TestPanics(t *testing.T) {
})
assert.Equal(t, tc.Outcome, testcapture.OutcomeFailed)
assert.Equal(t, tc.Messages, []testcapture.Message{
- testcapture.Log("panicked with incorrect type: expected string, but got 42"),
+ testcapture.Log("panicked with incorrect type: expected string, but got int: 42"),
})
}
diff --git a/internal/accept/accept.go b/internal/accept/accept.go
index 5ed6e66..c23905a 100644
--- a/internal/accept/accept.go
+++ b/internal/accept/accept.go
@@ -103,6 +103,11 @@ func (h Header) Negotiate(mediaTypes ...string) Option[string] {
return None[string]()
}
+ // if nothing was offered, we default to our own preferred option
+ if len(h.options) == 0 {
+ return Some(offers[0].OriginalValue)
+ }
+
// NOTE: ParseHeader() sorts options by descending weight, so the first match wins.
for _, opt := range h.options {
MEDIATYPE:
@@ -128,5 +133,5 @@ func (h Header) Negotiate(mediaTypes ...string) Option[string] {
}
}
- return Some(offers[0].OriginalValue)
+ return None[string]()
}
diff --git a/internal/accept/accept_test.go b/internal/accept/accept_test.go
index 90b6934..edfef42 100644
--- a/internal/accept/accept_test.go
+++ b/internal/accept/accept_test.go
@@ -12,6 +12,7 @@ import (
)
func TestAcceptWithHeader(t *testing.T) {
+ // asking for a wide range of formats, including wildcard matches
h := accept.ParseHeader([]string{"text/*;q=0.3, text/plain;format=flowed, text/plain;format=fixed;q=0.4, */*;q=0.5"})
assert.Equal(t, h.Negotiate(
@@ -38,6 +39,18 @@ func TestAcceptWithHeader(t *testing.T) {
"text/markdown", // matches with q=0.3
"text/plain", // matches with q=0.3 (but first wins)
), Some("text/markdown"))
+
+ // asking for specific formats only
+ h = accept.ParseHeader([]string{"image/png, image/jpeg"})
+
+ assert.Equal(t, h.Negotiate(
+ "text/plain",
+ "image/png",
+ ), Some("image/png"))
+
+ assert.Equal(t, h.Negotiate(
+ "text/plain",
+ ), None[string]())
}
func TestAcceptWithoutHeader(t *testing.T) {
diff --git a/microprom/handler_test.go b/microprom/handler_test.go
new file mode 100644
index 0000000..7278945
--- /dev/null
+++ b/microprom/handler_test.go
@@ -0,0 +1,142 @@
+// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
+// SPDX-License-Identifier: Apache-2.0
+
+package microprom_test
+
+import (
+ "context"
+ "errors"
+ "io"
+ "maps"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "go.xyrillian.de/gg/assert"
+ "go.xyrillian.de/gg/microprom"
+)
+
+func TestHandlerBasic(t *testing.T) {
+ // NOTE: Most happy path coverage is in `./testing/microprom`.
+ // This only covers the SortOutput = false case.
+
+ h := microprom.Handler{
+ Families: map[microprom.MetricFamilyName]microprom.MetricFamilyInfo{
+ "process": {
+ Type: microprom.MetricTypeInfo,
+ Help: "Information about this process.",
+ },
+ "foo": {
+ Type: microprom.MetricTypeGauge,
+ Help: "This metric family will not have any collected metrics and thus go unreported.",
+ },
+ },
+ Collect: func(ctx context.Context, ms *microprom.MetricSet) error {
+ names := microprom.NewLabelNames("version")
+ labels := ms.FormatLabels(names, "1.2.3")
+ ms.Add("process", labels, 1.0)
+ return nil
+ },
+ }
+
+ // test normal behavior
+ status, body, headers := getMetrics(t, h, nil)
+ assert.Equal(t, status, http.StatusOK)
+ assert.Equal(t, headers, http.Header{
+ "Content-Type": {"text/plain; version=0.0.4; charset=utf-8; escaping=underscores"},
+ })
+ assert.Equal(t, body, strings.TrimSpace(`
+# HELP process_info Information about this process.
+# TYPE process_info info
+process_info{version="1.2.3"} 1
+ `)+"\n")
+}
+
+func TestHandlerErrors(t *testing.T) {
+ h := microprom.Handler{
+ Families: map[microprom.MetricFamilyName]microprom.MetricFamilyInfo{
+ "process": {
+ Type: microprom.MetricTypeInfo,
+ Help: "Information about this process.",
+ },
+ },
+ Collect: func(ctx context.Context, ms *microprom.MetricSet) error {
+ return errors.New("kaboom")
+ },
+ }
+
+ // test unacceptable content negotiation
+ status, body, headers := getMetrics(t, h, http.Header{"Accept": {"application/json"}})
+ assert.Equal(t, status, http.StatusNotAcceptable)
+ assert.Equal(t, headers.Get("Content-Type"), "text/plain; charset=utf-8")
+ assert.Equal(t, body, "supported formats are text/plain and application/openmetrics-text\n")
+
+ // test error during h.Collect()
+ status, body, headers = getMetrics(t, h, nil)
+ assert.Equal(t, status, http.StatusInternalServerError)
+ assert.Equal(t, headers.Get("Content-Type"), "text/plain; charset=utf-8")
+ assert.Equal(t, body, "kaboom\n")
+
+ // test panic from invalid metric family name
+ h.Families["what is this?"] = microprom.MetricFamilyInfo{
+ Type: microprom.MetricTypeGauge,
+ Help: "invalid metric family name",
+ }
+ msg := assert.PanicsWith[string](t, func() { getMetrics(t, h, nil) })
+ assert.Equal(t, msg, `in family "what is this?": invalid family name (does not match /^[a-zA-Z_:][a-zA-Z0-9_:]*$/)`)
+ delete(h.Families, "what is this?")
+
+ // test panic from invalid metric type
+ h.Families["invalid"] = microprom.MetricFamilyInfo{
+ Type: 100,
+ Help: "invalid metric type",
+ }
+ msg = assert.PanicsWith[string](t, func() { getMetrics(t, h, nil) })
+ assert.Equal(t, msg, `in family "invalid": invalid value for microprom.MetricType: 100`)
+ delete(h.Families, "invalid")
+
+ // test panic from invalid label name
+ h.Collect = func(ctx context.Context, ms *microprom.MetricSet) error {
+ names := microprom.NewLabelNames("app:version")
+ labels := ms.FormatLabels(names, "1.2.3")
+ ms.Add("process", labels, 1.0)
+ return nil
+ }
+ msg = assert.PanicsWith[string](t, func() { getMetrics(t, h, nil) })
+ assert.Equal(t, msg, `invalid label name: "app:version"`)
+
+ // test panic from wrong number of label values
+ h.Collect = func(ctx context.Context, ms *microprom.MetricSet) error {
+ names := microprom.NewLabelNames("version", "build_date")
+ labels := ms.FormatLabels(names, "1.2.3") // forgot build_date
+ ms.Add("process", labels, 1.0)
+ return nil
+ }
+ msg = assert.PanicsWith[string](t, func() { getMetrics(t, h, nil) })
+ assert.Equal(t, msg, `expected 2 label values, but got 1`)
+
+ // test panic from using an undeclared metric family
+ h.Collect = func(ctx context.Context, ms *microprom.MetricSet) error {
+ ms.Add("invalid", "", 1.0)
+ return nil
+ }
+ msg = assert.PanicsWith[string](t, func() { getMetrics(t, h, nil) })
+ assert.Equal(t, msg, `no such family: invalid`)
+}
+
+func getMetrics(t *testing.T, h http.Handler, requestHeaders http.Header) (status int, responseBody string, responseHeaders http.Header) {
+ t.Helper()
+ r := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/metrics", nil)
+ maps.Copy(r.Header, requestHeaders)
+
+ w := httptest.NewRecorder()
+ h.ServeHTTP(w, r)
+ resp := w.Result()
+
+ buf, err := io.ReadAll(resp.Body)
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+ return resp.StatusCode, string(buf), resp.Header
+}
diff --git a/microprom/labels.go b/microprom/labels.go
index 51cdbb1..5b56c9c 100644
--- a/microprom/labels.go
+++ b/microprom/labels.go
@@ -54,7 +54,7 @@ func (ms *MetricSet) FormatLabels(n LabelNames, values ...string) Labels {
// NOTE on API structure: This is not part of ms.Add() to allow reusing label sets for multiple metrics.
if len(n.names) != len(values) {
- panic("arguments are not of equal length")
+ panic(fmt.Sprintf("expected %d label values, but got %d", len(n.names), len(values)))
}
if len(n.names) == 0 {
return ""
diff --git a/microprom/microprom.go b/microprom/microprom.go
index 86a5419..4055c8b 100644
--- a/microprom/microprom.go
+++ b/microprom/microprom.go
@@ -135,7 +135,7 @@ func NewMetricSet(syntax Syntax, families map[MetricFamilyName]MetricFamilyInfo)
func (ms *MetricSet) Add(name MetricFamilyName, labels Labels, value float64) {
_, ok := ms.metrics[name]
if !ok {
- panic("no such family: " + name)
+ panic("no such family: " + string(name))
}
ms.metrics[name] = append(ms.metrics[name], metric{labels, value})
}
diff --git a/testing/microprom/handler_test.go b/testing/microprom/handler_test.go
index 6aa60f0..93027e3 100644
--- a/testing/microprom/handler_test.go
+++ b/testing/microprom/handler_test.go
@@ -76,12 +76,6 @@ func TestHandlerFunctionallyIdenticalToPromhttp(t *testing.T) {
body2, headers2 = getMetrics(t, h2, http.Header{"Accept": {"application/openmetrics-text; version=1.0.0"}})
assert.Equal(t, strings.Split(body1, "\n"), strings.Split(body2, "\n"))
assert.Equal(t, headers1, headers2)
-
- // test invalid Accept header
- body1, headers1 = getMetrics(t, h1, http.Header{"Accept": {"image/*"}})
- body2, headers2 = getMetrics(t, h2, http.Header{"Accept": {"image/*"}})
- assert.Equal(t, strings.Split(body1, "\n"), strings.Split(body2, "\n"))
- assert.Equal(t, headers1, headers2)
}
func getMetrics(t *testing.T, h http.Handler, requestHeaders http.Header) (responseBody string, responseHeaders http.Header) {