Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions internal/harness/coverage_parity_live_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package harness_test

import (
"context"
"os"
"path/filepath"
"testing"

"github.com/vista-cloud-dev/m-cli/internal/engine"
"github.com/vista-cloud-dev/m-cli/internal/harness"
"github.com/vista-cloud-dev/m-cli/internal/mcov"
"github.com/vista-cloud-dev/m-parse/parse"
)

// TestResidentCoverageParityIRIS is the coverage half of G4 on the resident IRIS
// tier: resident coverage (cov^STDHARN → raw ##MON block → mcov.FromMonitor) must
// roll up to the SAME ByFile coverage as the host-orchestrated IRIS path
// (mcov.Run → %Monitor). Both use the same monitor data and the same parse-tree
// denominator, so they agree by construction. Resident coverage is IRIS-only
// (YDB stays the host-side view "TRACE" path), so this test is IRIS-bound. Opt-in:
//
// M_TEST_LIVE=1 M_STDLIB_SRC=$HOME/vista-cloud-dev/m-stdlib/src \
// M_IRIS_CONTAINER=vista-iris go test ./internal/harness/ -run TestResidentCoverageParityIRIS
func TestResidentCoverageParityIRIS(t *testing.T) {
stdlib := liveStdlibSrc(t)
container := os.Getenv("M_IRIS_CONTAINER")
if container == "" {
t.Skip("set M_IRIS_CONTAINER (e.g. vista-iris) to run the IRIS coverage parity test")
}
ns := envOr("M_IRIS_NAMESPACE", "USER")
ctx := context.Background()
stageDir := "/tmp/harness-cov-parity"

// Cover STDMATH (a pure-logic module) exercised by STDMATHTST.
covRoutine := "STDMATH"
suite := "STDMATHTST"
routinePath := filepath.Join(stdlib, covRoutine+".m")
suitePath := filepath.Join(filepath.Dir(stdlib), "tests", suite+".m")

var files []string
srcEntries, err := os.ReadDir(stdlib)
if err != nil {
t.Fatalf("read M_STDLIB_SRC: %v", err)
}
for _, e := range srcEntries {
if !e.IsDir() && filepath.Ext(e.Name()) == ".m" {
files = append(files, filepath.Join(stdlib, e.Name()))
}
}
files = append(files, suitePath)

eng := engine.New(engine.IRIS, engine.Options{Runner: engine.DockerRunner(container, ""), Namespace: ns})
if err := engine.IrisStageLoad(ctx, eng, container, stageDir, files); err != nil {
t.Fatalf("iris stage: %v", err)
}
defer engine.DockerUnstage(ctx, container, stageDir)

p, err := parse.New(ctx)
if err != nil {
t.Fatalf("parse.New: %v", err)
}
defer func() { _ = p.Close(ctx) }()

// Host-orchestrated IRIS coverage (mcov.Run → %Monitor).
hostRes, err := mcov.Run(ctx, p, eng, []string{routinePath}, []string{suite})
if err != nil {
t.Fatalf("host mcov.Run: %v", err)
}
hostBF := mcov.ByFile(hostRes)

// Resident coverage: cov^STDHARN → ##MON block → mcov.FromMonitor.
frame, err := harness.TriggerCoverage(ctx, eng, []string{suite}, []string{covRoutine})
if err != nil {
t.Fatalf("TriggerCoverage: %v", err)
}
_, _, mon, meta, err := harness.SplitFrame(frame)
if err != nil {
t.Fatalf("SplitFrame: %v\nframe:\n%s", err, frame)
}
if meta.Engine != "iris" {
t.Errorf("frame engine=%q, want iris", meta.Engine)
}
if mon == "" {
t.Fatalf("resident ##MON block is empty; frame:\n%s", frame)
}
resRes, err := mcov.FromMonitor(p, mon, []string{routinePath})
if err != nil {
t.Fatalf("FromMonitor: %v", err)
}
resBF := mcov.ByFile(resRes)

// The test must be meaningful (some lines, some covered).
if hostRes.Total() == 0 || hostRes.Covered() == 0 {
t.Fatalf("host coverage trivial: %d/%d", hostRes.Covered(), hostRes.Total())
}

// G4 coverage: resident ByFile == host ByFile.
if len(hostBF) != len(resBF) {
t.Fatalf("ByFile len: host %d, resident %d", len(hostBF), len(resBF))
}
for i := range hostBF {
if hostBF[i].Path != resBF[i].Path || hostBF[i].Covered != resBF[i].Covered || hostBF[i].Total != resBF[i].Total {
t.Errorf("coverage parity MISMATCH:\n host: %+v\n resident: %+v", hostBF[i], resBF[i])
}
}
t.Logf("coverage parity OK: %s %d/%d lines (host == resident)", covRoutine, hostRes.Covered(), hostRes.Total())
}
201 changes: 201 additions & 0 deletions internal/harness/harness.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// Package harness is the host-side trigger/render client for the resident
// pure-M harness (design §3.1, spec §9). The harness's contract is its output
// *frame*, not its transport: a deterministic line-delimited envelope of
// verbatim ^STDASSERT per-suite blocks + a verbatim LCOV block + provenance
// tags (§3.2). This package is a thin frame-splitter — it owns NO test/coverage
// parsing. Each suite block is handed to the unchanged mtest.ParseOutput and the
// LCOV block to the unchanged mcov consumers, which is what structurally
// guarantees G4 cross-engine parity (the resident tier and the file-side tier
// speak the exact same dialect).
package harness

