Files
pyscn/e2e/dead_code_e2e_test.go
DaisukeYoda 2da26eda28 chore: format code with go fmt
Run go fmt on all Go files to ensure consistent formatting

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-13 15:51:51 +09:00

451 lines
12 KiB
Go

package e2e
import (
"bytes"
"encoding/json"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
// TestDeadCodeE2EBasic tests basic dead code analysis command
func TestDeadCodeE2EBasic(t *testing.T) {
// Build the binary first
binaryPath := buildPyscnBinary(t)
defer os.Remove(binaryPath)
// Create test directory with Python files containing dead code
testDir := t.TempDir()
createTestPythonFile(t, testDir, "dead_code.py", `
def function_with_dead_code():
x = 42
return x
print("This is dead code after return")
unreachable_var = "never executed"
def conditional_dead_code(value):
if value > 0:
return value
else:
return 0
print("Unreachable code after complete if-else")
def function_with_break():
while True:
break
print("Dead code after break")
def function_with_continue():
for i in range(10):
continue
print("Dead code after continue")
def function_with_raise():
raise ValueError("Error")
print("Dead code after raise")
`)
// Run pyscn deadcode command
cmd := exec.Command(binaryPath, "deadcode", testDir)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
t.Fatalf("Command failed: %v\nStderr: %s", err, stderr.String())
}
output := stdout.String()
// Verify output contains expected dead code findings
if !strings.Contains(output, "Dead Code Analysis Report") {
t.Error("Output should contain 'Dead Code Analysis Report' header")
}
if !strings.Contains(output, "function_with_dead_code") {
t.Error("Output should contain 'function_with_dead_code'")
}
if !strings.Contains(output, "High") {
t.Error("Output should contain critical severity findings")
}
if !strings.Contains(output, "unreachable") {
t.Error("Output should mention unreachable code")
}
}
// TestDeadCodeE2EJSONOutput tests JSON output format
func TestDeadCodeE2EJSONOutput(t *testing.T) {
binaryPath := buildPyscnBinary(t)
defer os.Remove(binaryPath)
testDir := t.TempDir()
createTestPythonFile(t, testDir, "simple_dead.py", `
def simple_function():
return 42
print("Dead code") # This should be detected
`)
// Run with JSON format (outputs to file in temp directory)
outputDir := t.TempDir() // Create separate temp directory for output
// Create a temporary config file to specify output directory
createTestConfigFile(t, testDir, outputDir)
cmd := exec.Command(binaryPath, "deadcode", "--json", testDir)
cmd.Dir = testDir // Set working directory to ensure config file discovery works
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
t.Fatalf("Command failed: %v\nStderr: %s", err, stderr.String())
}
// Find the generated JSON file in outputDir
files, err := filepath.Glob(filepath.Join(outputDir, "deadcode_*.json"))
if err != nil || len(files) == 0 {
// List all files in outputDir for debugging
allFiles, _ := os.ReadDir(outputDir)
var fileNames []string
for _, f := range allFiles {
fileNames = append(fileNames, f.Name())
}
t.Fatalf("No JSON file generated in %s, files present: %v", outputDir, fileNames)
}
// Read and verify JSON file content
jsonContent, err := os.ReadFile(files[0])
if err != nil {
t.Fatalf("Failed to read JSON file: %v", err)
}
// No need to clean up - t.TempDir() handles it automatically
// Verify JSON output is valid
var result map[string]interface{}
if err := json.Unmarshal(jsonContent, &result); err != nil {
t.Fatalf("Invalid JSON output: %v\nContent: %s", err, string(jsonContent))
}
// Check that JSON contains expected structure
if _, ok := result["files"]; !ok {
t.Error("JSON output should contain 'files' field")
}
if _, ok := result["summary"]; !ok {
t.Error("JSON output should contain 'summary' field")
}
if _, ok := result["warnings"]; !ok {
t.Error("JSON output should contain 'warnings' field")
}
if _, ok := result["errors"]; !ok {
t.Error("JSON output should contain 'errors' field")
}
if _, ok := result["generated_at"]; !ok {
t.Error("JSON output should contain 'generated_at' field")
}
if _, ok := result["version"]; !ok {
t.Error("JSON output should contain 'version' field")
}
}
// TestDeadCodeE2EFlags tests various command line flags
func TestDeadCodeE2EFlags(t *testing.T) {
binaryPath := buildPyscnBinary(t)
defer os.Remove(binaryPath)
testDir := t.TempDir()
outputDir := t.TempDir()
// Create config file to control output directory
createTestConfigFile(t, testDir, outputDir)
createTestPythonFile(t, testDir, "flagtest.py", `
def critical_dead_code():
return "alive"
print("CRITICAL: Dead code after return") # Critical severity
def warning_dead_code(x):
if x == 1:
return x
elif x == 2:
return x
# Potential unreachable else (Warning severity)
print("WARNING: This might be unreachable")
`)
tests := []struct {
name string
args []string
shouldPass bool
}{
{
name: "min severity critical",
args: []string{"deadcode", "--min-severity", "critical", testDir},
shouldPass: true,
},
{
name: "show context",
args: []string{"deadcode", "--show-context", "--context-lines", "2", testDir},
shouldPass: true,
},
{
name: "help flag",
args: []string{"deadcode", "--help"},
shouldPass: true,
},
{
name: "yaml format",
args: []string{"deadcode", "--yaml", "--no-open", testDir},
shouldPass: true,
},
{
name: "csv format",
args: []string{"deadcode", "--csv", "--no-open", testDir},
shouldPass: true,
},
{
name: "sort by line",
args: []string{"deadcode", "--sort", "line", testDir},
shouldPass: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := exec.Command(binaryPath, tt.args...)
cmd.Dir = testDir // Set working directory to ensure config file discovery works
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if tt.shouldPass && err != nil {
t.Errorf("Command should pass but failed: %v\nStderr: %s", err, stderr.String())
} else if !tt.shouldPass && err == nil {
t.Error("Command should fail but passed")
}
})
}
}
// TestDeadCodeE2ESeverityFiltering tests severity filtering functionality
func TestDeadCodeE2ESeverityFiltering(t *testing.T) {
binaryPath := buildPyscnBinary(t)
defer os.Remove(binaryPath)
testDir := t.TempDir()
createTestPythonFile(t, testDir, "severity.py", `
def critical_example():
return 1
print("CRITICAL dead code after return")
def warning_example(x):
if False:
print("WARNING unreachable branch")
return x
`)
// Test critical severity only
cmd := exec.Command(binaryPath, "deadcode", "--min-severity", "critical", testDir)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
t.Fatalf("Command failed: %v\nStderr: %s", err, stderr.String())
}
output := stdout.String()
if !strings.Contains(output, "High") {
t.Error("Output should contain critical severity findings")
}
// Test all severities (default: warning and above)
cmd2 := exec.Command(binaryPath, "deadcode", "--min-severity", "info", testDir)
var stdout2, stderr2 bytes.Buffer
cmd2.Stdout = &stdout2
cmd2.Stderr = &stderr2
err2 := cmd2.Run()
if err2 != nil {
t.Fatalf("Command failed: %v\nStderr: %s", err2, stderr2.String())
}
output2 := stdout2.String()
// Should contain findings when info level is specified
if !strings.Contains(output2, "critical_example") {
t.Error("Output should contain critical_example function")
}
// The output should contain some analysis results
if !strings.Contains(output2, "Total Files: 1") {
t.Error("Output should show files were analyzed")
}
}
// TestDeadCodeE2EErrorHandling tests error scenarios
func TestDeadCodeE2EErrorHandling(t *testing.T) {
binaryPath := buildPyscnBinary(t)
defer os.Remove(binaryPath)
tests := []struct {
name string
args []string
}{
{
name: "no arguments",
args: []string{"deadcode"},
},
{
name: "nonexistent file",
args: []string{"deadcode", "/nonexistent/file.py"},
},
{
name: "directory with no Python files",
args: []string{"deadcode", "EMPTY_DIR_PLACEHOLDER"},
},
{
name: "invalid severity level",
args: []string{"deadcode", "--min-severity", "invalid", "."},
},
{
name: "invalid context lines",
args: []string{"deadcode", "--context-lines", "-5", "."},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Replace placeholder with actual empty directory
args := make([]string, len(tt.args))
copy(args, tt.args)
for i, arg := range args {
if arg == "EMPTY_DIR_PLACEHOLDER" {
args[i] = t.TempDir() // Create empty directory for this test
}
}
cmd := exec.Command(binaryPath, args...)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err == nil {
t.Error("Command should fail but passed")
}
// Should have meaningful error message
output := stderr.String() + stdout.String()
if len(output) == 0 {
t.Error("Should provide error message")
}
})
}
}
// TestDeadCodeE2EMultipleFiles tests analysis of multiple files
func TestDeadCodeE2EMultipleFiles(t *testing.T) {
binaryPath := buildPyscnBinary(t)
defer os.Remove(binaryPath)
testDir := t.TempDir()
// Create multiple Python files with different dead code patterns
createTestPythonFile(t, testDir, "file1.py", `
def func1():
return "alive"
print("Dead in file1")
`)
createTestPythonFile(t, testDir, "file2.py", `
def func2():
if True:
return 1
print("Dead in file2") # Unreachable
`)
createTestPythonFile(t, testDir, "file3.py", `
def func3():
try:
raise Exception("error")
print("Dead after raise")
except:
pass
`)
// Run dead code analysis on directory
cmd := exec.Command(binaryPath, "deadcode", testDir)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
t.Fatalf("Command failed: %v\nStderr: %s", err, stderr.String())
}
output := stdout.String()
// Should contain findings from files with actual dead code
if !strings.Contains(output, "func1") {
t.Error("Output should contain 'func1' from file1.py")
}
if !strings.Contains(output, "func3") {
t.Error("Output should contain 'func3' from file3.py")
}
if !strings.Contains(output, "file1.py") {
t.Error("Output should mention file1.py")
}
if !strings.Contains(output, "file3.py") {
t.Error("Output should mention file3.py")
}
// file2.py might not have detectable dead code with current implementation
// so we'll be more lenient about it
}
// TestDeadCodeE2EContextDisplay tests context line display functionality
func TestDeadCodeE2EContextDisplay(t *testing.T) {
binaryPath := buildPyscnBinary(t)
defer os.Remove(binaryPath)
testDir := t.TempDir()
createTestPythonFile(t, testDir, "context.py", `
def function_with_context():
# Line before dead code
x = 42
return x # This line returns
print("Dead code line") # This should be highlighted
# Line after dead code
`)
// Run with context display
cmd := exec.Command(binaryPath, "deadcode", "--show-context", "--context-lines", "2", testDir)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
t.Fatalf("Command failed: %v\nStderr: %s", err, stderr.String())
}
output := stdout.String()
// Should show context and detect dead code
if !strings.Contains(output, "function_with_context") {
t.Error("Output should contain the function name")
}
if !strings.Contains(output, "High") {
t.Error("Output should detect critical dead code")
}
// Context display might work differently than expected, so check for general structure
if !strings.Contains(output, "unreachable") {
t.Error("Output should mention unreachable code")
}
}