mirror of
https://github.com/ludo-technologies/pyscn.git
synced 2025-10-06 00:59:45 +03:00
- 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>
452 lines
15 KiB
Go
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")
|
|
}
|