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
3 changes: 2 additions & 1 deletion cmd/sin-code/internal/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ var (
editFormat string
editSymbol string
editGetwd = os.Getwd
editWriteFile = writeFileAtomic
)

var EditCmd = &cobra.Command{
Expand Down Expand Up @@ -174,7 +175,7 @@ func applyEdit(path string, req editRequest) (*editResult, error) {
if req.DryRun {
return res, nil
}
if _, err := writeFileAtomic(path, newContent, writeOpts{validate: false}); err != nil {
if _, err := editWriteFile(path, newContent, writeOpts{validate: false}); err != nil {
return nil, err
}
return res, nil
Expand Down
284 changes: 284 additions & 0 deletions cmd/sin-code/internal/edit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
// SPDX-License-Identifier: MIT
// Purpose: coverage tests for edit.go CLI and error branches (st-cov2).
package internal

import (
"io"
"os"
"path/filepath"
"strings"
"testing"
)

func captureEditCmd(t *testing.T, args []string, setup func()) string {
t.Helper()
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
setup()
err := EditCmd.RunE(EditCmd, args)
w.Close()
os.Stdout = oldStdout
out, _ := io.ReadAll(r)
if err != nil {
t.Fatalf("EditCmd failed: %v", err)
}
return string(out)
}

func TestEditCmd_RunE_Text(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "f.go")
os.WriteFile(p, []byte("package main\n\nfunc Hello() {}\n"), 0644)

oldAnchor := editAnchor
oldNewText := editNewText
oldFormat := editFormat
oldGetwd := editGetwd
defer func() { editAnchor = oldAnchor; editNewText = oldNewText; editFormat = oldFormat; editGetwd = oldGetwd }()

editGetwd = func() (string, error) { return dir, nil }
editAnchor = "3:" + LineHash("func Hello() {}")
editNewText = "func World() {}"
editFormat = "text"

out := captureEditCmd(t, []string{"f.go"}, func() {})
if !strings.Contains(out, "edited") {
t.Errorf("expected edited output, got %q", out)
}
data, _ := os.ReadFile(p)
if !strings.Contains(string(data), "func World()") {
t.Errorf("expected edit applied, got %q", data)
}
}

func TestEditCmd_RunE_JSON(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "f.go")
os.WriteFile(p, []byte("package main\n\nfunc Hello() {}\n"), 0644)

oldAnchor := editAnchor
oldNewText := editNewText
oldFormat := editFormat
oldGetwd := editGetwd
defer func() { editAnchor = oldAnchor; editNewText = oldNewText; editFormat = oldFormat; editGetwd = oldGetwd }()

editGetwd = func() (string, error) { return dir, nil }
editAnchor = "3:" + LineHash("func Hello() {}")
editNewText = "func World() {}"
editFormat = "json"

out := captureEditCmd(t, []string{"f.go"}, func() {})
if !strings.Contains(out, "\"path\"") {
t.Errorf("expected JSON output, got %q", out)
}
}

func TestEditCmd_RunE_DryRun(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "f.go")
os.WriteFile(p, []byte("package main\n\nfunc Hello() {}\n"), 0644)

oldAnchor := editAnchor
oldNewText := editNewText
oldDryRun := editDryRun
oldGetwd := editGetwd
defer func() { editAnchor = oldAnchor; editNewText = oldNewText; editDryRun = oldDryRun; editGetwd = oldGetwd }()

editGetwd = func() (string, error) { return dir, nil }
editAnchor = "3:" + LineHash("func Hello() {}")
editNewText = "func World() {}"
editDryRun = true

out := captureEditCmd(t, []string{"f.go"}, func() {})
if !strings.Contains(out, "---") {
t.Errorf("expected diff output, got %q", out)
}
data, _ := os.ReadFile(p)
if strings.Contains(string(data), "func World()") {
t.Error("dry run must not modify the file")
}
}

func TestEditCmd_RunE_InvalidPath(t *testing.T) {
if err := EditCmd.RunE(EditCmd, []string{"\x00invalid"}); err == nil {
t.Fatal("expected error for invalid path")
}
}

func TestApplyAnchorEdit_ParseAnchorError(t *testing.T) {
_, err := applyAnchorEdit([]string{"a"}, editRequest{Anchor: "bad"}, &editResult{})
if err == nil {
t.Fatal("expected error for invalid anchor")
}
}

func TestApplyAnchorEdit_ResolveError(t *testing.T) {
_, err := applyAnchorEdit([]string{"a"}, editRequest{Anchor: "1:deadbeef"}, &editResult{})
if err == nil {
t.Fatal("expected error for unresolvable anchor")
}
}

