mirror of
https://github.com/ludo-technologies/pyscn.git
synced 2025-10-06 00:59:45 +03:00
* chore: standardize project name to pyscn throughout codebase - Fix project name inconsistency from pyqol to pyscn in all files - Update HTML report titles and branding to pyscn - Rename config file: pyqol.yaml.example → .pyscn.yaml.example - Update test function names and documentation references - Clean copyright attribution in LICENSE file - Remove temporary files (.DS_Store, coverage files, venv) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: clean up CHANGELOG and remove legacy beta versions - Update CHANGELOG.md to reflect current state with v0.1.0-beta.13 - Remove references to deleted beta versions (beta.1-12, b7) - Add explanation note about removed versions with distribution issues - Update installation instructions to use latest beta version 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: DaisukeYoda <daisukeyoda@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
450 lines
12 KiB
Go
450 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 Detection Results") {
|
|
t.Error("Output should contain 'Dead Code Detection Results' header")
|
|
}
|
|
if !strings.Contains(output, "function_with_dead_code") {
|
|
t.Error("Output should contain 'function_with_dead_code'")
|
|
}
|
|
if !strings.Contains(output, "CRITICAL") {
|
|
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, "CRITICAL") {
|
|
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, "Files analyzed: 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, "CRITICAL") {
|
|
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")
|
|
}
|
|
} |