Skip to content

Commit 231424a

Browse files
committed
refactor: consolidate test helpers into testhelpers_test.go
- Delete testharness_test.go and testsupport_test.go - Move all shared test harness code (TestRequest, requestHarness, etc.) to testhelpers_test.go - Add missing imports (net/http/httptest, testing/synctest) - Fix call from rw.rq.Rendering.Store(true) to rw.rq.MarkWritten() - Add new tests for error handling and edge cases in test helpers Signed-off-by: Johan Lindh <johan@linkdata.se>
1 parent 12b30df commit 231424a

3 files changed

Lines changed: 240 additions & 260 deletions

File tree

‎testharness_test.go‎

Lines changed: 0 additions & 104 deletions
This file was deleted.

‎testhelpers_test.go‎

Lines changed: 240 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import (
99
"io"
1010
"log/slog"
1111
"net/http"
12+
"net/http/httptest"
1213
"reflect"
1314
"regexp"
1415
"runtime"
1516
"slices"
1617
"strings"
1718
"testing"
19+
"testing/synctest"
1820
"time"
1921

2022
"github.com/linkdata/deadlock"
@@ -113,7 +115,7 @@ func (rw testRequestWriter) UI(ui UI, params ...any) error {
113115
}
114116

115117
func (rw testRequestWriter) Write(p []byte) (n int, err error) {
116-
rw.rq.Rendering.Store(true)
118+
rw.rq.MarkWritten()
117119
return rw.Writer.Write(p)
118120
}
119121

@@ -623,3 +625,240 @@ func (ts *testSetter[T]) JawsGetHTML(elem *Element) (value T) {
623625
value = ts.val
624626
return
625627
}
628+
629+
// closeRequestInBubble shuts a test request and its Jaws down from inside a
630+
// synctest bubble, then waits for every bubbled goroutine (the request process
631+
// loop, its event caller, and the Jaws Serve loop) to exit. synctest.Test
632+
// requires the bubble to be free of live goroutines before it returns, so the
633+
// usual t.Cleanup-based teardown (which runs outside the bubble) is too late.
634+
func closeRequestInBubble(rq *testRequest) {
635+
rq.Close()
636+
rq.Jaws.Close()
637+
synctest.Wait()
638+
}
639+
640+
// TestRequest is a request harness intended for the jaws package's own tests.
641+
// The importable harness for other packages lives in
642+
// github.com/linkdata/jaws/jawstest.
643+
type TestRequest struct {
644+
*Request
645+
*requestHarness
646+
}
647+
648+
type requestHarness struct {
649+
Req *Request
650+
*httptest.ResponseRecorder
651+
ReadyCh chan struct{}
652+
DoneCh chan struct{}
653+
InCh chan wire.WsMsg
654+
OutCh chan wire.WsMsg
655+
BcastCh chan wire.Message
656+
ExpectPanic bool
657+
Panicked bool
658+
PanicVal any
659+
}
660+
661+
func newRequestHarness(jw *Jaws, r *http.Request) (rh *requestHarness) {
662+
if r == nil {
663+
r = httptest.NewRequest(http.MethodGet, "/", nil)
664+
}
665+
rr := httptest.NewRecorder()
666+
rr.Body = &bytes.Buffer{}
667+
rq := jw.NewRequest(r)
668+
if rq == nil || jw.UseRequest(rq.JawsKey, r) != rq {
669+
return nil
670+
}
671+
rh = &requestHarness{
672+
Req: rq,
673+
ResponseRecorder: rr,
674+
}
675+
// The subscribe/process/recycle dance lives in jw.TestServe. The onPanic
676+
// callback adds this harness's panic-expectation support: an expected panic is
677+
// captured for inspection while an unexpected one is re-raised exactly as
678+
// before. It reads ExpectPanic lazily so tests may set it after construction.
679+
rh.InCh, rh.OutCh, rh.BcastCh, rh.ReadyCh, rh.DoneCh = jw.TestServe(rq, func(recovered any) {
680+
if recovered == nil {
681+
return
682+
}
683+
if rh.ExpectPanic {
684+
rh.PanicVal = recovered
685+
rh.Panicked = true
686+
return
687+
}
688+
panic(recovered)
689+
})
690+
return
691+
}
692+
693+
// Close stops the test request's processing loop.
694+
func (rh *requestHarness) Close() {
695+
close(rh.InCh)
696+
}
697+
698+
// BodyString returns the recorded response body with surrounding whitespace removed.
699+
func (rh *requestHarness) BodyString() string {
700+
return strings.TrimSpace(rh.Body.String())
701+
}
702+
703+
// BodyHTML returns the recorded response body as trusted HTML.
704+
func (rh *requestHarness) BodyHTML() template.HTML {
705+
return template.HTML(rh.BodyString()) /* #nosec G203 */
706+
}
707+
708+
// NewTestRequest creates a TestRequest for use when testing.
709+
// Passing nil for r creates a GET / request with no body.
710+
func NewTestRequest(jw *Jaws, r *http.Request) (tr *TestRequest) {
711+
rh := newRequestHarness(jw, r)
712+
if rh != nil {
713+
tr = &TestRequest{
714+
Request: rh.Req,
715+
requestHarness: rh,
716+
}
717+
}
718+
return
719+
}
720+
721+
func TestErrEventUnhandled_Error(t *testing.T) {
722+
if got := ErrEventUnhandled.Error(); got != "event unhandled" {
723+
t.Fatalf("ErrEventUnhandled.Error() = %q, want %q", got, "event unhandled")
724+
}
725+
}
726+
727+
func TestNewRequestHarness_ReturnsNilOnClaimFailure(t *testing.T) {
728+
jw, err := New()
729+
if err != nil {
730+
t.Fatal(err)
731+
}
732+
t.Cleanup(jw.Close)
733+
734+
jw.reqPool.New = func() any {
735+
rq := (&Request{
736+
Jaws: jw,
737+
tagMap: make(map[any][]*Element),
738+
}).clearLocked()
739+
rq.claimed.Store(true)
740+
return rq
741+
}
742+
743+
hr := httptest.NewRequest(http.MethodGet, "/", nil)
744+
if rh := newRequestHarness(jw, hr); rh != nil {
745+
t.Fatal("expected nil harness when claim fails")
746+
}
747+
}
748+
749+
func TestNewTestRequest_PanicsWhenJawsClosed(t *testing.T) {
750+
jw, err := New()
751+
if err != nil {
752+
t.Fatal(err)
753+
}
754+
jw.Close()
755+
defer func() {
756+
if recover() == nil {
757+
t.Fatal("expected panic when the Jaws instance is closed")
758+
}
759+
}()
760+
NewTestRequest(jw, nil)
761+
}
762+
763+
func TestTestServe_TimesOutWhenServeNotRunning(t *testing.T) {
764+
// Without a running Serve/ServeWithTimeout loop nothing drains subCh, so the
765+
// subscription rendezvous in TestServe can neither complete nor see Done, and
766+
// it must panic after its 5s timeout. Run in a synctest bubble so that timeout
767+
// elapses in fake time rather than stalling the test for five real seconds.
768+
synctest.Test(t, func(t *testing.T) {
769+
jw, err := New()
770+
if err != nil {
771+
t.Fatal(err)
772+
}
773+
defer jw.Close()
774+
rq := jw.NewRequest(httptest.NewRequest(http.MethodGet, "/", nil))
775+
defer func() {
776+
s, ok := recover().(string)
777+
if !ok || !strings.Contains(s, "timed out subscribing") {
778+
t.Fatalf("expected timeout panic, got %v", s)
779+
}
780+
}()
781+
jw.TestServe(rq, func(any) {})
782+
t.Fatal("expected TestServe to panic")
783+
})
784+
}
785+
786+
func TestTestServe_PanicsWhenClosedAfterSubscribing(t *testing.T) {
787+
jw, err := New()
788+
if err != nil {
789+
t.Fatal(err)
790+
}
791+
rq := jw.NewRequest(httptest.NewRequest(http.MethodGet, "/", nil))
792+
subCh := make(chan subscription)
793+
jw.subCh = subCh
794+
readyCh := make(chan struct{})
795+
go func() {
796+
close(readyCh)
797+
<-subCh
798+
jw.Close()
799+
}()
800+
<-readyCh
801+
802+
defer func() {
803+
s, ok := recover().(string)
804+
if !ok || !strings.Contains(s, "Jaws instance is closed") {
805+
t.Fatalf("expected closed panic, got %v", s)
806+
}
807+
}()
808+
jw.TestServe(rq, func(any) {})
809+
t.Fatal("expected TestServe to panic")
810+
}
811+
812+
func TestTestServe_TimesOutAfterSubscribing(t *testing.T) {
813+
synctest.Test(t, func(t *testing.T) {
814+
jw, err := New()
815+
if err != nil {
816+
t.Fatal(err)
817+
}
818+
defer jw.Close()
819+
rq := jw.NewRequest(httptest.NewRequest(http.MethodGet, "/", nil))
820+
subCh := make(chan subscription)
821+
jw.subCh = subCh
822+
readyCh := make(chan struct{})
823+
go func() {
824+
close(readyCh)
825+
<-subCh
826+
}()
827+
<-readyCh
828+
829+
defer func() {
830+
s, ok := recover().(string)
831+
if !ok || !strings.Contains(s, "timed out subscribing") {
832+
t.Fatalf("expected timeout panic, got %v", s)
833+
}
834+
}()
835+
jw.TestServe(rq, func(any) {})
836+
t.Fatal("expected TestServe to panic")
837+
})
838+
}
839+
840+
func TestNewTestRequest_SuccessPathAndClose(t *testing.T) {
841+
jw, err := New()
842+
if err != nil {
843+
t.Fatal(err)
844+
}
845+
t.Cleanup(jw.Close)
846+
847+
go jw.Serve()
848+
849+
tr := NewTestRequest(jw, nil)
850+
if tr == nil {
851+
t.Fatal("expected test request")
852+
}
853+
854+
if tr.Initial() == nil {
855+
t.Fatal("expected initial request")
856+
}
857+
858+
tr.Close()
859+
select {
860+
case <-tr.DoneCh:
861+
case <-time.After(time.Second):
862+
t.Fatal("timeout waiting for test request shutdown")
863+
}
864+
}

0 commit comments

Comments
 (0)