Skip to content

Commit 38a0b69

Browse files
committed
test: cover compose.go mergeMatcher/cloneContent branches
codecov/changes flagged 8 uncovered lines in internal/schema/ compose.go — the schema-model port's mergeMatcher and cloneContent helpers. Add targeted same-package unit tests for: a/b nil matcher arms, min/max widening (required-wins, wider-max, optional-both, bounded+unbounded), Sequential OR, and the Columns deep-copy branch. compose.go is now 100% line and branch covered (gobco clean); full suite, lint, and mdsmith check green.
1 parent d511fa0 commit 38a0b69

1 file changed

Lines changed: 128 additions & 0 deletions

File tree

internal/schema/compose_test.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,3 +605,131 @@ func TestCompose_ContentConcatenatesOnMerge(t *testing.T) {
605605
assert.Equal(t, ContentKindCodeBlock, out.Sections[0].Content[0].Kind)
606606
assert.Equal(t, ContentKindTable, out.Sections[0].Content[1].Kind)
607607
}
608+
609+
// TestMergeMatcher_BNil covers the `b == nil` arm: a same-heading
610+
// scope whose Matcher is nil in the later input keeps the earlier
611+
// input's matcher.
612+
func TestMergeMatcher_BNil(t *testing.T) {
613+
a := &Matcher{Regex: "Meta"}
614+
got := mergeMatcher(a, nil)
615+
require.NotNil(t, got)
616+
assert.Equal(t, "Meta", got.Regex)
617+
assert.NotSame(t, a, got, "result must be a clone, not the input pointer")
618+
}
619+
620+
// TestMergeMatcher_ANil covers the symmetric `a == nil` arm.
621+
func TestMergeMatcher_ANil(t *testing.T) {
622+
b := &Matcher{Regex: "Meta"}
623+
got := mergeMatcher(nil, b)
624+
require.NotNil(t, got)
625+
assert.Equal(t, "Meta", got.Regex)
626+
assert.NotSame(t, b, got)
627+
}
628+
629+
// TestMergeMatcher_WiderMaxAndRepeatSet covers two branches: the
630+
// `bMax > max` widening (b's bounded max is larger than a's) and
631+
// the non-(1,1) `else` that sets an explicit Repeat. a allows
632+
// 1..2, b allows 1..5 -> merged is 1..5.
633+
func TestMergeMatcher_WiderMaxAndRepeatSet(t *testing.T) {
634+
a := &Matcher{Regex: "Step", Repeat: Repeat{Set: true, Min: 1, Max: 2}}
635+
b := &Matcher{Regex: "Step", Repeat: Repeat{Set: true, Min: 1, Max: 5}}
636+
got := mergeMatcher(a, b)
637+
require.NotNil(t, got)
638+
gotMin, gotMax := got.Repeat.Bounds()
639+
assert.Equal(t, 1, gotMin)
640+
assert.Equal(t, 5, gotMax, "the wider max must win")
641+
assert.True(t, got.Repeat.Set, "non-(1,1) bounds must set Repeat explicitly")
642+
}
643+
644+
// TestMergeMatcher_RequiredWinsOverOptional covers the min-widening
645+
// branch plus the explicit-Repeat else: an optional run (min 0)
646+
// merged with a required single (1,1) yields a required matcher.
647+
func TestMergeMatcher_RequiredWinsOverOptional(t *testing.T) {
648+
optional := &Matcher{Regex: "Meta", Repeat: Repeat{Set: true, Min: 0, Max: 1}}
649+
required := &Matcher{Regex: "Meta"} // exactly one
650+
got := mergeMatcher(optional, required)
651+
require.NotNil(t, got)
652+
gotMin, _ := got.Repeat.Bounds()
653+
assert.Equal(t, 1, gotMin, "required (min 1) must win over optional (min 0)")
654+
}
655+
656+
// TestCloneContent_CopiesColumns covers the `e.Columns != nil`
657+
// deep-copy branch: a table ContentEntry's Columns slice must be
658+
// cloned, not aliased, when two same-heading scopes merge.
659+
func TestCloneContent_CopiesColumns(t *testing.T) {
660+
cols := []string{"Name", "Type"}
661+
a := &Schema{Sections: []Scope{{
662+
Heading: "Schema", Matcher: &Matcher{Regex: "Schema"},
663+
Content: []ContentEntry{{Kind: ContentKindTable, Columns: cols}},
664+
}}}
665+
b := &Schema{Sections: []Scope{{
666+
Heading: "Schema", Matcher: &Matcher{Regex: "Schema"},
667+
}}}
668+
out, err := Compose(a, b)
669+
require.NoError(t, err)
670+
require.Len(t, out.Sections, 1)
671+
require.Len(t, out.Sections[0].Content, 1)
672+
got := out.Sections[0].Content[0].Columns
673+
require.Equal(t, []string{"Name", "Type"}, got)
674+
cols[0] = "MUTATED"
675+
assert.Equal(t, "Name", out.Sections[0].Content[0].Columns[0],
676+
"cloneContent must deep-copy Columns, not alias the input slice")
677+
}
678+
679+
// TestMergeMatcher_SequentialOredAndUnbounded covers the
680+
// `a.Sequential || b.Sequential` true arms and the unbounded-max
681+
// path (aMax/bMax == 0): merging an unbounded sequential run with
682+
// an unbounded plain run keeps Sequential and stays unbounded.
683+
func TestMergeMatcher_SequentialOredAndUnbounded(t *testing.T) {
684+
a := &Matcher{
685+
Regex: "Step",
686+
Repeat: Repeat{Set: true, Min: 1, Max: 0}, // 1..unbounded
687+
Sequential: true,
688+
}
689+
b := &Matcher{
690+
Regex: "Step",
691+
Repeat: Repeat{Set: true, Min: 1, Max: 0},
692+
}
693+
got := mergeMatcher(a, b)
694+
require.NotNil(t, got)
695+
assert.True(t, got.Sequential, "Sequential must OR across inputs")
696+
gotMin, gotMax := got.Repeat.Bounds()
697+
assert.Equal(t, 1, gotMin)
698+
assert.Equal(t, 0, gotMax, "unbounded max stays unbounded")
699+
}
700+
701+
// TestMergeMatcher_SequentialFromBOnly covers the right operand of
702+
// the `||` (a.Sequential false, b.Sequential true).
703+
func TestMergeMatcher_SequentialFromBOnly(t *testing.T) {
704+
a := &Matcher{Regex: "Step", Repeat: Repeat{Set: true, Min: 1, Max: 0}}
705+
b := &Matcher{Regex: "Step", Repeat: Repeat{Set: true, Min: 1, Max: 0}, Sequential: true}
706+
got := mergeMatcher(a, b)
707+
require.NotNil(t, got)
708+
assert.True(t, got.Sequential)
709+
}
710+
711+
// TestMergeMatcher_OptionalBothSides covers the `min == 1` false
712+
// branch: two optional (min 0) runs merge to an optional run, so
713+
// the explicit-Repeat else fires with min 0.
714+
func TestMergeMatcher_OptionalBothSides(t *testing.T) {
715+
a := &Matcher{Regex: "Notes", Repeat: Repeat{Set: true, Min: 0, Max: 1}}
716+
b := &Matcher{Regex: "Notes", Repeat: Repeat{Set: true, Min: 0, Max: 1}}
717+
got := mergeMatcher(a, b)
718+
require.NotNil(t, got)
719+
gotMin, _ := got.Repeat.Bounds()
720+
assert.Equal(t, 0, gotMin, "two optional runs stay optional")
721+
assert.True(t, got.Repeat.Optional())
722+
}
723+
724+
// TestMergeMatcher_BoundedAThenUnboundedB covers the `bMax != 0`
725+
// false arm: a is bounded (max 3), b is unbounded (max 0). Either
726+
// side unbounded makes the merged run unbounded.
727+
func TestMergeMatcher_BoundedAThenUnboundedB(t *testing.T) {
728+
a := &Matcher{Regex: "Step", Repeat: Repeat{Set: true, Min: 1, Max: 3}}
729+
b := &Matcher{Regex: "Step", Repeat: Repeat{Set: true, Min: 1, Max: 0}}
730+
got := mergeMatcher(a, b)
731+
require.NotNil(t, got)
732+
_, gotMax := got.Repeat.Bounds()
733+
assert.Equal(t, 0, gotMax,
734+
"a bounded max merged with an unbounded one yields unbounded")
735+
}

0 commit comments

Comments
 (0)