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
34 changes: 34 additions & 0 deletions internal/mcov/byfile_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package mcov

import "testing"

// ByFile rolls a Result up per source file, preserving first-seen order, with
// Covered counting lines that ran at least once.
func TestByFile(t *testing.T) {
r := Result{Lines: []LineCov{
{Path: "MATH.m", Hits: 1},
{Path: "MATH.m", Hits: 0},
{Path: "STR.m", Hits: 3},
{Path: "MATH.m", Hits: 2}, // out-of-order line for an already-seen file
}}
got := ByFile(r)
if len(got) != 2 {
t.Fatalf("ByFile returned %d files, want 2: %+v", len(got), got)
}
// First-seen order: MATH.m before STR.m.
if got[0].Path != "MATH.m" || got[0].Total != 3 || got[0].Covered != 2 {
t.Errorf("got[0] = %+v, want MATH.m 2/3", got[0])
}
if got[1].Path != "STR.m" || got[1].Total != 1 || got[1].Covered != 1 {
t.Errorf("got[1] = %+v, want STR.m 1/1", got[1])
}
if p := got[0].Percent(); p < 66.6 || p > 66.7 {
t.Errorf("MATH.m percent = %.2f, want ~66.67", p)
}
}

func TestByFileEmpty(t *testing.T) {
if got := ByFile(Result{}); len(got) != 0 {
t.Errorf("ByFile(empty) = %+v, want none", got)
}
}
35 changes: 35 additions & 0 deletions internal/mcov/coverage.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,41 @@ func Run(ctx context.Context, p *parse.Parser, eng engine.Engine, routinePaths,
return Result{Lines: lines, Stdout: res.Stdout}, nil
}

// FileCov is one source file's coverage rollup.
type FileCov struct {
Path string
Covered int
Total int
}

// Percent is the file's line coverage (0 when it has no executable lines).
func (f FileCov) Percent() float64 {
if f.Total == 0 {
return 0
}
return 100 * float64(f.Covered) / float64(f.Total)
}

// ByFile rolls a Result up per source file in first-seen line order — the
// shared rollup behind `m coverage`'s per-file report and `m watch --coverage`.
func ByFile(r Result) []FileCov {
idx := map[string]int{}
var out []FileCov
for _, l := range r.Lines {
i, ok := idx[l.Path]
if !ok {
i = len(out)
idx[l.Path] = i
out = append(out, FileCov{Path: l.Path})
}
out[i].Total++
if l.Hits > 0 {
out[i].Covered++
}
}
return out
}

// LCOV renders the result as an LCOV tracefile (one SF block per source file).
func LCOV(r Result) string {
byPath := map[string][]LineCov{}
Expand Down
100 changes: 100 additions & 0 deletions internal/mtest/affected_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package mtest_test

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

"github.com/vista-cloud-dev/m-cli/internal/mtest"
)

// ReferencedRoutines returns the external routines a suite calls — the basis
// for mapping a changed routine to the suites that exercise it.
func TestReferencedRoutines(t *testing.T) {
src := []byte(`MATHTST ; math suite
do start^STDASSERT(.pass,.fail)
do tAdd(.pass,.fail)
do report^STDASSERT(pass,fail)
quit
tAdd(pass,fail) ;@TEST "add"
do eq^STDASSERT(.pass,.fail,$$add^MATH(2,3),5,"2+3")
do helper
quit
helper ;
do log^STDLOG("done")
quit
`)
got, err := mtest.ReferencedRoutines(mustParser(t), src)
if err != nil {
t.Fatal(err)
}
// External targets only, sorted; local label calls (tAdd, helper) excluded.
want := []string{"MATH", "STDASSERT", "STDLOG"}
if len(got) != len(want) {
t.Fatalf("ReferencedRoutines = %v, want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("ReferencedRoutines = %v, want %v", got, want)
}
}
}

