coverage: make targets + close zero-cov gaps (namegen, sessionstream)
Adds `make coverage` (per-package + total via -coverpkg=./...), `make coverage-html`, and `make coverage-total` (CI-friendly). Wires coverage.out/coverage.html through `make clean` and .gitignore. Closes the two easy zero-coverage packages: namegen (77.8%) and sessionstream (93.5%). Total statement coverage 51.7% -> 52.1%.
This commit is contained in:
parent
88425fb857
commit
18bf89eae9
5 changed files with 204 additions and 11 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -18,3 +18,5 @@ wtf/*.deb
|
||||||
id_rsa
|
id_rsa
|
||||||
.env
|
.env
|
||||||
/todos
|
/todos
|
||||||
|
/coverage.out
|
||||||
|
/coverage.html
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,8 @@ Always run `make build` before commit.
|
||||||
|
|
||||||
## Testing Guidance
|
## Testing Guidance
|
||||||
|
|
||||||
- Primary automated coverage is `go test ./...`.
|
- Primary automated coverage is `go test ./...` (wired through `make test`).
|
||||||
|
- `make coverage` runs the suite with `-coverpkg=./...` and prints per-package averages plus a total; `make coverage-html` writes a browsable report to `coverage.html`; `make coverage-total` prints just the total (for scripts/CI).
|
||||||
- For lifecycle changes, smoke-test with `vm run` end-to-end (covers create + start + boot + ssh).
|
- For lifecycle changes, smoke-test with `vm run` end-to-end (covers create + start + boot + ssh).
|
||||||
- If guest provisioning changes, document whether existing images must be rebuilt or recreated.
|
- If guest provisioning changes, document whether existing images must be rebuilt or recreated.
|
||||||
|
|
||||||
|
|
|
||||||
25
Makefile
25
Makefile
|
|
@ -28,7 +28,7 @@ GO_LDFLAGS := -X banger/internal/buildinfo.Version=$(VERSION) -X banger/internal
|
||||||
|
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
.PHONY: help build banger bangerd test fmt tidy clean install bench-create lint lint-go lint-shell
|
.PHONY: help build banger bangerd test fmt tidy clean install bench-create lint lint-go lint-shell coverage coverage-html coverage-total
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@printf '%s\n' \
|
@printf '%s\n' \
|
||||||
|
|
@ -36,10 +36,13 @@ help:
|
||||||
' make build Build ./build/bin/banger, ./build/bin/bangerd, and ./build/bin/banger-vsock-agent' \
|
' make build Build ./build/bin/banger, ./build/bin/bangerd, and ./build/bin/banger-vsock-agent' \
|
||||||
' make install Build and install banger, bangerd, and the companion vsock helper' \
|
' make install Build and install banger, bangerd, and the companion vsock helper' \
|
||||||
' make test Run go test ./...' \
|
' make test Run go test ./...' \
|
||||||
|
' make coverage Run tests with coverage; print per-package + total' \
|
||||||
|
' make coverage-html Open a browsable per-line HTML report (writes coverage.html)' \
|
||||||
|
' make coverage-total Print just the total statement coverage (for scripts/CI)' \
|
||||||
' make lint Run gofmt + go vet + shellcheck (errors)' \
|
' make lint Run gofmt + go vet + shellcheck (errors)' \
|
||||||
' make fmt Format Go sources under cmd/ and internal/' \
|
' make fmt Format Go sources under cmd/ and internal/' \
|
||||||
' make tidy Run go mod tidy' \
|
' make tidy Run go mod tidy' \
|
||||||
' make clean Remove built Go binaries' \
|
' make clean Remove built Go binaries and coverage artefacts' \
|
||||||
' make bench-create Benchmark vm create and SSH readiness with scripts/bench-create.sh'
|
' make bench-create Benchmark vm create and SSH readiness with scripts/bench-create.sh'
|
||||||
|
|
||||||
build: $(BINARIES)
|
build: $(BINARIES)
|
||||||
|
|
@ -59,6 +62,22 @@ $(VSOCK_AGENT_BIN): $(BUILD_INPUTS) go.mod go.sum
|
||||||
test:
|
test:
|
||||||
$(GO) test ./...
|
$(GO) test ./...
|
||||||
|
|
||||||
|
# Coverage targets use -coverpkg=./... so packages without their own
|
||||||
|
# tests still get counted when another package exercises them (common
|
||||||
|
# for daemon/* subpackages). coverage.out is gitignored.
|
||||||
|
coverage:
|
||||||
|
$(GO) test -coverpkg=./... -coverprofile=coverage.out ./...
|
||||||
|
@echo ''
|
||||||
|
@echo 'Per-package:'
|
||||||
|
@$(GO) tool cover -func=coverage.out | awk -F'\t+' '/^total:/ {total=$$NF; next} {pkg=$$1; sub("banger/", "", pkg); sub("/[^/]+:[0-9]+:$$", "", pkg); pkgs[pkg]+=1; covered[pkg]+=$$NF+0} END {for (p in pkgs) printf " %-40s %.1f%% (avg of %d funcs)\n", p, covered[p]/pkgs[p], pkgs[p] | "sort"; print ""; print "Total statement coverage:", total}'
|
||||||
|
|
||||||
|
coverage-html: coverage
|
||||||
|
$(GO) tool cover -html=coverage.out -o coverage.html
|
||||||
|
@echo 'wrote coverage.html'
|
||||||
|
|
||||||
|
coverage-total:
|
||||||
|
@$(GO) test -coverpkg=./... -coverprofile=coverage.out ./... >/dev/null 2>&1 && $(GO) tool cover -func=coverage.out | awk '/^total:/ {print $$NF}'
|
||||||
|
|
||||||
lint: lint-go lint-shell
|
lint: lint-go lint-shell
|
||||||
|
|
||||||
lint-go:
|
lint-go:
|
||||||
|
|
@ -80,7 +99,7 @@ tidy:
|
||||||
$(GO) mod tidy
|
$(GO) mod tidy
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf "$(BUILD_BIN_DIR)"
|
rm -rf "$(BUILD_BIN_DIR)" coverage.out coverage.html
|
||||||
|
|
||||||
bench-create: build
|
bench-create: build
|
||||||
BANGER_BIN="$(abspath $(BANGER_BIN))" bash ./scripts/bench-create.sh $(ARGS)
|
BANGER_BIN="$(abspath $(BANGER_BIN))" bash ./scripts/bench-create.sh $(ARGS)
|
||||||
|
|
|
||||||
54
internal/namegen/namegen_test.go
Normal file
54
internal/namegen/namegen_test.go
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
package namegen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerate(t *testing.T) {
|
||||||
|
adjSet := make(map[string]struct{}, len(adjectives))
|
||||||
|
for _, a := range adjectives {
|
||||||
|
adjSet[a] = struct{}{}
|
||||||
|
}
|
||||||
|
subSet := make(map[string]struct{}, len(substantives))
|
||||||
|
for _, s := range substantives {
|
||||||
|
subSet[s] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[string]int)
|
||||||
|
for i := 0; i < 200; i++ {
|
||||||
|
name := Generate()
|
||||||
|
parts := strings.Split(name, "-")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
t.Fatalf("expected adj-noun form, got %q", name)
|
||||||
|
}
|
||||||
|
if _, ok := adjSet[parts[0]]; !ok {
|
||||||
|
t.Fatalf("unknown adjective %q in %q", parts[0], name)
|
||||||
|
}
|
||||||
|
if _, ok := subSet[parts[1]]; !ok {
|
||||||
|
t.Fatalf("unknown substantive %q in %q", parts[1], name)
|
||||||
|
}
|
||||||
|
seen[name]++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimal variety check: adj-noun cartesian product is thousands of
|
||||||
|
// combinations; 200 draws should hit more than a couple.
|
||||||
|
if len(seen) < 10 {
|
||||||
|
t.Fatalf("expected varied output, only saw %d distinct names", len(seen))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRandomIndex(t *testing.T) {
|
||||||
|
if got := randomIndex(0); got != 0 {
|
||||||
|
t.Fatalf("randomIndex(0) = %d, want 0", got)
|
||||||
|
}
|
||||||
|
if got := randomIndex(1); got != 0 {
|
||||||
|
t.Fatalf("randomIndex(1) = %d, want 0", got)
|
||||||
|
}
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
n := randomIndex(7)
|
||||||
|
if n < 0 || n >= 7 {
|
||||||
|
t.Fatalf("randomIndex(7) = %d, out of range", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
117
internal/sessionstream/sessionstream_test.go
Normal file
117
internal/sessionstream/sessionstream_test.go
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
package sessionstream
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWriteReadFrameRoundtrip(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
channel byte
|
||||||
|
payload []byte
|
||||||
|
}{
|
||||||
|
{"stdout_bytes", ChannelStdout, []byte("hello world")},
|
||||||
|
{"stderr_bytes", ChannelStderr, []byte{0x00, 0xff, 0x7f}},
|
||||||
|
{"empty_payload", ChannelStdin, nil},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := WriteFrame(&buf, tc.channel, tc.payload); err != nil {
|
||||||
|
t.Fatalf("WriteFrame: %v", err)
|
||||||
|
}
|
||||||
|
ch, got, err := ReadFrame(&buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFrame: %v", err)
|
||||||
|
}
|
||||||
|
if ch != tc.channel {
|
||||||
|
t.Fatalf("channel = %d, want %d", ch, tc.channel)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, tc.payload) && !(len(got) == 0 && len(tc.payload) == 0) {
|
||||||
|
t.Fatalf("payload = %q, want %q", got, tc.payload)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type shortWriter struct {
|
||||||
|
failAfter int
|
||||||
|
written int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *shortWriter) Write(p []byte) (int, error) {
|
||||||
|
s.written += len(p)
|
||||||
|
if s.written > s.failAfter {
|
||||||
|
return 0, io.ErrShortWrite
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteFrameWriterError(t *testing.T) {
|
||||||
|
w := &shortWriter{failAfter: 2}
|
||||||
|
err := WriteFrame(w, ChannelStdout, []byte("payload"))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error from short writer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadFrameTruncated(t *testing.T) {
|
||||||
|
_, _, err := ReadFrame(bytes.NewReader([]byte{0x02, 0x00}))
|
||||||
|
if !errors.Is(err, io.ErrUnexpectedEOF) && err == nil {
|
||||||
|
t.Fatalf("expected EOF-ish error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header OK, but payload truncated.
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.Write([]byte{ChannelStdout, 0x00, 0x00, 0x00, 0x05})
|
||||||
|
buf.Write([]byte("ab"))
|
||||||
|
if _, _, err := ReadFrame(&buf); err == nil {
|
||||||
|
t.Fatal("expected truncated payload error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestControlRoundtrip(t *testing.T) {
|
||||||
|
code := 42
|
||||||
|
msg := ControlMessage{Type: "exit", ExitCode: &code}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := WriteControl(&buf, msg); err != nil {
|
||||||
|
t.Fatalf("WriteControl: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := ReadNextControl(&buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadNextControl: %v", err)
|
||||||
|
}
|
||||||
|
if got.Type != "exit" {
|
||||||
|
t.Fatalf("type = %q, want exit", got.Type)
|
||||||
|
}
|
||||||
|
if got.ExitCode == nil || *got.ExitCode != 42 {
|
||||||
|
t.Fatalf("exit_code = %v, want 42", got.ExitCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadControlBadJSON(t *testing.T) {
|
||||||
|
if _, err := ReadControl([]byte("{not json")); err == nil {
|
||||||
|
t.Fatal("expected JSON error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadNextControlWrongChannel(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := WriteFrame(&buf, ChannelStdout, []byte("not a control frame")); err != nil {
|
||||||
|
t.Fatalf("WriteFrame: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := ReadNextControl(&buf); err == nil {
|
||||||
|
t.Fatal("expected error for non-control channel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatConstant(t *testing.T) {
|
||||||
|
if FormatV1 != "stdio_mux_v1" {
|
||||||
|
t.Fatalf("FormatV1 = %q, want stdio_mux_v1", FormatV1)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue