Skip to content

Commit 8ba762f

Browse files
committed
docs: clarify runtime tag comparability checks and update test logging
Signed-off-by: Johan Lindh <johan@linkdata.se>
1 parent 57b9021 commit 8ba762f

2 files changed

Lines changed: 26 additions & 34 deletions

File tree

‎jaws.go‎

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,10 @@
6666
//
6767
// Always run the tests with the -race flag (or -tags debug). Both set
6868
// deadlock.Debug, which enables the debug-gated runtime invariant checks: the
69-
// lock-order verification described above, the late-handler panic, and the
70-
// tag-comparability check in [github.com/linkdata/jaws/lib/tag]. Those branches
71-
// are compile-time dead in normal builds, so a plain "go test" neither
72-
// exercises them nor reports their statement coverage. CI builds with
69+
// lock-order verification described above and the late-handler panic. Those
70+
// branches are compile-time dead in normal builds, so a plain "go test" neither
71+
// exercises them nor reports their statement coverage. Runtime tag-comparability
72+
// checks in [github.com/linkdata/jaws/lib/tag] run in every build. CI builds with
7373
// -tags debug -race.
7474
package jaws
7575

@@ -759,11 +759,9 @@ func (jw *Jaws) Broadcast(msg wire.Message) {
759759
// [Jaws.reportMisuse], if any tag is not comparable at runtime.
760760
//
761761
// Expanded tags become map keys in the processing loop (wantMessage's lookup in
762-
// Request.tagMap). A value can pass tag expansion's static comparability check yet
763-
// be non-comparable at runtime (a comparable struct holding e.g. a func in an
764-
// interface field), which panics when hashed. [tag.NewErrNotComparable] runs the
765-
// runtime value check that ensureUsableTag only performs in debug builds, so this
766-
// rejects a bad Dest before it can panic the Serve goroutine and crash the process.
762+
// Request.tagMap). TagExpand rejects the known runtime-non-comparable
763+
// struct/array case, and this guard remains as a final defense before a bad Dest
764+
// can panic the Serve goroutine and crash the process.
767765
func (jw *Jaws) dropNonComparableTags(tags []any) []any {
768766
for _, tagValue := range tags {
769767
if cmperr := tag.NewErrNotComparable(tagValue); cmperr != nil {
@@ -793,14 +791,13 @@ func (jw *Jaws) setDirty(tags []any) {
793791
// Note that if any of the tags implement [tag.TagGetter], it will be called
794792
// with a nil [Request]. Prefer using [Request.Dirty] which avoids this.
795793
//
796-
// A tag that is comparable statically but not at runtime (a comparable struct
797-
// holding e.g. a func in an interface field) panics the calling goroutine when
798-
// hashed as a map key, in non-debug builds where the expansion check is skipped.
799-
// The panic is contained to the caller (the lock is released and the [Jaws.Serve]
800-
// loop is unaffected); it is not logged-and-dropped the way [Jaws.Broadcast]
801-
// handles such a [wire.Message.Dest], because that hashing happens in the Serve
802-
// goroutine where a panic would crash the process. [Request.Dirty] behaves the
803-
// same as Dirty here.
794+
// A tag that somehow passes expansion but is still not hashable panics the
795+
// calling goroutine when hashed as a map key. The panic is contained to the
796+
// caller (the lock is released and the [Jaws.Serve] loop is unaffected); it is
797+
// not logged-and-dropped the way [Jaws.Broadcast] handles such a
798+
// [wire.Message.Dest], because that hashing happens in the Serve goroutine where
799+
// a panic would crash the process. [Request.Dirty] behaves the same as Dirty
800+
// here.
804801
func (jw *Jaws) Dirty(dirtyTags ...any) {
805802
// Use TagExpand+MustLog rather than MustTagExpand: with a nil Context the
806803
// latter panics on an illegal tag even in production, unlike the sibling

‎jaws_test.go‎

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"errors"
88
"html/template"
99
"io"
10-
"log/slog"
1110
"mime"
1211
"net/http"
1312
"net/http/httptest"
@@ -466,9 +465,8 @@ func TestJaws_BroadcastMultiRuntimeNonComparable(t *testing.T) {
466465
}
467466

468467
// TestJaws_dropNonComparableTags exercises the Broadcast comparability guard
469-
// directly. Broadcast cannot reach it through tag.TagExpand under deadlock.Debug
470-
// (the -race build) because ensureUsableTag rejects non-comparable tags first, so
471-
// the guard is tested with a synthetic tag slice.
468+
// directly. TagExpand rejects runtime-non-comparable struct/array tags before
469+
// Broadcast reaches this guard, so the guard is tested with a synthetic tag slice.
472470
func TestJaws_dropNonComparableTags(t *testing.T) {
473471
jw, err := New()
474472
if err != nil {
@@ -509,10 +507,9 @@ func TestJaws_dropNonComparableTags(t *testing.T) {
509507

510508
// TestJaws_setDirtyPanicReleasesLock verifies the documented defer-unlock in
511509
// setDirty: a tag comparable statically but not at runtime panics when used as a
512-
// map key, yet jw.mu is still released so the instance stays usable. The public
513-
// Dirty path cannot reach this under deadlock.Debug (the -race build), where
514-
// ensureUsableTag rejects such tags during expansion, so setDirty is exercised
515-
// directly with a synthetic tag.
510+
// map key, yet jw.mu is still released so the instance stays usable. Public Dirty
511+
// rejects this tag during expansion, so setDirty is exercised directly with a
512+
// synthetic tag.
516513
func TestJaws_setDirtyPanicReleasesLock(t *testing.T) {
517514
jw, err := New()
518515
if err != nil {
@@ -750,15 +747,13 @@ func TestBroadcast_RejectsUnhashableTagDest(t *testing.T) {
750747
t.Fatal(err)
751748
}
752749
defer jw.Close()
753-
var logbuf bytes.Buffer
754-
jw.Logger = slog.New(slog.NewTextHandler(&logbuf, nil))
750+
logger := &captureErrorLogger{}
751+
jw.Logger = logger
755752

756753
// A statically comparable struct holding a func in an interface field panics
757754
// when used as a map key. Such a Dest must be rejected before it reaches the
758755
// Serve loop, where wantMessage's tagMap lookup would panic and tear the loop
759-
// down. In debug builds tag.TagExpand rejects it; in non-debug (production)
760-
// builds TagExpand is lenient and the hashableTag check in Broadcast catches
761-
// it. Either way, Broadcast must not panic and must not queue the message.
756+
// down. Broadcast must not panic and must not queue the message in any build.
762757
type ifaceHolder struct{ any }
763758
var panicked bool
764759
func() {
@@ -778,11 +773,11 @@ func TestBroadcast_RejectsUnhashableTagDest(t *testing.T) {
778773
t.Fatalf("unhashable Dest was queued for the Serve loop: %T(%#v)", msg.Dest, msg.Dest)
779774
default:
780775
}
781-
if logbuf.Len() == 0 {
782-
t.Error("expected the rejected broadcast tag to be logged")
776+
if !errors.Is(logger.err, tag.ErrNotUsableAsTag) {
777+
t.Errorf("logged error = %v, want ErrNotUsableAsTag", logger.err)
783778
}
784-
if !deadlock.Debug && !strings.Contains(logbuf.String(), "not comparable") {
785-
t.Errorf("non-debug build should reject the non-comparable tag, got %q", logbuf.String())
779+
if !errors.Is(logger.err, tag.ErrNotComparable) {
780+
t.Errorf("logged error = %v, want ErrNotComparable", logger.err)
786781
}
787782
}
788783

0 commit comments

Comments
 (0)