func TestApplyAnchorEdit_EndAnchorParseError(t *testing.T) {
lines := []string{"a", "b", "c"}
_, err := applyAnchorEdit(lines, editRequest{
Anchor: "1:" + LineHash("a"),
EndAnchor: "bad",
}, &editResult{})
if err == nil {
t.Fatal("expected error for invalid end anchor")
}
}

func TestApplyAnchorEdit_EndAnchorResolveError(t *testing.T) {
lines := []string{"a", "b", "c"}
_, err := applyAnchorEdit(lines, editRequest{
Anchor: "1:" + LineHash("a"),
EndAnchor: "2:deadbeef",
}, &editResult{})
if err == nil {
t.Fatal("expected error for unresolvable end anchor")
}
}

func TestApplyAnchorEdit_InvalidInsert_Cli(t *testing.T) {
lines := []string{"a", "b", "c"}
_, err := applyAnchorEdit(lines, editRequest{
Anchor: "1:" + LineHash("a"),

Check failure on line 148 in cmd/sin-code/internal/edit_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

File is not properly formatted (gofmt)
NewText: "x",
Insert: "middle",
}, &editResult{})
if err == nil {
t.Fatal("expected error for invalid insert")
}
}

func TestApplyStringEdit_OldStringNotFound(t *testing.T) {
_, err := applyStringEdit([]string{"a"}, "abc", editRequest{OldString: "x"}, &editResult{}, new(bool))
if err == nil {
t.Fatal("expected error when old string not found")
}
}

func TestApplyStringEdit_ReplaceAll(t *testing.T) {
res := &editResult{}
updated, err := applyStringEdit([]string{"x", "x"}, "x\nx\n", editRequest{OldString: "x", NewString: "y", ReplaceAll: true}, res, new(bool))
if err != nil {
t.Fatal(err)
}
if len(updated) != 2 || updated[0] != "y" || updated[1] != "y" {
t.Fatalf("expected all replaced, got %v", updated)
}
}

func TestApplySymbolEdit_NoEngine(t *testing.T) {
_, err := applySymbolEdit([]string{"x"}, "x.txt", "x", editRequest{Symbol: "foo"}, &editResult{})
if err == nil {
t.Fatal("expected error for unsupported language")
}
}

func TestApplySymbolEdit_NotFound_Cli(t *testing.T) {
p := filepath.Join(t.TempDir(), "f.go")
os.WriteFile(p, []byte("package main\nfunc Hello() {}\n"), 0644)
data, _ := os.ReadFile(p)
_, err := applySymbolEdit([]string{"package main", "func Hello() {}"}, p, string(data), editRequest{Symbol: "Missing"}, &editResult{})
if err == nil {
t.Fatal("expected error for missing symbol")
}
}

func TestApplySymbolEdit_Ambiguous_Cli(t *testing.T) {
p := filepath.Join(t.TempDir(), "f.go")
os.WriteFile(p, []byte("package main\nfunc Hello() {}\nfunc Hello() {}\n"), 0644)
data, _ := os.ReadFile(p)
_, err := applySymbolEdit([]string{"package main", "func Hello() {}", "func Hello() {}"}, p, string(data), editRequest{Symbol: "Hello"}, &editResult{})
if err == nil {
t.Fatal("expected error for ambiguous symbol")
}
}

func TestApplySymbolEdit_InvalidInsert_Cli(t *testing.T) {
p := filepath.Join(t.TempDir(), "f.go")
os.WriteFile(p, []byte("package main\nfunc Hello() {}\n"), 0644)
data, _ := os.ReadFile(p)
_, err := applySymbolEdit([]string{"package main", "func Hello() {}"}, p, string(data), editRequest{Symbol: "Hello", Insert: "middle"}, &editResult{})
if err == nil {
t.Fatal("expected error for invalid insert")
}
}

func TestApplySymbolEdit_InvalidRange(t *testing.T) {
p := filepath.Join(t.TempDir(), "f.go")
os.WriteFile(p, []byte("package main\nfunc Hello() {}\n"), 0644)
data, _ := os.ReadFile(p)
// Force an invalid range by overriding the parsed outline via a malformed file? Instead, use a small
// trick: the symbol's EndLine will be within the line slice, but the function checks startIdx<0 etc.
// We can pass a single-line content so EndLine-1 > len(lines)-1.
_, err := applySymbolEdit([]string{"package main"}, p, string(data), editRequest{Symbol: "Hello"}, &editResult{})
if err == nil {
t.Fatal("expected error for invalid range")
}
}

func TestApplyEdit_FileNotFound(t *testing.T) {
_, err := applyEdit("/nonexistent/path.go", editRequest{Anchor: "1:deadbeef"})
if err == nil {
t.Fatal("expected error for missing file")
}
}