import (
"errors"
"strconv"
"strings"
)

// Frame delimiter lines. They never collide with ^STDASSERT / LCOV content,
// which never begin with "##".
const (
hdrHarness = "##M-HARNESS" // header: frame=/tier=/engine=/ns=
hdrSuite = "##SUITE" // ##SUITE ^NAME
hdrEnd = "##END" // ##END ^NAME exit=N (closes a suite)
hdrLCOV = "##LCOV" // ##LCOV … verbatim LCOV tracefile
hdrMon = "##MON" // ##MON … raw IRIS line-monitor counts (MLINE:…)
hdrEndMon = "##END-MON" // closes the ##MON block
hdrTrailer = "##END-HARNESS" // trailer: suites=/pass=/fail= cross-check
)

var (
// ErrNoFrame means the input had no ##M-HARNESS header — not a frame.
ErrNoFrame = errors.New("harness: missing ##M-HARNESS header")
// ErrTruncated means the stream ended before the ##END-HARNESS trailer, or
// the trailer's suite count disagrees with the blocks parsed — a dropped
// connection. Partial results are still returned alongside it.
ErrTruncated = errors.New("harness: truncated frame (missing or mismatched trailer)")
)

// SuiteBlock is one per-suite payload: Name is the suite (the ##SUITE token with
// any leading ^ stripped, so it matches mtest.TestSuite.Name), Body is the
// verbatim ^STDASSERT text between the ##SUITE and ##END lines (fed unchanged to
// mtest.ParseOutput), Exit is the engine exit code from the ##END line.
type SuiteBlock struct {
Name string
Body string
Exit int
}

// FrameMeta carries the header provenance (render label) and the trailer
// cross-check totals.
type FrameMeta struct {
Frame int
Tier string
Engine string
NS string
Suites int
Pass int
Fail int
}

