"""
Comprehensive unit tests for the Go code analyzer.
Tests cover:
- GoAnalyzer class initialization and methods
- Regex-based parsing for Go files
- Struct extraction with fields
- Interface extraction with methods
- Function and method extraction
- Import extraction
- Constants and variables
- Error handling with AnalysisError and ParsingError
- Edge cases and invalid syntax
"""
from unittest.mock import patch
import pytest
from src.knowledge_graph.analyzers.go import GoAnalyzer
class TestGoAnalyzer:
"""Test suite for Go code analyzer."""
def setup_method(self):
"""Set up test fixtures."""
self.analyzer = GoAnalyzer()
self.repo_path = "/test/repo"
def test_initialization(self):
"""Test analyzer initialization."""
assert self.analyzer is not None
assert len(self.analyzer.supported_extensions) == 1
assert ".go" in self.analyzer.supported_extensions
assert len(self.analyzer.patterns) > 0
def test_can_analyze_supported_extensions(self):
"""Test file analysis capability for supported extensions."""
test_cases = [
("main.go", True),
("server.go", True),
("MAIN.GO", True), # Case insensitive
("util.go", True),
]
for file_path, expected in test_cases:
assert self.analyzer.can_analyze(file_path) == expected
def test_can_analyze_unsupported_extensions(self):
"""Test file analysis capability for unsupported extensions."""
test_cases = [
("script.py", False),
("app.js", False),
("style.css", False),
("README.md", False),
("go.mod", False),
("go.sum", False),
]
for file_path, expected in test_cases:
assert self.analyzer.can_analyze(file_path) == expected
@pytest.mark.asyncio
async def test_analyze_file_simple_struct(self):
"""Test analyzing a simple Go struct."""
go_content = """
package main
type User struct {
ID int
Name string
Email string
IsActive bool
}
"""
with patch.object(self.analyzer, "read_file_content", return_value=go_content):
result = await self.analyzer.analyze_file(
"/test/user.go", self.repo_path,
)
assert result is not None
assert result["package"] == "main"
assert result["language"] == "Go"
assert len(result["structs"]) == 1
struct = result["structs"][0]
assert struct["name"] == "User"
assert struct["type"] == "struct"
assert struct["exported"] is True
assert len(struct["fields"]) == 4
# Check field details
field_names = [f["name"] for f in struct["fields"]]
assert "ID" in field_names
assert "Name" in field_names
assert "Email" in field_names
assert "IsActive" in field_names
@pytest.mark.asyncio
async def test_analyze_file_with_methods(self):
"""Test analyzing Go methods."""
go_content = """
package main
type Calculator struct {
value int
}
func (c *Calculator) Add(n int) {
c.value += n
}
func (c Calculator) GetValue() int {
return c.value
}
func (c *Calculator) Reset() {
c.value = 0
}
"""
with patch.object(self.analyzer, "read_file_content", return_value=go_content):
result = await self.analyzer.analyze_file(
"/test/calculator.go", self.repo_path,
)
assert result is not None
functions = result["functions"]
# Should have methods
methods = [f for f in functions if f["type"] == "method"]
assert len(methods) == 3
# Check method details
method_names = [m["name"] for m in methods]
assert "Add" in method_names
assert "GetValue" in method_names
assert "Reset" in method_names
# Check receiver types
for method in methods:
assert method["receiver"] == "Calculator" or method["receiver"] == "*Calculator"
@pytest.mark.asyncio
async def test_analyze_file_regular_functions(self):
"""Test analyzing regular Go functions."""
go_content = """
package utils
func Add(a, b int) int {
return a + b
}
func Multiply(x, y float64) float64 {
return x * y
}
func processData(data []string) []string {
return data
}
"""
with patch.object(self.analyzer, "read_file_content", return_value=go_content):
result = await self.analyzer.analyze_file(
"/test/utils.go", self.repo_path,
)
assert result is not None
functions = result["functions"]
# Should have regular functions
regular_funcs = [f for f in functions if f["type"] == "function"]
assert len(regular_funcs) >= 2 # Excludes unexported processData
# Check exported functions
func_names = [f["name"] for f in regular_funcs]
assert "Add" in func_names
assert "Multiply" in func_names
# Check exported status
for func in regular_funcs:
if func["name"] in ["Add", "Multiply"]:
assert func["exported"] is True
@pytest.mark.asyncio
async def test_analyze_file_interfaces(self):
"""Test analyzing Go interfaces."""
go_content = """
package main
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
"""
with patch.object(self.analyzer, "read_file_content", return_value=go_content):
result = await self.analyzer.analyze_file(
"/test/interfaces.go", self.repo_path,
)
assert result is not None
interfaces = result["interfaces"]
assert len(interfaces) == 4
# Check interface details
interface_names = [i["name"] for i in interfaces]
assert "Reader" in interface_names
assert "Writer" in interface_names
assert "ReadWriter" in interface_names
assert "Closer" in interface_names
# Check exported status
for iface in interfaces:
assert iface["exported"] is True
@pytest.mark.asyncio
async def test_analyze_file_imports(self):
"""Test analyzing Go import statements."""
go_content = """
package main
import (
"fmt"
"os"
"net/http"
"github.com/gorilla/mux"
"github.com/user/myapp/utils"
utils2 "github.com/user/myapp/utils2"
)
import "strings"
"""
with patch.object(self.analyzer, "read_file_content", return_value=go_content):
result = await self.analyzer.analyze_file(
"/test/main.go", self.repo_path,
)
assert result is not None
imports = result["imports"]
assert len(imports) >= 7
# Check import paths
paths = [imp["path"] for imp in imports]
assert "fmt" in paths
assert "os" in paths
assert "net/http" in paths
assert "github.com/gorilla/mux" in paths
assert "strings" in paths
# Check aliased import
aliased = [imp for imp in imports if imp.get("alias")]
assert len(aliased) >= 1
assert aliased[0]["alias"] == "utils2"
@pytest.mark.asyncio
async def test_analyze_file_constants(self):
"""Test analyzing Go constants."""
go_content = """
package config
const (
DefaultPort = 8080
MaxConnections = 100
ApiVersion = "v1"
)
const TimeoutSeconds = 30
"""
with patch.object(self.analyzer, "read_file_content", return_value=go_content):
result = await self.analyzer.analyze_file(
"/test/config.go", self.repo_path,
)
assert result is not None
constants = result["constants"]
assert len(constants) >= 4
# Check constant names
const_names = [c["name"] for c in constants]
assert "DefaultPort" in const_names
assert "MaxConnections" in const_names
assert "ApiVersion" in const_names
assert "TimeoutSeconds" in const_names
# All should be exported
for const in constants:
assert const["exported"] is True
@pytest.mark.asyncio
async def test_analyze_file_variables(self):
"""Test analyzing Go variables."""
go_content = """
package main
var (
AppName = "MyApp"
Version = "1.0.0"
Debug = false
)
var DefaultConfig Config
"""
with patch.object(self.analyzer, "read_file_content", return_value=go_content):
result = await self.analyzer.analyze_file(
"/test/vars.go", self.repo_path,
)
assert result is not None
variables = result["variables"]
assert len(variables) >= 3 # May not capture all variable formats
# Check variable names
var_names = [v["name"] for v in variables]
assert "AppName" in var_names
assert "Version" in var_names
assert "Debug" in var_names
# DefaultConfig may not be captured by the regex
@pytest.mark.asyncio
async def test_analyze_file_type_aliases(self):
"""Test analyzing Go type aliases."""
go_content = """
package types
type UserID string
type Age int
type Score float64
type Handler func(int) error
"""
with patch.object(self.analyzer, "read_file_content", return_value=go_content):
result = await self.analyzer.analyze_file(
"/test/types.go", self.repo_path,
)
assert result is not None
types = result["types"]
assert len(types) >= 4
# Check type aliases
type_names = [t["name"] for t in types]
assert "UserID" in type_names
assert "Age" in type_names
assert "Score" in type_names
assert "Handler" in type_names
@pytest.mark.asyncio
async def test_analyze_file_exports(self):
"""Test extracting exported symbols."""
go_content = """
package api
type PublicStruct struct {
Field string
}
type privateStruct struct {
field string
}
func PublicFunction() {}
func privateFunction() {}
type PublicInterface interface {
Method()
}
type UserID string
type userId int
"""
with patch.object(self.analyzer, "read_file_content", return_value=go_content):
result = await self.analyzer.analyze_file(
"/test/api.go", self.repo_path,
)
assert result is not None
exports = result["exports"]
# Should only include capitalized (exported) symbols
assert "PublicStruct" in exports
assert "PublicFunction" in exports
assert "PublicInterface" in exports
assert "UserID" in exports
# Should NOT include lowercase (unexported) symbols
assert "privateStruct" not in exports
assert "privateFunction" not in exports
assert "userId" not in exports
@pytest.mark.asyncio
async def test_analyze_file_dependencies(self):
"""Test dependency extraction from imports."""
go_content = """
package main
import (
"fmt" // Standard library
"os" // Standard library
"github.com/gin-gonic/gin"
"github.com/user/project/utils"
"example.com/package"
)
"""
with patch.object(self.analyzer, "read_file_content", return_value=go_content):
result = await self.analyzer.analyze_file(
"/test/main.go", self.repo_path,
)
assert result is not None
dependencies = result["dependencies"]
# Should include external packages (with dots or github.com)
assert "github.com/gin-gonic/gin" in dependencies
assert "github.com/user/project" in dependencies
# Package extraction may split at first level
assert "example.com" in dependencies or "example.com/package" in dependencies
# Should NOT include standard library imports (no dots)
assert "fmt" not in dependencies
assert "os" not in dependencies
@pytest.mark.asyncio
async def test_analyze_file_struct_with_tags(self):
"""Test analyzing structs with field tags."""
go_content = """
package models
type User struct {
ID int `json:"id" db:"user_id"`
Name string `json:"name" validate:"required"`
Email string `json:"email,omitempty"`
CreatedAt int64 `json:"created_at"`
}
"""
with patch.object(self.analyzer, "read_file_content", return_value=go_content):
result = await self.analyzer.analyze_file(
"/test/models.go", self.repo_path,
)
assert result is not None
structs = result["structs"]
assert len(structs) == 1
struct = structs[0]
assert len(struct["fields"]) == 4
@pytest.mark.asyncio
async def test_analyze_file_nested_structs(self):
"""Test analyzing nested structs."""
go_content = """
package models
type Address struct {
Street string
City string
State string
}
type User struct {
Name string
Address Address
Emails []string
}
"""
with patch.object(self.analyzer, "read_file_content", return_value=go_content):
result = await self.analyzer.analyze_file(
"/test/nested.go", self.repo_path,
)
assert result is not None
structs = result["structs"]
assert len(structs) == 2
# Check struct names
struct_names = [s["name"] for s in structs]
assert "Address" in struct_names
assert "User" in struct_names
@pytest.mark.asyncio
async def test_analyze_file_with_generics(self):
"""Test analyzing Go code with generics (Go 1.18+)."""
go_content = """
package utils
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() T {
if len(s.items) == 0 {
var zero T
return zero
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item
}
"""
with patch.object(self.analyzer, "read_file_content", return_value=go_content):
result = await self.analyzer.analyze_file(
"/test/generics.go", self.repo_path,
)
assert result is not None
# Should handle generics without crashing
assert result["structs"] is not None
assert result["functions"] is not None
@pytest.mark.asyncio
async def test_analyze_file_read_failure(self):
"""Test handling of file read failures."""
with patch.object(self.analyzer, "read_file_content", return_value=None):
result = await self.analyzer.analyze_file(
"/test/nonexistent.go", self.repo_path,
)
# Should return empty result
assert result is not None
assert result["file_path"] == "/test/nonexistent.go"
assert result["structs"] == []
assert result["functions"] == []
assert result["interfaces"] == []
@pytest.mark.asyncio
async def test_analyze_file_with_syntax_errors(self):
"""Test handling of files with syntax errors."""
invalid_go = """
package broken
func incomplete(
// Missing closing parenthesis
"""
with patch.object(self.analyzer, "read_file_content", return_value=invalid_go):
result = await self.analyzer.analyze_file(
"/test/broken.go", self.repo_path,
)
# Should not raise exception, returns whatever it can parse
assert result is not None
@pytest.mark.asyncio
async def test_analyze_file_with_content_provided(self):
"""Test analyzing with content provided directly."""
go_content = """
package main
func Hello() string {
return "world"
}
"""
result = await self.analyzer.analyze_file(
"/test/hello.go", self.repo_path, content=go_content,
)
assert result is not None
assert result["package"] == "main"
assert len(result["functions"]) >= 1
def test_extract_package(self):
"""Test package name extraction."""
go_content = """
package mypackage
import "fmt"
"""
package_name = self.analyzer._extract_package(go_content)
assert package_name == "mypackage"
def test_extract_package_missing(self):
"""Test package extraction when missing."""
go_content = "// No package declaration"
package_name = self.analyzer._extract_package(go_content)
assert package_name == ""
def test_extract_block_simple(self):
"""Test extracting code blocks."""
content = """{
x := 1
y := 2
}"""
block = self.analyzer._extract_block(content)
assert block.startswith("{")
assert block.endswith("}")
assert "x := 1" in block
def test_extract_block_nested(self):
"""Test extracting nested code blocks."""
content = """{
if true {
fmt.Println("nested")
}
}"""
block = self.analyzer._extract_block(content)
assert block.count("{") == block.count("}")
assert "nested" in block
def test_extract_block_with_strings(self):
"""Test extracting blocks with string literals containing braces."""
content = """{
msg := "Hello {world}"
fmt.Println(msg)
}"""
block = self.analyzer._extract_block(content)
assert block.startswith("{")
assert block.endswith("}")
assert "Hello {world}" in block
def test_extract_block_empty(self):
"""Test extracting from empty content."""
block = self.analyzer._extract_block("")
assert block == ""
block = self.analyzer._extract_block("no braces here")
assert block == ""
@pytest.mark.asyncio
async def test_empty_file(self):
"""Test analyzing an empty Go file."""
go_content = ""
with patch.object(self.analyzer, "read_file_content", return_value=go_content):
result = await self.analyzer.analyze_file(
"/test/empty.go", self.repo_path,
)
assert result is not None
assert len(result["structs"]) == 0
assert len(result["functions"]) == 0
assert len(result["interfaces"]) == 0
@pytest.mark.asyncio
async def test_file_with_only_comments(self):
"""Test analyzing a file with only comments."""
go_content = """
// This is a comment file
/* Multi-line comment
with multiple lines
*/
"""
with patch.object(self.analyzer, "read_file_content", return_value=go_content):
result = await self.analyzer.analyze_file(
"/test/comments.go", self.repo_path,
)
assert result is not None
assert len(result["structs"]) == 0
assert len(result["functions"]) == 0
@pytest.mark.asyncio
async def test_complex_go_file(self):
"""Test analyzing a complex Go file with all features."""
go_content = """
package server
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
const (
DefaultPort = 8080
APIVersion = "v1"
)
var (
ServerName = "MyServer"
Debug = false
)
type Config struct {
Port int
Timeout int
}
type Server interface {
Start() error
Stop() error
}
type HTTPServer struct {
config Config
router *gin.Engine
}
func NewHTTPServer(config Config) *HTTPServer {
return &HTTPServer{
config: config,
router: gin.Default(),
}
}
func (s *HTTPServer) Start() error {
addr := fmt.Sprintf(":%d", s.config.Port)
return s.router.Run(addr)
}
func (s *HTTPServer) Stop() error {
return nil
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}
type HandlerFunc func(http.ResponseWriter, *http.Request)
"""
with patch.object(self.analyzer, "read_file_content", return_value=go_content):
result = await self.analyzer.analyze_file(
"/test/server.go", self.repo_path,
)
assert result is not None
assert result["package"] == "server"
assert len(result["imports"]) >= 3
assert len(result["constants"]) >= 2
assert len(result["variables"]) >= 2
assert len(result["structs"]) >= 2
assert len(result["interfaces"]) >= 1
assert len(result["functions"]) >= 4 # Methods + regular functions
assert len(result["types"]) >= 1 # HandlerFunc
# Check exports - only exported symbols (types, functions, etc)
exports = result["exports"]
assert "Config" in exports
assert "Server" in exports
assert "HTTPServer" in exports
assert "NewHTTPServer" in exports
# Constants and variables may not be included in exports list
# The _extract_exports only includes structs, interfaces, functions, and types
@pytest.mark.asyncio
async def test_analyze_file_pointer_receivers(self):
"""Test analyzing methods with pointer vs value receivers."""
go_content = """
package main
type Counter struct {
count int
}
func (c *Counter) Increment() {
c.count++
}
func (c Counter) GetCount() int {
return c.count
}
"""
with patch.object(self.analyzer, "read_file_content", return_value=go_content):
result = await self.analyzer.analyze_file(
"/test/counter.go", self.repo_path,
)
assert result is not None
functions = result["functions"]
methods = [f for f in functions if f["type"] == "method"]
assert len(methods) == 2
# Check receiver types
increment = next(m for m in methods if m["name"] == "Increment")
assert increment["receiver"] == "*Counter"
get_count = next(m for m in methods if m["name"] == "GetCount")
assert get_count["receiver"] == "Counter"
@pytest.mark.asyncio
async def test_analyze_file_unexported_symbols(self):
"""Test that unexported symbols are properly identified."""
go_content = """
package internal
type PublicStruct struct {
PublicField string
privateField int
}
type privateStruct struct {
field string
}
func PublicFunc() {}
func privateFunc() {}
const PublicConst = 1
const privateConst = 2
var PublicVar = "public"
var privateVar = "private"
"""
with patch.object(self.analyzer, "read_file_content", return_value=go_content):
result = await self.analyzer.analyze_file(
"/test/internal.go", self.repo_path,
)
assert result is not None
# Check exported status
for struct in result["structs"]:
if struct["name"] == "PublicStruct":
assert struct["exported"] is True
elif struct["name"] == "privateStruct":
assert struct["exported"] is False
for func in result["functions"]:
if func["name"] == "PublicFunc":
assert func["exported"] is True
elif func["name"] == "privateFunc":
assert func["exported"] is False