Script, GH action to check that all go files compile, and any tests pass. (#16)

This commit is contained in:
Jeff Winkler
2025-11-15 13:47:49 -05:00
committed by GitHub
parent 762635d7d9
commit f7b5b24dd5
8 changed files with 160 additions and 150 deletions

29
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: CI
on:
push:
branches:
- '**'
pull_request:
branches:
- '**'
permissions:
contents: read
jobs:
build-test:
name: Build and Test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.25.4'
cache: true
- name: Run CI checks
run: bash ./ci-check.sh

10
.gitignore vendored
View File

@@ -36,3 +36,13 @@ go.work.sum
# Air artifacts
*tmp/
# binaries
internal/examples/chatroom/chatroom
internal/examples/counter/counter
internal/examples/countercomp/countercomp
internal/examples/greeter/greeter
internal/examples/livereload/livereload
internal/examples/plugins/plugins
internal/examples/realtimechart/realtimechart
internal/examples/picocss/picocss

56
ci-check.sh Executable file
View File

@@ -0,0 +1,56 @@
#!/usr/bin/env bash
set -e
set -u
set -o pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT"
echo "== CI: Format code =="
go fmt ./...
echo "OK: formatting complete"
echo "== CI: Run go vet =="
if ! go vet ./...; then
echo "ERROR: go vet failed."
exit 1
fi
echo "OK: go vet passed"
echo "== CI: Build all packages =="
if ! go build ./...; then
echo "ERROR: go build ./... failed."
exit 1
fi
echo "OK: packages built"
echo "== CI: Build example apps under internal/examples =="
if [ -d "internal/examples" ]; then
count=0
while IFS= read -r -d '' mainfile; do
dir="$(dirname "$mainfile")"
echo "Building $dir"
if ! (cd "$dir" && go build); then
echo "ERROR: example build failed: $dir"
exit 1
fi
count=$((count + 1))
done < <(find internal/examples -type f -name "main.go" -print0)
if [ "$count" -eq 0 ]; then
echo "NOTE: no example main.go files found under internal/examples"
else
echo "OK: built $count example(s)"
fi
else
echo "NOTE: internal/examples not found, skipping example builds"
fi
echo "== CI: Run tests =="
if ! go test ./... 2>&1 | grep -v '\[no test files\]'; then
echo "ERROR: tests failed."
exit 1
fi
echo "SUCCESS: All checks passed."
exit 0

View File

@@ -273,13 +273,17 @@ func (c *Context) SyncElements(elem h.H) {
// SyncSignals pushes the current signal changes to the browser immediately
// over the live SSE event stream.
func (c *Context) SyncSignals() {
sse := c.getSSE()
if sse == nil {
patchChan := c.getPatchChan()
if patchChan == nil {
c.app.logWarn(c, "signals out of sync: no sse stream")
return
}
updatedSigs := make(map[string]any)
for id, sig := range c.signals {
c.signals.Range(func(idAny, val any) bool {
// We know the types.
sig, _ := val.(*signal) // adjust *Signal to your actual signal type
id, _ := val.(string)
if sig.err != nil {
c.app.logWarn(c, "signal out of sync'%s': %v", sig.id, sig.err)
}
@@ -287,7 +291,8 @@ func (c *Context) SyncSignals() {
updatedSigs[id] = fmt.Sprintf("%v", sig.v)
sig.changed = false
}
}
return true // continue iteration
})
if len(updatedSigs) != 0 {
outgoingSignals, _ := json.Marshal(updatedSigs)
patchChan <- patch{patchTypeSignals, string(outgoingSignals)}

View File

@@ -83,11 +83,6 @@ func main() {
}
`)),
)
// Uncomment for inspector. Plugin candidate.
// v.AppendToFoot(
// h.Script(h.Src("https://cdn.jsdelivr.net/gh/dataSPA/dataSPA-inspector@latest/dataspa-inspector.bundled.js"), h.Type("module")),
// h.Raw("<dataspa-inspector/>"),
// )
rooms := NewRooms[Chat, UserInfo]("Clojure", "Dotnet", "Go", "Java", "JS", "Kotlin", "Python", "Rust")
rooms.Start()
@@ -144,24 +139,13 @@ func main() {
c.View(func() h.H {
var tabs []h.H
rooms.Visit(func(n string) {
if n == roomNameString {
tabs = append(tabs, h.Li(
h.A(
h.Href(""),
h.Attr("aria-current", "page"),
h.Text(n),
switchRoomAction.OnClick(WithSignal(roomName, n)),
),
))
} else {
tabs = append(tabs, h.Li(
h.A(
h.Href("#"),
h.Text(n),
switchRoomAction.OnClick(via.WithSignal(roomName, n)),
),
))
}
tabs = append(tabs, h.Li(
h.A(
h.If(n == roomNameString, h.Attr("aria-current", "page")),
h.Text(n),
switchRoomAction.OnClick(WithSignal(roomName, n)),
),
))
})
var messages []h.H

View File

@@ -46,4 +46,3 @@ func main() {
v.Start()
}

6
via.go
View File

@@ -174,6 +174,11 @@ func (v *V) registerCtx(c *Context) {
}
v.contextRegistry[c.id] = c
v.logDebug(c, "new context added to registry")
v.summarize()
}
func (v *V) summarize() {
fmt.Println("Have", len(v.contextRegistry), "sessions")
}
func (v *V) unregisterCtx(id string) {
@@ -184,6 +189,7 @@ func (v *V) unregisterCtx(id string) {
}
v.logDebug(nil, "ctx '%s' removed from registry", id)
delete(v.contextRegistry, id)
v.summarize()
}
func (v *V) getCtx(id string) (*Context, error) {

View File

@@ -1,165 +1,86 @@
package via
import (
"compress/gzip"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/andybalholm/brotli"
"github.com/go-via/via/h"
"github.com/stretchr/testify/assert"
)
func TestCompressionBrotli(t *testing.T) {
func TestPageRoute(t *testing.T) {
v := New()
v.Page("/", func(c *Context) {
c.View(func() h.H {
return h.Div(h.Text(strings.Repeat("Hello Via! ", 200)))
})
})
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Accept-Encoding", "br")
w := httptest.NewRecorder()
v.handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "br", w.Header().Get("Content-Encoding"))
reader := brotli.NewReader(w.Body)
decompressed, err := io.ReadAll(reader)
assert.NoError(t, err)
assert.Contains(t, string(decompressed), "Hello Via!")
}
func TestCompressionGzip(t *testing.T) {
v := New()
v.Page("/", func(c *Context) {
c.View(func() h.H {
return h.Div(h.Text(strings.Repeat("Hello Via! ", 200)))
})
})
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Accept-Encoding", "gzip")
w := httptest.NewRecorder()
v.handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "gzip", w.Header().Get("Content-Encoding"))
reader, err := gzip.NewReader(w.Body)
assert.NoError(t, err)
decompressed, err := io.ReadAll(reader)
assert.NoError(t, err)
assert.Contains(t, string(decompressed), "Hello Via!")
}
func TestCompressionNone(t *testing.T) {
v := New()
v.Page("/", func(c *Context) {
c.View(func() h.H {
return h.Div(h.Text(strings.Repeat("Hello Via! ", 200)))
return h.Div(h.Text("Hello Via!"))
})
})
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
v.handler.ServeHTTP(w, req)
v.mux.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Empty(t, w.Header().Get("Content-Encoding"))
assert.Contains(t, w.Body.String(), "Hello Via!")
assert.Contains(t, w.Body.String(), "<!doctype html>")
}
func TestCompressionMinSize(t *testing.T) {
func TestDatastarJS(t *testing.T) {
v := New()
v.Page("/", func(c *Context) {
c.View(func() h.H {
return h.Div(h.Text("Small"))
})
})
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Accept-Encoding", "br")
w := httptest.NewRecorder()
v.handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Empty(t, w.Header().Get("Content-Encoding"))
}
func TestCompressionLargeResponse(t *testing.T) {
v := New()
v.Page("/", func(c *Context) {
c.View(func() h.H {
return h.Div(h.Text(strings.Repeat("A", 2000)))
})
})
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Accept-Encoding", "br")
w := httptest.NewRecorder()
v.handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "br", w.Header().Get("Content-Encoding"))
}
func TestCompressionDatastarJS(t *testing.T) {
v := New()
req := httptest.NewRequest("GET", "/_datastar.js", nil)
req.Header.Set("Accept-Encoding", "br")
w := httptest.NewRecorder()
v.handler.ServeHTTP(w, req)
v.mux.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "br", w.Header().Get("Content-Encoding"))
assert.Equal(t, "application/javascript", w.Header().Get("Content-Type"))
}
func TestCompressionBrotliPreferred(t *testing.T) {
func TestSignal(t *testing.T) {
var sig *signal
v := New()
v.Page("/", func(c *Context) {
sig = c.Signal("test")
c.View(func() h.H { return h.Div() })
})
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
v.mux.ServeHTTP(w, req)
assert.Equal(t, "test", sig.v.Interface())
}
func TestAction(t *testing.T) {
var trigger *actionTrigger
var sig *signal
v := New()
v.Page("/", func(c *Context) {
trigger = c.Action(func() {})
sig = c.Signal("value")
c.View(func() h.H {
return h.Div(h.Text(strings.Repeat("Hello Via! ", 200)))
return h.Div(
h.Button(trigger.OnClick()),
h.Input(trigger.OnChange()),
h.Input(trigger.OnKeyDown("Enter")),
h.Button(trigger.OnClick(WithSignal(sig, "test"))),
h.Button(trigger.OnClick(WithSignalInt(sig, 42))),
)
})
})
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Accept-Encoding", "gzip, br")
w := httptest.NewRecorder()
v.handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "br", w.Header().Get("Content-Encoding"))
v.mux.ServeHTTP(w, req)
body := w.Body.String()
assert.Contains(t, body, "data-on:click")
assert.Contains(t, body, "data-on:change__debounce.200ms")
assert.Contains(t, body, "data-on:keydown")
assert.Contains(t, body, "/_action/")
}
func TestCompressionZstdDisabled(t *testing.T) {
func TestConfig(t *testing.T) {
v := New()
v.Page("/", func(c *Context) {
c.View(func() h.H {
return h.Div(h.Text(strings.Repeat("Hello Via! ", 200)))
})
})
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Accept-Encoding", "zstd, br, gzip")
w := httptest.NewRecorder()
v.handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "br", w.Header().Get("Content-Encoding"), "Should use Brotli, not zstd")
assert.NotEqual(t, "zstd", w.Header().Get("Content-Encoding"))
v.Config(Options{DocumentTitle: "Test"})
assert.Equal(t, "Test", v.cfg.DocumentTitle)
}