// SplitFrame splits the result frame (§3.2) into per-suite ^STDASSERT blocks,
// the LCOV block, the raw ##MON line-monitor block (both empty when coverage was
// not requested), and the provenance / summary metadata. It is delimiter-
// scanning only — no test or coverage parsing happens here. Unrecognized ##
// directives are skipped (forward-compat). A missing header is ErrNoFrame; a
// missing/mismatched trailer is ErrTruncated, returned alongside whatever was
// parsed so a caller can still render it.
func SplitFrame(frame string) ([]SuiteBlock, string, string, FrameMeta, error) {
var (
meta FrameMeta
suites []SuiteBlock
lcov strings.Builder
mon strings.Builder
sawHeader bool
sawTrailer bool
inLCOV bool
inMon bool
cur *SuiteBlock
body strings.Builder
)

flushSuite := func() {
if cur != nil {
cur.Body = body.String()
suites = append(suites, *cur)
cur = nil
body.Reset()
}
}

for _, line := range strings.Split(frame, "\n") {
switch {
case strings.HasPrefix(line, hdrHarness):
sawHeader = true
parseHeader(line, &meta)
case strings.HasPrefix(line, hdrTrailer):
flushSuite()
inLCOV, inMon = false, false
sawTrailer = true
parseTrailer(line, &meta)
case strings.HasPrefix(line, hdrSuite):
flushSuite()
inLCOV, inMon = false, false
name := strings.TrimSpace(strings.TrimPrefix(line, hdrSuite))
name = strings.TrimPrefix(name, "^")
cur = &SuiteBlock{Name: name, Exit: -1}
case line == hdrMon || strings.HasPrefix(line, hdrMon+" "):
flushSuite()
inLCOV, inMon = false, true
case strings.HasPrefix(line, hdrEndMon):
inMon = false
case strings.HasPrefix(line, hdrEnd) && !strings.HasPrefix(line, hdrTrailer):
if cur != nil {
cur.Exit = parseExit(line)
cur.Body = body.String()
suites = append(suites, *cur)
cur = nil
body.Reset()
}
case line == hdrLCOV || strings.HasPrefix(line, hdrLCOV+" "):
flushSuite()
inLCOV, inMon = true, false
case strings.HasPrefix(line, "##"):
// Unknown directive — skip, never fatal (forward-compat).
case inMon:
mon.WriteString(line)
mon.WriteByte('\n')
case inLCOV:
lcov.WriteString(line)
lcov.WriteByte('\n')
case cur != nil:
body.WriteString(line)
body.WriteByte('\n')
}
}
flushSuite()

if !sawHeader {
return suites, lcov.String(), mon.String(), meta, ErrNoFrame
}
if !sawTrailer || meta.Suites != len(suites) {
return suites, lcov.String(), mon.String(), meta, ErrTruncated
}
return suites, lcov.String(), mon.String(), meta, nil
}

// parseHeader reads `##M-HARNESS frame=1 tier=integration engine=iris ns=VEHU`.
func parseHeader(line string, m *FrameMeta) {
for k, v := range kvFields(strings.TrimPrefix(line, hdrHarness)) {
switch k {
case "frame":
m.Frame = atoi(v)
case "tier":
m.Tier = v
case "engine":
m.Engine = v
case "ns":
m.NS = v
}
}
}

// parseTrailer reads `##END-HARNESS suites=2 pass=2 fail=1`.
func parseTrailer(line string, m *FrameMeta) {
for k, v := range kvFields(strings.TrimPrefix(line, hdrTrailer)) {
switch k {
case "suites":
m.Suites = atoi(v)
case "pass":
m.Pass = atoi(v)
case "fail":
m.Fail = atoi(v)
}
}
}

// parseExit reads the exit code from `##END ^NAME exit=N`.
func parseExit(line string) int {
for k, v := range kvFields(line) {
if k == "exit" {
return atoi(v)
}
}
return -1
}

// kvFields splits a run of space-separated key=value tokens into a map.
func kvFields(s string) map[string]string {
out := map[string]string{}
for _, tok := range strings.Fields(s) {
if eq := strings.IndexByte(tok, '='); eq > 0 {
out[tok[:eq]] = tok[eq+1:]
}
}
return out
}

func atoi(s string) int {
n, _ := strconv.Atoi(s)
return n
}
Loading
Loading