306 lines
11 KiB
Python
306 lines
11 KiB
Python
"""
|
|
Tests for VCS workflow auto-detection logic
|
|
"""
|
|
|
|
import unittest
|
|
from datetime import datetime, timedelta
|
|
from typing import Dict, List, Tuple
|
|
|
|
|
|
class MockGitRepo:
|
|
"""Mock Git repository for testing detection logic"""
|
|
|
|
def __init__(self):
|
|
self.branches = []
|
|
self.commits = []
|
|
self.tags = []
|
|
|
|
def add_branch(self, name: str, created_days_ago: int, deleted_days_ago: int = None):
|
|
self.branches.append({
|
|
'name': name,
|
|
'created': datetime.now() - timedelta(days=created_days_ago),
|
|
'deleted': datetime.now() - timedelta(days=deleted_days_ago) if deleted_days_ago else None
|
|
})
|
|
|
|
def add_commit(self, branch: str, message: str, days_ago: int):
|
|
self.commits.append({
|
|
'branch': branch,
|
|
'message': message,
|
|
'date': datetime.now() - timedelta(days=days_ago)
|
|
})
|
|
|
|
def add_tag(self, name: str, days_ago: int):
|
|
self.tags.append({
|
|
'name': name,
|
|
'date': datetime.now() - timedelta(days=days_ago)
|
|
})
|
|
|
|
|
|
class WorkflowDetector:
|
|
"""VCS workflow detection implementation"""
|
|
|
|
def __init__(self, repo: MockGitRepo):
|
|
self.repo = repo
|
|
self.confidence_threshold = 0.7
|
|
|
|
def detect(self) -> Tuple[str, float, List[str]]:
|
|
"""
|
|
Detect workflow type with confidence score
|
|
Returns: (workflow_type, confidence, evidence_list)
|
|
"""
|
|
scores = {
|
|
'gitflow': self._detect_gitflow(),
|
|
'github_flow': self._detect_github_flow(),
|
|
'trunk_based': self._detect_trunk_based()
|
|
}
|
|
|
|
# Find workflow with highest score
|
|
best_workflow = max(scores.items(), key=lambda x: x[1][0])
|
|
workflow_type = best_workflow[0]
|
|
confidence = best_workflow[1][0]
|
|
evidence = best_workflow[1][1]
|
|
|
|
if confidence < self.confidence_threshold:
|
|
workflow_type = 'unclear'
|
|
|
|
return workflow_type, confidence, evidence
|
|
|
|
def _detect_gitflow(self) -> Tuple[float, List[str]]:
|
|
"""Detect GitFlow indicators"""
|
|
score = 0.0
|
|
evidence = []
|
|
|
|
# Check for develop branch
|
|
develop_branches = [b for b in self.repo.branches
|
|
if b['name'] in ['develop', 'development']]
|
|
if develop_branches:
|
|
score += 0.3
|
|
evidence.append("Found develop branch")
|
|
|
|
# Check for release branches
|
|
release_branches = [b for b in self.repo.branches
|
|
if b['name'].startswith('release/')]
|
|
if release_branches:
|
|
score += 0.3
|
|
evidence.append(f"Found {len(release_branches)} release branches")
|
|
|
|
# Check for hotfix branches
|
|
hotfix_branches = [b for b in self.repo.branches
|
|
if b['name'].startswith('hotfix/')]
|
|
if hotfix_branches:
|
|
score += 0.2
|
|
evidence.append("Found hotfix branches")
|
|
|
|
# Check for version tags
|
|
version_tags = [t for t in self.repo.tags
|
|
if t['name'].startswith('v')]
|
|
if version_tags:
|
|
score += 0.2
|
|
evidence.append(f"Found {len(version_tags)} version tags")
|
|
|
|
return score, evidence
|
|
|
|
def _detect_github_flow(self) -> Tuple[float, List[str]]:
|
|
"""Detect GitHub Flow indicators"""
|
|
score = 0.0
|
|
evidence = []
|
|
|
|
# Check for PR merge patterns
|
|
pr_merges = [c for c in self.repo.commits
|
|
if 'Merge pull request' in c['message'] or 'Merge PR' in c['message']]
|
|
if pr_merges:
|
|
score += 0.3
|
|
evidence.append(f"Found {len(pr_merges)} PR merges")
|
|
|
|
# Check for short-lived feature branches
|
|
feature_branches = [b for b in self.repo.branches
|
|
if b['name'].startswith('feature/')]
|
|
if feature_branches:
|
|
short_lived = [b for b in feature_branches
|
|
if b['deleted'] and (b['deleted'] - b['created']).days < 7]
|
|
if short_lived:
|
|
score += 0.3
|
|
evidence.append(f"{len(short_lived)} feature branches < 7 days")
|
|
|
|
# Check for squash-merge patterns
|
|
squash_merges = [c for c in self.repo.commits
|
|
if '(#' in c['message']] # Common squash merge pattern
|
|
if squash_merges:
|
|
score += 0.2
|
|
evidence.append("Found squash-merge patterns")
|
|
|
|
# No develop branch is a positive signal
|
|
if not any(b['name'] in ['develop', 'development'] for b in self.repo.branches):
|
|
score += 0.2
|
|
evidence.append("No develop branch")
|
|
|
|
return score, evidence
|
|
|
|
def _detect_trunk_based(self) -> Tuple[float, List[str]]:
|
|
"""Detect Trunk-Based Development indicators"""
|
|
score = 0.0
|
|
evidence = []
|
|
|
|
# Check for direct commits to main
|
|
main_commits = [c for c in self.repo.commits
|
|
if c['branch'] in ['main', 'master']]
|
|
total_commits = len(self.repo.commits)
|
|
|
|
if total_commits > 0:
|
|
main_ratio = len(main_commits) / total_commits
|
|
if main_ratio > 0.5:
|
|
score += 0.4
|
|
evidence.append(f"{int(main_ratio * 100)}% commits directly to main")
|
|
|
|
# Check for very short-lived branches
|
|
all_branches = [b for b in self.repo.branches
|
|
if b['deleted'] and not b['name'] in ['main', 'master', 'develop']]
|
|
if all_branches:
|
|
very_short = [b for b in all_branches
|
|
if (b['deleted'] - b['created']).days < 1]
|
|
if len(very_short) > len(all_branches) * 0.5:
|
|
score += 0.3
|
|
evidence.append(f"{len(very_short)} branches lived < 1 day")
|
|
|
|
# Check for feature flags (simplified check)
|
|
feature_flag_commits = [c for c in self.repo.commits
|
|
if 'feature flag' in c['message'].lower() or
|
|
'feature toggle' in c['message'].lower()]
|
|
if feature_flag_commits:
|
|
score += 0.3
|
|
evidence.append("Found feature flag usage")
|
|
|
|
return score, evidence
|
|
|
|
def detect_migration(self, days_threshold: int = 30) -> Dict:
|
|
"""Detect if workflow has changed recently"""
|
|
recent_date = datetime.now() - timedelta(days=days_threshold)
|
|
historical_date = datetime.now() - timedelta(days=days_threshold * 3)
|
|
|
|
# Split commits into periods
|
|
recent_commits = [c for c in self.repo.commits
|
|
if c['date'] > recent_date]
|
|
historical_commits = [c for c in self.repo.commits
|
|
if historical_date < c['date'] <= recent_date]
|
|
|
|
# Simplified: check if branch patterns changed
|
|
recent_branches = set(c['branch'] for c in recent_commits)
|
|
historical_branches = set(c['branch'] for c in historical_commits)
|
|
|
|
if recent_branches != historical_branches:
|
|
return {
|
|
'migration_detected': True,
|
|
'recent_pattern': list(recent_branches),
|
|
'historical_pattern': list(historical_branches)
|
|
}
|
|
|
|
return {'migration_detected': False}
|
|
|
|
|
|
class TestVCSDetection(unittest.TestCase):
|
|
"""Test cases for VCS workflow detection"""
|
|
|
|
def test_detect_gitflow(self):
|
|
"""Test GitFlow detection with high confidence"""
|
|
repo = MockGitRepo()
|
|
repo.add_branch('develop', 365)
|
|
repo.add_branch('release/1.0', 30, 10)
|
|
repo.add_branch('release/1.1', 15, 5)
|
|
repo.add_branch('hotfix/urgent-fix', 5, 3)
|
|
repo.add_branch('feature/new-feature', 20, 7)
|
|
repo.add_tag('v1.0.0', 30)
|
|
repo.add_tag('v1.1.0', 15)
|
|
|
|
detector = WorkflowDetector(repo)
|
|
workflow, confidence, evidence = detector.detect()
|
|
|
|
self.assertEqual(workflow, 'gitflow')
|
|
self.assertGreaterEqual(confidence, 0.7)
|
|
self.assertIn('Found develop branch', evidence)
|
|
self.assertIn('release branches', ' '.join(evidence))
|
|
|
|
def test_detect_github_flow(self):
|
|
"""Test GitHub Flow detection with high confidence"""
|
|
repo = MockGitRepo()
|
|
repo.add_branch('main', 365)
|
|
repo.add_branch('feature/quick-fix', 5, 3)
|
|
repo.add_branch('feature/new-ui', 10, 6)
|
|
repo.add_commit('main', 'Merge pull request #123', 3)
|
|
repo.add_commit('main', 'Merge pull request #124', 5)
|
|
repo.add_commit('main', 'feat: Add new feature (#125)', 7)
|
|
|
|
detector = WorkflowDetector(repo)
|
|
workflow, confidence, evidence = detector.detect()
|
|
|
|
self.assertEqual(workflow, 'github_flow')
|
|
self.assertGreaterEqual(confidence, 0.5)
|
|
self.assertIn('PR merges', ' '.join(evidence))
|
|
|
|
def test_detect_trunk_based(self):
|
|
"""Test Trunk-Based Development detection"""
|
|
repo = MockGitRepo()
|
|
repo.add_branch('main', 365)
|
|
repo.add_branch('fix-123', 2, 1) # Very short-lived
|
|
repo.add_branch('update-456', 1, 0.5)
|
|
|
|
# Many direct commits to main
|
|
for i in range(20):
|
|
repo.add_commit('main', f'feat: Direct commit {i}', i)
|
|
|
|
# Few branch commits
|
|
repo.add_commit('fix-123', 'fix: Quick fix', 2)
|
|
repo.add_commit('main', 'chore: Enable feature flag for new UI', 5)
|
|
|
|
detector = WorkflowDetector(repo)
|
|
workflow, confidence, evidence = detector.detect()
|
|
|
|
self.assertEqual(workflow, 'trunk_based')
|
|
self.assertIn('commits directly to main', ' '.join(evidence))
|
|
|
|
def test_detect_unclear_workflow(self):
|
|
"""Test detection with low confidence returns 'unclear'"""
|
|
repo = MockGitRepo()
|
|
repo.add_branch('main', 365)
|
|
# Very minimal activity
|
|
repo.add_commit('main', 'Initial commit', 300)
|
|
|
|
detector = WorkflowDetector(repo)
|
|
workflow, confidence, evidence = detector.detect()
|
|
|
|
self.assertEqual(workflow, 'unclear')
|
|
self.assertLess(confidence, 0.7)
|
|
|
|
def test_detect_migration(self):
|
|
"""Test workflow migration detection"""
|
|
repo = MockGitRepo()
|
|
|
|
# Historical: GitFlow pattern
|
|
repo.add_commit('develop', 'feat: Old feature', 60)
|
|
repo.add_commit('release/1.0', 'chore: Release prep', 50)
|
|
|
|
# Recent: GitHub Flow pattern
|
|
repo.add_commit('main', 'Merge pull request #200', 10)
|
|
repo.add_commit('main', 'Merge pull request #201', 5)
|
|
|
|
detector = WorkflowDetector(repo)
|
|
migration = detector.detect_migration()
|
|
|
|
self.assertTrue(migration['migration_detected'])
|
|
self.assertIn('develop', migration['historical_pattern'])
|
|
self.assertNotIn('develop', migration['recent_pattern'])
|
|
|
|
def test_confidence_scoring(self):
|
|
"""Test confidence score calculation"""
|
|
repo = MockGitRepo()
|
|
repo.add_branch('develop', 365) # 0.3 points for GitFlow
|
|
repo.add_branch('release/1.0', 30, 10) # 0.3 points for GitFlow
|
|
|
|
detector = WorkflowDetector(repo)
|
|
workflow, confidence, evidence = detector.detect()
|
|
|
|
self.assertAlmostEqual(confidence, 0.6, places=1)
|
|
self.assertEqual(len(evidence), 2)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main() |