Skip to content

Commit 78bf73e

Browse files
authored
test(coverage): st-cov2 cleanup — read/edit/map tests + test hooks for applyEdit (#129)
Salvages working-tree coverage changes from the PR-merge cleanup: - edit.go: add editWriteFile test hook to make applyEdit testable - map.go: track per-module file counts and add sliceContains helper - map_test.go: add CLI invalid-path, hot-path truncation, module-sort tests - read_test.go: add read.go CLI/error-branch coverage tests - edit_test.go: add edit.go CLI/error-branch coverage tests - stcov2_coverage_test.go: targeted tests for remaining sub-100% branches Co-authored-by: Delqhi <delqhi@users.noreply.github.com>
1 parent 31dca6d commit 78bf73e

6 files changed

Lines changed: 1352 additions & 1 deletion

File tree

cmd/sin-code/internal/edit.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ var (
3232
editFormat string
3333
editSymbol string
3434
editGetwd = os.Getwd
35+
editWriteFile = writeFileAtomic
3536
)
3637

3738
var EditCmd = &cobra.Command{
@@ -174,7 +175,7 @@ func applyEdit(path string, req editRequest) (*editResult, error) {
174175
if req.DryRun {
175176
return res, nil
176177
}
177-
if _, err := writeFileAtomic(path, newContent, writeOpts{validate: false}); err != nil {
178+
if _, err := editWriteFile(path, newContent, writeOpts{validate: false}); err != nil {
178179
return nil, err
179180
}
180181
return res, nil

cmd/sin-code/internal/edit_test.go

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
// SPDX-License-Identifier: MIT
2+
// Purpose: coverage tests for edit.go CLI and error branches (st-cov2).
3+
package internal
4+
5+
import (
6+
"io"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
"testing"
11+
)
12+
13+
func captureEditCmd(t *testing.T, args []string, setup func()) string {
14+
t.Helper()
15+
oldStdout := os.Stdout
16+
r, w, _ := os.Pipe()
17+
os.Stdout = w
18+
setup()
19+
err := EditCmd.RunE(EditCmd, args)
20+
w.Close()
21+
os.Stdout = oldStdout
22+
out, _ := io.ReadAll(r)
23+
if err != nil {
24+
t.Fatalf("EditCmd failed: %v", err)
25+
}
26+
return string(out)
27+
}
28+
29+
func TestEditCmd_RunE_Text(t *testing.T) {
30+
dir := t.TempDir()
31+
p := filepath.Join(dir, "f.go")
32+
os.WriteFile(p, []byte("package main\n\nfunc Hello() {}\n"), 0644)
33+
34+
oldAnchor := editAnchor
35+
oldNewText := editNewText
36+
oldFormat := editFormat
37+
oldGetwd := editGetwd
38+
defer func() { editAnchor = oldAnchor; editNewText = oldNewText; editFormat = oldFormat; editGetwd = oldGetwd }()
39+
40+
editGetwd = func() (string, error) { return dir, nil }
41+
editAnchor = "3:" + LineHash("func Hello() {}")
42+
editNewText = "func World() {}"
43+
editFormat = "text"
44+
45+
out := captureEditCmd(t, []string{"f.go"}, func() {})
46+
if !strings.Contains(out, "edited") {
47+
t.Errorf("expected edited output, got %q", out)
48+
}
49+
data, _ := os.ReadFile(p)
50+
if !strings.Contains(string(data), "func World()") {
51+
t.Errorf("expected edit applied, got %q", data)
52+
}
53+
}
54+
55+
func TestEditCmd_RunE_JSON(t *testing.T) {
56+
dir := t.TempDir()
57+
p := filepath.Join(dir, "f.go")
58+
os.WriteFile(p, []byte("package main\n\nfunc Hello() {}\n"), 0644)
59+
60+
oldAnchor := editAnchor
61+
oldNewText := editNewText
62+
oldFormat := editFormat
63+
oldGetwd := editGetwd
64+
defer func() { editAnchor = oldAnchor; editNewText = oldNewText; editFormat = oldFormat; editGetwd = oldGetwd }()
65+
66+
editGetwd = func() (string, error) { return dir, nil }
67+
editAnchor = "3:" + LineHash("func Hello() {}")
68+
editNewText = "func World() {}"
69+
editFormat = "json"
70+
71+
out := captureEditCmd(t, []string{"f.go"}, func() {})
72+
if !strings.Contains(out, "\"path\"") {
73+
t.Errorf("expected JSON output, got %q", out)
74+
}
75+
}
76+
77+
func TestEditCmd_RunE_DryRun(t *testing.T) {
78+
dir := t.TempDir()
79+
p := filepath.Join(dir, "f.go")
80+
os.WriteFile(p, []byte("package main\n\nfunc Hello() {}\n"), 0644)
81+
82+
oldAnchor := editAnchor
83+
oldNewText := editNewText
84+
oldDryRun := editDryRun
85+
oldGetwd := editGetwd
86+
defer func() { editAnchor = oldAnchor; editNewText = oldNewText; editDryRun = oldDryRun; editGetwd = oldGetwd }()
87+
88+
editGetwd = func() (string, error) { return dir, nil }
89+
editAnchor = "3:" + LineHash("func Hello() {}")
90+
editNewText = "func World() {}"
91+
editDryRun = true
92+
93+
out := captureEditCmd(t, []string{"f.go"}, func() {})
94+
if !strings.Contains(out, "---") {
95+
t.Errorf("expected diff output, got %q", out)
96+
}
97+
data, _ := os.ReadFile(p)
98+
if strings.Contains(string(data), "func World()") {
99+
t.Error("dry run must not modify the file")
100+
}
101+
}
102+
103+
func TestEditCmd_RunE_InvalidPath(t *testing.T) {
104+
if err := EditCmd.RunE(EditCmd, []string{"\x00invalid"}); err == nil {
105+
t.Fatal("expected error for invalid path")
106+
}
107+
}
108+
109+
func TestApplyAnchorEdit_ParseAnchorError(t *testing.T) {
110+
_, err := applyAnchorEdit([]string{"a"}, editRequest{Anchor: "bad"}, &editResult{})
111+
if err == nil {
112+
t.Fatal("expected error for invalid anchor")
113+
}
114+
}
115+
116+
func TestApplyAnchorEdit_ResolveError(t *testing.T) {
117+
_, err := applyAnchorEdit([]string{"a"}, editRequest{Anchor: "1:deadbeef"}, &editResult{})
118+
if err == nil {
119+
t.Fatal("expected error for unresolvable anchor")
120+
}
121+
}
122+
123+
func TestApplyAnchorEdit_EndAnchorParseError(t *testing.T) {
124+
lines := []string{"a", "b", "c"}
125+
_, err := applyAnchorEdit(lines, editRequest{
126+
Anchor: "1:" + LineHash("a"),
127+
EndAnchor: "bad",
128+
}, &editResult{})
129+
if err == nil {
130+
t.Fatal("expected error for invalid end anchor")
131+
}
132+
}
133+
134+
func TestApplyAnchorEdit_EndAnchorResolveError(t *testing.T) {
135+
lines := []string{"a", "b", "c"}
136+
_, err := applyAnchorEdit(lines, editRequest{
137+
Anchor: "1:" + LineHash("a"),
138+
EndAnchor: "2:deadbeef",
139+
}, &editResult{})
140+
if err == nil {
141+
t.Fatal("expected error for unresolvable end anchor")
142+
}
143+
}
144+
145+
func TestApplyAnchorEdit_InvalidInsert_Cli(t *testing.T) {
146+
lines := []string{"a", "b", "c"}
147+
_, err := applyAnchorEdit(lines, editRequest{
148+
Anchor: "1:" + LineHash("a"),
149+
NewText: "x",
150+
Insert: "middle",
151+
}, &editResult{})
152+
if err == nil {
153+
t.Fatal("expected error for invalid insert")
154+
}
155+
}
156+
157+
func TestApplyStringEdit_OldStringNotFound(t *testing.T) {
158+
_, err := applyStringEdit([]string{"a"}, "abc", editRequest{OldString: "x"}, &editResult{}, new(bool))
159+
if err == nil {
160+
t.Fatal("expected error when old string not found")
161+
}
162+
}
163+
164+
func TestApplyStringEdit_ReplaceAll(t *testing.T) {
165+
res := &editResult{}
166+
updated, err := applyStringEdit([]string{"x", "x"}, "x\nx\n", editRequest{OldString: "x", NewString: "y", ReplaceAll: true}, res, new(bool))
167+
if err != nil {
168+
t.Fatal(err)
169+
}
170+
if len(updated) != 2 || updated[0] != "y" || updated[1] != "y" {
171+
t.Fatalf("expected all replaced, got %v", updated)
172+
}
173+
}
174+
175+
func TestApplySymbolEdit_NoEngine(t *testing.T) {
176+
_, err := applySymbolEdit([]string{"x"}, "x.txt", "x", editRequest{Symbol: "foo"}, &editResult{})
177+
if err == nil {
178+
t.Fatal("expected error for unsupported language")
179+
}
180+
}
181+
182+
func TestApplySymbolEdit_NotFound_Cli(t *testing.T) {
183+
p := filepath.Join(t.TempDir(), "f.go")
184+
os.WriteFile(p, []byte("package main\nfunc Hello() {}\n"), 0644)
185+
data, _ := os.ReadFile(p)
186+
_, err := applySymbolEdit([]string{"package main", "func Hello() {}"}, p, string(data), editRequest{Symbol: "Missing"}, &editResult{})
187+
if err == nil {
188+
t.Fatal("expected error for missing symbol")
189+
}
190+
}
191+
192+
func TestApplySymbolEdit_Ambiguous_Cli(t *testing.T) {
193+
p := filepath.Join(t.TempDir(), "f.go")
194+
os.WriteFile(p, []byte("package main\nfunc Hello() {}\nfunc Hello() {}\n"), 0644)
195+
data, _ := os.ReadFile(p)
196+
_, err := applySymbolEdit([]string{"package main", "func Hello() {}", "func Hello() {}"}, p, string(data), editRequest{Symbol: "Hello"}, &editResult{})
197+
if err == nil {
198+
t.Fatal("expected error for ambiguous symbol")
199+
}
200+
}
201+
202+
func TestApplySymbolEdit_InvalidInsert_Cli(t *testing.T) {
203+
p := filepath.Join(t.TempDir(), "f.go")
204+
os.WriteFile(p, []byte("package main\nfunc Hello() {}\n"), 0644)
205+
data, _ := os.ReadFile(p)
206+
_, err := applySymbolEdit([]string{"package main", "func Hello() {}"}, p, string(data), editRequest{Symbol: "Hello", Insert: "middle"}, &editResult{})
207+
if err == nil {
208+
t.Fatal("expected error for invalid insert")
209+
}
210+
}
211+
212+
func TestApplySymbolEdit_InvalidRange(t *testing.T) {
213+
p := filepath.Join(t.TempDir(), "f.go")
214+
os.WriteFile(p, []byte("package main\nfunc Hello() {}\n"), 0644)
215+
data, _ := os.ReadFile(p)
216+
// Force an invalid range by overriding the parsed outline via a malformed file? Instead, use a small
217+
// trick: the symbol's EndLine will be within the line slice, but the function checks startIdx<0 etc.
218+
// We can pass a single-line content so EndLine-1 > len(lines)-1.
219+
_, err := applySymbolEdit([]string{"package main"}, p, string(data), editRequest{Symbol: "Hello"}, &editResult{})
220+
if err == nil {
221+
t.Fatal("expected error for invalid range")
222+
}
223+
}
224+
225+
func TestApplyEdit_FileNotFound(t *testing.T) {
226+
_, err := applyEdit("/nonexistent/path.go", editRequest{Anchor: "1:deadbeef"})
227+
if err == nil {
228+
t.Fatal("expected error for missing file")
229+
}
230+
}
231+
232+
func TestApplyEdit_SymbolError(t *testing.T) {
233+
dir := t.TempDir()
234+
p := filepath.Join(dir, "x.txt")
235+
os.WriteFile(p, []byte("hello\n"), 0644)
236+
_, err := applyEdit(p, editRequest{Symbol: "Foo", NewText: "bar"})
237+
if err == nil {
238+
t.Fatal("expected error for unsupported language")
239+
}
240+
}
241+
242+
func TestApplyEdit_InsertAfterMissingNewText(t *testing.T) {
243+
dir := t.TempDir()
244+
p := filepath.Join(dir, "f.go")
245+
os.WriteFile(p, []byte("package main\nfunc Hello() {}\n"), 0644)
246+
_, err := applyEdit(p, editRequest{
247+
Anchor: "2:" + LineHash("func Hello() {}"),
248+
Insert: "after",
249+
})
250+
if err == nil {
251+
t.Fatal("expected error for insert without new-text")
252+
}
253+
}
254+
255+
func TestApplyEdit_SymbolInsertMissingNewText(t *testing.T) {
256+
dir := t.TempDir()
257+
p := filepath.Join(dir, "f.go")
258+
os.WriteFile(p, []byte("package main\nfunc Hello() {}\n"), 0644)
259+
_, err := applyEdit(p, editRequest{
260+
Symbol: "Hello",
261+
Insert: "before",
262+
})
263+
if err == nil {
264+
t.Fatal("expected error for symbol insert without new-text")
265+
}
266+
}
267+
268+
func TestApplyEdit_SymbolReplaceMissingNewText(t *testing.T) {
269+
dir := t.TempDir()
270+
p := filepath.Join(dir, "f.go")
271+
os.WriteFile(p, []byte("package main\nfunc Hello() {}\n"), 0644)
272+
_, err := applyEdit(p, editRequest{Symbol: "Hello"})
273+
if err == nil {
274+
t.Fatal("expected error for symbol replace without new-text")
275+
}
276+
}
277+
278+
func TestUnifiedDiff_NoChange(t *testing.T) {
279+
before := []string{"a", "b", "c"}
280+
after := []string{"a", "b", "c"}
281+
if diff := unifiedDiff("f.txt", before, after); diff != "" {
282+
t.Fatalf("expected empty diff, got %q", diff)
283+
}
284+
}

cmd/sin-code/internal/map.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,15 @@ func mapArchitecture(root, action string) (*mapResult, error) {
127127
lang := detectLanguage(path)
128128
languages[lang]++
129129

130+
// Track per-module file counts and languages.
131+
dir := filepath.Dir(rel)
132+
if m, ok := modules[dir]; ok && dir != "." && dir != "" {
133+
m.Files++
134+
if !sliceContains(m.Languages, lang) {
135+
m.Languages = append(m.Languages, lang)
136+
}
137+
}
138+
130139
if strings.Contains(strings.ToLower(rel), "_test") || strings.Contains(strings.ToLower(rel), "test_") || strings.Contains(strings.ToLower(rel), ".spec.") || strings.Contains(strings.ToLower(rel), ".test.") {
131140
testFiles++
132141
}
@@ -324,6 +333,15 @@ func min(a, b int) int {
324333
return b
325334
}
326335

336+
func sliceContains(haystack []string, needle string) bool {
337+
for _, s := range haystack {
338+
if s == needle {
339+
return true
340+
}
341+
}
342+
return false
343+
}
344+
327345
func init() {
328346
RegisterVersionCmd(MapCmd)
329347
MapCmd.Flags().StringVarP(&mapAction, "action", "a", "map", "Action: map|summary|graph|hotpaths")

cmd/sin-code/internal/map_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,3 +778,45 @@ func TestMapArchitecture_LargeFileSkip(t *testing.T) {
778778
}
779779
_ = result.Summary
780780
}
781+
782+
func TestMapCmd_InvalidPath_Cli(t *testing.T) {
783+
mapFormat = "text"
784+
mapAction = "map"
785+
if err := MapCmd.RunE(MapCmd, []string{"\x00invalid"}); err == nil {
786+
t.Fatal("expected error for invalid path")
787+
}
788+
}
789+
790+
func TestMapArchitecture_HotPathsTruncated(t *testing.T) {
791+
dir := t.TempDir()
792+
os.WriteFile(filepath.Join(dir, "shared.go"), []byte("package main\nfunc Shared() {}\n"), 0644)
793+
for i := 0; i < 25; i++ {
794+
os.WriteFile(filepath.Join(dir, fmt.Sprintf("f%d.go", i)), []byte(fmt.Sprintf("package main\nimport _ \"pkg%d\"\nfunc F%d() {}\n", i, i)), 0644)
795+
}
796+
result, err := mapArchitecture(dir, "map")
797+
if err != nil {
798+
t.Fatalf("mapArchitecture failed: %v", err)
799+
}
800+
if len(result.HotPaths) > 20 {
801+
t.Errorf("expected hot paths capped at 20, got %d", len(result.HotPaths))
802+
}
803+
}
804+
805+
func TestMapArchitecture_ModulesSorted(t *testing.T) {
806+
dir := t.TempDir()
807+
os.MkdirAll(filepath.Join(dir, "a"), 0755)
808+
os.MkdirAll(filepath.Join(dir, "b"), 0755)
809+
os.WriteFile(filepath.Join(dir, "a", "a.go"), []byte("package a\n"), 0644)
810+
os.WriteFile(filepath.Join(dir, "b", "b1.go"), []byte("package b\n"), 0644)
811+
os.WriteFile(filepath.Join(dir, "b", "b2.go"), []byte("package b\n"), 0644)
812+
result, err := mapArchitecture(dir, "map")
813+
if err != nil {
814+
t.Fatalf("mapArchitecture failed: %v", err)
815+
}
816+
if len(result.Modules) < 2 {
817+
t.Fatalf("expected modules, got %d", len(result.Modules))
818+
}
819+
if result.Modules[0].Files < result.Modules[1].Files {
820+
t.Errorf("modules not sorted by file count: %v", result.Modules)
821+
}
822+
}

0 commit comments

Comments
 (0)