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

import (
"go/ast"
"go/parser"
"go/token"
"testing"
)

func parseSingleFunc(t *testing.T, src string) (*token.FileSet, *ast.FuncDecl) {
t.Helper()

fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "sample.go", src, 0)
if err != nil {
t.Fatalf("parse source: %v", err)
}

for _, decl := range file.Decls {
if fn, ok := decl.(*ast.FuncDecl); ok {
return fset, fn
}
}
t.Fatal("source does not contain function declaration")
return nil, nil
}

func hashFuncFromSource(t *testing.T, src string) FuncInfo {
t.Helper()

fset, fn := parseSingleFunc(t, src)
return New(fset).HashFunc("sample", "sample.go", fn)
}

func TestHashFuncNormalizesIdentifierAndLiteralNames(t *testing.T) {
a := hashFuncFromSource(t, `package sample
func validateUser(user string) error {
if user == "" {
return errors.New("empty user name")
}
return nil
}`)

b := hashFuncFromSource(t, `package sample
func validateOrder(order string) error {
if order == "abc" {
return fmt.New("empty order code")
}
return nil
}`)

if a.TopHash != b.TopHash {
t.Fatalf("expected structurally equivalent functions to have same top hash: %d != %d", a.TopHash, b.TopHash)
}
}

func TestHashFuncPreservesOperators(t *testing.T) {
a := hashFuncFromSource(t, `package sample
func add(a, b int) int {
c := a + b
d := c + b
return d
}`)

b := hashFuncFromSource(t, `package sample
func sub(a, b int) int {
c := a - b
d := c + b
return d
}`)

if a.TopHash == b.TopHash {
t.Fatal("expected different operators to produce different top hashes")
}
}

func TestHashFuncPreservesStatementOrder(t *testing.T) {
a := hashFuncFromSource(t, `package sample
func first() int {
a := 1
if a > 0 {
return a
}
return 0
}`)

b := hashFuncFromSource(t, `package sample
func second() int {
if a > 0 {
return a
}
a := 1
return 0
}`)

if a.TopHash == b.TopHash {
t.Fatal("expected statement order to affect top hash")
}
}

func TestHashFuncInfoMetadata(t *testing.T) {
info := hashFuncFromSource(t, `package sample
func add(a, b int) int {
c := a + b
d := c + b
return d
}`)

if info.Name != "sample.add" {
t.Fatalf("Name = %q, want %q", info.Name, "sample.add")
}
if info.File != "sample.go" {
t.Fatalf("File = %q, want sample.go", info.File)
}
if info.Line != 2 {
t.Fatalf("Line = %d, want 2", info.Line)
}
if info.NumStmts != 3 {
t.Fatalf("NumStmts = %d, want 3", info.NumStmts)
}
if len(info.StmtSeq) != info.NumStmts {
t.Fatalf("len(StmtSeq) = %d, want %d", len(info.StmtSeq), info.NumStmts)
}
if info.NumLines != 5 {
t.Fatalf("NumLines = %d, want 5", info.NumLines)
}
}

func TestQualifiedName(t *testing.T) {
tests := []struct {
name string
src string
want string
}{
{
name: "function",
src: `package sample
func Run() {}`,
want: "sample.Run",
},
{
name: "value receiver",
src: `package sample
func (s Store) Run() {}`,
want: "sample.(Store).Run",
},
{
name: "pointer receiver",
src: `package sample
func (s *Store) Run() {}`,
want: "sample.(*Store).Run",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, fn := parseSingleFunc(t, tt.src)
if got := qualifiedName("sample", fn); got != tt.want {
t.Fatalf("qualifiedName() = %q, want %q", got, tt.want)
}
})
}
}
51 changes: 51 additions & 0 deletions internal/hash/similarity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package hash

import "testing"

func TestSimilarity(t *testing.T) {
tests := []struct {
name string
a []uint64
b []uint64
want float64
}{
{name: "both empty", a: nil, b: nil, want: 1.0},
{name: "one empty", a: []uint64{1}, b: nil, want: 0.0},
{name: "identical", a: []uint64{1, 2, 3}, b: []uint64{1, 2, 3}, want: 1.0},
{name: "one insertion", a: []uint64{1, 2, 3}, b: []uint64{1, 9, 2, 3}, want: 0.75},
{name: "one substitution", a: []uint64{1, 2, 3, 4}, b: []uint64{1, 2, 9, 4}, want: 0.75},
{name: "completely different same length", a: []uint64{1, 2, 3}, b: []uint64{4, 5, 6}, want: 0.0},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Similarity(&FuncInfo{StmtSeq: tt.a}, &FuncInfo{StmtSeq: tt.b})
if got != tt.want {
t.Fatalf("Similarity(%v, %v) = %v, want %v", tt.a, tt.b, got, tt.want)
}
})
}
}