func TestApplyEdit_SymbolError(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "x.txt")
os.WriteFile(p, []byte("hello\n"), 0644)
_, err := applyEdit(p, editRequest{Symbol: "Foo", NewText: "bar"})
if err == nil {
t.Fatal("expected error for unsupported language")
}
}

func TestApplyEdit_InsertAfterMissingNewText(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "f.go")
os.WriteFile(p, []byte("package main\nfunc Hello() {}\n"), 0644)
_, err := applyEdit(p, editRequest{
Anchor: "2:" + LineHash("func Hello() {}"),
Insert: "after",
})
if err == nil {
t.Fatal("expected error for insert without new-text")
}
}

func TestApplyEdit_SymbolInsertMissingNewText(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "f.go")
os.WriteFile(p, []byte("package main\nfunc Hello() {}\n"), 0644)
_, err := applyEdit(p, editRequest{
Symbol: "Hello",
Insert: "before",
})
if err == nil {
t.Fatal("expected error for symbol insert without new-text")
}
}

func TestApplyEdit_SymbolReplaceMissingNewText(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "f.go")
os.WriteFile(p, []byte("package main\nfunc Hello() {}\n"), 0644)
_, err := applyEdit(p, editRequest{Symbol: "Hello"})
if err == nil {
t.Fatal("expected error for symbol replace without new-text")
}
}

func TestUnifiedDiff_NoChange(t *testing.T) {
before := []string{"a", "b", "c"}
after := []string{"a", "b", "c"}
if diff := unifiedDiff("f.txt", before, after); diff != "" {
t.Fatalf("expected empty diff, got %q", diff)
}
}
18 changes: 18 additions & 0 deletions cmd/sin-code/internal/map.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,15 @@ func mapArchitecture(root, action string) (*mapResult, error) {
lang := detectLanguage(path)
languages[lang]++

// Track per-module file counts and languages.
dir := filepath.Dir(rel)
if m, ok := modules[dir]; ok && dir != "." && dir != "" {
m.Files++
if !sliceContains(m.Languages, lang) {
m.Languages = append(m.Languages, lang)
}
}

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.") {
testFiles++
}
Expand Down Expand Up @@ -324,6 +333,15 @@ func min(a, b int) int {
return b
}

func sliceContains(haystack []string, needle string) bool {
for _, s := range haystack {
if s == needle {
return true
}
}
return false
}

func init() {
RegisterVersionCmd(MapCmd)
MapCmd.Flags().StringVarP(&mapAction, "action", "a", "map", "Action: map|summary|graph|hotpaths")
Expand Down
42 changes: 42 additions & 0 deletions cmd/sin-code/internal/map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -778,3 +778,45 @@ func TestMapArchitecture_LargeFileSkip(t *testing.T) {
}
_ = result.Summary
}

func TestMapCmd_InvalidPath_Cli(t *testing.T) {
mapFormat = "text"
mapAction = "map"
if err := MapCmd.RunE(MapCmd, []string{"\x00invalid"}); err == nil {
t.Fatal("expected error for invalid path")
}
}

func TestMapArchitecture_HotPathsTruncated(t *testing.T) {
dir := t.TempDir()
os.WriteFile(filepath.Join(dir, "shared.go"), []byte("package main\nfunc Shared() {}\n"), 0644)
for i := 0; i < 25; i++ {
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)
}
result, err := mapArchitecture(dir, "map")
if err != nil {
t.Fatalf("mapArchitecture failed: %v", err)
}
if len(result.HotPaths) > 20 {
t.Errorf("expected hot paths capped at 20, got %d", len(result.HotPaths))
}
}

func TestMapArchitecture_ModulesSorted(t *testing.T) {
dir := t.TempDir()
os.MkdirAll(filepath.Join(dir, "a"), 0755)
os.MkdirAll(filepath.Join(dir, "b"), 0755)
os.WriteFile(filepath.Join(dir, "a", "a.go"), []byte("package a\n"), 0644)
os.WriteFile(filepath.Join(dir, "b", "b1.go"), []byte("package b\n"), 0644)
os.WriteFile(filepath.Join(dir, "b", "b2.go"), []byte("package b\n"), 0644)
result, err := mapArchitecture(dir, "map")
if err != nil {
t.Fatalf("mapArchitecture failed: %v", err)
}
if len(result.Modules) < 2 {
t.Fatalf("expected modules, got %d", len(result.Modules))
}
if result.Modules[0].Files < result.Modules[1].Files {
t.Errorf("modules not sorted by file count: %v", result.Modules)
}
}
Loading
Loading