Files
pyscn/integration/clone_integration_test.go
DaisukeYoda 245e75788c test: update test expectations for unified report format
- Update E2E tests to expect "Clone Detection Analysis Report" instead of "Clone Detection Results"
- Update E2E tests to expect "Dead Code Analysis Report" instead of "Dead Code Detection Results"
- Update severity expectations from "CRITICAL" to "High" in dead code tests
- Update statistics label expectations in integration tests
- All tests now pass with the unified report format

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 00:26:15 +09:00

452 lines
15 KiB
Go

package integration
import (
"bytes"
"context"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ludo-technologies/pyscn/app"
"github.com/ludo-technologies/pyscn/domain"
"github.com/ludo-technologies/pyscn/internal/constants"
"github.com/ludo-technologies/pyscn/service"
)
// TestCloneDetectionIntegration tests the complete clone detection workflow
func TestCloneDetectionIntegration(t *testing.T) {
// Create services with real implementations
fileReader := service.NewFileReader()
outputFormatter := service.NewCloneOutputFormatter()
configLoader := service.NewCloneConfigurationLoader()
cloneService := service.NewCloneService()
// Create use case with real dependencies
useCase, err := app.NewCloneUseCaseBuilder().
WithService(cloneService).
WithFileReader(fileReader).
WithFormatter(outputFormatter).
WithConfigLoader(configLoader).
Build()
require.NoError(t, err, "Should create use case successfully")
// Create test request
var outputBuffer bytes.Buffer
request := domain.CloneRequest{
Paths: []string{"../testdata/python/simple"}, // Use existing test data
Recursive: true,
IncludePatterns: []string{"*.py"},
ExcludePatterns: []string{"*test*.py"},
MinLines: 3,
MinNodes: 5,
SimilarityThreshold: 0.7,
Type1Threshold: constants.DefaultType1CloneThreshold,
Type2Threshold: constants.DefaultType2CloneThreshold,
Type3Threshold: constants.DefaultType3CloneThreshold,
Type4Threshold: constants.DefaultType4CloneThreshold,
OutputFormat: domain.OutputFormatText,
OutputWriter: &outputBuffer,
ShowDetails: true,
GroupClones: false,
MaxEditDistance: 50.0,
CloneTypes: []domain.CloneType{domain.Type1Clone, domain.Type2Clone, domain.Type3Clone, domain.Type4Clone},
}
// Execute the use case
ctx := context.Background()
err = useCase.Execute(ctx, request)
// The test might fail due to missing file reader implementation
// but we should test the structure and workflow
if err != nil {
// Check if it's an expected error (like file not found)
t.Logf("Expected error during integration test: %v", err)
// Verify error handling
assert.Error(t, err, "Should handle errors gracefully")
return
}
// If successful, verify the output
output := outputBuffer.String()
assert.NotEmpty(t, output, "Should produce output")
// Basic output validation
assert.Contains(t, output, "Clone Detection Analysis Report", "Should contain results header")
}
// TestCloneUseCaseBuilder tests the builder pattern for creating use cases
func TestCloneUseCaseBuilder(t *testing.T) {
builder := app.NewCloneUseCaseBuilder()
// Test building without required dependencies
_, err := builder.Build()
assert.Error(t, err, "Should fail when required dependencies are missing")
assert.Contains(t, err.Error(), "clone service is required", "Should specify missing service")
// Test building with all dependencies
fileReader := service.NewFileReader()
outputFormatter := service.NewCloneOutputFormatter()
configLoader := service.NewCloneConfigurationLoader()
cloneService := service.NewCloneService()
useCase, err := builder.
WithService(cloneService).
WithFileReader(fileReader).
WithFormatter(outputFormatter).
WithConfigLoader(configLoader).
Build()
assert.NoError(t, err, "Should build successfully with all dependencies")
assert.NotNil(t, useCase, "Should return valid use case")
}
// TestCloneServiceWithMockData tests the clone service with mock data
func TestCloneServiceWithMockData(t *testing.T) {
cloneService := service.NewCloneService()
// Test computing similarity between code fragments
fragment1 := `def hello_world():
print("Hello, World!")
return True`
fragment2 := `def hello_world():
print("Hello, World!")
return True`
fragment3 := `def goodbye_world():
print("Goodbye, World!")
return False`
ctx := context.Background()
// Test identical fragments
similarity, err := cloneService.ComputeSimilarity(ctx, fragment1, fragment2)
if err != nil {
// Expected if parser is not fully implemented
t.Logf("Parser not implemented, skipping similarity test: %v", err)
return
}
assert.Equal(t, 1.0, similarity, "Identical fragments should have similarity of 1.0")
// Test different fragments
similarity, err = cloneService.ComputeSimilarity(ctx, fragment1, fragment3)
if err == nil {
assert.Less(t, similarity, 1.0, "Different fragments should have similarity < 1.0")
assert.Greater(t, similarity, 0.0, "Different fragments should have similarity > 0.0")
}
}
// TestCloneOutputFormatterIntegration tests the output formatter with different formats
func TestCloneOutputFormatterIntegration(t *testing.T) {
formatter := service.NewCloneOutputFormatter()
// Create sample response
location1 := &domain.CloneLocation{
FilePath: "/test/file1.py",
StartLine: 1,
EndLine: 10,
StartCol: 1,
EndCol: 20,
}
location2 := &domain.CloneLocation{
FilePath: "/test/file2.py",
StartLine: 15,
EndLine: 24,
StartCol: 1,
EndCol: 20,
}
clone1 := &domain.Clone{
ID: 1,
Type: domain.Type1Clone,
Location: location1,
Size: 20,
LineCount: 10,
}
clone2 := &domain.Clone{
ID: 2,
Type: domain.Type1Clone,
Location: location2,
Size: 18,
LineCount: 10,
}
clonePair := &domain.ClonePair{
ID: 1,
Clone1: clone1,
Clone2: clone2,
Similarity: constants.DefaultType1CloneThreshold,
Distance: 1.0,
Type: domain.Type1Clone,
Confidence: 0.92,
}
statistics := &domain.CloneStatistics{
TotalClones: 2,
TotalClonePairs: 1,
TotalCloneGroups: 0,
ClonesByType: map[string]int{"Type-1": 1},
AverageSimilarity: 0.95,
LinesAnalyzed: 500,
FilesAnalyzed: 2,
}
response := &domain.CloneResponse{
Clones: []*domain.Clone{clone1, clone2},
ClonePairs: []*domain.ClonePair{clonePair},
CloneGroups: []*domain.CloneGroup{},
Statistics: statistics,
Duration: 1000,
Success: true,
}
// Test text format
var textBuffer bytes.Buffer
err := formatter.FormatCloneResponse(response, domain.OutputFormatText, &textBuffer)
assert.NoError(t, err, "Should format as text without error")
textOutput := textBuffer.String()
assert.Contains(t, textOutput, "Clone Detection Analysis Report", "Should contain header")
assert.Contains(t, textOutput, "Files Analyzed: 2", "Should contain statistics")
assert.Contains(t, textOutput, "Clone Pairs: 1", "Should contain pair count")
assert.Contains(t, textOutput, "Type-1", "Should contain clone type")
assert.Contains(t, textOutput, "similarity: 0.950", "Should contain similarity")
// Test JSON format
var jsonBuffer bytes.Buffer
err = formatter.FormatCloneResponse(response, domain.OutputFormatJSON, &jsonBuffer)
assert.NoError(t, err, "Should format as JSON without error")
jsonOutput := jsonBuffer.String()
assert.Contains(t, jsonOutput, `"success": true`, "Should contain success field")
assert.Contains(t, jsonOutput, `"total_clones": 2`, "Should contain clone count")
assert.Contains(t, jsonOutput, `"similarity": 0.95`, "Should contain similarity")
// Test YAML format
var yamlBuffer bytes.Buffer
err = formatter.FormatCloneResponse(response, domain.OutputFormatYAML, &yamlBuffer)
assert.NoError(t, err, "Should format as YAML without error")
yamlOutput := yamlBuffer.String()
assert.Contains(t, yamlOutput, "success: true", "Should contain success field")
assert.Contains(t, yamlOutput, "total_clones: 2", "Should contain clone count")
// Test CSV format
var csvBuffer bytes.Buffer
err = formatter.FormatCloneResponse(response, domain.OutputFormatCSV, &csvBuffer)
assert.NoError(t, err, "Should format as CSV without error")
csvOutput := csvBuffer.String()
lines := strings.Split(csvOutput, "\n")
assert.GreaterOrEqual(t, len(lines), 2, "Should have header and data lines")
// Check CSV header
header := lines[0]
assert.Contains(t, header, "pair_id", "Should contain pair_id column")
assert.Contains(t, header, "clone_type", "Should contain clone_type column")
assert.Contains(t, header, "similarity", "Should contain similarity column")
assert.Contains(t, header, "clone1_file", "Should contain clone1_file column")
assert.Contains(t, header, "clone2_file", "Should contain clone2_file column")
}
// TestCloneConfigurationLoaderIntegration tests configuration loading and saving
func TestCloneConfigurationLoaderIntegration(t *testing.T) {
configLoader := service.NewCloneConfigurationLoader()
// Test getting default configuration
defaultConfig := configLoader.GetDefaultCloneConfig()
assert.NotNil(t, defaultConfig, "Should return default configuration")
assert.Equal(t, 5, defaultConfig.MinLines, "Default min lines should be 5")
assert.Equal(t, 10, defaultConfig.MinNodes, "Default min nodes should be 10")
assert.Equal(t, 0.8, defaultConfig.SimilarityThreshold, "Default similarity threshold should be 0.8")
// Validate default configuration
err := defaultConfig.Validate()
assert.NoError(t, err, "Default configuration should be valid")
// Test configuration merging in use case
useCase := createTestCloneUseCase(t)
// Test with empty results handling
var outputBuffer bytes.Buffer
request := domain.CloneRequest{
Paths: []string{"/nonexistent/path"},
OutputFormat: domain.OutputFormatText,
OutputWriter: &outputBuffer,
MinLines: 5,
MinNodes: 10,
SimilarityThreshold: 0.8,
Type1Threshold: constants.DefaultType1CloneThreshold,
Type2Threshold: constants.DefaultType2CloneThreshold,
Type3Threshold: constants.DefaultType3CloneThreshold,
Type4Threshold: constants.DefaultType4CloneThreshold,
MaxEditDistance: 50.0,
CloneTypes: []domain.CloneType{domain.Type1Clone, domain.Type2Clone, domain.Type3Clone, domain.Type4Clone},
}
ctx := context.Background()
err = useCase.Execute(ctx, request)
// Should handle nonexistent path gracefully
if err != nil {
assert.Contains(t, err.Error(), "collect files", "Should fail at file collection stage")
} else {
// If no error, should produce empty results
output := outputBuffer.String()
assert.Contains(t, output, "No", "Should indicate no results")
}
}
// TestCloneStatisticsIntegration tests statistics calculation
func TestCloneStatisticsIntegration(t *testing.T) {
formatter := service.NewCloneOutputFormatter()
// Create statistics
stats := &domain.CloneStatistics{
TotalClones: 10,
TotalClonePairs: 5,
TotalCloneGroups: 3,
ClonesByType: map[string]int{
"Type-1": 2,
"Type-2": 2,
"Type-3": 1,
},
AverageSimilarity: 0.87,
LinesAnalyzed: 2500,
FilesAnalyzed: 15,
}
// Test statistics formatting in different formats
formats := []domain.OutputFormat{
domain.OutputFormatText,
domain.OutputFormatJSON,
domain.OutputFormatYAML,
domain.OutputFormatCSV,
}
for _, format := range formats {
t.Run(string(format), func(t *testing.T) {
var buffer bytes.Buffer
err := formatter.FormatCloneStatistics(stats, format, &buffer)
assert.NoError(t, err, "Should format statistics without error")
output := buffer.String()
assert.NotEmpty(t, output, "Should produce output")
// Common checks for all formats
switch format {
case domain.OutputFormatText:
assert.Contains(t, output, "Clone Detection Statistics", "Should contain header")
assert.Contains(t, output, "Files analyzed: 15", "Should contain file count")
assert.Contains(t, output, "Lines analyzed: 2500", "Should contain line count")
assert.Contains(t, output, "Clone pairs: 5", "Should contain pair count")
case domain.OutputFormatJSON:
assert.Contains(t, output, `"total_clones": 10`, "Should contain clone count")
assert.Contains(t, output, `"average_similarity": 0.87`, "Should contain similarity")
case domain.OutputFormatYAML:
assert.Contains(t, output, "total_clones: 10", "Should contain clone count")
assert.Contains(t, output, "files_analyzed: 15", "Should contain file count")
case domain.OutputFormatCSV:
lines := strings.Split(output, "\n")
assert.GreaterOrEqual(t, len(lines), 2, "Should have multiple lines")
assert.Contains(t, output, "metric,value", "Should have CSV header")
}
})
}
}
// TestCloneDetectionErrorHandling tests error handling scenarios
func TestCloneDetectionErrorHandling(t *testing.T) {
useCase := createTestCloneUseCase(t)
// Test invalid request validation
invalidRequest := domain.CloneRequest{
Paths: []string{}, // Invalid: empty paths
MinLines: -1, // Invalid: negative
}
ctx := context.Background()
err := useCase.Execute(ctx, invalidRequest)
assert.Error(t, err, "Should fail validation")
assert.Contains(t, err.Error(), "validation failed", "Should indicate validation error")
// Test request with invalid thresholds
invalidThresholds := domain.CloneRequest{
Paths: []string{"/test"},
MinLines: 5,
MinNodes: 10,
Type1Threshold: 0.5, // Invalid: should be > type2_threshold
Type2Threshold: 0.8,
}
err = useCase.Execute(ctx, invalidThresholds)
assert.Error(t, err, "Should fail validation")
assert.Contains(t, err.Error(), "type1_threshold should be > type2_threshold", "Should indicate threshold error")
}
// Helper function to create test use case
func createTestCloneUseCase(t *testing.T) *app.CloneUseCase {
fileReader := service.NewFileReader()
outputFormatter := service.NewCloneOutputFormatter()
configLoader := service.NewCloneConfigurationLoader()
cloneService := service.NewCloneService()
useCase, err := app.NewCloneUseCaseBuilder().
WithService(cloneService).
WithFileReader(fileReader).
WithFormatter(outputFormatter).
WithConfigLoader(configLoader).
Build()
require.NoError(t, err, "Should create use case successfully")
return useCase
}
// TestCloneDetectionPerformance tests basic performance characteristics
func TestCloneDetectionPerformance(t *testing.T) {
// This is a basic performance test - in a real scenario, you'd want more sophisticated benchmarks
useCase := createTestCloneUseCase(t)
// Test with minimal data to ensure reasonable performance
var outputBuffer bytes.Buffer
request := domain.CloneRequest{
Paths: []string{"../testdata/python/simple"}, // Small test dataset
OutputFormat: domain.OutputFormatJSON, // Efficient format
OutputWriter: &outputBuffer,
MinLines: 10, // Higher threshold for faster processing
MinNodes: 20,
SimilarityThreshold: 0.9, // Higher threshold for faster processing
Type1Threshold: 0.98,
Type2Threshold: constants.DefaultType1CloneThreshold,
Type3Threshold: 0.90,
Type4Threshold: 0.85,
MaxEditDistance: 10.0, // Lower distance for faster processing
CloneTypes: []domain.CloneType{domain.Type1Clone}, // Only one type for speed
}
ctx := context.Background()
// Measure execution time
// start := time.Now()
err := useCase.Execute(ctx, request)
// duration := time.Since(start)
if err != nil {
// Expected for integration test environment
t.Logf("Performance test skipped due to missing test data: %v", err)
return
}
// Basic performance assertion - should complete within reasonable time
// assert.Less(t, duration, 5*time.Second, "Should complete within 5 seconds for small dataset")
// Verify output is produced
output := outputBuffer.String()
assert.NotEmpty(t, output, "Should produce output")
}