func TestEditDistance(t *testing.T) {
tests := []struct {
name string
a []uint64
b []uint64
want int
}{
{name: "same", a: []uint64{1, 2, 3}, b: []uint64{1, 2, 3}, want: 0},
{name: "insert", a: []uint64{1, 3}, b: []uint64{1, 2, 3}, want: 1},
{name: "delete", a: []uint64{1, 2, 3}, b: []uint64{1, 3}, want: 1},
{name: "substitute", a: []uint64{1, 2, 3}, b: []uint64{1, 9, 3}, want: 1},
{name: "empty", a: nil, b: []uint64{1, 2}, want: 2},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := editDistance(tt.a, tt.b); got != tt.want {
t.Fatalf("editDistance(%v, %v) = %d, want %d", tt.a, tt.b, got, tt.want)
}
})
}
}
174 changes: 174 additions & 0 deletions internal/load/load_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package load

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

func writeFile(t *testing.T, path, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("mkdir %s: %v", filepath.Dir(path), err)
}
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatalf("write %s: %v", path, err)
}
}

func TestLoadDirectoryRecursively(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "a.go"), `package sample
func One() int {
a := 1
b := 2
return a + b
}`)
writeFile(t, filepath.Join(dir, "nested", "b.go"), `package sample
func Two() int {
a := 1
b := 2
return a + b
}`)

result, err := Load([]string{dir}, false)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if len(result.Funcs) != 2 {
t.Fatalf("len(Funcs) = %d, want 2", len(result.Funcs))
}
if result.Fset == nil {
t.Fatal("Fset is nil")
}
}

func TestLoadExcludesTests(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "a.go"), `package sample
func One() int {
a := 1
b := 2
return a + b
}`)
writeFile(t, filepath.Join(dir, "a_test.go"), `package sample
func TestOne() int {
a := 1
b := 2
return a + b
}`)

result, err := Load([]string{dir}, true)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if len(result.Funcs) != 1 {
t.Fatalf("len(Funcs) = %d, want 1", len(result.Funcs))
}
if got := result.Funcs[0].Name; got != "sample.One" {
t.Fatalf("loaded function = %q, want sample.One", got)
}
}

func TestLoadIncludesTestsWhenNotExcluded(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "a.go"), `package sample
func One() int {
a := 1
b := 2
return a + b
}`)
writeFile(t, filepath.Join(dir, "a_test.go"), `package sample
func TestOne() int {
a := 1
b := 2
return a + b
}`)

result, err := Load([]string{dir}, false)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if len(result.Funcs) != 2 {
t.Fatalf("len(Funcs) = %d, want 2", len(result.Funcs))
}
}

func TestLoadSkipsShortFunctionsAndUnparseableFiles(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "short.go"), `package sample
func Short() int {
return 1
}`)
writeFile(t, filepath.Join(dir, "bad.go"), `package sample
func Broken(`)
writeFile(t, filepath.Join(dir, "good.go"), `package sample
func Good() int {
a := 1
b := 2
return a + b
}`)

result, err := Load([]string{dir}, false)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if len(result.Funcs) != 1 {
t.Fatalf("len(Funcs) = %d, want 1", len(result.Funcs))
}
if got := result.Funcs[0].Name; got != "sample.Good" {
t.Fatalf("loaded function = %q, want sample.Good", got)
}
}

func TestLoadSkipsHiddenAndVendorDirectories(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "good.go"), `package sample
func Good() int {
a := 1
b := 2
return a + b
}`)
writeFile(t, filepath.Join(dir, ".hidden", "hidden.go"), `package hidden
func Hidden() int {
a := 1
b := 2
return a + b
}`)
writeFile(t, filepath.Join(dir, "vendor", "vendored.go"), `package vendor
func Vendored() int {
a := 1
b := 2
return a + b
}`)

result, err := Load([]string{dir}, false)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if len(result.Funcs) != 1 {
t.Fatalf("len(Funcs) = %d, want 1", len(result.Funcs))
}
if got := result.Funcs[0].Name; got != "sample.Good" {
t.Fatalf("loaded function = %q, want sample.Good", got)
}
}

func TestLoadSingleFile(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "single.go")
writeFile(t, file, `package sample
func Single() int {
a := 1
b := 2
return a + b
}`)

result, err := Load([]string{file}, false)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if len(result.Funcs) != 1 {
t.Fatalf("len(Funcs) = %d, want 1", len(result.Funcs))
}
}
Loading