// Affected picks the suites that exercise any changed routine: the suite's own
// routine changed, or it references a changed routine.
func TestAffected(t *testing.T) {
suites := []mtest.TestSuite{
{Name: "MATHTST", Deps: []string{"MATH", "STDASSERT"}},
{Name: "STRTST", Deps: []string{"STRING", "STDASSERT"}},
}
names := func(ss []mtest.TestSuite) []string {
out := make([]string, len(ss))
for i, s := range ss {
out[i] = s.Name
}
return out
}
cases := []struct {
desc string
changed map[string]bool
want []string
}{
{"dep of one suite", map[string]bool{"MATH": true}, []string{"MATHTST"}},
{"shared dep hits both", map[string]bool{"STDASSERT": true}, []string{"MATHTST", "STRTST"}},
{"suite's own routine", map[string]bool{"STRTST": true}, []string{"STRTST"}},
{"unrelated routine", map[string]bool{"NOPE": true}, nil},
}
for _, c := range cases {
got := names(mtest.Affected(suites, c.changed))
if len(got) != len(c.want) {
t.Errorf("%s: Affected = %v, want %v", c.desc, got, c.want)
continue
}
for i := range c.want {
if got[i] != c.want[i] {
t.Errorf("%s: Affected = %v, want %v", c.desc, got, c.want)
break
}
}
}
}

// Discover records each suite's external dependencies so the watch loop can do
// affected-test selection without re-parsing on every save.
func TestDiscoverDeps(t *testing.T) {
dir := t.TempDir()
src, _ := os.ReadFile("testdata/SAMPLETST.m")
if err := os.WriteFile(filepath.Join(dir, "SAMPLETST.m"), src, 0o644); err != nil {
t.Fatal(err)
}
suites, err := mtest.Discover(mustParser(t), []string{dir})
if err != nil {
t.Fatal(err)
}
if len(suites) != 1 {
t.Fatalf("got %d suites, want 1", len(suites))
}
if len(suites[0].Deps) != 1 || suites[0].Deps[0] != "STDASSERT" {
t.Errorf("Deps = %v, want [STDASSERT]", suites[0].Deps)
}
}
53 changes: 52 additions & 1 deletion internal/mtest/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"sort"
"strings"

"github.com/vista-cloud-dev/m-cli/internal/workspace"
"github.com/vista-cloud-dev/m-parse/parse"
)

Expand All @@ -25,6 +26,7 @@ type TestSuite struct {
Path string
Protocol string // routine hosting start/report (STDASSERT, TESTRUN, …)
Cases []TestCase
Deps []string // external routines the suite calls (upper, sorted) — for affected-test selection
}

var (
Expand Down Expand Up @@ -158,12 +160,61 @@ func Discover(p *parse.Parser, paths []string) ([]TestSuite, error) {
if err != nil {
return nil, err
}
suites = append(suites, TestSuite{Name: name, Path: f, Protocol: DetectProtocol(src), Cases: cases})
deps, err := ReferencedRoutines(p, src)
if err != nil {
return nil, err
}
suites = append(suites, TestSuite{Name: name, Path: f, Protocol: DetectProtocol(src), Cases: cases, Deps: deps})
}
sort.Slice(suites, func(i, j int) bool { return suites[i].Name < suites[j].Name })
return suites, nil
}

// ReferencedRoutines returns the external routines src calls out to — every
// LABEL^ROUTINE / ^ROUTINE / $$^ROUTINE site — upper-cased and sorted. Local
// (bare-label / intra-routine) calls are excluded: they add no cross-file
// dependency. This maps a changed routine to the suites that exercise it.
func ReferencedRoutines(p *parse.Parser, src []byte) ([]string, error) {
tree, err := p.Parse(context.Background(), src)
if err != nil {
return nil, err
}
defer tree.Close()
set := map[string]bool{}
for _, r := range workspace.References(tree.RootNode(), "") {
if r.TargetRoutine != "" { // "" == current routine (a local call)
set[r.TargetRoutine] = true
}
}
out := make([]string, 0, len(set))
for r := range set {
out = append(out, r)
}
sort.Strings(out)
return out, nil
}

// Affected returns the suites that exercise any of the changed routines: a
// suite matches when its own routine changed (the suite file itself) or it
// references a changed routine via its Deps. changed holds upper-cased routine
// names. Order is preserved from suites.
func Affected(suites []TestSuite, changed map[string]bool) []TestSuite {
var out []TestSuite
for _, s := range suites {
if changed[strings.ToUpper(s.Name)] {
out = append(out, s)
continue
}
for _, d := range s.Deps {
if changed[d] {
out = append(out, s)
break
}
}
}
return out
}

func childOfType(n parse.Node, typ string) (parse.Node, bool) {
for i := uint32(0); i < n.ChildCount(); i++ {
if c := n.Child(i); c.Type() == typ {
Expand Down
Loading
Loading