diff --git a/boolsetter.go b/boolsetter.go index 1997c55..954a491 100644 --- a/boolsetter.go +++ b/boolsetter.go @@ -36,5 +36,5 @@ func makeBoolSetter(v interface{}) BoolSetter { case *atomic.Value: return atomicSetter{v} } - panic(fmt.Errorf("expected jaws.BoolGetter or bool, not %T", v)) + panic(fmt.Errorf("expected jaws.BoolSetter or bool, not %T", v)) } diff --git a/clickhandler_test.go b/clickhandler_test.go index ac77c79..f791ef8 100644 --- a/clickhandler_test.go +++ b/clickhandler_test.go @@ -2,7 +2,6 @@ package jaws import ( "testing" - "time" "github.com/linkdata/jaws/what" ) @@ -22,8 +21,7 @@ func (tje *testJawsClick) JawsClick(e *Element, name string) (err error) { var _ ClickHandler = (*testJawsClick)(nil) func Test_clickHandlerWapper_JawsEvent(t *testing.T) { - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() + th := newTestHelper(t) nextJid = 0 rq := newTestRequest() defer rq.Close() @@ -34,14 +32,15 @@ func Test_clickHandlerWapper_JawsEvent(t *testing.T) { } want := `
inner
` - if got := string(rq.Div("inner", tjc)); got != want { + rq.Div("inner", tjc) + if got := rq.BodyString(); got != want { t.Errorf("Request.Div() = %q, want %q", got, want) } rq.inCh <- wsMsg{Data: "text", Jid: 1, What: what.Input} select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case s := <-rq.outCh: t.Errorf("%q", s) default: @@ -49,8 +48,8 @@ func Test_clickHandlerWapper_JawsEvent(t *testing.T) { rq.inCh <- wsMsg{Data: "adam", Jid: 1, What: what.Click} select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case name := <-tjc.clickCh: if name != "adam" { t.Error(name) diff --git a/element.go b/element.go index d610748..905444e 100644 --- a/element.go +++ b/element.go @@ -12,9 +12,9 @@ import ( // An Element is an instance of a *Request, an UI object and a Jid. type Element struct { - ui UI // (read-only) the UI object - jid jid.Jid // (read-only) JaWS ID, unique to this Element within it's Request - *Request // (read-only) the Request the Element belongs to + ui UI // (read-only) the UI object + jid jid.Jid // (read-only) JaWS ID, unique to this Element within it's Request + rq *Request // (read-only) the Request the Element belongs to // internals updating bool // about to have Update() called wsQueue []wsMsg // changes queued @@ -22,17 +22,47 @@ type Element struct { } func (e *Element) String() string { - return fmt.Sprintf("Element{%T, id=%q, Tags: %v}", e.ui, e.jid, e.Request.TagsOf(e)) + return fmt.Sprintf("Element{%T, id=%q, Tags: %v}", e.ui, e.jid, e.rq.TagsOf(e)) +} + +// Jaws returns the Jaws the Element belongs to. +func (e *Element) Jaws() *Jaws { + return e.rq.Jaws +} + +// Request returns the Request the Element belongs to. +func (e *Element) Request() *Request { + return e.rq +} + +// Session returns the Elements's Session, or nil. +func (e *Element) Session() *Session { + return e.rq.Session() +} + +// Get calls Session().Get() +func (e *Element) Get(key string) (val interface{}) { + return e.Session().Get(key) +} + +// Set calls Session().Get() +func (e *Element) Set(key string, val interface{}) { + e.Session().Set(key, val) +} + +// Dirty calls Request().Dirty() +func (e *Element) Dirty(tags ...interface{}) { + e.rq.Dirty(tags...) } // Tag adds the given tags to the Element. func (e *Element) Tag(tags ...interface{}) { - e.Request.Tag(e, tags...) + e.rq.Tag(e, tags...) } // HasTag returns true if this Element has the given tag. func (e *Element) HasTag(tag interface{}) bool { - return e.Request.HasTag(e, tag) + return e.rq.HasTag(e, tag) } // Jid returns the JaWS ID for this Element, unique within it's Request. @@ -46,8 +76,8 @@ func (e *Element) Ui() UI { } // Render calls Request.JawsRender() for this Element. -func (e *Element) Render(w io.Writer, params []interface{}) { - e.Request.JawsRender(e, w, params) +func (e *Element) Render(w io.Writer, params []interface{}) error { + return e.rq.JawsRender(e, w, params) } func (e *Element) queue(wht what.What, data string) { @@ -58,7 +88,7 @@ func (e *Element) queue(wht what.What, data string) { What: wht, }) } else { - e.Request.cancelFn(ErrWebsocketQueueOverflow) + e.rq.cancel(ErrWebsocketQueueOverflow) } } diff --git a/element_test.go b/element_test.go index c623305..697c7c7 100644 --- a/element_test.go +++ b/element_test.go @@ -21,7 +21,7 @@ type testUi struct { getCalled int32 setCalled int32 s string - renderFn func(e *Element, w io.Writer, params []any) + renderFn func(e *Element, w io.Writer, params []any) error updateFn func(e *Element) } @@ -39,12 +39,13 @@ func (tss *testUi) JawsSetString(e *Element, s string) error { return nil } -func (tss *testUi) JawsRender(e *Element, w io.Writer, params []any) { +func (tss *testUi) JawsRender(e *Element, w io.Writer, params []any) (err error) { e.Tag(tss) atomic.AddInt32(&tss.renderCalled, 1) if tss.renderFn != nil { - tss.renderFn(e, w, params) + err = tss.renderFn(e, w, params) } + return } func (tss *testUi) JawsUpdate(e *Element) { @@ -54,8 +55,22 @@ func (tss *testUi) JawsUpdate(e *Element) { } } +func TestElement_helpers(t *testing.T) { + is := newTestHelper(t) + rq := newTestRequest() + defer rq.Close() + + tss := &testUi{} + e := rq.NewElement(tss) + is.Equal(e.Jaws(), rq.jw.Jaws) + is.Equal(e.Request(), rq.Request) + is.Equal(e.Session(), nil) + e.Set("foo", "bar") // no session, so no effect + is.Equal(e.Get("foo"), nil) +} + func TestElement_Tag(t *testing.T) { - is := testHelper{t} + is := newTestHelper(t) rq := newTestRequest() defer rq.Close() @@ -68,7 +83,7 @@ func TestElement_Tag(t *testing.T) { } func TestElement_Queued(t *testing.T) { - is := testHelper{t} + th := newTestHelper(t) rq := newTestRequest() defer rq.Close() @@ -85,7 +100,7 @@ func TestElement_Queued(t *testing.T) { e.Order([]jid.Jid{1, 2}) replaceHtml := template.HTML(fmt.Sprintf("
", e.Jid().String())) e.Replace(replaceHtml) - is.Equal(e.wsQueue, []wsMsg{ + th.Equal(e.wsQueue, []wsMsg{ { Data: "hidden\n", Jid: e.jid, @@ -141,26 +156,25 @@ func TestElement_Queued(t *testing.T) { } pendingRq := rq.Jaws.NewRequest(httptest.NewRequest(http.MethodGet, "/", nil)) - pendingRq.UI(tss) + RequestWriter{pendingRq, httptest.NewRecorder()}.UI(tss) rq.UI(tss) rq.Jaws.Dirty(tss) rq.Dirty(tss) - tmr := time.NewTimer(testTimeout) for atomic.LoadInt32(&tss.updateCalled) < 1 { select { - case <-tmr.C: - is.Fail() + case <-th.C: + th.Timeout() default: time.Sleep(time.Millisecond) } } - is.Equal(tss.updateCalled, int32(1)) - is.Equal(tss.renderCalled, int32(2)) + th.Equal(tss.updateCalled, int32(1)) + th.Equal(tss.renderCalled, int32(2)) } func TestElement_ReplacePanicsOnMissingId(t *testing.T) { - is := testHelper{t} + is := newTestHelper(t) rq := newTestRequest() defer rq.Close() defer func() { diff --git a/html.go b/html.go index a46eb0b..2f8d1de 100644 --- a/html.go +++ b/html.go @@ -4,7 +4,6 @@ import ( "html/template" "io" "strconv" - "strings" "github.com/linkdata/jaws/jid" ) @@ -70,12 +69,6 @@ func WriteHtmlInput(w io.Writer, jid jid.Jid, typ, val string, attrs ...string) return } -func HtmlInput(jid jid.Jid, typ, val string, attrs ...string) template.HTML { - var sb strings.Builder - _ = WriteHtmlInput(&sb, jid, typ, val, attrs...) - return template.HTML(sb.String()) // #nosec G203 -} - func WriteHtmlInner(w io.Writer, jid jid.Jid, tag, typ string, inner template.HTML, attrs ...string) (err error) { need := 1 + len(tag)*2 + jidPrealloc + 8 + len(typ) + 1 + 1 + getAttrsLen(attrs) + 1 + len(inner) + 2 + 1 b := make([]byte, 0, need) @@ -96,12 +89,6 @@ func WriteHtmlInner(w io.Writer, jid jid.Jid, tag, typ string, inner template.HT return } -func HtmlInner(jid jid.Jid, tag, typ string, inner template.HTML, attrs ...string) template.HTML { - var sb strings.Builder - _ = WriteHtmlInner(&sb, jid, tag, typ, inner, attrs...) - return template.HTML(sb.String()) // #nosec G203 -} - func WriteHtmlSelect(w io.Writer, jid jid.Jid, nba *NamedBoolArray, attrs ...string) (err error) { need := 12 + jidPrealloc + 2 + getAttrsLen(attrs) + 2 + 10 nba.ReadLocked(func(nba []*NamedBool) { @@ -132,9 +119,3 @@ func WriteHtmlSelect(w io.Writer, jid jid.Jid, nba *NamedBoolArray, attrs ...str _, err = w.Write(b) return } - -func HtmlSelect(jid jid.Jid, nba *NamedBoolArray, attrs ...string) template.HTML { - var sb strings.Builder - _ = WriteHtmlSelect(&sb, jid, nba, attrs...) - return template.HTML(sb.String()) // #nosec G203 -} diff --git a/html_test.go b/html_test.go index df11c65..27d8831 100644 --- a/html_test.go +++ b/html_test.go @@ -2,6 +2,7 @@ package jaws import ( "html/template" + "strings" "testing" "github.com/linkdata/jaws/jid" @@ -17,7 +18,7 @@ func TestHtmlInput(t *testing.T) { tests := []struct { name string args args - want template.HTML + want string }{ { name: "HtmlInput no attrs", @@ -61,7 +62,11 @@ func TestHtmlInput(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := HtmlInput(tt.args.jid, tt.args.typ, tt.args.val, tt.args.attrs...); got != tt.want { + var sb strings.Builder + if err := WriteHtmlInput(&sb, tt.args.jid, tt.args.typ, tt.args.val, tt.args.attrs...); err != nil { + t.Fatal(err) + } + if got := sb.String(); got != tt.want { t.Errorf("HtmlInput() = %v, want %v", got, tt.want) } }) @@ -79,7 +84,7 @@ func TestHtmlInner(t *testing.T) { tests := []struct { name string args args - want template.HTML + want string }{ { name: "HtmlInner no attrs", @@ -115,7 +120,11 @@ func TestHtmlInner(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := HtmlInner(tt.args.jid, tt.args.tag, tt.args.typ, tt.args.inner, tt.args.attrs...); got != tt.want { + var sb strings.Builder + if err := WriteHtmlInner(&sb, tt.args.jid, tt.args.tag, tt.args.typ, tt.args.inner, tt.args.attrs...); err != nil { + t.Fatal(err) + } + if got := sb.String(); got != tt.want { t.Errorf("HtmlInner() = %v, want %v", got, tt.want) } }) @@ -131,7 +140,7 @@ func TestHtmlSelect(t *testing.T) { tests := []struct { name string args args - want template.HTML + want string }{ { name: "HtmlSelect empty NamedBoolArray and one attr", @@ -166,7 +175,11 @@ func TestHtmlSelect(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := HtmlSelect(tt.args.jid, tt.args.val, tt.args.attrs...); got != tt.want { + var sb strings.Builder + if err := WriteHtmlSelect(&sb, tt.args.jid, tt.args.val, tt.args.attrs...); err != nil { + t.Fatal(err) + } + if got := sb.String(); got != tt.want { t.Errorf("HtmlSelect() = %v, want %v", got, tt.want) } }) diff --git a/jaws.go b/jaws.go index 5525506..2e8db72 100644 --- a/jaws.go +++ b/jaws.go @@ -23,6 +23,7 @@ import ( "sort" "strconv" "strings" + "sync" "sync/atomic" "time" @@ -48,11 +49,11 @@ type Jaws struct { unsubCh chan chan Message updateTicker *time.Ticker headPrefix string + reqPool sync.Pool mu deadlock.RWMutex // protects following kg *bufio.Reader closeCh chan struct{} - pending map[uint64]*Request - active map[*Request]struct{} + requests map[uint64]*Request sessions map[uint64]*Session dirty map[interface{}]int dirtOrder int @@ -61,8 +62,8 @@ type Jaws struct { // NewWithDone returns a new JaWS object using the given completion channel. // This is expected to be created once per HTTP server and handles // publishing HTML changes across all connections. -func NewWithDone(doneCh <-chan struct{}) *Jaws { - return &Jaws{ +func NewWithDone(doneCh <-chan struct{}) (jw *Jaws) { + jw = &Jaws{ CookieName: DefaultCookieName, doneCh: doneCh, bcastCh: make(chan Message, 1), @@ -71,11 +72,17 @@ func NewWithDone(doneCh <-chan struct{}) *Jaws { updateTicker: time.NewTicker(DefaultUpdateInterval), headPrefix: HeadHTML([]string{JavascriptPath}, nil), kg: bufio.NewReader(rand.Reader), - pending: make(map[uint64]*Request), - active: make(map[*Request]struct{}), + requests: make(map[uint64]*Request), sessions: make(map[uint64]*Session), dirty: make(map[interface{}]int), } + jw.reqPool.New = func() any { + return (&Request{ + Jaws: jw, + tagMap: make(map[any][]*Element), + }).clearLocked() + } + return } // New returns a new JaWS object that must be closed using Close(). @@ -107,15 +114,13 @@ func (jw *Jaws) Done() <-chan struct{} { return jw.doneCh } -// RequestCount returns the number of active and pending Requests. +// RequestCount returns the number of Requests. // -// "Active" Requests are those for which there is a WebSocket connection -// and messages are being routed for. "Pending" are those for which the -// initial HTTP request has been made but we have not yet received the -// WebSocket callback and started processing. +// The count includes all Requests, including those being rendered, +// those waiting for the WebSocket callback and those active. func (jw *Jaws) RequestCount() (n int) { jw.mu.RLock() - n = len(jw.pending) + len(jw.active) + n = len(jw.requests) jw.mu.RUnlock() return } @@ -169,9 +174,9 @@ func (jw *Jaws) NewRequest(hr *http.Request) (rq *Request) { defer jw.mu.Unlock() for rq == nil { jawsKey := jw.nonZeroRandomLocked() - if _, ok := jw.pending[jawsKey]; !ok { - rq = getRequest(jw, jawsKey, hr) - jw.pending[jawsKey] = rq + if _, ok := jw.requests[jawsKey]; !ok { + rq = jw.getRequestLocked(jawsKey, hr) + jw.requests[jawsKey] = rq } } return @@ -201,11 +206,9 @@ func (jw *Jaws) UseRequest(jawsKey uint64, hr *http.Request) (rq *Request) { if jawsKey != 0 { var err error jw.mu.Lock() - if waitingRq, ok := jw.pending[jawsKey]; ok { - if err = waitingRq.start(hr); err == nil { - delete(jw.pending, jawsKey) + if waitingRq, ok := jw.requests[jawsKey]; ok { + if err = waitingRq.claim(hr); err == nil { rq = waitingRq - jw.active[rq] = struct{}{} } } jw.mu.Unlock() @@ -391,11 +394,8 @@ func (jw *Jaws) distributeDirt() int { var reqs []*Request if len(dirt) > 0 { - reqs = make([]*Request, 0, len(jw.pending)+len(jw.active)) - for _, rq := range jw.pending { - reqs = append(reqs, rq) - } - for rq := range jw.active { + reqs = make([]*Request, 0, len(jw.requests)) + for _, rq := range jw.requests { reqs = append(reqs, rq) } } @@ -441,17 +441,15 @@ func (jw *Jaws) Alert(lvl, msg string) { // Count returns the number of requests waiting for their WebSocket callbacks. func (jw *Jaws) Pending() (n int) { jw.mu.RLock() - n = len(jw.pending) + for _, rq := range jw.requests { + if !rq.claimed { + n++ + } + } jw.mu.RUnlock() return } -func (jw *Jaws) deactivate(rq *Request) { - jw.mu.Lock() - delete(jw.active, rq) - jw.mu.Unlock() -} - // ServeWithTimeout begins processing requests with the given timeout. // It is intended to run on it's own goroutine. // It returns when the completion channel is closed. @@ -525,13 +523,7 @@ func (jw *Jaws) Serve() { jw.ServeWithTimeout(time.Second * 10) } -func (jw *Jaws) subscribe(rq *Request, minSize int) chan Message { - size := minSize - if rq != nil { - if size = 4 + len(rq.elems)*4; size < minSize { - size = minSize - } - } +func (jw *Jaws) subscribe(rq *Request, size int) chan Message { msgCh := make(chan Message, size) select { case <-jw.Done(): @@ -549,45 +541,19 @@ func (jw *Jaws) unsubscribe(msgCh chan Message) { } } -func maybeErrPendingCancelled(rq *Request, deadline time.Time) (err error) { - if err = context.Cause(rq.ctx); err == nil && rq.Created.Before(deadline) { - err = newErrNoWebSocketRequest(rq) - } - if err != nil { - err = newErrPendingCancelled(rq, err) - } - return -} - func (jw *Jaws) maintenance(requestTimeout time.Duration) { - var killReqs []*Request - var killSess []uint64 - - jw.mu.RLock() - now := time.Now() - deadline := now.Add(-requestTimeout) - for _, rq := range jw.pending { - if err := jw.Log(maybeErrPendingCancelled(rq, deadline)); err != nil { - rq.cancel(err) - killReqs = append(killReqs, rq) + deadline := time.Now().Add(-requestTimeout) + jw.mu.Lock() + defer jw.mu.Unlock() + for _, rq := range jw.requests { + if rq.maintenance(deadline) { + jw.recycleLocked(rq) } } for k, sess := range jw.sessions { if sess.isDead() { - killSess = append(killSess, k) - } - } - jw.mu.RUnlock() - - if len(killReqs)+len(killSess) > 0 { - jw.mu.Lock() - for _, rq := range killReqs { - delete(jw.pending, rq.JawsKey) - } - for _, k := range killSess { delete(jw.sessions, k) } - jw.mu.Unlock() } } @@ -712,3 +678,35 @@ func (jw *Jaws) Append(target interface{}, html template.HTML) { Data: html, }) } + +func (jw *Jaws) getRequestLocked(jawsKey uint64, hr *http.Request) (rq *Request) { + rq = jw.reqPool.Get().(*Request) + rq.JawsKey = jawsKey + rq.Created = time.Now() + rq.Initial = hr + rq.ctx, rq.cancelFn = context.WithCancelCause(context.Background()) + if hr != nil { + rq.remoteIP = parseIP(hr.RemoteAddr) + if sess := jw.getSessionLocked(getCookieSessionsIds(hr.Header, jw.CookieName), rq.remoteIP); sess != nil { + sess.addRequest(rq) + rq.session = sess + } + } + return rq +} + +func (jw *Jaws) recycleLocked(rq *Request) { + rq.mu.Lock() + defer rq.mu.Unlock() + if rq.JawsKey != 0 { + delete(jw.requests, rq.JawsKey) + rq.clearLocked() + jw.reqPool.Put(rq) + } +} + +func (jw *Jaws) recycle(rq *Request) { + jw.mu.Lock() + defer jw.mu.Unlock() + jw.recycleLocked(rq) +} diff --git a/jaws_test.go b/jaws_test.go index 7dc689b..5e51582 100644 --- a/jaws_test.go +++ b/jaws_test.go @@ -18,7 +18,7 @@ import ( ) func TestJaws_parseIP(t *testing.T) { - is := testHelper{t} + is := newTestHelper(t) is.True(!parseIP("").IsValid()) is.True(parseIP("192.168.0.1").Compare(netip.MustParseAddr("192.168.0.1")) == 0) is.True(parseIP("192.168.0.2:1234").Compare(netip.MustParseAddr("192.168.0.2")) == 0) @@ -35,7 +35,7 @@ func TestJaws_parseIP(t *testing.T) { func TestJaws_getCookieSessionsIds(t *testing.T) { const sessId = 1234 sessCookie := JawsKeyString(sessId) - is := testHelper{t} + is := newTestHelper(t) is.Equal(getCookieSessionsIds(nil, "meh"), nil) is.Equal(getCookieSessionsIds(http.Header{}, "meh"), nil) is.Equal(getCookieSessionsIds(http.Header{"Cookie": []string{}}, "meh"), nil) @@ -52,7 +52,7 @@ func TestJaws_MultipleCloseCalls(t *testing.T) { } func TestJaws_MakeID(t *testing.T) { - is := testHelper{t} + is := newTestHelper(t) jw := New() defer jw.Close() go jw.Serve() @@ -64,7 +64,7 @@ func TestJaws_MakeID(t *testing.T) { } func TestJaws_maybePanic(t *testing.T) { - is := testHelper{t} + is := newTestHelper(t) defer func() { if recover() == nil { is.Fail() @@ -74,7 +74,7 @@ func TestJaws_maybePanic(t *testing.T) { } func TestJaws_Logger(t *testing.T) { - is := testHelper{t} + is := newTestHelper(t) jw := New() defer jw.Close() var b bytes.Buffer @@ -87,7 +87,7 @@ func TestJaws_Logger(t *testing.T) { } func TestJaws_MustLog(t *testing.T) { - is := testHelper{t} + is := newTestHelper(t) jw := New() defer jw.Close() @@ -118,12 +118,11 @@ func TestJaws_BroadcastDoesntBlockWhenClosed(t *testing.T) { } func TestJaws_BroadcastWaitsWhenFull(t *testing.T) { - is := testHelper{t} - + th := newTestHelper(t) jw := New() go jw.ServeWithTimeout(testTimeout) - subCh := jw.subscribe(nil, 0) + subCh := jw.subscribe(jw.NewRequest(nil), 0) defer jw.unsubscribe(subCh) // ensure our sub has been processed @@ -132,21 +131,21 @@ func TestJaws_BroadcastWaitsWhenFull(t *testing.T) { // send two broadcasts select { - case <-time.NewTimer(testTimeout).C: - is.Fail() + case <-th.C: + th.Timeout() case jw.bcastCh <- Message{What: what.Reload}: } select { - case <-time.NewTimer(testTimeout).C: - is.Fail() + case <-th.C: + th.Timeout() case jw.bcastCh <- Message{What: what.Reload}: } // read one of the broadcasts, the other is // left to fall into the retry loop select { - case <-time.NewTimer(testTimeout).C: - is.Fail() + case <-th.C: + th.Timeout() case <-subCh: } @@ -155,30 +154,31 @@ func TestJaws_BroadcastWaitsWhenFull(t *testing.T) { // finally, read the msg select { - case <-time.NewTimer(testTimeout).C: - is.Fail() + case <-th.C: + th.Timeout() case <-subCh: } } func TestJaws_BroadcastFullClosesChannel(t *testing.T) { - is := testHelper{t} + th := newTestHelper(t) jw := New() go jw.ServeWithTimeout(time.Millisecond) doneCh := make(chan struct{}) failCh := make(chan struct{}) - subCh1 := jw.subscribe(nil, 0) + subCh1 := jw.subscribe(jw.NewRequest(nil), 0) + defer jw.unsubscribe(subCh1) - subCh2 := jw.subscribe(nil, 0) + subCh2 := jw.subscribe(jw.NewRequest(nil), 0) defer jw.unsubscribe(subCh2) jw.subCh <- subscription{} jw.subCh <- subscription{} go func() { select { - case <-time.NewTimer(testTimeout).C: + case <-th.C: close(failCh) case <-subCh2: close(doneCh) @@ -186,16 +186,16 @@ func TestJaws_BroadcastFullClosesChannel(t *testing.T) { }() select { - case <-time.NewTimer(testTimeout).C: - is.Fail() + case <-th.C: + th.Timeout() case jw.bcastCh <- Message{What: what.Reload}: } select { - case <-time.NewTimer(testTimeout).C: - is.Fail() + case <-th.C: + th.Timeout() case <-failCh: - is.Fail() + th.Timeout() case <-doneCh: } @@ -205,89 +205,91 @@ func TestJaws_BroadcastFullClosesChannel(t *testing.T) { select { case msg, ok := <-subCh1: - is.True(!ok) - is.Equal(msg, Message{}) + th.True(!ok) + th.Equal(msg, Message{}) default: } } func TestJaws_UseRequest(t *testing.T) { - is := testHelper{t} + th := newTestHelper(t) jw := New() defer jw.Close() - is.Equal(0, jw.RequestCount()) + th.Equal(0, jw.RequestCount()) rq1 := jw.NewRequest(nil) - is.True(rq1.JawsKey != 0) + th.True(rq1.JawsKey != 0) rq2 := jw.NewRequest(&http.Request{RemoteAddr: "10.0.0.2:1010"}) - is.True(rq2.JawsKey != 0) - is.True(rq1.JawsKey != rq2.JawsKey) - is.Equal(jw.Pending(), 2) + th.True(rq2.JawsKey != 0) + th.True(rq1.JawsKey != rq2.JawsKey) + th.Equal(jw.Pending(), 2) rqfail := jw.UseRequest(0, nil) // wrong JawsKey - is.Equal(rqfail, nil) - is.Equal(jw.Pending(), 2) + th.Equal(rqfail, nil) + th.Equal(jw.Pending(), 2) rqfail = jw.UseRequest(rq1.JawsKey, &http.Request{RemoteAddr: "10.0.0.1:1010"}) // wrong IP, expect blank - is.Equal(rqfail, nil) - is.Equal(jw.Pending(), 2) + th.Equal(rqfail, nil) + th.Equal(jw.Pending(), 2) rqfail = jw.UseRequest(rq2.JawsKey, &http.Request{RemoteAddr: "10.0.0.1:1010"}) // wrong IP, expect .2 - is.Equal(rqfail, nil) - is.Equal(jw.Pending(), 2) + th.Equal(rqfail, nil) + th.Equal(jw.Pending(), 2) rq2ret := jw.UseRequest(rq2.JawsKey, &http.Request{RemoteAddr: "10.0.0.2:1212"}) // different port is OK - is.Equal(rq2, rq2ret) - is.Equal(jw.Pending(), 1) + th.Equal(rq2, rq2ret) + th.Equal(jw.Pending(), 1) rq1ret := jw.UseRequest(rq1.JawsKey, nil) - is.Equal(rq1, rq1ret) - is.Equal(jw.Pending(), 0) + th.Equal(rq1, rq1ret) + th.Equal(jw.Pending(), 0) } func TestJaws_BlockingRandomPanics(t *testing.T) { - is := testHelper{t} + th := newTestHelper(t) defer func() { if recover() == nil { - is.Fail() + th.Error("expected error") } }() jw := New() defer jw.Close() jw.kg = bufio.NewReader(&bytes.Buffer{}) jw.NewRequest(nil) - is.Fail() } func TestJaws_CleansUpUnconnected(t *testing.T) { - const numReqs = 1000 - is := testHelper{t} + const numReqs = 100 + th := newTestHelper(t) jw := New() defer jw.Close() var b bytes.Buffer w := bufio.NewWriter(&b) jw.Logger = log.New(w, "", 0) hr := httptest.NewRequest(http.MethodGet, "/", nil) - is.Equal(jw.Pending(), 0) + th.Equal(jw.Pending(), 0) deadline := time.Now().Add(testTimeout) var expectLen int for i := 0; i < numReqs; i++ { rq := jw.NewRequest(hr) - if (i % (numReqs / 10)) == 0 { + if (i % (numReqs / 5)) == 0 { elem := rq.NewElement(NewUiDiv(makeHtmlGetter("meh"))) for j := 0; j < maxWsQueueLengthPerElement*10; j++ { elem.SetInner("foo") } } - err := maybeErrPendingCancelled(rq, deadline) + err := context.Cause(rq.ctx) + if err == nil && rq.Created.Before(deadline) { + err = newErrPendingCancelled(rq, newErrNoWebSocketRequest(rq)) + } if err == nil { t.Fatal("expected error") } expectLen += len(err.Error() + "\n") } - is.Equal(jw.Pending(), numReqs) + th.Equal(jw.Pending(), numReqs) go jw.ServeWithTimeout(time.Millisecond) @@ -299,25 +301,23 @@ func TestJaws_CleansUpUnconnected(t *testing.T) { } } - is.Equal(jw.Pending(), 0) + th.Equal(jw.Pending(), 0) jw.Close() select { - case <-time.NewTimer(testTimeout).C: - is.Fail() + case <-th.C: + th.Timeout() case <-jw.Done(): } w.Flush() if x := b.Len(); x != expectLen { t.Log(b.String()) - is.Equal(b.Len(), expectLen) + th.Equal(b.Len(), expectLen) } } func TestJaws_UnconnectedLivesUntilDeadline(t *testing.T) { - is := testHelper{t} - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() + th := newTestHelper(t) jw := New() defer jw.Close() @@ -328,36 +328,39 @@ func TestJaws_UnconnectedLivesUntilDeadline(t *testing.T) { rq2.Created = time.Now().Add(-time.Second * 10) rq2ctx := rq2.Context() - is.Equal(jw.Pending(), 2) + th.Equal(jw.Pending(), 2) go jw.ServeWithTimeout(time.Second) for jw.Pending() > 1 { select { - case <-tmr.C: - is.Fatal("timeout") + case <-th.C: + th.Timeout() case <-jw.Done(): - is.Error("unexpected close") + th.Error("unexpected close") default: time.Sleep(time.Millisecond) } } - is.Equal(jw.Pending(), 1) + th.Equal(jw.Pending(), 1) jw.Close() select { - case <-tmr.C: - is.Fatal("timeout") + case <-th.C: + th.Timeout() case <-jw.Done(): } - is.NoErr(context.Cause(rq1ctx)) - is.True(errors.Is(context.Cause(rq2ctx), ErrNoWebSocketRequest{})) - // neither should have been recycled - is.Equal(rq1.Jaws, jw) - is.Equal(rq2.Jaws, jw) + th.Equal(rq1.Jaws, jw) + th.Equal(rq2.Jaws, jw) + + th.NoErr(context.Cause(rq1ctx)) + if !errors.Is(context.Cause(rq2ctx), ErrNoWebSocketRequest{}) { + th.Error(context.Cause(rq2ctx)) + } + } func TestJaws_BroadcastsCallable(t *testing.T) { @@ -382,7 +385,7 @@ func TestJaws_BroadcastsCallable(t *testing.T) { } func TestJaws_subscribeOnClosedReturnsNil(t *testing.T) { - is := testHelper{t} + th := newTestHelper(t) jw := New() jw.Close() <-jw.doneCh @@ -393,23 +396,23 @@ func TestJaws_subscribeOnClosedReturnsNil(t *testing.T) { } } - is.Equal(jw.subscribe(nil, 1), nil) + th.Equal(jw.subscribe(jw.NewRequest(nil), 1), nil) } func TestJaws_GenerateHeadHTML(t *testing.T) { const extraScript = "someExtraScript.js?disregard" const extraStyle = "http://other.server/someExtraStyle.css" - is := testHelper{t} + th := newTestHelper(t) jw := New() jw.Close() jw.GenerateHeadHTML() - is.True(strings.Contains(string(jw.headPrefix), JavascriptPath)) + th.True(strings.Contains(string(jw.headPrefix), JavascriptPath)) jw.GenerateHeadHTML(extraScript, extraStyle) - is.True(strings.Contains(string(jw.headPrefix), JavascriptPath)) - is.True(strings.Contains(string(jw.headPrefix), extraScript)) - is.True(strings.Contains(string(jw.headPrefix), extraStyle)) + th.True(strings.Contains(string(jw.headPrefix), JavascriptPath)) + th.True(strings.Contains(string(jw.headPrefix), extraScript)) + th.True(strings.Contains(string(jw.headPrefix), extraStyle)) - is.True(jw.GenerateHeadHTML("random.crap") != nil) - is.True(jw.GenerateHeadHTML("\n") != nil) + th.True(jw.GenerateHeadHTML("random.crap") != nil) + th.True(jw.GenerateHeadHTML("\n") != nil) } diff --git a/js_test.go b/js_test.go index 3206236..b00a06a 100644 --- a/js_test.go +++ b/js_test.go @@ -3,6 +3,7 @@ package jaws import ( "bytes" "compress/gzip" + _ "embed" "hash/fnv" "strconv" "strings" @@ -12,33 +13,39 @@ import ( func Test_Javascript(t *testing.T) { const prefix = "/jaws/jaws." const suffix = ".js" - is := testHelper{t} + th := newTestHelper(t) h := fnv.New64a() _, err := h.Write(JavascriptText) - is.NoErr(err) - is.Equal(JavascriptPath, prefix+strconv.FormatUint(h.Sum64(), 36)+suffix) + th.NoErr(err) + th.Equal(JavascriptPath, prefix+strconv.FormatUint(h.Sum64(), 36)+suffix) - is.True(len(JavascriptText) > 0) - is.True(len(JavascriptGZip) > 0) - is.True(len(JavascriptGZip) < len(JavascriptText)) + th.True(len(JavascriptText) > 0) + th.True(len(JavascriptGZip) > 0) + th.True(len(JavascriptGZip) < len(JavascriptText)) b := bytes.Buffer{} gw := gzip.NewWriter(&b) _, err = gw.Write(JavascriptText) - is.NoErr(err) - is.NoErr(gw.Close()) - is.Equal(b.Bytes(), JavascriptGZip) + th.NoErr(err) + th.NoErr(gw.Close()) + th.Equal(b.Bytes(), JavascriptGZip) } func Test_HeadHTML(t *testing.T) { const extraScript = "someExtraScript.js" const extraStyle = "someExtraStyle.css" - is := testHelper{t} + th := newTestHelper(t) txt := HeadHTML(nil, nil) - is.Equal(strings.Contains(string(txt), JavascriptPath), false) + th.Equal(strings.Contains(string(txt), JavascriptPath), false) txt = HeadHTML([]string{JavascriptPath, extraScript}, []string{extraStyle}) - is.Equal(strings.Contains(string(txt), JavascriptPath), true) - is.Equal(strings.Contains(string(txt), extraScript), true) - is.Equal(strings.Contains(string(txt), extraStyle), true) + th.Equal(strings.Contains(string(txt), JavascriptPath), true) + th.Equal(strings.Contains(string(txt), extraScript), true) + th.Equal(strings.Contains(string(txt), extraStyle), true) +} + +func TestJawsKeyString(t *testing.T) { + th := newTestHelper(t) + th.Equal(JawsKeyString(0), "") + th.Equal(JawsKeyString(1), "1") } diff --git a/namedbool_test.go b/namedbool_test.go index 10e0ac5..5702000 100644 --- a/namedbool_test.go +++ b/namedbool_test.go @@ -6,7 +6,7 @@ import ( ) func TestNamedBool(t *testing.T) { - is := testHelper{t} + is := newTestHelper(t) nba := NewNamedBoolArray() nba.Add("1", "one") diff --git a/namedboolarray_test.go b/namedboolarray_test.go index 52adb9f..73273b7 100644 --- a/namedboolarray_test.go +++ b/namedboolarray_test.go @@ -7,7 +7,7 @@ import ( ) func Test_NamedBoolArray(t *testing.T) { - is := testHelper{t} + is := newTestHelper(t) nba := NewNamedBoolArray() is.Equal(len(nba.data), 0) diff --git a/request.go b/request.go index bffe14b..d98fe62 100644 --- a/request.go +++ b/request.go @@ -6,12 +6,12 @@ import ( "fmt" "html" "html/template" + "io" "net/http" "net/netip" "slices" "strconv" "strings" - "sync" "sync/atomic" "time" @@ -37,6 +37,8 @@ type Request struct { remoteIP netip.Addr // (read-only) remote IP, or nil session *Session // (read-only) session, if established mu deadlock.RWMutex // protects following + claimed bool // if UseRequest() has been called for it + running bool // if ServeHTTP() is running todoDirt []interface{} // dirty tags ctx context.Context // current context, derived from either Jaws or WS HTTP req cancelFn context.CancelCauseFunc // cancel function @@ -54,31 +56,6 @@ type eventFnCall struct { const maxWsQueueLengthPerElement = 20 var ErrWebsocketQueueOverflow = errors.New("websocket queue overflow") -var requestPool = sync.Pool{New: newRequest} - -func newRequest() interface{} { - rq := &Request{ - tagMap: make(map[interface{}][]*Element), - } - return rq -} - -func getRequest(jw *Jaws, jawsKey uint64, hr *http.Request) (rq *Request) { - rq = requestPool.Get().(*Request) - rq.Jaws = jw - rq.JawsKey = jawsKey - rq.Initial = hr - rq.Created = time.Now() - rq.ctx, rq.cancelFn = context.WithCancelCause(context.Background()) - if hr != nil { - rq.remoteIP = parseIP(hr.RemoteAddr) - if sess := jw.getSessionLocked(getCookieSessionsIds(hr.Header, jw.CookieName), rq.remoteIP); sess != nil { - sess.addRequest(rq) - rq.session = sess - } - } - return rq -} func (rq *Request) JawsKeyString() string { jawsKey := uint64(0) @@ -92,20 +69,26 @@ func (rq *Request) String() string { return "Request<" + rq.JawsKeyString() + ">" } -func (rq *Request) start(hr *http.Request) (err error) { +var ErrRequestAlreadyClaimed = errors.New("request already claimed") + +func (rq *Request) claim(hr *http.Request) (err error) { + rq.mu.Lock() + defer rq.mu.Unlock() + if rq.claimed { + return ErrRequestAlreadyClaimed + } var actualIP netip.Addr ctx := context.Background() if hr != nil { actualIP = parseIP(hr.RemoteAddr) ctx = hr.Context() } - rq.mu.Lock() if equalIP(rq.remoteIP, actualIP) { rq.ctx, rq.cancelFn = context.WithCancelCause(ctx) + rq.claimed = true } else { err = fmt.Errorf("/jaws/%s: expected IP %q, got %q", rq.JawsKeyString(), rq.remoteIP.String(), actualIP.String()) } - rq.mu.Unlock() return } @@ -122,28 +105,20 @@ func (rq *Request) killSession() { rq.mu.Unlock() } -func (rq *Request) recycle() { - rq.mu.Lock() - jw := rq.Jaws - if jw != nil { - rq.Jaws = nil - rq.JawsKey = 0 - rq.connectFn = nil - rq.Initial = nil - rq.Created = time.Time{} - rq.ctx = context.Background() - rq.cancelFn = nil - rq.todoDirt = rq.todoDirt[:0] - rq.remoteIP = netip.Addr{} - rq.elems = rq.elems[:0] - rq.killSessionLocked() - clear(rq.tagMap) - } - rq.mu.Unlock() - if jw != nil { - jw.deactivate(rq) - requestPool.Put(rq) - } +func (rq *Request) clearLocked() *Request { + rq.JawsKey = 0 + rq.connectFn = nil + rq.Created = time.Time{} + rq.Initial = nil + rq.claimed = false + rq.running = false + rq.ctx, rq.cancelFn = context.WithCancelCause(context.Background()) + rq.todoDirt = rq.todoDirt[:0] + rq.remoteIP = netip.Addr{} + rq.elems = rq.elems[:0] + rq.killSessionLocked() + clear(rq.tagMap) + return rq } // HeadHTML returns the HTML code needed to write in the HTML page's HEAD section. @@ -194,17 +169,31 @@ func (rq *Request) Context() (ctx context.Context) { return } -func (rq *Request) cancel(err error) { - if rq != nil { - rq.mu.RLock() - cancelFn := rq.cancelFn - rq.mu.RUnlock() - if cancelFn != nil { - cancelFn(err) +func (rq *Request) maintenance(deadline time.Time) (doRecycle bool) { + rq.mu.Lock() + defer rq.mu.Unlock() + if doRecycle = (!rq.running && rq.Created.Before(deadline)); doRecycle { + rq.cancelLocked(newErrNoWebSocketRequest(rq)) + } + return +} + +func (rq *Request) cancelLocked(err error) { + if rq.JawsKey != 0 && rq.ctx.Err() == nil { + if !rq.running { + err = newErrPendingCancelled(rq, err) } + rq.cancelFn(rq.Jaws.Log(err)) + rq.killSessionLocked() } } +func (rq *Request) cancel(err error) { + rq.mu.Lock() + defer rq.mu.Unlock() + rq.cancelLocked(err) +} + // Alert attempts to show an alert message on the current request webpage if it has an HTML element with the id 'jaws-alert'. // The lvl argument should be one of Bootstraps alert levels: primary, secondary, success, danger, warning, info, light or dark. // @@ -275,7 +264,6 @@ func (rq *Request) Register(tagitem interface{}, params ...interface{}) jid.Jid elem := rq.NewElement(uib) uib.parseGetter(elem, tagitem) uib.parseParams(elem, params) - rq.Dirty(uib.Tag) return elem.jid } @@ -305,9 +293,9 @@ var nextJid Jid func (rq *Request) newElementLocked(ui UI) (elem *Element) { elem = &Element{ - jid: Jid(atomic.AddInt64((*int64)(&nextJid), 1)), - ui: ui, - Request: rq, + jid: Jid(atomic.AddInt64((*int64)(&nextJid), 1)), + ui: ui, + rq: rq, } rq.elems = append(rq.elems, elem) return @@ -371,8 +359,8 @@ func (rq *Request) tagExpanded(elem *Element, expandedtags []interface{}) { // Tag adds the given tags to the given Element. func (rq *Request) Tag(elem *Element, tags ...interface{}) { - if elem != nil && len(tags) > 0 && elem.Request == rq { - rq.tagExpanded(elem, MustTagExpand(elem.Request, tags)) + if elem != nil && len(tags) > 0 && elem.rq == rq { + rq.tagExpanded(elem, MustTagExpand(elem.rq, tags)) } } @@ -413,7 +401,6 @@ func (rq *Request) process(broadcastMsgCh chan Message, incomingMsgCh <-chan wsM defer func() { rq.killSession() - rq.Jaws.deactivate(rq) rq.Jaws.unsubscribe(broadcastMsgCh) close(eventCallCh) for { @@ -430,6 +417,7 @@ func (rq *Request) process(broadcastMsgCh chan Message, incomingMsgCh <-chan wsM } rq.Jaws.MustLog(err) } + rq.Jaws.recycle(rq) return } } @@ -643,7 +631,7 @@ func (rq *Request) sendQueue(outboundCh chan<- string, wsQueue []wsMsg) []wsMsg } func (rq *Request) deleteElementLocked(e *Element) { - e.Request = nil + e.rq = nil rq.elems = slices.DeleteFunc(rq.elems, func(elem *Element) bool { return elem == e }) for k := range rq.tagMap { rq.tagMap[k] = slices.DeleteFunc(rq.tagMap[k], func(elem *Element) bool { return elem == e }) @@ -701,3 +689,7 @@ func (rq *Request) onConnect() (err error) { } return } + +func (rq *Request) Writer(w io.Writer) RequestWriter { + return RequestWriter{Request: rq, Writer: w} +} diff --git a/request_test.go b/request_test.go index 755a673..5c1a1e8 100644 --- a/request_test.go +++ b/request_test.go @@ -29,7 +29,7 @@ func fillWsCh(ch chan string) { } func TestRequest_Registrations(t *testing.T) { - is := testHelper{t} + is := newTestHelper(t) rq := newTestRequest() defer rq.Close() @@ -49,28 +49,12 @@ func TestRequest_Registrations(t *testing.T) { is.True(jid != jid2) } -func TestRequest_SendPanicsAfterRecycle(t *testing.T) { - is := testHelper{t} - defer func() { - e := recover() - if e == nil { - is.Fail() - } - t.Log(e) - }() - jw := New() - defer jw.Close() - rq := jw.NewRequest(nil) - rq.recycle() - rq.Jaws.Broadcast(Message{}) -} - func TestRequest_HeadHTML(t *testing.T) { - is := testHelper{t} + is := newTestHelper(t) jw := New() defer jw.Close() rq := jw.NewRequest(nil) - defer rq.recycle() + defer jw.recycle(rq) txt := rq.HeadHTML() is.Equal(strings.Contains(string(txt), rq.JawsKeyString()), true) @@ -78,7 +62,7 @@ func TestRequest_HeadHTML(t *testing.T) { } func TestRequest_SendArrivesOk(t *testing.T) { - is := testHelper{t} + is := newTestHelper(t) rq := newTestRequest() defer rq.Close() jid := rq.Register("foo") @@ -96,9 +80,7 @@ func TestRequest_SendArrivesOk(t *testing.T) { } func TestRequest_OutboundRespectsJawsClosed(t *testing.T) { - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() - is := testHelper{t} + th := newTestHelper(t) rq := newTestRequest() defer rq.Close() jw := rq.jw @@ -107,24 +89,24 @@ func TestRequest_OutboundRespectsJawsClosed(t *testing.T) { rq.ctx = context.Background() rq.Register(tag, func(e *Element, evt what.What, val string) error { atomic.AddInt32(&callCount, 1) - is.Equal(1, jw.RequestCount()) + th.Equal(1, jw.RequestCount()) jw.Close() return nil }) fillWsCh(rq.outCh) jw.Broadcast(Message{Dest: Tag("foo"), What: what.Hook, Data: "bar"}) select { - case <-tmr.C: - is.Equal(int(atomic.LoadInt32(&callCount)), 0) - is.Fail() + case <-th.C: + th.Equal(int(atomic.LoadInt32(&callCount)), 0) + th.Timeout() case <-rq.Done(): - is.Error("request ctx cancelled too soon") + th.Error("request ctx cancelled too soon") case <-jw.Done(): } - is.Equal(int(atomic.LoadInt32(&callCount)), 1) + th.Equal(int(atomic.LoadInt32(&callCount)), 1) select { case <-rq.Done(): - is.Error("request ctx cancelled too soon") + th.Error("request ctx cancelled too soon") default: } if jw.log.Len() != 0 { @@ -133,9 +115,7 @@ func TestRequest_OutboundRespectsJawsClosed(t *testing.T) { } func TestRequest_OutboundRespectsContextDone(t *testing.T) { - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() - is := testHelper{t} + th := newTestHelper(t) rq := newTestRequest() defer rq.Close() var callCount int32 @@ -149,25 +129,25 @@ func TestRequest_OutboundRespectsContextDone(t *testing.T) { rq.jw.Broadcast(Message{Dest: Tag("foo"), What: what.Hook, Data: "bar"}) select { - case <-tmr.C: - is.Equal(int(atomic.LoadInt32(&callCount)), 0) - is.Fatal(int(atomic.LoadInt32(&callCount))) + case <-th.C: + th.Equal(int(atomic.LoadInt32(&callCount)), 0) + th.Timeout() case <-rq.jw.Done(): - is.Fatal("jaws done too soon") + th.Fatal("jaws done too soon") case <-rq.ctx.Done(): } - is.Equal(int(atomic.LoadInt32(&callCount)), 1) + th.Equal(int(atomic.LoadInt32(&callCount)), 1) select { case <-rq.jw.Done(): - is.Fatal("jaws done too soon") + th.Fatal("jaws done too soon") default: } } func TestRequest_OutboundOverflowPanicsWithNoLogger(t *testing.T) { - is := testHelper{t} + th := newTestHelper(t) rq := newTestRequest() rq.expectPanic = true rq.jw.Logger = nil @@ -176,16 +156,16 @@ func TestRequest_OutboundOverflowPanicsWithNoLogger(t *testing.T) { fillWsCh(rq.outCh) rq.Jaws.Broadcast(Message{Dest: Tag("foo"), What: what.Inner, Data: "bar"}) select { - case <-time.NewTimer(testTimeout).C: - is.Fail() + case <-th.C: + th.Timeout() case <-rq.doneCh: - is.Equal(len(rq.outCh), cap(rq.outCh)) - is.True(rq.panicked) + th.Equal(len(rq.outCh), cap(rq.outCh)) + th.True(rq.panicked) } } func TestRequest_Trigger(t *testing.T) { - is := testHelper{t} + th := newTestHelper(t) rq := newTestRequest() defer rq.Close() gotFooCall := make(chan struct{}) @@ -207,32 +187,32 @@ func TestRequest_Trigger(t *testing.T) { // rq.Broadcast(Message{Dest: Tag("err"), What: what.Input, Data: "baz"}) rq.jw.Broadcast(Message{Dest: Tag("end"), What: what.Input, Data: ""}) // to know when to stop select { - case <-time.NewTimer(testTimeout).C: - is.Fail() - case <-rq.outCh: - is.Fail() + case <-th.C: + th.Timeout() + case s := <-rq.outCh: + th.Fatal(s) case <-gotFooCall: - is.Fail() + th.Fatal("gotFooCall") case <-gotEndCall: } // global broadcast should invoke fn rq.jw.Broadcast(Message{Dest: Tag("foo"), What: what.Input, Data: "bar"}) select { - case <-time.NewTimer(testTimeout).C: - is.Fail() - case <-rq.outCh: - is.Fail() + case <-th.C: + th.Timeout() + case s := <-rq.outCh: + th.Fatal(s) case <-gotFooCall: } // fn returning error should send an danger alert message rq.jw.Broadcast(Message{Dest: Tag("err"), What: what.Input, Data: "omg"}) select { - case <-time.NewTimer(testTimeout).C: - is.Fail() + case <-th.C: + th.Timeout() case msg := <-rq.outCh: - is.Equal(msg, (&wsMsg{ + th.Equal(msg, (&wsMsg{ Data: "danger\nomg", Jid: jid.Jid(0), What: what.Alert, @@ -241,7 +221,7 @@ func TestRequest_Trigger(t *testing.T) { } func TestRequest_EventFnQueue(t *testing.T) { - is := testHelper{t} + th := newTestHelper(t) rq := newTestRequest() defer rq.Close() @@ -253,7 +233,7 @@ func TestRequest_EventFnQueue(t *testing.T) { count := int(atomic.AddInt32(&callCount, 1)) if val != strconv.Itoa(count) { t.Logf("val=%s, count=%d, cap=%d", val, count, cap(rq.outCh)) - is.Fail() + th.Fail() } if count == 1 { close(firstDoneCh) @@ -269,33 +249,32 @@ func TestRequest_EventFnQueue(t *testing.T) { } select { - case <-time.NewTimer(testTimeout * 100).C: - is.Fail() + case <-th.C: + th.Timeout() case <-rq.doneCh: - is.Fail() + th.Fatal("doneCh") case <-firstDoneCh: } - is.Equal(atomic.LoadInt32(&callCount), int32(1)) + th.Equal(atomic.LoadInt32(&callCount), int32(1)) atomic.StoreInt32(&sleepDone, 1) - is.Equal(rq.panicVal, nil) + th.Equal(rq.panicVal, nil) - tmr := time.NewTimer(testTimeout * 100) for int(atomic.LoadInt32(&callCount)) < cap(rq.outCh) { select { - case <-tmr.C: + case <-th.C: t.Logf("callCount=%d, cap=%d", atomic.LoadInt32(&callCount), cap(rq.outCh)) - is.Equal(rq.panicVal, nil) - is.Fail() + th.Equal(rq.panicVal, nil) + th.Timeout() default: time.Sleep(time.Millisecond) } } - is.Equal(atomic.LoadInt32(&callCount), int32(cap(rq.outCh))) + th.Equal(atomic.LoadInt32(&callCount), int32(cap(rq.outCh))) } func TestRequest_EventFnQueueOverflowPanicsWithNoLogger(t *testing.T) { - is := testHelper{t} + th := newTestHelper(t) rq := newTestRequest() defer rq.Close() @@ -308,26 +287,23 @@ func TestRequest_EventFnQueueOverflowPanicsWithNoLogger(t *testing.T) { rq.expectPanic = true rq.jw.Logger = nil - tmr := time.NewTimer(testTimeout) - jid := jidForTag(rq.Request, Tag("bomb")) - defer tmr.Stop() for { select { case <-rq.doneCh: - is.True(rq.panicked) - is.True(strings.Contains(rq.panicVal.(error).Error(), "eventCallCh is full sending")) + th.True(rq.panicked) + th.True(strings.Contains(rq.panicVal.(error).Error(), "eventCallCh is full sending")) return - case <-tmr.C: - is.Fail() + case <-th.C: + th.Timeout() case rq.inCh <- wsMsg{Jid: jid, What: what.Input}: } } } func TestRequest_IgnoresIncomingMsgsDuringShutdown(t *testing.T) { - is := testHelper{t} + th := newTestHelper(t) rq := newTestRequest() defer rq.Close() @@ -355,39 +331,38 @@ func TestRequest_IgnoresIncomingMsgsDuringShutdown(t *testing.T) { time.Sleep(time.Millisecond) waited++ } - is.Equal(atomic.LoadInt32(&spewState), int32(1)) - is.Equal(cap(rq.outCh), len(rq.outCh)) - is.True(waited < 1000) + th.Equal(atomic.LoadInt32(&spewState), int32(1)) + th.Equal(cap(rq.outCh), len(rq.outCh)) + th.True(waited < 1000) // sending a message will now fail the rq since the // outbound channel is full, but with the // event fn holding it won't be able to end select { case rq.bcastCh <- Message{Dest: Tag("foo"), What: what.Inner, Data: ""}: - case <-time.NewTimer(testTimeout).C: - is.Fail() + case <-th.C: + th.Timeout() case <-rq.doneCh: - is.Fail() + th.Fatal() } // rq should now be in shutdown phase draining channels // while waiting for the event fn to return - tmr := time.NewTimer(testTimeout) for i := 0; i < cap(rq.outCh)*2; i++ { select { case <-rq.doneCh: - is.Fail() - case <-tmr.C: - is.Fail() + th.Fatal() + case <-th.C: + th.Timeout() default: rq.Jaws.Broadcast(Message{Dest: rq}) } select { case rq.inCh <- wsMsg{}: case <-rq.doneCh: - is.Fail() - case <-tmr.C: - is.Fail() + th.Fatal() + case <-th.C: + th.Timeout() } } @@ -396,18 +371,17 @@ func TestRequest_IgnoresIncomingMsgsDuringShutdown(t *testing.T) { select { case <-rq.doneCh: - is.True(atomic.LoadInt32(&callCount) > 1) - case <-time.NewTimer(testTimeout).C: - is.Fail() + th.True(atomic.LoadInt32(&callCount) > 1) + case <-th.C: + th.Timeout() } // log data should contain message that we were unable to deliver error - is.True(strings.Contains(rq.jw.log.String(), "outboundMsgCh full sending event")) + th.True(strings.Contains(rq.jw.log.String(), "outboundMsgCh full sending event")) } func TestRequest_Alert(t *testing.T) { - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() + th := newTestHelper(t) tj := newTestJaws() defer tj.Close() rq1 := tj.newRequest(nil) @@ -415,8 +389,8 @@ func TestRequest_Alert(t *testing.T) { rq1.Alert("info", "\nnot\tescaped") select { - case <-tmr.C: - t.Fatal("timeout") + case <-th.C: + th.Timeout() case s := <-rq1.outCh: if s != "Alert\t\t\"info\\n\\nnot\\tescaped\"\n" { t.Errorf("%q", s) @@ -430,8 +404,7 @@ func TestRequest_Alert(t *testing.T) { } func TestRequest_Redirect(t *testing.T) { - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() + th := newTestHelper(t) tj := newTestJaws() defer tj.Close() rq1 := tj.newRequest(nil) @@ -439,8 +412,8 @@ func TestRequest_Redirect(t *testing.T) { rq1.Redirect("some-url") select { - case <-tmr.C: - t.Fatal("timeout") + case <-th.C: + th.Timeout() case s := <-rq1.outCh: if s != "Redirect\t\t\"some-url\"\n" { t.Errorf("%q", s) @@ -454,15 +427,14 @@ func TestRequest_Redirect(t *testing.T) { } func TestRequest_AlertError(t *testing.T) { - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() + th := newTestHelper(t) tj := newTestJaws() defer tj.Close() rq := tj.newRequest(nil) rq.AlertError(errors.New("\nshould-be-escaped")) select { - case <-tmr.C: - t.Fatal("timeout") + case <-th.C: + th.Timeout() case s := <-rq.outCh: if s != "Alert\t\t\"danger\\n<html>\\nshould-be-escaped\"\n" { t.Errorf("%q", s) @@ -471,49 +443,47 @@ func TestRequest_AlertError(t *testing.T) { } func TestRequest_DeleteByTag(t *testing.T) { - is := testHelper{t} - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() + th := newTestHelper(t) tj := newTestJaws() defer tj.Close() nextJid = 0 rq1 := tj.newRequest(nil) ui1 := &testUi{} e11 := rq1.NewElement(ui1) - is.Equal(e11.jid, Jid(1)) + th.Equal(e11.jid, Jid(1)) e11.Tag(Tag("e11"), Tag("foo")) e12 := rq1.NewElement(ui1) - is.Equal(e12.jid, Jid(2)) + th.Equal(e12.jid, Jid(2)) e12.Tag(Tag("e12")) e13 := rq1.NewElement(ui1) - is.Equal(e13.jid, Jid(3)) + th.Equal(e13.jid, Jid(3)) e13.Tag(Tag("e13"), Tag("bar")) rq2 := tj.newRequest(nil) ui2 := &testUi{} e21 := rq2.NewElement(ui2) - is.Equal(e21.jid, Jid(4)) + th.Equal(e21.jid, Jid(4)) e21.Tag(Tag("e21"), Tag("foo")) e22 := rq2.NewElement(ui2) - is.Equal(e22.jid, Jid(5)) + th.Equal(e22.jid, Jid(5)) e22.Tag(Tag("e22")) e23 := rq2.NewElement(ui2) - is.Equal(e23.jid, Jid(6)) + th.Equal(e23.jid, Jid(6)) e23.Tag(Tag("e23")) tj.Delete([]any{Tag("foo"), Tag("bar"), Tag("nothere"), Tag("e23")}) select { - case <-tmr.C: - t.Fatal("timeout") + case <-th.C: + th.Timeout() case s := <-rq1.outCh: if s != "Delete\tJid.1\t\"\"\nDelete\tJid.3\t\"\"\n" { t.Errorf("%q", s) } } select { - case <-tmr.C: - t.Fatal("timeout") + case <-th.C: + th.Timeout() case s := <-rq2.outCh: if s != "Delete\tJid.4\t\"\"\nDelete\tJid.6\t\"\"\n" { t.Errorf("%q", s) @@ -522,8 +492,7 @@ func TestRequest_DeleteByTag(t *testing.T) { } func TestRequest_HtmlIdBroadcast(t *testing.T) { - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() + th := newTestHelper(t) tj := newTestJaws() defer tj.Close() rq1 := tj.newRequest(nil) @@ -535,16 +504,16 @@ func TestRequest_HtmlIdBroadcast(t *testing.T) { Data: "inner", }) select { - case <-tmr.C: - t.Fatal("timeout") + case <-th.C: + th.Timeout() case s := <-rq1.outCh: if s != "Inner\tfooId\t\"inner\"\n" { t.Errorf("%q", s) } } select { - case <-tmr.C: - t.Fatal("timeout") + case <-th.C: + th.Timeout() case s := <-rq2.outCh: if s != "Inner\tfooId\t\"inner\"\n" { t.Errorf("%q", s) @@ -560,23 +529,23 @@ func jidForTag(rq *Request, tag interface{}) jid.Jid { } func TestRequest_ConnectFn(t *testing.T) { - is := testHelper{t} + th := newTestHelper(t) rq := newTestRequest() defer rq.Close() - is.Equal(rq.GetConnectFn(), nil) - is.NoErr(rq.onConnect()) + th.Equal(rq.GetConnectFn(), nil) + th.NoErr(rq.onConnect()) wantErr := errors.New("getouttahere") fn := func(rq *Request) error { return wantErr } rq.SetConnectFn(fn) - is.Equal(rq.onConnect(), wantErr) + th.Equal(rq.onConnect(), wantErr) } func TestRequest_WsQueueOverflowCancels(t *testing.T) { - is := testHelper{t} + th := newTestHelper(t) jw := New() defer jw.Close() hr := httptest.NewRequest(http.MethodGet, "/", nil) @@ -588,29 +557,28 @@ func TestRequest_WsQueueOverflowCancels(t *testing.T) { } }() select { - case <-time.NewTimer(testTimeout).C: - is.Fail() + case <-th.C: + th.Timeout() case <-rq.Done(): } - is.Equal(context.Cause(rq.Context()), ErrWebsocketQueueOverflow) + th.True(errors.Is(context.Cause(rq.Context()), ErrWebsocketQueueOverflow)) } func TestRequest_Dirty(t *testing.T) { - is := testHelper{t} + th := newTestHelper(t) rq := newTestRequest() defer rq.Close() tss := &testUi{s: "foo"} - h := rq.UI(NewUiText(tss)) - is.Equal(tss.getCalled, int32(1)) - is.True(strings.Contains(string(h), "foo")) + rq.UI(NewUiText(tss)) + th.Equal(tss.getCalled, int32(1)) + th.True(strings.Contains(string(rq.BodyString()), "foo")) rq.Dirty(tss) - tmr := time.NewTimer(testTimeout) for atomic.LoadInt32(&tss.getCalled) < 2 { select { - case <-tmr.C: - is.Fail() + case <-th.C: + th.Timeout() default: time.Sleep(time.Millisecond) } @@ -618,8 +586,7 @@ func TestRequest_Dirty(t *testing.T) { } func TestRequest_UpdatePanicLogs(t *testing.T) { - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() + th := newTestHelper(t) nextJid = 0 rq := newTestRequest() defer rq.Close() @@ -631,8 +598,8 @@ func TestRequest_UpdatePanicLogs(t *testing.T) { rq.UI(tss) rq.Dirty(tss) select { - case <-tmr.C: - t.Fatal("timeout") + case <-th.C: + th.Timeout() case <-rq.doneCh: } if s := rq.jw.log.String(); !strings.Contains(s, "wildpanic") { @@ -641,8 +608,7 @@ func TestRequest_UpdatePanicLogs(t *testing.T) { } func TestRequest_IncomingRemove(t *testing.T) { - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() + th := newTestHelper(t) nextJid = 0 rq := newTestRequest() defer rq.Close() @@ -651,16 +617,16 @@ func TestRequest_IncomingRemove(t *testing.T) { rq.UI(NewUiText(tss)) select { - case <-tmr.C: - t.Fatal("timeout") + case <-th.C: + th.Timeout() case rq.inCh <- wsMsg{What: what.Remove, Data: "Jid.1"}: } elem := rq.getElementByJid(1) for elem != nil { select { - case <-tmr.C: - t.Fatal("timeout") + case <-th.C: + th.Timeout() default: time.Sleep(time.Millisecond) elem = rq.getElementByJid(1) @@ -669,8 +635,7 @@ func TestRequest_IncomingRemove(t *testing.T) { } func TestRequest_IncomingClick(t *testing.T) { - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() + th := newTestHelper(t) nextJid = 0 rq := newTestRequest() defer rq.Close() @@ -689,14 +654,14 @@ func TestRequest_IncomingClick(t *testing.T) { rq.Div("2", tjc2) select { - case <-tmr.C: - t.Fatal("timeout") + case <-th.C: + th.Timeout() case rq.inCh <- wsMsg{What: what.Click, Data: "name\tJid.1\tJid.2"}: } select { - case <-tmr.C: - t.Fatal("timeout") + case <-th.C: + th.Timeout() case s := <-tjc2.clickCh: if s != "name" { t.Error(s) @@ -710,16 +675,16 @@ func TestRequest_IncomingClick(t *testing.T) { } func TestRequest_CustomErrors(t *testing.T) { - is := testHelper{t} + th := newTestHelper(t) rq := newTestRequest() defer rq.Close() cause := newErrNoWebSocketRequest(rq.Request) err := newErrPendingCancelled(rq.Request, cause) - is.True(errors.Is(err, ErrPendingCancelled{})) - is.True(errors.Is(err, ErrNoWebSocketRequest{})) - is.Equal(errors.Is(cause, ErrPendingCancelled{}), false) + th.True(errors.Is(err, ErrPendingCancelled{})) + th.True(errors.Is(err, ErrNoWebSocketRequest{})) + th.Equal(errors.Is(cause, ErrPendingCancelled{}), false) var target1 ErrNoWebSocketRequest - is.True(errors.As(err, &target1)) + th.True(errors.As(err, &target1)) var target2 ErrPendingCancelled - is.Equal(errors.As(cause, &target2), false) + th.Equal(errors.As(cause, &target2), false) } diff --git a/requestwriter.go b/requestwriter.go new file mode 100644 index 0000000..351271d --- /dev/null +++ b/requestwriter.go @@ -0,0 +1,12 @@ +package jaws + +import "io" + +type RequestWriter struct { + *Request + io.Writer +} + +func (rw RequestWriter) UI(ui UI, params ...interface{}) error { + return rw.JawsRender(rw.NewElement(ui), rw.Writer, params) +} diff --git a/servehttp_test.go b/servehttp_test.go index 536be28..ae35b1d 100644 --- a/servehttp_test.go +++ b/servehttp_test.go @@ -11,7 +11,7 @@ func TestServeHTTP_GetJavascript(t *testing.T) { go jw.Serve() defer jw.Close() - is := testHelper{t} + is := newTestHelper(t) mux := http.NewServeMux() mux.Handle("/jaws/", jw) @@ -51,7 +51,7 @@ func TestServeHTTP_GetJavascript(t *testing.T) { } func TestServeHTTP_GetPing(t *testing.T) { - is := testHelper{t} + is := newTestHelper(t) jw := New() go jw.Serve() defer jw.Close() @@ -92,7 +92,7 @@ func TestServeHTTP_GetPing(t *testing.T) { } func TestServeHTTP_GetKey(t *testing.T) { - is := testHelper{t} + is := newTestHelper(t) jw := New() go jw.Serve() defer jw.Close() @@ -109,9 +109,9 @@ func TestServeHTTP_GetKey(t *testing.T) { is.Equal(w.Code, http.StatusNotFound) is.Equal(w.Header()["Cache-Control"], nil) + w = httptest.NewRecorder() rq := jw.NewRequest(req) req = httptest.NewRequest("", "/jaws/"+rq.JawsKeyString(), nil) - w = httptest.NewRecorder() jw.ServeHTTP(w, req) is.Equal(w.Code, http.StatusUpgradeRequired) is.Equal(w.Header()["Cache-Control"], nil) diff --git a/session_test.go b/session_test.go index 48fc5c0..8edd8e8 100644 --- a/session_test.go +++ b/session_test.go @@ -212,8 +212,7 @@ func TestSession_Requests(t *testing.T) { } func TestSession_Delete(t *testing.T) { - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() + th := newTestHelper(t) ts := newTestServer() defer ts.Close() go ts.jw.ServeWithTimeout(time.Second) @@ -277,7 +276,7 @@ func TestSession_Delete(t *testing.T) { } ts.rq.Register("byebye", func(e *Element, evt what.What, val string) error { - sess2 := ts.jw.GetSession(e.Request.Initial) + sess2 := ts.jw.GetSession(e.Request().Initial) if x := sess2; x != ts.sess { t.Error(x) } @@ -348,8 +347,8 @@ func TestSession_Delete(t *testing.T) { } select { - case <-tmr.C: - t.Fatal("timeout") + case <-th.C: + th.Timeout() case rr, ok := <-resultChan: if ok { if x := rr.err; x != nil { @@ -402,7 +401,7 @@ func TestSession_Cleanup(t *testing.T) { t.Error(x) } - r1.recycle() + jw.recycle(r1) r1 = nil sess.deadline = time.Now() if x := jw.SessionCount(); x != 1 { @@ -425,7 +424,7 @@ func TestSession_ReplacesOld(t *testing.T) { defer jw.Close() go jw.ServeWithTimeout(time.Second) - is := testHelper{t} + is := newTestHelper(t) is.Equal(jw.SessionCount(), 0) @@ -480,7 +479,7 @@ func TestSession_ReplacesOld(t *testing.T) { is.Equal(jw.GetSession(h4), s1) is.Equal(len(w4.Result().Cookies()), 0) - r4.recycle() + jw.recycle(r4) w3 := httptest.NewRecorder() h3 := httptest.NewRequest("GET", "/", nil) diff --git a/template.go b/template.go index d5e6a8c..d0d25c1 100644 --- a/template.go +++ b/template.go @@ -39,16 +39,22 @@ func (t Template) String() string { return fmt.Sprintf("{%q, %s}", t.Template.Name(), TagString(t.Dot)) } -func (t Template) JawsRender(e *Element, w io.Writer, params []interface{}) { - if expandedtags, err := TagExpand(e.Request, t.Dot); err != ErrIllegalTagType { - e.Request.tagExpanded(e, expandedtags) +func (t Template) JawsRender(e *Element, w io.Writer, params []interface{}) error { + if expandedtags, err := TagExpand(e.Request(), t.Dot); err != ErrIllegalTagType { + e.Request().tagExpanded(e, expandedtags) } var sb strings.Builder for _, s := range parseParams(e, params) { sb.WriteByte(' ') sb.WriteString(s) } - maybePanic(t.Execute(w, With{Element: e, Dot: t.Dot, Attrs: template.HTMLAttr(sb.String())})) // #nosec G203 + attrs := template.HTMLAttr(sb.String()) // #nosec G203 + return t.Execute(w, With{ + Element: e, + RequestWriter: e.Request().Writer(w), + Dot: t.Dot, + Attrs: attrs, + }) } func (t Template) JawsUpdate(e *Element) { diff --git a/template_test.go b/template_test.go index 2d5571f..88cfd1c 100644 --- a/template_test.go +++ b/template_test.go @@ -7,7 +7,7 @@ import ( ) func TestTemplate_String(t *testing.T) { - is := testHelper{t} + is := newTestHelper(t) rq := newTestRequest() defer rq.Close() diff --git a/testhelper_test.go b/testhelper_test.go index fa528af..9bbd19d 100644 --- a/testhelper_test.go +++ b/testhelper_test.go @@ -3,52 +3,53 @@ package jaws import ( "reflect" "testing" + "time" ) -type testHelper struct{ *testing.T } +type testHelper struct { + *time.Timer + *testing.T +} -func testNil(object any) (bool, reflect.Type) { - if object == nil { - return true, nil +func newTestHelper(t *testing.T) (th *testHelper) { + th = &testHelper{ + T: t, + Timer: time.NewTimer(time.Second * 3), } - value := reflect.ValueOf(object) - kind := value.Kind() - return kind >= reflect.Chan && kind <= reflect.Slice && value.IsNil(), value.Type() + t.Cleanup(th.Cleanup) + return } -func testEqual(a, b any) bool { - if reflect.DeepEqual(a, b) { - return true - } - aIsNil, aType := testNil(a) - bIsNil, bType := testNil(b) - if !(aIsNil && bIsNil) { - return false - } - return aType == nil || bType == nil || (aType == bType) +func (th *testHelper) Cleanup() { + th.Timer.Stop() } -func (th testHelper) Equal(a, b any) { +func (th *testHelper) Equal(a, b any) { if !testEqual(a, b) { th.Helper() th.Errorf("%T(%v) != %T(%v)", a, a, b, b) } } -func (th testHelper) True(a bool) { +func (th *testHelper) True(a bool) { if !a { th.Helper() th.Error("not true") } } -func (th testHelper) NoErr(err error) { +func (th *testHelper) NoErr(err error) { if err != nil { th.Helper() th.Error(err) } } +func (th *testHelper) Timeout() { + th.Helper() + th.Fatal("timeout") +} + func Test_testHelper(t *testing.T) { mustEqual := func(a, b any) { if !testEqual(a, b) { @@ -74,3 +75,24 @@ func Test_testHelper(t *testing.T) { mustNotEqual((*testing.T)(nil), (*testHelper)(nil)) mustNotEqual(int(1), int32(1)) } + +func testNil(object any) (bool, reflect.Type) { + if object == nil { + return true, nil + } + value := reflect.ValueOf(object) + kind := value.Kind() + return kind >= reflect.Chan && kind <= reflect.Slice && value.IsNil(), value.Type() +} + +func testEqual(a, b any) bool { + if reflect.DeepEqual(a, b) { + return true + } + aIsNil, aType := testNil(a) + bIsNil, bType := testNil(b) + if !(aIsNil && bIsNil) { + return false + } + return aType == nil || bType == nil || (aType == bType) +} diff --git a/testjaws_test.go b/testjaws_test.go index de3c63e..a396a1f 100644 --- a/testjaws_test.go +++ b/testjaws_test.go @@ -28,6 +28,7 @@ func newTestJaws() (tj *testJaws) { type testRequest struct { hr *http.Request + rr *httptest.ResponseRecorder jw *testJaws readyCh chan struct{} doneCh chan struct{} @@ -40,6 +41,7 @@ type testRequest struct { panicked bool panicVal any *Request + RequestWriter } func (tj *testJaws) newRequest(hr *http.Request) (tr *testRequest) { @@ -48,6 +50,8 @@ func (tj *testJaws) newRequest(hr *http.Request) (tr *testRequest) { } ctx, cancel := context.WithTimeout(context.Background(), time.Hour) hr = hr.WithContext(ctx) + rr := httptest.NewRecorder() + rr.Body = &bytes.Buffer{} rq := tj.NewRequest(hr) if rq == nil || tj.UseRequest(rq.JawsKey, hr) != rq { panic("failed to create or use jaws.Request") @@ -58,16 +62,18 @@ func (tj *testJaws) newRequest(hr *http.Request) (tr *testRequest) { } tr = &testRequest{ - hr: hr, - jw: tj, - readyCh: make(chan struct{}), - doneCh: make(chan struct{}), - inCh: make(chan wsMsg), - outCh: make(chan string, cap(bcastCh)), - bcastCh: bcastCh, - ctx: ctx, - cancel: cancel, - Request: rq, + hr: hr, + rr: rr, + jw: tj, + readyCh: make(chan struct{}), + doneCh: make(chan struct{}), + inCh: make(chan wsMsg), + outCh: make(chan string, cap(bcastCh)), + bcastCh: bcastCh, + ctx: ctx, + cancel: cancel, + Request: rq, + RequestWriter: rq.Writer(rr), } go func() { @@ -81,17 +87,29 @@ func (tj *testJaws) newRequest(hr *http.Request) (tr *testRequest) { }() close(tr.readyCh) tr.process(tr.bcastCh, tr.inCh, tr.outCh) // usubs from bcase, closes outCh - tr.recycle() + tr.jw.recycle(tr.Request) }() return } +func (tr *testRequest) BodyString() string { + return tr.rr.Body.String() +} + +func (tr *testRequest) BodyHtml() template.HTML { + return template.HTML(tr.BodyString()) +} + func (tr *testRequest) Close() { tr.cancel() tr.jw.Close() } +func (tr *testRequest) Write(buf []byte) (int, error) { + return tr.rr.Write(buf) +} + func newTestRequest() (tr *testRequest) { tj := newTestJaws() return tj.newRequest(nil) diff --git a/ui.go b/ui.go index aa5b2d7..81cc575 100644 --- a/ui.go +++ b/ui.go @@ -2,7 +2,6 @@ package jaws import ( "fmt" - "html/template" "io" "strings" ) @@ -10,28 +9,24 @@ import ( // If any of these functions panic, the Request will be closed and the panic logged. // Optionally you may also implement ClickHandler and/or EventHandler. type UI interface { - JawsRender(e *Element, w io.Writer, params []interface{}) + JawsRender(e *Element, w io.Writer, params []interface{}) error JawsUpdate(e *Element) } -func (rq *Request) UI(ui UI, params ...interface{}) template.HTML { - var sb strings.Builder - rq.JawsRender(rq.NewElement(ui), &sb, params) - return template.HTML(sb.String()) // #nosec G203 -} - -func (rq *Request) JawsRender(elem *Element, w io.Writer, params []interface{}) { - elem.ui.JawsRender(elem, w, params) - if rq.Jaws.Debug { - var sb strings.Builder - _, _ = fmt.Fprintf(&sb, "", "==>") + " -->")) } - sb.WriteByte(']') - _, _ = w.Write([]byte(strings.ReplaceAll(sb.String(), "-->", "==>") + " -->")) } + return } diff --git a/ui_test.go b/ui_test.go index ee2c7f4..de69da3 100644 --- a/ui_test.go +++ b/ui_test.go @@ -1,9 +1,14 @@ package jaws import ( + "bytes" + "html/template" "io" + "net/http" + "net/http/httptest" "strings" "testing" + "time" ) type testStringer struct{} @@ -14,17 +19,115 @@ func (s testStringer) String() string { func TestRequest_JawsRender_DebugOutput(t *testing.T) { - is := testHelper{t} + is := newTestHelper(t) rq := newTestRequest() defer rq.Close() rq.Jaws.Debug = true - h := string(rq.UI(&testUi{renderFn: func(e *Element, w io.Writer, params []any) { + rq.UI(&testUi{renderFn: func(e *Element, w io.Writer, params []any) error { e.Tag(Tag("footag")) - e.Tag(e.Request) + e.Tag(e.Request()) e.Tag(testStringer{}) - }})) + return nil + }}) + h := rq.BodyString() t.Log(h) is.True(strings.Contains(h, "footag")) is.True(strings.Contains(h, "*jaws.testUi")) is.True(strings.Contains(h, testStringer{}.String())) } + +func TestRequest_InsideTemplate(t *testing.T) { + jw := New() + defer jw.Close() + nextJid = 4 + + const tmplText = "(" + + "{{$.Initial.URL.Path}}" + + "{{$.A `a`}}" + + "{{$.Button `button`}}" + + "{{$.Checkbox .TheBool `checkbox`}}" + + "{{$.Container `container` .TheContainer}}" + + "{{$.Date .TheTime `date`}}" + + "{{$.Div `div`}}" + + "{{$.Img `img`}}" + + "{{$.Label `label`}}" + + "{{$.Li `li`}}" + + "{{$.Number .TheNumber}}" + + "{{$.Password .TheString}}" + + "{{$.Radio .TheBool}}" + + "{{$.Range .TheNumber}}" + + "{{$.Select .TheSelector}}" + + "{{$.Span `span`}}" + + "{{$.Tbody .TheContainer}}" + + "{{$.Td `td`}}" + + "{{$.Template `nested` .TheDot}}" + + "{{$.Text .TheString}}" + + "{{$.Textarea .TheString}}" + + "{{$.Tr `tr`}}" + + ")" + const nestedTmplText = "" + + "{{$.Initial.URL.Path}}" + + "{{with .Dot}}{{.}}{{$.Span `span2`}}{{end}}" + + "" + const want = "(" + + "/path" + + "a" + + "" + + "" + + "" + + "" + + "
div
" + + "" + + "" + + "
  • li
  • " + + "" + + "" + + "" + + "" + + "" + + "span" + + "" + + "td" + + "/pathdotspan2" + + "" + + "" + + "tr" + + ")" + + jw.Template = template.Must(template.New("nested").Parse(nestedTmplText)) + tmpl := template.Must(template.New("normal").Parse(tmplText)) + w := httptest.NewRecorder() + w.Body = &bytes.Buffer{} + hr := httptest.NewRequest(http.MethodGet, "/path", nil) + rq := jw.NewRequest(hr) + testDate, _ := time.Parse(ISO8601, "1901-02-03") + dot := struct { + RequestWriter + TheBool BoolSetter + TheContainer Container + TheTime TimeSetter + TheNumber FloatSetter + TheString StringSetter + TheSelector SelectHandler + TheDot any + }{ + RequestWriter: RequestWriter{rq, w}, + TheBool: newTestSetter(true), + TheContainer: &testContainer{}, + TheTime: newTestSetter(testDate), + TheNumber: newTestSetter(float64(1.2)), + TheString: newTestSetter("bar"), + TheSelector: &testNamedBoolArray{ + setCalled: make(chan struct{}), + NamedBoolArray: NewNamedBoolArray(), + }, + TheDot: "dot", + } + if err := tmpl.Execute(w, dot); err != nil { + t.Fatal(err) + } + w.Flush() + if x := w.Body.String(); x != want { + t.Errorf("%q", x) + } +} diff --git a/uia.go b/uia.go index 6da006b..6285c9a 100644 --- a/uia.go +++ b/uia.go @@ -1,7 +1,6 @@ package jaws import ( - "html/template" "io" ) @@ -9,8 +8,8 @@ type UiA struct { UiHtmlInner } -func (ui *UiA) JawsRender(e *Element, w io.Writer, params []interface{}) { - ui.renderInner(e, w, "a", "", params) +func (ui *UiA) JawsRender(e *Element, w io.Writer, params []interface{}) error { + return ui.renderInner(e, w, "a", "", params) } func NewUiA(innerHtml HtmlGetter) *UiA { @@ -21,6 +20,6 @@ func NewUiA(innerHtml HtmlGetter) *UiA { } } -func (rq *Request) A(innerHtml interface{}, params ...interface{}) template.HTML { +func (rq RequestWriter) A(innerHtml interface{}, params ...interface{}) error { return rq.UI(NewUiA(makeHtmlGetter(innerHtml)), params...) } diff --git a/uia_test.go b/uia_test.go index 90001f9..7e8407b 100644 --- a/uia_test.go +++ b/uia_test.go @@ -54,7 +54,8 @@ func TestRequest_A(t *testing.T) { nextJid = 0 rq := newTestRequest() defer rq.Close() - if got := rq.A(tt.args.innerHtml, tt.args.params...); !reflect.DeepEqual(got, tt.want) { + rq.A(tt.args.innerHtml, tt.args.params...) + if got := rq.BodyHtml(); !reflect.DeepEqual(got, tt.want) { t.Errorf("Request.A() = %v, want %v", got, tt.want) } }) diff --git a/uibutton.go b/uibutton.go index 604c303..7f5763e 100644 --- a/uibutton.go +++ b/uibutton.go @@ -1,7 +1,6 @@ package jaws import ( - "html/template" "io" ) @@ -9,8 +8,8 @@ type UiButton struct { UiHtmlInner } -func (ui *UiButton) JawsRender(e *Element, w io.Writer, params []interface{}) { - ui.renderInner(e, w, "button", "button", params) +func (ui *UiButton) JawsRender(e *Element, w io.Writer, params []interface{}) error { + return ui.renderInner(e, w, "button", "button", params) } func NewUiButton(innerHtml HtmlGetter) *UiButton { @@ -21,6 +20,6 @@ func NewUiButton(innerHtml HtmlGetter) *UiButton { } } -func (rq *Request) Button(innerHtml interface{}, params ...interface{}) template.HTML { +func (rq RequestWriter) Button(innerHtml interface{}, params ...interface{}) error { return rq.UI(NewUiButton(makeHtmlGetter(innerHtml)), params...) } diff --git a/uibutton_test.go b/uibutton_test.go index fd759fc..594cc29 100644 --- a/uibutton_test.go +++ b/uibutton_test.go @@ -9,7 +9,8 @@ func TestRequest_Button(t *testing.T) { rq := newTestRequest() defer rq.Close() want := `` - if got := string(rq.Button("inner")); got != want { + rq.Button("inner") + if got := rq.BodyString(); got != want { t.Errorf("Request.Button() = %q, want %q", got, want) } } diff --git a/uicheckbox.go b/uicheckbox.go index 23d59b1..17c32c6 100644 --- a/uicheckbox.go +++ b/uicheckbox.go @@ -1,7 +1,6 @@ package jaws import ( - "html/template" "io" ) @@ -9,8 +8,8 @@ type UiCheckbox struct { UiInputBool } -func (ui *UiCheckbox) JawsRender(e *Element, w io.Writer, params []interface{}) { - ui.renderBoolInput(e, w, "checkbox", params...) +func (ui *UiCheckbox) JawsRender(e *Element, w io.Writer, params []interface{}) error { + return ui.renderBoolInput(e, w, "checkbox", params...) } func NewUiCheckbox(g BoolSetter) *UiCheckbox { @@ -21,6 +20,6 @@ func NewUiCheckbox(g BoolSetter) *UiCheckbox { } } -func (rq *Request) Checkbox(value interface{}, params ...interface{}) template.HTML { +func (rq RequestWriter) Checkbox(value interface{}, params ...interface{}) error { return rq.UI(NewUiCheckbox(makeBoolSetter(value)), params...) } diff --git a/uicheckbox_test.go b/uicheckbox_test.go index ec89cc1..62eadb9 100644 --- a/uicheckbox_test.go +++ b/uicheckbox_test.go @@ -3,29 +3,28 @@ package jaws import ( "errors" "testing" - "time" "github.com/linkdata/jaws/what" ) func TestRequest_Checkbox(t *testing.T) { - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() + th := newTestHelper(t) nextJid = 0 rq := newTestRequest() defer rq.Close() ts := newTestSetter(true) want := `` - if got := string(rq.Checkbox(ts)); got != want { + rq.Checkbox(ts) + if got := rq.BodyString(); got != want { t.Errorf("Request.Checkbox() = %q, want %q", got, want) } val := false rq.inCh <- wsMsg{Data: "false", Jid: 1, What: what.Input} select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case <-ts.setCalled: } if ts.Get() != val { @@ -41,8 +40,8 @@ func TestRequest_Checkbox(t *testing.T) { ts.Set(val) rq.Dirty(ts) select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case s := <-rq.outCh: if s != "Value\tJid.1\t\"true\"\n" { t.Errorf("%q", s) @@ -57,8 +56,8 @@ func TestRequest_Checkbox(t *testing.T) { rq.inCh <- wsMsg{Data: "omg", Jid: 1, What: what.Input} select { - case <-tmr.C: - t.Error("timeout waiting for Alert") + case <-th.C: + th.Timeout() case s := <-rq.outCh: if s != "Alert\t\t\"danger\\nstrconv.ParseBool: parsing "omg": invalid syntax\"\n" { t.Errorf("wrong Alert: %q", s) @@ -68,8 +67,8 @@ func TestRequest_Checkbox(t *testing.T) { ts.err = errors.New("meh") rq.inCh <- wsMsg{Data: "true", Jid: 1, What: what.Input} select { - case <-tmr.C: - t.Error("timeout waiting for Alert") + case <-th.C: + th.Timeout() case s := <-rq.outCh: if s != "Alert\t\t\"danger\\nmeh\"\n" { t.Errorf("wrong Alert: %q", s) diff --git a/uicontainer.go b/uicontainer.go index 605fb0a..7a2d6ac 100644 --- a/uicontainer.go +++ b/uicontainer.go @@ -1,7 +1,6 @@ package jaws import ( - "html/template" "io" ) @@ -19,10 +18,10 @@ func NewUiContainer(outerHtmlTag string, c Container) *UiContainer { } } -func (ui *UiContainer) JawsRender(e *Element, w io.Writer, params []interface{}) { - ui.renderContainer(e, w, ui.OuterHtmlTag, params) +func (ui *UiContainer) JawsRender(e *Element, w io.Writer, params []interface{}) error { + return ui.renderContainer(e, w, ui.OuterHtmlTag, params) } -func (rq *Request) Container(outerHtmlTag string, c Container, params ...interface{}) template.HTML { +func (rq RequestWriter) Container(outerHtmlTag string, c Container, params ...interface{}) error { return rq.UI(NewUiContainer(outerHtmlTag, c), params...) } diff --git a/uicontainer_test.go b/uicontainer_test.go index caf97b2..84c179e 100644 --- a/uicontainer_test.go +++ b/uicontainer_test.go @@ -60,7 +60,8 @@ func TestRequest_Container(t *testing.T) { nextJid = 0 rq := newTestRequest() defer rq.Close() - if got := rq.Container("div", tt.args.c, tt.args.params...); !reflect.DeepEqual(got, tt.want) { + rq.Container("div", tt.args.c, tt.args.params...) + if got := rq.BodyHtml(); !reflect.DeepEqual(got, tt.want) { t.Errorf("Request.Container() = %v, want %v", got, tt.want) } }) @@ -171,7 +172,9 @@ func TestRequest_Container_Alteration(t *testing.T) { ui := NewUiContainer("div", tt.c) elem := rq.NewElement(ui) var sb strings.Builder - ui.JawsRender(elem, &sb, nil) + if err := ui.JawsRender(elem, &sb, nil); err != nil { + t.Fatal(err) + } tt.c.contents = tt.l ui.JawsUpdate(elem) if !slices.Equal(elem.wsQueue, tt.want) { diff --git a/uidate.go b/uidate.go index 98180e7..54ddf77 100644 --- a/uidate.go +++ b/uidate.go @@ -1,7 +1,6 @@ package jaws import ( - "html/template" "io" ) @@ -11,8 +10,8 @@ type UiDate struct { UiInputDate } -func (ui *UiDate) JawsRender(e *Element, w io.Writer, params []interface{}) { - ui.renderDateInput(e, w, e.Jid(), "date", params...) +func (ui *UiDate) JawsRender(e *Element, w io.Writer, params []interface{}) error { + return ui.renderDateInput(e, w, e.Jid(), "date", params...) } func NewUiDate(g TimeSetter) *UiDate { @@ -23,6 +22,6 @@ func NewUiDate(g TimeSetter) *UiDate { } } -func (rq *Request) Date(value interface{}, params ...interface{}) template.HTML { +func (rq RequestWriter) Date(value interface{}, params ...interface{}) error { return rq.UI(NewUiDate(makeTimeSetter(value)), params...) } diff --git a/uidate_test.go b/uidate_test.go index eba6cae..5b05e9e 100644 --- a/uidate_test.go +++ b/uidate_test.go @@ -10,13 +10,15 @@ import ( ) func TestRequest_Date(t *testing.T) { + th := newTestHelper(t) nextJid = 0 rq := newTestRequest() defer rq.Close() ts := newTestSetter(time.Now()) want := fmt.Sprintf(``, ts.Get().Format(ISO8601)) - if got := string(rq.Date(ts)); got != want { + rq.Date(ts) + if got := rq.BodyString(); got != want { t.Errorf("Request.Date() = %q, want %q", got, want) } @@ -25,8 +27,8 @@ func TestRequest_Date(t *testing.T) { tmr := time.NewTimer(testTimeout) defer tmr.Stop() select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case <-ts.setCalled: } if ts.Get() != val { @@ -42,8 +44,8 @@ func TestRequest_Date(t *testing.T) { ts.Set(val) rq.Dirty(ts) select { - case <-tmr.C: - t.Error("timeout waiting for Value") + case <-th.C: + th.Timeout() case s := <-rq.outCh: if s != fmt.Sprintf("Value\tJid.1\t\"%s\"\n", val.Format(ISO8601)) { t.Error("wrong Value") @@ -58,8 +60,8 @@ func TestRequest_Date(t *testing.T) { rq.inCh <- wsMsg{Data: "omg", Jid: 1, What: what.Input} select { - case <-tmr.C: - t.Error("timeout waiting for Alert") + case <-th.C: + th.Timeout() case s := <-rq.outCh: if s != "Alert\t\t\"danger\\nparsing time "omg" as "2006-01-02": cannot parse "omg" as "2006"\"\n" { t.Errorf("wrong Alert: %q", s) @@ -69,8 +71,8 @@ func TestRequest_Date(t *testing.T) { ts.err = errors.New("meh") rq.inCh <- wsMsg{Data: val.Format(ISO8601), Jid: 1, What: what.Input} select { - case <-tmr.C: - t.Error("timeout waiting for Alert") + case <-th.C: + th.Timeout() case s := <-rq.outCh: if s != "Alert\t\t\"danger\\nmeh\"\n" { t.Errorf("wrong Alert: %q", s) diff --git a/uidiv.go b/uidiv.go index a12046f..ededbf0 100644 --- a/uidiv.go +++ b/uidiv.go @@ -1,7 +1,6 @@ package jaws import ( - "html/template" "io" ) @@ -9,8 +8,8 @@ type UiDiv struct { UiHtmlInner } -func (ui *UiDiv) JawsRender(e *Element, w io.Writer, params []interface{}) { - ui.renderInner(e, w, "div", "", params) +func (ui *UiDiv) JawsRender(e *Element, w io.Writer, params []interface{}) error { + return ui.renderInner(e, w, "div", "", params) } func NewUiDiv(innerHtml HtmlGetter) *UiDiv { @@ -21,6 +20,6 @@ func NewUiDiv(innerHtml HtmlGetter) *UiDiv { } } -func (rq *Request) Div(innerHtml interface{}, params ...interface{}) template.HTML { +func (rq RequestWriter) Div(innerHtml interface{}, params ...interface{}) error { return rq.UI(NewUiDiv(makeHtmlGetter(innerHtml)), params...) } diff --git a/uidiv_test.go b/uidiv_test.go index ba80c32..48c4fb7 100644 --- a/uidiv_test.go +++ b/uidiv_test.go @@ -9,7 +9,8 @@ func TestRequest_Div(t *testing.T) { rq := newTestRequest() defer rq.Close() want := `
    inner
    ` - if got := string(rq.Div("inner")); got != want { + rq.Div("inner") + if got := rq.BodyString(); got != want { t.Errorf("Request.Div() = %q, want %q", got, want) } } diff --git a/uihtml.go b/uihtml.go index 0457da8..8bbad2d 100644 --- a/uihtml.go +++ b/uihtml.go @@ -14,7 +14,7 @@ type UiHtml struct { func (ui *UiHtml) parseGetter(e *Element, getter interface{}) { if getter != nil { if tagger, ok := getter.(TagGetter); ok { - ui.Tag = tagger.JawsGetTag(e.Request) + ui.Tag = tagger.JawsGetTag(e.Request()) if ch, ok := getter.(ClickHandler); ok { e.handlers = append(e.handlers, clickHandlerWapper{ch}) } @@ -63,10 +63,11 @@ func (ui *UiHtml) parseParams(elem *Element, params []interface{}) (attrs []stri return } -func (ui *UiHtml) JawsRender(e *Element, w io.Writer, params []interface{}) { +func (ui *UiHtml) JawsRender(e *Element, w io.Writer, params []interface{}) (err error) { if h, ok := ui.Tag.(UI); ok { - h.JawsRender(e, w, params) + err = h.JawsRender(e, w, params) } + return } func (ui *UiHtml) JawsUpdate(e *Element) { diff --git a/uihtml_test.go b/uihtml_test.go index 4dc4543..4816cde 100644 --- a/uihtml_test.go +++ b/uihtml_test.go @@ -6,7 +6,6 @@ import ( "io" "strings" "testing" - "time" "github.com/linkdata/jaws/what" ) @@ -27,16 +26,20 @@ func (tje *testJawsEvent) JawsEvent(e *Element, wht what.What, val string) (err } func (tje *testJawsEvent) JawsGetTag(*Request) (tag any) { - return tje.tag + if tje.tag != nil { + return tje.tag + } + return nil } -func (tje *testJawsEvent) JawsRender(e *Element, w io.Writer, params []any) { +func (tje *testJawsEvent) JawsRender(e *Element, w io.Writer, params []any) error { w.Write([]byte(fmt.Sprint(params))) - tje.msgCh <- "JawsRender" + tje.msgCh <- fmt.Sprintf("JawsRender(%d)", e.jid) + return nil } func (tje *testJawsEvent) JawsUpdate(e *Element) { - tje.msgCh <- "JawsUpdate" + tje.msgCh <- fmt.Sprintf("JawsUpdate(%d)", e.jid) } var _ ClickHandler = (*testJawsEvent)(nil) @@ -45,8 +48,7 @@ var _ TagGetter = (*testJawsEvent)(nil) var _ UI = (*testJawsEvent)(nil) func TestUiHtml_JawsEvent(t *testing.T) { - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() + th := newTestHelper(t) nextJid = 0 rq := newTestRequest() defer rq.Close() @@ -59,8 +61,8 @@ func TestUiHtml_JawsEvent(t *testing.T) { rq.inCh <- wsMsg{Data: "text", Jid: id, What: what.Input} select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case s := <-tje.msgCh: if s != "JawsEvent: Input \"text\"" { t.Error(s) @@ -69,8 +71,8 @@ func TestUiHtml_JawsEvent(t *testing.T) { rq.inCh <- wsMsg{Data: "name", Jid: id, What: what.Click} select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case s := <-msgCh: if s != "JawsClick: \"name\"" { t.Error(s) @@ -79,21 +81,27 @@ func TestUiHtml_JawsEvent(t *testing.T) { tje.tag = tje id2 := rq.Register(tje) + th.Equal(id2, Jid(2)) rq.inCh <- wsMsg{Data: "text2", Jid: id2, What: what.Input} select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case s := <-tje.msgCh: if s != "JawsEvent: Input \"text2\"" { t.Error(s) } } + // nothing should be marked dirty, + // but if it is, this ensures the + // test fails reliably + rq.jw.distributeDirt() + rq.inCh <- wsMsg{Data: "name2", Jid: id2, What: what.Click} select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case s := <-msgCh: if s != "JawsClick: \"name2\"" { t.Error(s) @@ -102,22 +110,24 @@ func TestUiHtml_JawsEvent(t *testing.T) { rq.Dirty(tje) select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case s := <-msgCh: - if s != "JawsUpdate" { + if s != "JawsUpdate(2)" { t.Error(s) } } elem := rq.getElementByJid(id2) var sb strings.Builder - elem.ui.JawsRender(elem, &sb, []any{"attr"}) + if err := elem.ui.JawsRender(elem, &sb, []any{"attr"}); err != nil { + t.Fatal(err) + } select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case s := <-msgCh: - if s != "JawsRender" { + if s != "JawsRender(2)" { t.Error(s) } } diff --git a/uihtmlinner.go b/uihtmlinner.go index 42e06bb..955e5ae 100644 --- a/uihtmlinner.go +++ b/uihtmlinner.go @@ -9,10 +9,10 @@ type UiHtmlInner struct { HtmlGetter } -func (ui *UiHtmlInner) renderInner(e *Element, w io.Writer, htmltag, htmltype string, params []interface{}) { +func (ui *UiHtmlInner) renderInner(e *Element, w io.Writer, htmltag, htmltype string, params []interface{}) error { ui.parseGetter(e, ui.HtmlGetter) attrs := ui.parseParams(e, params) - maybePanic(WriteHtmlInner(w, e.Jid(), htmltag, htmltype, ui.JawsGetHtml(e), attrs...)) + return WriteHtmlInner(w, e.Jid(), htmltag, htmltype, ui.JawsGetHtml(e), attrs...) } func (ui *UiHtmlInner) JawsUpdate(e *Element) { diff --git a/uihtmlinner_test.go b/uihtmlinner_test.go index d11a9ef..958de8f 100644 --- a/uihtmlinner_test.go +++ b/uihtmlinner_test.go @@ -20,7 +20,9 @@ func TestUiHtmlInner_JawsUpdate(t *testing.T) { ui := NewUiDiv(ts) elem := rq.NewElement(ui) var sb strings.Builder - ui.JawsRender(elem, &sb, nil) + if err := ui.JawsRender(elem, &sb, nil); err != nil { + t.Fatal(err) + } wantHtml := "
    first
    " if sb.String() != wantHtml { t.Errorf("got %q, want %q", sb.String(), wantHtml) diff --git a/uiimg.go b/uiimg.go index 9cded64..bdc74d6 100644 --- a/uiimg.go +++ b/uiimg.go @@ -1,7 +1,6 @@ package jaws import ( - "html/template" "io" "strconv" ) @@ -19,10 +18,10 @@ func (ui *UiImg) SrcAttr(e *Element) string { return src } -func (ui *UiImg) JawsRender(e *Element, w io.Writer, params []interface{}) { +func (ui *UiImg) JawsRender(e *Element, w io.Writer, params []interface{}) error { ui.parseGetter(e, ui.StringSetter) attrs := append(ui.parseParams(e, params), "src="+ui.SrcAttr(e)) - maybePanic(WriteHtmlInner(w, e.Jid(), "img", "", "", attrs...)) + return WriteHtmlInner(w, e.Jid(), "img", "", "", attrs...) } func (ui *UiImg) JawsUpdate(u *Element) { @@ -35,6 +34,6 @@ func NewUiImg(g StringSetter) *UiImg { } } -func (rq *Request) Img(imageSrc interface{}, params ...interface{}) template.HTML { +func (rq RequestWriter) Img(imageSrc interface{}, params ...interface{}) error { return rq.UI(NewUiImg(makeStringSetter(imageSrc)), params...) } diff --git a/uiimg_test.go b/uiimg_test.go index 59fcbf4..46da5e1 100644 --- a/uiimg_test.go +++ b/uiimg_test.go @@ -3,10 +3,10 @@ package jaws import ( "strconv" "testing" - "time" ) func TestRequest_Img(t *testing.T) { + th := newTestHelper(t) nextJid = 0 rq := newTestRequest() defer rq.Close() @@ -14,17 +14,17 @@ func TestRequest_Img(t *testing.T) { ts := newTestSetter("\"quoted.png\"") wantHtml := "" - if gotHtml := string(rq.Img(ts, "hidden")); gotHtml != wantHtml { + rq.Img(ts, "hidden") + if gotHtml := rq.BodyString(); gotHtml != wantHtml { t.Errorf("Request.Img() = %q, want %q", gotHtml, wantHtml) } - tmr := time.NewTimer(testTimeout) ts.Set("unquoted.jpg") rq.Dirty(ts) select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case s := <-rq.outCh: if s != "SAttr\tJid.1\t\"src\\n\\\"unquoted.jpg\\\"\"\n" { t.Error(strconv.Quote(s)) diff --git a/uiinputbool.go b/uiinputbool.go index be8110c..43114a9 100644 --- a/uiinputbool.go +++ b/uiinputbool.go @@ -12,7 +12,7 @@ type UiInputBool struct { BoolSetter } -func (ui *UiInputBool) renderBoolInput(e *Element, w io.Writer, htmltype string, params ...interface{}) { +func (ui *UiInputBool) renderBoolInput(e *Element, w io.Writer, htmltype string, params ...interface{}) error { ui.parseGetter(e, ui.BoolSetter) attrs := ui.parseParams(e, params) v := ui.JawsGetBool(e) @@ -20,7 +20,7 @@ func (ui *UiInputBool) renderBoolInput(e *Element, w io.Writer, htmltype string, if v { attrs = append(attrs, "checked") } - maybePanic(WriteHtmlInput(w, e.Jid(), htmltype, "", attrs...)) + return WriteHtmlInput(w, e.Jid(), htmltype, "", attrs...) } func (ui *UiInputBool) JawsUpdate(e *Element) { diff --git a/uiinputdate.go b/uiinputdate.go index 4b6ddcd..86bc687 100644 --- a/uiinputdate.go +++ b/uiinputdate.go @@ -16,11 +16,11 @@ func (ui *UiInputDate) str() string { return ui.Last.Load().(time.Time).Format(ISO8601) } -func (ui *UiInputDate) renderDateInput(e *Element, w io.Writer, jid Jid, htmltype string, params ...interface{}) { +func (ui *UiInputDate) renderDateInput(e *Element, w io.Writer, jid Jid, htmltype string, params ...interface{}) error { ui.parseGetter(e, ui.TimeSetter) attrs := ui.parseParams(e, params) ui.Last.Store(ui.JawsGetTime(e)) - maybePanic(WriteHtmlInput(w, e.Jid(), htmltype, ui.str(), attrs...)) + return WriteHtmlInput(w, e.Jid(), htmltype, ui.str(), attrs...) } func (ui *UiInputDate) JawsUpdate(e *Element) { diff --git a/uiinputfloat.go b/uiinputfloat.go index d596c2f..a3d3aee 100644 --- a/uiinputfloat.go +++ b/uiinputfloat.go @@ -16,11 +16,11 @@ func (ui *UiInputFloat) str() string { return strconv.FormatFloat(ui.Last.Load().(float64), 'f', -1, 64) } -func (ui *UiInputFloat) renderFloatInput(e *Element, w io.Writer, htmltype string, params ...interface{}) { +func (ui *UiInputFloat) renderFloatInput(e *Element, w io.Writer, htmltype string, params ...interface{}) error { ui.parseGetter(e, ui.FloatSetter) attrs := ui.parseParams(e, params) ui.Last.Store(ui.JawsGetFloat(e)) - maybePanic(WriteHtmlInput(w, e.Jid(), htmltype, ui.str(), attrs...)) + return WriteHtmlInput(w, e.Jid(), htmltype, ui.str(), attrs...) } func (ui *UiInputFloat) JawsUpdate(e *Element) { diff --git a/uiinputtext.go b/uiinputtext.go index d0f085e..3f13de7 100644 --- a/uiinputtext.go +++ b/uiinputtext.go @@ -11,12 +11,12 @@ type UiInputText struct { StringSetter } -func (ui *UiInputText) renderStringInput(e *Element, w io.Writer, htmltype string, params ...interface{}) { +func (ui *UiInputText) renderStringInput(e *Element, w io.Writer, htmltype string, params ...interface{}) error { ui.parseGetter(e, ui.StringSetter) attrs := ui.parseParams(e, params) v := ui.JawsGetString(e) ui.Last.Store(v) - maybePanic(WriteHtmlInput(w, e.Jid(), htmltype, v, attrs...)) + return WriteHtmlInput(w, e.Jid(), htmltype, v, attrs...) } func (ui *UiInputText) JawsUpdate(e *Element) { diff --git a/uilabel.go b/uilabel.go index feae95f..2577f3f 100644 --- a/uilabel.go +++ b/uilabel.go @@ -1,7 +1,6 @@ package jaws import ( - "html/template" "io" ) @@ -9,8 +8,8 @@ type UiLabel struct { UiHtmlInner } -func (ui *UiLabel) JawsRender(e *Element, w io.Writer, params []interface{}) { - ui.renderInner(e, w, "label", "", params) +func (ui *UiLabel) JawsRender(e *Element, w io.Writer, params []interface{}) error { + return ui.renderInner(e, w, "label", "", params) } func NewUiLabel(innerHtml HtmlGetter) *UiLabel { @@ -21,6 +20,6 @@ func NewUiLabel(innerHtml HtmlGetter) *UiLabel { } } -func (rq *Request) Label(innerHtml interface{}, params ...interface{}) template.HTML { +func (rq RequestWriter) Label(innerHtml interface{}, params ...interface{}) error { return rq.UI(NewUiLabel(makeHtmlGetter(innerHtml)), params...) } diff --git a/uilabel_test.go b/uilabel_test.go index 252a74d..0bc0fa8 100644 --- a/uilabel_test.go +++ b/uilabel_test.go @@ -9,7 +9,8 @@ func TestRequest_Label(t *testing.T) { rq := newTestRequest() defer rq.Close() want := `` - if got := string(rq.Label("inner")); got != want { + rq.Label("inner") + if got := rq.BodyString(); got != want { t.Errorf("Request.Label() = %q, want %q", got, want) } } diff --git a/uili.go b/uili.go index 1ec7a38..7800e7d 100644 --- a/uili.go +++ b/uili.go @@ -1,7 +1,6 @@ package jaws import ( - "html/template" "io" ) @@ -9,8 +8,8 @@ type UiLi struct { UiHtmlInner } -func (ui *UiLi) JawsRender(e *Element, w io.Writer, params []interface{}) { - ui.renderInner(e, w, "li", "", params) +func (ui *UiLi) JawsRender(e *Element, w io.Writer, params []interface{}) error { + return ui.renderInner(e, w, "li", "", params) } func NewUiLi(innerHtml HtmlGetter) *UiLi { @@ -21,6 +20,6 @@ func NewUiLi(innerHtml HtmlGetter) *UiLi { } } -func (rq *Request) Li(innerHtml interface{}, params ...interface{}) template.HTML { +func (rq RequestWriter) Li(innerHtml interface{}, params ...interface{}) error { return rq.UI(NewUiLi(makeHtmlGetter(innerHtml)), params...) } diff --git a/uili_test.go b/uili_test.go index 24f35e4..0c1c675 100644 --- a/uili_test.go +++ b/uili_test.go @@ -9,7 +9,8 @@ func TestRequest_Li(t *testing.T) { rq := newTestRequest() defer rq.Close() want := `
  • inner
  • ` - if got := string(rq.Li("inner")); got != want { + rq.Li("inner") + if got := rq.BodyString(); got != want { t.Errorf("Request.Li() = %q, want %q", got, want) } } diff --git a/uinumber.go b/uinumber.go index 46c4351..3aaffad 100644 --- a/uinumber.go +++ b/uinumber.go @@ -1,7 +1,6 @@ package jaws import ( - "html/template" "io" ) @@ -9,8 +8,8 @@ type UiNumber struct { UiInputFloat } -func (ui *UiNumber) JawsRender(e *Element, w io.Writer, params []interface{}) { - ui.renderFloatInput(e, w, "number", params...) +func (ui *UiNumber) JawsRender(e *Element, w io.Writer, params []interface{}) error { + return ui.renderFloatInput(e, w, "number", params...) } func NewUiNumber(g FloatSetter) *UiNumber { @@ -21,6 +20,6 @@ func NewUiNumber(g FloatSetter) *UiNumber { } } -func (rq *Request) Number(value interface{}, params ...interface{}) template.HTML { +func (rq RequestWriter) Number(value interface{}, params ...interface{}) error { return rq.UI(NewUiNumber(makeFloatSetter(value)), params...) } diff --git a/uinumber_test.go b/uinumber_test.go index c9a482f..3d3b1ff 100644 --- a/uinumber_test.go +++ b/uinumber_test.go @@ -4,29 +4,28 @@ import ( "errors" "fmt" "testing" - "time" "github.com/linkdata/jaws/what" ) func TestRequest_Number(t *testing.T) { - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() + th := newTestHelper(t) nextJid = 0 rq := newTestRequest() defer rq.Close() ts := newTestSetter(float64(1.2)) want := fmt.Sprintf(``, ts.Get()) - if got := string(rq.Number(ts)); got != want { + rq.Number(ts) + if got := rq.BodyString(); got != want { t.Errorf("Request.Number() = %q, want %q", got, want) } val := float64(2.3) rq.inCh <- wsMsg{Data: fmt.Sprint(val), Jid: 1, What: what.Input} select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case <-ts.setCalled: } if ts.Get() != val { @@ -42,8 +41,8 @@ func TestRequest_Number(t *testing.T) { ts.Set(val) rq.Dirty(ts) select { - case <-tmr.C: - t.Error("timeout waiting for Value") + case <-th.C: + th.Timeout() case s := <-rq.outCh: if s != fmt.Sprintf("Value\tJid.1\t\"%v\"\n", val) { t.Error("wrong Value") @@ -58,8 +57,8 @@ func TestRequest_Number(t *testing.T) { rq.inCh <- wsMsg{Data: "omg", Jid: 1, What: what.Input} select { - case <-tmr.C: - t.Error("timeout waiting for Alert") + case <-th.C: + th.Timeout() case s := <-rq.outCh: if s != "Alert\t\t\"danger\\nstrconv.ParseFloat: parsing "omg": invalid syntax\"\n" { t.Errorf("wrong Alert: %q", s) @@ -69,8 +68,8 @@ func TestRequest_Number(t *testing.T) { ts.err = errors.New("meh") rq.inCh <- wsMsg{Data: fmt.Sprint(val), Jid: 1, What: what.Input} select { - case <-tmr.C: - t.Error("timeout waiting for Alert") + case <-th.C: + th.Timeout() case s := <-rq.outCh: if s != "Alert\t\t\"danger\\nmeh\"\n" { t.Errorf("wrong Alert: %q", s) diff --git a/uioption.go b/uioption.go index 9921b47..d11bced 100644 --- a/uioption.go +++ b/uioption.go @@ -7,14 +7,14 @@ import ( type UiOption struct{ *NamedBool } -func (ui UiOption) JawsRender(e *Element, w io.Writer, params []interface{}) { +func (ui UiOption) JawsRender(e *Element, w io.Writer, params []interface{}) error { e.Tag(ui.NamedBool) attrs := parseParams(e, params) attrs = append(attrs, `value="`+html.EscapeString(ui.JawsGetString(e))+`"`) if ui.Checked() { attrs = append(attrs, "selected") } - maybePanic(WriteHtmlInner(w, e.Jid(), "option", "", ui.JawsGetHtml(e), attrs...)) + return WriteHtmlInner(w, e.Jid(), "option", "", ui.JawsGetHtml(e), attrs...) } func (ui UiOption) JawsUpdate(e *Element) { diff --git a/uioption_test.go b/uioption_test.go index a0bc792..4f68351 100644 --- a/uioption_test.go +++ b/uioption_test.go @@ -3,12 +3,10 @@ package jaws import ( "strings" "testing" - "time" ) func TestUiOption(t *testing.T) { - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() + th := newTestHelper(t) nextJid = 0 rq := newTestRequest() defer rq.Close() @@ -19,7 +17,9 @@ func TestUiOption(t *testing.T) { ui := UiOption{nb} elem := rq.NewElement(ui) var sb strings.Builder - ui.JawsRender(elem, &sb, []any{"hidden"}) + if err := ui.JawsRender(elem, &sb, []any{"hidden"}); err != nil { + t.Fatal(err) + } wantHtml := "" if gotHtml := sb.String(); gotHtml != wantHtml { t.Errorf("got %q, want %q", gotHtml, wantHtml) @@ -28,8 +28,8 @@ func TestUiOption(t *testing.T) { nb.Set(false) rq.Dirty(nb) select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case s := <-rq.outCh: if s != "RAttr\tJid.1\t\"selected\"\n" { t.Errorf("%q", s) @@ -39,8 +39,8 @@ func TestUiOption(t *testing.T) { nb.Set(true) rq.Dirty(nb) select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case s := <-rq.outCh: if s != "SAttr\tJid.1\t\"selected\\n\"\n" { t.Errorf("%q", s) diff --git a/uipassword.go b/uipassword.go index c7ce90d..9a164a3 100644 --- a/uipassword.go +++ b/uipassword.go @@ -1,7 +1,6 @@ package jaws import ( - "html/template" "io" ) @@ -9,8 +8,8 @@ type UiPassword struct { UiInputText } -func (ui *UiPassword) JawsRender(e *Element, w io.Writer, params []interface{}) { - ui.renderStringInput(e, w, "password", params...) +func (ui *UiPassword) JawsRender(e *Element, w io.Writer, params []interface{}) error { + return ui.renderStringInput(e, w, "password", params...) } func NewUiPassword(g StringSetter) *UiPassword { @@ -21,6 +20,6 @@ func NewUiPassword(g StringSetter) *UiPassword { } } -func (rq *Request) Password(value interface{}, params ...interface{}) template.HTML { +func (rq RequestWriter) Password(value interface{}, params ...interface{}) error { return rq.UI(NewUiPassword(makeStringSetter(value)), params...) } diff --git a/uipassword_test.go b/uipassword_test.go index 18c733a..f8b9ea3 100644 --- a/uipassword_test.go +++ b/uipassword_test.go @@ -10,7 +10,8 @@ func TestRequest_Password(t *testing.T) { defer rq.Close() ts := newTestSetter("") want := `` - if got := string(rq.Password(ts)); got != want { + rq.Password(ts) + if got := rq.BodyString(); got != want { t.Errorf("Request.Password() = %q, want %q", got, want) } } diff --git a/uiradio.go b/uiradio.go index f144059..69a034a 100644 --- a/uiradio.go +++ b/uiradio.go @@ -1,7 +1,6 @@ package jaws import ( - "html/template" "io" ) @@ -9,8 +8,8 @@ type UiRadio struct { UiInputBool } -func (ui *UiRadio) JawsRender(e *Element, w io.Writer, params []interface{}) { - ui.renderBoolInput(e, w, "radio", params...) +func (ui *UiRadio) JawsRender(e *Element, w io.Writer, params []interface{}) error { + return ui.renderBoolInput(e, w, "radio", params...) } func NewUiRadio(vp BoolSetter) *UiRadio { @@ -21,6 +20,6 @@ func NewUiRadio(vp BoolSetter) *UiRadio { } } -func (rq *Request) Radio(value interface{}, params ...interface{}) template.HTML { +func (rq RequestWriter) Radio(value interface{}, params ...interface{}) error { return rq.UI(NewUiRadio(makeBoolSetter(value)), params...) } diff --git a/uiradio_test.go b/uiradio_test.go index ddc6bed..1e0c715 100644 --- a/uiradio_test.go +++ b/uiradio_test.go @@ -11,7 +11,8 @@ func TestRequest_Radio(t *testing.T) { ts := newTestSetter(true) want := `` - if got := string(rq.Radio(ts)); got != want { + rq.Radio(ts) + if got := rq.BodyString(); got != want { t.Errorf("Request.Radio() = %q, want %q", got, want) } } diff --git a/uiradiogroup.go b/uiradiogroup.go index 561c170..114a735 100644 --- a/uiradiogroup.go +++ b/uiradiogroup.go @@ -29,7 +29,7 @@ func (rq *Request) RadioGroup(nba *NamedBoolArray) (rel []RadioElement) { // Radio renders a HTML input element of type 'radio'. func (re RadioElement) Radio(params ...interface{}) template.HTML { var sb strings.Builder - re.radio.Render(&sb, append(params, re.nameAttr)) + maybePanic(re.radio.Render(&sb, append(params, re.nameAttr))) return template.HTML(sb.String()) // #nosec G203 } @@ -37,6 +37,6 @@ func (re RadioElement) Radio(params ...interface{}) template.HTML { func (re *RadioElement) Label(params ...interface{}) template.HTML { var sb strings.Builder forAttr := string(re.radio.jid.AppendQuote([]byte("for="))) - re.label.Render(&sb, append(params, forAttr)) + maybePanic(re.label.Render(&sb, append(params, forAttr))) return template.HTML(sb.String()) // #nosec G203 } diff --git a/uirange.go b/uirange.go index a0fe67f..ca759ce 100644 --- a/uirange.go +++ b/uirange.go @@ -1,7 +1,6 @@ package jaws import ( - "html/template" "io" ) @@ -9,8 +8,8 @@ type UiRange struct { UiInputFloat } -func (ui *UiRange) JawsRender(e *Element, w io.Writer, params []interface{}) { - ui.renderFloatInput(e, w, "range", params...) +func (ui *UiRange) JawsRender(e *Element, w io.Writer, params []interface{}) error { + return ui.renderFloatInput(e, w, "range", params...) } func NewUiRange(g FloatSetter) *UiRange { @@ -21,6 +20,6 @@ func NewUiRange(g FloatSetter) *UiRange { } } -func (rq *Request) Range(value interface{}, params ...interface{}) template.HTML { +func (rq RequestWriter) Range(value interface{}, params ...interface{}) error { return rq.UI(NewUiRange(makeFloatSetter(value)), params...) } diff --git a/uirange_test.go b/uirange_test.go index 2422bd0..d62b54a 100644 --- a/uirange_test.go +++ b/uirange_test.go @@ -3,27 +3,26 @@ package jaws import ( "errors" "testing" - "time" "github.com/linkdata/jaws/what" ) func TestRequest_Range(t *testing.T) { + th := newTestHelper(t) nextJid = 0 rq := newTestRequest() defer rq.Close() ts := newTestSetter(float64(1)) want := `` - if got := string(rq.Range(ts)); got != want { + rq.Range(ts) + if got := rq.BodyString(); got != want { t.Errorf("Request.Range() = %q, want %q", got, want) } - tmr := time.NewTimer(testTimeout) rq.inCh <- wsMsg{Data: "2.1", Jid: 1, What: what.Input} - defer tmr.Stop() select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case <-ts.setCalled: } if ts.Get() != 2.1 { @@ -37,8 +36,8 @@ func TestRequest_Range(t *testing.T) { ts.Set(2.3) rq.Dirty(ts) select { - case <-tmr.C: - t.Error("timeout waiting for Value") + case <-th.C: + th.Timeout() case s := <-rq.outCh: if s != "Value\tJid.1\t\"2.3\"\n" { t.Error(s) @@ -54,8 +53,8 @@ func TestRequest_Range(t *testing.T) { ts.err = errors.New("meh") rq.inCh <- wsMsg{Data: "3.4", Jid: 1, What: what.Input} select { - case <-tmr.C: - t.Error("timeout waiting for Alert") + case <-th.C: + th.Timeout() case s := <-rq.outCh: if s != "Alert\t\t\"danger\\nmeh\"\n" { t.Errorf("wrong Alert: %q", s) diff --git a/uiselect.go b/uiselect.go index 08cc4de..dfeb0b6 100644 --- a/uiselect.go +++ b/uiselect.go @@ -1,7 +1,6 @@ package jaws import ( - "html/template" "io" "github.com/linkdata/jaws/what" @@ -19,8 +18,8 @@ func NewUiSelect(sh SelectHandler) *UiSelect { } } -func (ui *UiSelect) JawsRender(e *Element, w io.Writer, params []interface{}) { - ui.renderContainer(e, w, "select", params) +func (ui *UiSelect) JawsRender(e *Element, w io.Writer, params []interface{}) error { + return ui.renderContainer(e, w, "select", params) } func (ui *UiSelect) JawsUpdate(e *Element) { @@ -39,6 +38,6 @@ func (ui *UiSelect) JawsEvent(e *Element, wht what.What, val string) (err error) return ui.UiHtml.JawsEvent(e, wht, val) } -func (rq *Request) Select(sh SelectHandler, params ...interface{}) template.HTML { +func (rq RequestWriter) Select(sh SelectHandler, params ...interface{}) error { return rq.UI(NewUiSelect(sh), params...) } diff --git a/uiselect_test.go b/uiselect_test.go index 76e38dd..b3caf9d 100644 --- a/uiselect_test.go +++ b/uiselect_test.go @@ -3,7 +3,6 @@ package jaws import ( "errors" "testing" - "time" "github.com/linkdata/deadlock" "github.com/linkdata/jaws/what" @@ -31,6 +30,7 @@ func (ts *testNamedBoolArray) JawsSetString(e *Element, val string) (err error) } func TestRequest_Select(t *testing.T) { + th := newTestHelper(t) nextJid = 0 rq := newTestRequest() defer rq.Close() @@ -44,7 +44,8 @@ func TestRequest_Select(t *testing.T) { a.Set("1", true) wantHtml := "" - if gotHtml := string(rq.Select(a, "disabled")); gotHtml != wantHtml { + rq.Select(a, "disabled") + if gotHtml := rq.BodyString(); gotHtml != wantHtml { t.Errorf("Request.Select() = %q, want %q", gotHtml, wantHtml) } @@ -55,12 +56,10 @@ func TestRequest_Select(t *testing.T) { t.Error("2 is checked") } - tmr := time.NewTimer(testTimeout) rq.inCh <- wsMsg{Data: "2", Jid: 1, What: what.Input} - defer tmr.Stop() select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case <-a.setCalled: } @@ -81,8 +80,8 @@ func TestRequest_Select(t *testing.T) { rq.Dirty(a) select { - case <-tmr.C: - t.Error("timeout waiting for Value") + case <-th.C: + th.Timeout() case s := <-rq.outCh: if s != "Value\tJid.1\t\"\"\n" { t.Error("wrong Value") @@ -99,8 +98,8 @@ func TestRequest_Select(t *testing.T) { a.err = errors.New("meh") rq.inCh <- wsMsg{Data: "1", Jid: 1, What: what.Input} select { - case <-tmr.C: - t.Error("timeout waiting for Alert") + case <-th.C: + th.Timeout() case s := <-rq.outCh: if s != "Alert\t\t\"danger\\nmeh\"\n" { t.Errorf("wrong Alert: %q", s) diff --git a/uispan.go b/uispan.go index 093c6c4..538bce6 100644 --- a/uispan.go +++ b/uispan.go @@ -1,7 +1,6 @@ package jaws import ( - "html/template" "io" ) @@ -9,8 +8,8 @@ type UiSpan struct { UiHtmlInner } -func (ui *UiSpan) JawsRender(e *Element, w io.Writer, params []interface{}) { - ui.renderInner(e, w, "span", "", params) +func (ui *UiSpan) JawsRender(e *Element, w io.Writer, params []interface{}) error { + return ui.renderInner(e, w, "span", "", params) } func NewUiSpan(innerHtml HtmlGetter) *UiSpan { @@ -21,6 +20,6 @@ func NewUiSpan(innerHtml HtmlGetter) *UiSpan { } } -func (rq *Request) Span(innerHtml interface{}, params ...interface{}) template.HTML { +func (rq RequestWriter) Span(innerHtml interface{}, params ...interface{}) error { return rq.UI(NewUiSpan(makeHtmlGetter(innerHtml)), params...) } diff --git a/uispan_test.go b/uispan_test.go index 64733e6..41d8ca6 100644 --- a/uispan_test.go +++ b/uispan_test.go @@ -9,7 +9,8 @@ func TestRequest_Span(t *testing.T) { rq := newTestRequest() defer rq.Close() want := `inner` - if got := string(rq.Span("inner")); got != want { + rq.Span("inner") + if got := rq.BodyString(); got != want { t.Errorf("Request.Span() = %q, want %q", got, want) } } diff --git a/uitbody.go b/uitbody.go index 8631682..3953be7 100644 --- a/uitbody.go +++ b/uitbody.go @@ -1,7 +1,6 @@ package jaws import ( - "html/template" "io" ) @@ -17,10 +16,10 @@ func NewUiTbody(c Container) *UiTbody { } } -func (ui *UiTbody) JawsRender(e *Element, w io.Writer, params []interface{}) { - ui.renderContainer(e, w, "tbody", params) +func (ui *UiTbody) JawsRender(e *Element, w io.Writer, params []interface{}) error { + return ui.renderContainer(e, w, "tbody", params) } -func (rq *Request) Tbody(c Container, params ...interface{}) template.HTML { +func (rq RequestWriter) Tbody(c Container, params ...interface{}) error { return rq.UI(NewUiTbody(c), params...) } diff --git a/uitbody_test.go b/uitbody_test.go index c94a73e..cbd2db7 100644 --- a/uitbody_test.go +++ b/uitbody_test.go @@ -9,7 +9,8 @@ func TestRequest_Tbody(t *testing.T) { rq := newTestRequest() defer rq.Close() want := `` - if got := string(rq.Tbody(&testContainer{})); got != want { + rq.Tbody(&testContainer{}) + if got := rq.BodyString(); got != want { t.Errorf("Request.Span() = %q, want %q", got, want) } } diff --git a/uitd.go b/uitd.go index b815566..69c6a95 100644 --- a/uitd.go +++ b/uitd.go @@ -1,7 +1,6 @@ package jaws import ( - "html/template" "io" ) @@ -9,8 +8,8 @@ type UiTd struct { UiHtmlInner } -func (ui *UiTd) JawsRender(e *Element, w io.Writer, params []interface{}) { - ui.renderInner(e, w, "td", "", params) +func (ui *UiTd) JawsRender(e *Element, w io.Writer, params []interface{}) error { + return ui.renderInner(e, w, "td", "", params) } func NewUiTd(innerHtml HtmlGetter) *UiTd { @@ -21,6 +20,6 @@ func NewUiTd(innerHtml HtmlGetter) *UiTd { } } -func (rq *Request) Td(innerHtml interface{}, params ...interface{}) template.HTML { +func (rq RequestWriter) Td(innerHtml interface{}, params ...interface{}) error { return rq.UI(NewUiTd(makeHtmlGetter(innerHtml)), params...) } diff --git a/uitd_test.go b/uitd_test.go index 8daa651..0bc350b 100644 --- a/uitd_test.go +++ b/uitd_test.go @@ -9,7 +9,8 @@ func TestRequest_Td(t *testing.T) { rq := newTestRequest() defer rq.Close() want := `inner` - if got := string(rq.Td("inner")); got != want { + rq.Td("inner") + if got := rq.BodyString(); got != want { t.Errorf("Request.Td() = %q, want %q", got, want) } } diff --git a/uitemplate.go b/uitemplate.go index c887b0a..a07e2fe 100644 --- a/uitemplate.go +++ b/uitemplate.go @@ -1,9 +1,5 @@ package jaws -import ( - "html/template" -) - type UiTemplate struct { Template } @@ -17,6 +13,6 @@ func NewUiTemplate(t Template) UiTemplate { // // The templ argument can either be a string, in which case Jaws.Template.Lookup() will // be used to resolve it. Or it can be a *template.Template directly. -func (rq *Request) Template(templ, dot interface{}, params ...interface{}) template.HTML { +func (rq RequestWriter) Template(templ, dot interface{}, params ...interface{}) error { return rq.UI(NewUiTemplate(rq.MakeTemplate(templ, dot)), params...) } diff --git a/uitemplate_test.go b/uitemplate_test.go index 6a78984..1af920c 100644 --- a/uitemplate_test.go +++ b/uitemplate_test.go @@ -11,7 +11,7 @@ import ( ) func TestRequest_Template(t *testing.T) { - is := testHelper{t} + is := newTestHelper(t) type args struct { templ interface{} dot interface{} @@ -64,13 +64,14 @@ func TestRequest_Template(t *testing.T) { t.Fail() }() } - got := rq.Template(tt.args.templ, tt.args.dot, tt.args.params...) + rq.Template(tt.args.templ, tt.args.dot, tt.args.params...) + got := rq.BodyHtml() is.Equal(len(rq.elems), 1) elem := rq.elems[0] if tt.errtxt != "" { t.Fail() } - gotTags := elem.TagsOf(elem) + gotTags := elem.Request().TagsOf(elem) is.Equal(len(tt.tags), len(gotTags)) for _, tag := range tt.tags { is.True(elem.HasTag(tag)) @@ -96,11 +97,11 @@ func (td *templateDot) JawsClick(e *Element, name string) error { var _ ClickHandler = &templateDot{} func TestRequest_Template_Event(t *testing.T) { - is := testHelper{t} + is := newTestHelper(t) rq := newTestRequest() defer rq.Close() dot := &templateDot{clickedCh: make(chan struct{})} - _ = rq.Template("testtemplate", dot) + rq.Template("testtemplate", dot) rq.jw.Broadcast(Message{ Dest: dot, What: what.Update, diff --git a/uitext.go b/uitext.go index e654b33..54ac17c 100644 --- a/uitext.go +++ b/uitext.go @@ -1,7 +1,6 @@ package jaws import ( - "html/template" "io" ) @@ -9,8 +8,8 @@ type UiText struct { UiInputText } -func (ui *UiText) JawsRender(e *Element, w io.Writer, params []interface{}) { - ui.renderStringInput(e, w, "text", params...) +func (ui *UiText) JawsRender(e *Element, w io.Writer, params []interface{}) error { + return ui.renderStringInput(e, w, "text", params...) } func NewUiText(vp StringSetter) (ui *UiText) { @@ -21,6 +20,6 @@ func NewUiText(vp StringSetter) (ui *UiText) { } } -func (rq *Request) Text(value interface{}, params ...interface{}) template.HTML { +func (rq RequestWriter) Text(value interface{}, params ...interface{}) error { return rq.UI(NewUiText(makeStringSetter(value)), params...) } diff --git a/uitext_test.go b/uitext_test.go index 629a2f4..ce886e7 100644 --- a/uitext_test.go +++ b/uitext_test.go @@ -3,27 +3,26 @@ package jaws import ( "errors" "testing" - "time" "github.com/linkdata/jaws/what" ) func TestRequest_Text(t *testing.T) { + th := newTestHelper(t) nextJid = 0 rq := newTestRequest() defer rq.Close() ss := newTestSetter("foo") want := `` - if got := string(rq.Text(ss)); got != want { + rq.Text(ss) + if got := rq.BodyString(); got != want { t.Errorf("Request.Text() = %q, want %q", got, want) } - tmr := time.NewTimer(testTimeout) rq.inCh <- wsMsg{Data: "bar", Jid: 1, What: what.Input} - defer tmr.Stop() select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case <-ss.setCalled: } if ss.Get() != "bar" { @@ -37,8 +36,8 @@ func TestRequest_Text(t *testing.T) { ss.Set("quux") rq.Dirty(ss) select { - case <-tmr.C: - t.Error("timeout waiting for Value") + case <-th.C: + th.Timeout() case s := <-rq.outCh: if s != "Value\tJid.1\t\"quux\"\n" { t.Error("wrong Value") @@ -54,8 +53,8 @@ func TestRequest_Text(t *testing.T) { ss.err = errors.New("meh") rq.inCh <- wsMsg{Data: "omg", Jid: 1, What: what.Input} select { - case <-tmr.C: - t.Error("timeout waiting for Alert") + case <-th.C: + th.Timeout() case s := <-rq.outCh: if s != "Alert\t\t\"danger\\nmeh\"\n" { t.Errorf("wrong Alert: %q", s) diff --git a/uitextarea.go b/uitextarea.go index d8cf02a..03538ea 100644 --- a/uitextarea.go +++ b/uitextarea.go @@ -9,10 +9,10 @@ type UiTextarea struct { UiInputText } -func (ui *UiTextarea) JawsRender(e *Element, w io.Writer, params []interface{}) { +func (ui *UiTextarea) JawsRender(e *Element, w io.Writer, params []interface{}) error { ui.parseGetter(e, ui.StringSetter) attrs := ui.parseParams(e, params) - maybePanic(WriteHtmlInner(w, e.Jid(), "textarea", "", template.HTML(ui.JawsGetString(e)), attrs...)) // #nosec G203 + return WriteHtmlInner(w, e.Jid(), "textarea", "", template.HTML(ui.JawsGetString(e)), attrs...) // #nosec G203 } func (ui *UiTextarea) JawsUpdate(e *Element) { @@ -27,6 +27,6 @@ func NewUiTextarea(g StringSetter) (ui *UiTextarea) { } } -func (rq *Request) Textarea(value interface{}, params ...interface{}) template.HTML { +func (rq RequestWriter) Textarea(value interface{}, params ...interface{}) error { return rq.UI(NewUiTextarea(makeStringSetter(value)), params...) } diff --git a/uitextarea_test.go b/uitextarea_test.go index 90254d3..f2b0af1 100644 --- a/uitextarea_test.go +++ b/uitextarea_test.go @@ -2,27 +2,26 @@ package jaws import ( "testing" - "time" "github.com/linkdata/jaws/what" ) func TestRequest_Textarea(t *testing.T) { + th := newTestHelper(t) nextJid = 0 rq := newTestRequest() defer rq.Close() ss := newTestSetter("foo") want := `` - if got := string(rq.Textarea(ss)); got != want { + rq.Textarea(ss) + if got := rq.BodyString(); got != want { t.Errorf("Request.Textarea() = %q, want %q", got, want) } rq.inCh <- wsMsg{Data: "bar", Jid: 1, What: what.Input} - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() select { - case <-tmr.C: - t.Fail() + case <-th.C: + th.Timeout() case <-ss.setCalled: } if ss.Get() != "bar" { @@ -36,8 +35,8 @@ func TestRequest_Textarea(t *testing.T) { ss.Set("quux") rq.Dirty(ss) select { - case <-tmr.C: - t.Fail() + case <-th.C: + th.Timeout() case s := <-rq.outCh: if s != "Inner\tJid.1\t\"quux\"\n" { t.Fail() diff --git a/uitr.go b/uitr.go index 86d78e1..e422903 100644 --- a/uitr.go +++ b/uitr.go @@ -1,7 +1,6 @@ package jaws import ( - "html/template" "io" ) @@ -9,8 +8,8 @@ type UiTr struct { UiHtmlInner } -func (ui *UiTr) JawsRender(e *Element, w io.Writer, params []interface{}) { - ui.renderInner(e, w, "tr", "", params) +func (ui *UiTr) JawsRender(e *Element, w io.Writer, params []interface{}) error { + return ui.renderInner(e, w, "tr", "", params) } func NewUiTr(innerHtml HtmlGetter) *UiTr { @@ -21,6 +20,6 @@ func NewUiTr(innerHtml HtmlGetter) *UiTr { } } -func (rq *Request) Tr(innerHtml interface{}, params ...interface{}) template.HTML { +func (rq RequestWriter) Tr(innerHtml interface{}, params ...interface{}) error { return rq.UI(NewUiTr(makeHtmlGetter(innerHtml)), params...) } diff --git a/uitr_test.go b/uitr_test.go index 7fdfcf6..3e87fad 100644 --- a/uitr_test.go +++ b/uitr_test.go @@ -9,7 +9,8 @@ func TestRequest_Tr(t *testing.T) { rq := newTestRequest() defer rq.Close() want := `inner` - if got := string(rq.Tr("inner")); got != want { + rq.Tr("inner") + if got := rq.BodyString(); got != want { t.Errorf("Request.Tr() = %q, want %q", got, want) } } diff --git a/uiwrapcontainer.go b/uiwrapcontainer.go index 9ff04d5..7842d3c 100644 --- a/uiwrapcontainer.go +++ b/uiwrapcontainer.go @@ -16,7 +16,7 @@ type uiWrapContainer struct { contents []*Element } -func (ui *uiWrapContainer) renderContainer(e *Element, w io.Writer, outerhtmltag string, params []interface{}) { +func (ui *uiWrapContainer) renderContainer(e *Element, w io.Writer, outerhtmltag string, params []interface{}) error { ui.parseGetter(e, ui.Container) attrs := ui.parseParams(e, params) b := e.jid.AppendStartTagAttr(nil, outerhtmltag) @@ -27,18 +27,22 @@ func (ui *uiWrapContainer) renderContainer(e *Element, w io.Writer, outerhtmltag b = append(b, '>') _, err := w.Write(b) if err == nil { - for _, cui := range ui.Container.JawsContains(e.Request) { - elem := e.Request.NewElement(cui) - ui.contents = append(ui.contents, elem) - elem.Render(w, nil) + for _, cui := range ui.Container.JawsContains(e.Request()) { + if err == nil { + elem := e.Request().NewElement(cui) + ui.contents = append(ui.contents, elem) + err = elem.Render(w, nil) + } } b = b[:0] b = append(b, "') - _, err = w.Write(b) + if err == nil { + _, err = w.Write(b) + } } - maybePanic(err) + return err } func (ui *uiWrapContainer) JawsUpdate(e *Element) { @@ -47,7 +51,7 @@ func (ui *uiWrapContainer) JawsUpdate(e *Element) { oldMap := make(map[UI]*Element) newMap := make(map[UI]struct{}) - newContents := ui.Container.JawsContains(e.Request) + newContents := ui.Container.JawsContains(e.Request()) for _, t := range newContents { newMap[t] = struct{}{} } @@ -65,7 +69,7 @@ func (ui *uiWrapContainer) JawsUpdate(e *Element) { for _, cui := range newContents { var elem *Element if elem = oldMap[cui]; elem == nil { - elem = e.Request.NewElement(cui) + elem = e.Request().NewElement(cui) toAppend = append(toAppend, elem) } ui.contents = append(ui.contents, elem) @@ -75,12 +79,12 @@ func (ui *uiWrapContainer) JawsUpdate(e *Element) { for _, elem := range toRemove { e.Remove(elem.jid.String()) - e.deleteElement(elem) + e.Request().deleteElement(elem) } for _, elem := range toAppend { var sb strings.Builder - elem.ui.JawsRender(elem, &sb, nil) + maybePanic(elem.ui.JawsRender(elem, &sb, nil)) e.Append(template.HTML(sb.String())) // #nosec G203 } diff --git a/with.go b/with.go index 39c92a9..c7681f7 100644 --- a/with.go +++ b/with.go @@ -6,6 +6,7 @@ import ( type With struct { *Element + RequestWriter Dot interface{} Attrs template.HTMLAttr } diff --git a/ws.go b/ws.go index ac4c59b..e0d821d 100644 --- a/ws.go +++ b/ws.go @@ -7,31 +7,46 @@ import ( "nhooyr.io/websocket" ) +func (rq *Request) startServe() (ok bool) { + rq.mu.Lock() + if ok = !rq.running && rq.claimed; ok { + rq.running = true + } + rq.mu.Unlock() + return +} + +func (rq *Request) stopServe() { + rq.mu.Lock() + rq.running = false + rq.mu.Unlock() +} + // ServeHTTP implements http.HanderFunc. // -// Assumes UseRequest() have been successfully called for the Request. +// Requires UseRequest() have been successfully called for the Request. func (rq *Request) ServeHTTP(w http.ResponseWriter, r *http.Request) { - ws, err := websocket.Accept(w, r, nil) - if err == nil { - if err = rq.onConnect(); err == nil { - incomingMsgCh := make(chan wsMsg) - broadcastMsgCh := rq.Jaws.subscribe(rq, 1) - outboundCh := make(chan string, cap(broadcastMsgCh)) - go wsReader(rq.ctx, rq.cancelFn, rq.Jaws.Done(), incomingMsgCh, ws) // closes incomingMsgCh - go wsWriter(rq.ctx, rq.cancelFn, rq.Jaws.Done(), outboundCh, ws) // calls ws.Close() - rq.process(broadcastMsgCh, incomingMsgCh, outboundCh) // unsubscribes broadcastMsgCh, closes outboundMsgCh - } else { - defer ws.Close(websocket.StatusNormalClosure, err.Error()) - var msg wsMsg - msg.FillAlert(rq.Jaws.Log(err)) - _ = ws.Write(r.Context(), websocket.MessageText, msg.Append(nil)) + if rq.startServe() { + defer rq.stopServe() + ws, err := websocket.Accept(w, r, nil) + if err == nil { + if err = rq.onConnect(); err == nil { + incomingMsgCh := make(chan wsMsg) + broadcastMsgCh := rq.Jaws.subscribe(rq, 4+len(rq.elems)*4) + outboundCh := make(chan string, cap(broadcastMsgCh)) + go wsReader(rq.ctx, rq.cancelFn, rq.Jaws.Done(), incomingMsgCh, ws) // closes incomingMsgCh + go wsWriter(rq.ctx, rq.cancelFn, rq.Jaws.Done(), outboundCh, ws) // calls ws.Close() + rq.process(broadcastMsgCh, incomingMsgCh, outboundCh) // unsubscribes broadcastMsgCh, closes outboundMsgCh + } else { + defer ws.Close(websocket.StatusNormalClosure, err.Error()) + var msg wsMsg + msg.FillAlert(rq.Jaws.Log(err)) + _ = ws.Write(r.Context(), websocket.MessageText, msg.Append(nil)) + } } - } - if err != nil { rq.cancel(err) - _ = rq.Jaws.Log(err) + rq.Jaws.recycle(rq) } - rq.recycle() } // wsReader reads websocket text messages, parses them and sends them on incomingMsgCh. diff --git a/ws_test.go b/ws_test.go index c888939..e0576c9 100644 --- a/ws_test.go +++ b/ws_test.go @@ -21,6 +21,7 @@ type testServer struct { ctx context.Context cancel context.CancelFunc hr *http.Request + rr *httptest.ResponseRecorder rq *Request sess *Session srv *httptest.Server @@ -30,8 +31,9 @@ type testServer struct { func newTestServer() (ts *testServer) { jw := New() ctx, cancel := context.WithTimeout(context.Background(), time.Hour) + rr := httptest.NewRecorder() hr := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx) - sess := jw.NewSession(nil, hr) + sess := jw.NewSession(rr, hr) rq := jw.NewRequest(hr) if rq != jw.UseRequest(rq.JawsKey, hr) { panic("UseRequest failed") @@ -41,6 +43,7 @@ func newTestServer() (ts *testServer) { ctx: ctx, cancel: cancel, hr: hr, + rr: rr, rq: rq, sess: sess, connectedCh: make(chan struct{}), @@ -85,10 +88,11 @@ func (ts *testServer) Close() { func TestWS_UpgradeRequired(t *testing.T) { jw := New() defer jw.Close() - rq := jw.NewRequest(nil) - - req := httptest.NewRequest("", "/jaws/"+rq.JawsKeyString(), nil) w := httptest.NewRecorder() + hr := httptest.NewRequest("", "/", nil) + rq := jw.NewRequest(hr) + jw.UseRequest(rq.JawsKey, hr) + req := httptest.NewRequest("", "/jaws/"+rq.JawsKeyString(), nil) rq.ServeHTTP(w, req) if w.Code != http.StatusUpgradeRequired { t.Error(w.Code) @@ -124,8 +128,7 @@ func TestWS_ConnectFnFails(t *testing.T) { } func TestWS_NormalExchange(t *testing.T) { - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() + th := newTestHelper(t) ts := newTestServer() defer ts.Close() @@ -156,8 +159,8 @@ func TestWS_NormalExchange(t *testing.T) { t.Fatal(err) } select { - case <-tmr.C: - t.Fatal("timeout") + case <-th.C: + th.Timeout() case <-gotCallCh: } @@ -176,8 +179,7 @@ func TestWS_NormalExchange(t *testing.T) { } func TestReader_RespectsContextDone(t *testing.T) { - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() + th := newTestHelper(t) ts := newTestServer() defer ts.Close() @@ -206,15 +208,14 @@ func TestReader_RespectsContextDone(t *testing.T) { ts.cancel() select { - case <-tmr.C: - t.Error("did not unblock") + case <-th.C: + th.Timeout() case <-doneCh: } } func TestReader_RespectsJawsDone(t *testing.T) { - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() + th := newTestHelper(t) ts := newTestServer() defer ts.Close() @@ -238,15 +239,14 @@ func TestReader_RespectsJawsDone(t *testing.T) { } select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case <-doneCh: } } func TestWriter_SendsThePayload(t *testing.T) { - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() + th := newTestHelper(t) ts := newTestServer() defer ts.Close() @@ -268,14 +268,14 @@ func TestWriter_SendsThePayload(t *testing.T) { msg := wsMsg{Jid: Jid(1234)} select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case outCh <- msg.Format(): } select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case <-doneCh: } @@ -290,15 +290,14 @@ func TestWriter_SendsThePayload(t *testing.T) { } select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case <-client.CloseRead(ts.ctx).Done(): } } func TestWriter_RespectsContext(t *testing.T) { - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() + th := newTestHelper(t) ts := newTestServer() defer ts.Close() @@ -316,16 +315,15 @@ func TestWriter_RespectsContext(t *testing.T) { ts.cancel() select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case <-doneCh: return } } func TestWriter_RespectsJawsDone(t *testing.T) { - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() + th := newTestHelper(t) ts := newTestServer() defer ts.Close() @@ -343,15 +341,14 @@ func TestWriter_RespectsJawsDone(t *testing.T) { ts.jw.Close() select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case <-doneCh: } } func TestWriter_RespectsOutboundClosed(t *testing.T) { - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() + th := newTestHelper(t) ts := newTestServer() defer ts.Close() @@ -368,8 +365,8 @@ func TestWriter_RespectsOutboundClosed(t *testing.T) { close(outCh) select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case <-doneCh: } @@ -379,8 +376,7 @@ func TestWriter_RespectsOutboundClosed(t *testing.T) { } func TestWriter_ReportsError(t *testing.T) { - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() + th := newTestHelper(t) ts := newTestServer() defer ts.Close() @@ -397,14 +393,14 @@ func TestWriter_ReportsError(t *testing.T) { msg := wsMsg{Jid: Jid(1234)} select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case outCh <- msg.Format(): } select { - case <-tmr.C: - t.Error("timeout") + case <-th.C: + th.Timeout() case <-doneCh: } @@ -415,8 +411,7 @@ func TestWriter_ReportsError(t *testing.T) { } func TestReader_ReportsError(t *testing.T) { - tmr := time.NewTimer(testTimeout) - defer tmr.Stop() + th := newTestHelper(t) ts := newTestServer() defer ts.Close() @@ -438,8 +433,8 @@ func TestReader_ReportsError(t *testing.T) { } select { - case <-tmr.C: - t.Fatal("timeout") + case <-th.C: + th.Timeout() case <-doneCh: }