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
|
||||
.env
|
||||
/todos
|
||||
/coverage.out
|
||||
/coverage.html
|
||||
|
|
|
|||
|
|
@ -51,7 +51,8 @@ Always run `make build` before commit.
|
|||
|
||||
## 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).
|
||||
- If guest provisioning changes, document whether existing images must be rebuilt or recreated.
|
||||
|
||||
|
|
|
|||
39
Makefile
39
Makefile
|
|
@ -28,19 +28,22 @@ GO_LDFLAGS := -X banger/internal/buildinfo.Version=$(VERSION) -X banger/internal
|
|||
|
||||
.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:
|
||||
@printf '%s\n' \
|
||||
'Targets:' \
|
||||
' 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 test Run go test ./...' \
|
||||
' make lint Run gofmt + go vet + shellcheck (errors)' \
|
||||
' make fmt Format Go sources under cmd/ and internal/' \
|
||||
' make tidy Run go mod tidy' \
|
||||
' make clean Remove built Go binaries' \
|
||||
' make bench-create Benchmark vm create and SSH readiness with scripts/bench-create.sh'
|
||||
' 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 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 fmt Format Go sources under cmd/ and internal/' \
|
||||
' make tidy Run go mod tidy' \
|
||||
' make clean Remove built Go binaries and coverage artefacts' \
|
||||
' make bench-create Benchmark vm create and SSH readiness with scripts/bench-create.sh'
|
||||
|
||||
build: $(BINARIES)
|
||||
|
||||
|
|
@ -59,6 +62,22 @@ $(VSOCK_AGENT_BIN): $(BUILD_INPUTS) go.mod go.sum
|
|||
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-go:
|
||||
|
|
@ -80,7 +99,7 @@ tidy:
|
|||
$(GO) mod tidy
|
||||
|
||||
clean:
|
||||
rm -rf "$(BUILD_BIN_DIR)"
|
||||
rm -rf "$(BUILD_BIN_DIR)" coverage.out coverage.html
|
||||
|
||||
bench-create: build
|
||||
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