fancygit 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,441 @@
1
+ import pytest
2
+ from unittest.mock import patch, MagicMock, call
3
+ import sys
4
+ import os
5
+
6
+ # Add project root to path for imports
7
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
8
+
9
+ from fancygit import FancyGit
10
+
11
+ @pytest.mark.integration
12
+ class TestFancyGitCommandWorkflows:
13
+ """Integration tests for complete command workflows"""
14
+
15
+ def setup_method(self):
16
+ """Setup method called before each test"""
17
+ with patch.object(FancyGit, '_load_commands', return_value=[
18
+ 'add', 'commit', 'push', 'pull', 'status', 'branch', 'checkout',
19
+ 'merge', 'rebase', 'reset', 'log', 'diff', 'stash', 'rm', 'mv', 'revert',
20
+ 'welcome', 'confirmation', 'ai', 'colors', 'insights', 'visualize'
21
+ ]):
22
+ with patch.object(FancyGit, '_load_confirmation_state', return_value=False):
23
+ with patch.object(FancyGit, '_load_ai_analysis_state', return_value=False):
24
+ with patch.object(FancyGit, '_load_output_coloring_state', return_value=False):
25
+ with patch.object(FancyGit, '_load_animation_type', return_value='dots'):
26
+ self.fancy_git = FancyGit()
27
+
28
+ @patch('src.git_runner.GitRunner.run_git_command')
29
+ def test_complete_add_commit_workflow(self, mock_run_git):
30
+ """Test complete workflow: add -> commit -> push"""
31
+ # Mock the sequence of git commands
32
+ mock_run_git.side_effect = [
33
+ (0, "", ""), # git add .
34
+ (0, "[main 1234567] Initial commit\n 1 file changed, 1 insertion(+)", ""), # git commit
35
+ (0, "Enumerating objects: 3, done.\nTo github.com:user/repo.git\n 1234567..abcdefg main -> main", "") # git push
36
+ ]
37
+
38
+ # Execute workflow
39
+ add_result = self.fancy_git.execute_command('add', '.')
40
+ commit_result = self.fancy_git.execute_command('commit', '-m', 'Initial commit')
41
+ push_result = self.fancy_git.execute_command('push', 'origin', 'main')
42
+
43
+ # Verify all commands succeeded
44
+ assert add_result is True
45
+ assert commit_result is True
46
+ assert push_result is True
47
+
48
+ # Verify the correct sequence of calls
49
+ expected_calls = [
50
+ call(['add', '.']),
51
+ call(['commit', '-m', 'Initial commit']),
52
+ call(['push', 'origin', 'main'])
53
+ ]
54
+ mock_run_git.assert_has_calls(expected_calls)
55
+
56
+ @patch('src.git_runner.GitRunner.run_git_command')
57
+ def test_branch_creation_and_checkout_workflow(self, mock_run_git):
58
+ """Test workflow: create branch -> checkout -> work -> merge back"""
59
+ mock_run_git.side_effect = [
60
+ (0, "", ""), # git branch feature-branch
61
+ (0, "Switched to branch 'feature-branch'", ""), # git checkout feature-branch
62
+ (0, "", ""), # git add .
63
+ (0, "[feature-branch abcdef123] Add feature\n 1 file changed, 1 insertion(+)", ""), # git commit
64
+ (0, "Switched to branch 'main'", ""), # git checkout main
65
+ (0, "Merge made by the 'recursive' strategy.\n 1 file changed, 1 insertion(+)", ""), # git merge feature-branch
66
+ ]
67
+
68
+ # Execute workflow
69
+ branch_result = self.fancy_git.execute_command('branch', 'feature-branch')
70
+ checkout_result = self.fancy_git.execute_command('checkout', 'feature-branch')
71
+ add_result = self.fancy_git.execute_command('add', '.')
72
+ commit_result = self.fancy_git.execute_command('commit', '-m', 'Add feature')
73
+ checkout_main_result = self.fancy_git.execute_command('checkout', 'main')
74
+ merge_result = self.fancy_git.execute_command('merge', 'feature-branch')
75
+
76
+ # Verify all commands succeeded
77
+ assert all(result is True for result in [
78
+ branch_result, checkout_result, add_result,
79
+ commit_result, checkout_main_result, merge_result
80
+ ])
81
+
82
+ # Verify the correct sequence of calls
83
+ expected_calls = [
84
+ call(['branch', 'feature-branch']),
85
+ call(['checkout', 'feature-branch']),
86
+ call(['add', '.']),
87
+ call(['commit', '-m', 'Add feature']),
88
+ call(['checkout', 'main']),
89
+ call(['merge', 'feature-branch'])
90
+ ]
91
+ mock_run_git.assert_has_calls(expected_calls)
92
+
93
+ @patch('src.git_runner.GitRunner.run_git_command')
94
+ def test_stash_workflow(self, mock_run_git):
95
+ """Test workflow: stash changes -> pull -> stash pop"""
96
+ mock_run_git.side_effect = [
97
+ (0, "Saved working directory and index state WIP on main: 1234567 Work in progress", ""), # git stash push
98
+ (0, "Already up to date.", ""), # git pull origin main
99
+ (0, "On branch main\nChanges not staged for commit:\n modified: test.py", ""), # git stash pop
100
+ ]
101
+
102
+ # Execute workflow
103
+ stash_result = self.fancy_git.execute_command('stash', 'push', '-m', 'Work in progress')
104
+ pull_result = self.fancy_git.execute_command('pull', 'origin', 'main')
105
+ stash_pop_result = self.fancy_git.execute_command('stash', 'pop')
106
+
107
+ # Verify all commands succeeded
108
+ assert stash_result is True
109
+ assert pull_result is True
110
+ assert stash_pop_result is True
111
+
112
+ # Verify the correct sequence of calls
113
+ expected_calls = [
114
+ call(['stash', 'push', '-m', 'Work in progress']),
115
+ call(['pull', 'origin', 'main']),
116
+ call(['stash', 'pop'])
117
+ ]
118
+ mock_run_git.assert_has_calls(expected_calls)
119
+
120
+ @patch('src.git_runner.GitRunner.run_git_command')
121
+ def test_reset_and_revert_workflow(self, mock_run_git):
122
+ """Test workflow: reset hard -> revert commit"""
123
+ mock_run_git.side_effect = [
124
+ (0, "HEAD is now at 1234567 Previous commit", ""), # git reset --hard HEAD~1
125
+ (0, "[main abcdef123] Revert \"Previous commit\"\n 1 file changed, 1 deletion(-)", ""), # git revert HEAD
126
+ ]
127
+
128
+ # Execute workflow
129
+ reset_result = self.fancy_git.execute_command('reset', '--hard', 'HEAD~1')
130
+ revert_result = self.fancy_git.execute_command('revert', 'HEAD')
131
+
132
+ # Verify all commands succeeded
133
+ assert reset_result is True
134
+ assert revert_result is True
135
+
136
+ # Verify the correct sequence of calls
137
+ expected_calls = [
138
+ call(['reset', '--hard', 'HEAD~1']),
139
+ call(['revert', 'HEAD'])
140
+ ]
141
+ mock_run_git.assert_has_calls(expected_calls)
142
+
143
+ @patch('src.git_runner.GitRunner.run_git_command')
144
+ def test_conflict_resolution_workflow(self, mock_run_git):
145
+ """Test workflow: handle merge conflicts"""
146
+ mock_run_git.side_effect = [
147
+ (1, "", "error: Merge conflict in README.md\nAutomatic merge failed; fix conflicts and then commit the result."), # git merge feature-branch
148
+ (0, "Unstaged changes after reset:\nM\tREADME.md", ""), # git reset --merge
149
+ (0, "Merge made by the 'recursive' strategy.\n 1 file changed, 1 insertion(+)", ""), # git merge feature-branch (after fixing conflicts)
150
+ ]
151
+
152
+ # Execute workflow
153
+ merge_conflict_result = self.fancy_git.execute_command('merge', 'feature-branch')
154
+ reset_result = self.fancy_git.execute_command('reset', '--merge')
155
+ merge_success_result = self.fancy_git.execute_command('merge', 'feature-branch')
156
+
157
+ # Verify conflict detection and resolution
158
+ assert merge_conflict_result is False # First merge should fail
159
+ assert reset_result is True
160
+ assert merge_success_result is True # Second merge should succeed
161
+
162
+ # Verify the correct sequence of calls
163
+ expected_calls = [
164
+ call(['merge', 'feature-branch']),
165
+ call(['reset', '--merge']),
166
+ call(['merge', 'feature-branch'])
167
+ ]
168
+ mock_run_git.assert_has_calls(expected_calls)
169
+
170
+ @patch('src.git_runner.GitRunner.run_git_command')
171
+ def test_file_operations_workflow(self, mock_run_git):
172
+ """Test workflow: file rename, modify, remove"""
173
+ mock_run_git.side_effect = [
174
+ (0, "Renaming 'old.txt' to 'new.txt'", ""), # git mv old.txt new.txt
175
+ (0, "", ""), # git add new.txt
176
+ (0, "[main 1234567] Rename file\n 1 file changed, 0 insertions(+), 0 deletions(-)\n rename old.txt => new.txt (100%)", ""), # git commit
177
+ (0, "rm 'new.txt'", ""), # git rm new.txt
178
+ (0, "[main abcdef123] Remove file\n 1 file changed, 1 deletion(-)", ""), # git commit
179
+ ]
180
+
181
+ # Execute workflow
182
+ mv_result = self.fancy_git.execute_command('mv', 'old.txt', 'new.txt')
183
+ add_result = self.fancy_git.execute_command('add', 'new.txt')
184
+ commit_mv_result = self.fancy_git.execute_command('commit', '-m', 'Rename file')
185
+ rm_result = self.fancy_git.execute_command('rm', 'new.txt')
186
+ commit_rm_result = self.fancy_git.execute_command('commit', '-m', 'Remove file')
187
+
188
+ # Verify all commands succeeded
189
+ assert all(result is True for result in [
190
+ mv_result, add_result, commit_mv_result, rm_result, commit_rm_result
191
+ ])
192
+
193
+ # Verify the correct sequence of calls
194
+ expected_calls = [
195
+ call(['mv', 'old.txt', 'new.txt']),
196
+ call(['add', 'new.txt']),
197
+ call(['commit', '-m', 'Rename file']),
198
+ call(['rm', 'new.txt']),
199
+ call(['commit', '-m', 'Remove file'])
200
+ ]
201
+ mock_run_git.assert_has_calls(expected_calls)
202
+
203
+ @pytest.mark.integration
204
+ class TestFancyGitSpecialCommandWorkflows:
205
+ """Integration tests for special command workflows"""
206
+
207
+ def setup_method(self):
208
+ """Setup method called before each test"""
209
+ with patch.object(FancyGit, '_load_commands', return_value=[
210
+ 'add', 'commit', 'push', 'pull', 'status', 'branch', 'checkout',
211
+ 'merge', 'rebase', 'reset', 'log', 'diff', 'stash', 'rm', 'mv',
212
+ 'welcome', 'confirmation', 'ai', 'colors', 'insights', 'visualize'
213
+ ]):
214
+ with patch.object(FancyGit, '_load_confirmation_state', return_value=False):
215
+ with patch.object(FancyGit, '_load_ai_analysis_state', return_value=False):
216
+ with patch.object(FancyGit, '_load_output_coloring_state', return_value=False):
217
+ with patch.object(FancyGit, '_load_animation_type', return_value='dots'):
218
+ self.fancy_git = FancyGit()
219
+
220
+ @patch('src.git_runner.GitRunner.run_git_command')
221
+ @patch('src.ollama_client.OllamaClient.test_connection')
222
+ @patch('src.ollama_client.OllamaClient.analyze_error_messages')
223
+ def test_ai_error_analysis_workflow(self, mock_analyze, mock_test_connection, mock_run_git):
224
+ """Test AI error analysis workflow"""
225
+ mock_test_connection.return_value = True
226
+ mock_analyze.return_value = "This error occurs when the file doesn't exist. Check the file path and try again."
227
+ mock_run_git.return_value = (1, "", "error: pathspec 'nonexistent.txt' did not match any files")
228
+
229
+ # Enable AI analysis
230
+ ai_enable_result = self.fancy_git.execute_command('ai', 'on')
231
+ assert ai_enable_result is True
232
+
233
+ # Execute command that produces error
234
+ with patch('src.loading_animation.LoadingContext'):
235
+ add_result = self.fancy_git.execute_command('add', 'nonexistent.txt')
236
+
237
+ # Verify AI analysis was triggered
238
+ assert add_result is False
239
+ mock_analyze.assert_called_once()
240
+
241
+ @patch('src.git_runner.GitRunner.run_git_command')
242
+ @patch('src.output_colorizer.OutputColorizer.colorize_output')
243
+ def test_output_coloring_workflow(self, mock_colorize, mock_run_git):
244
+ """Test output coloring workflow"""
245
+ mock_run_git.return_value = (0, "On branch main\nnothing to commit, working tree clean", "")
246
+ mock_colorize.return_value = ("On branch main\nnothing to commit, working tree clean", "")
247
+
248
+ # Enable output coloring
249
+ colors_enable_result = self.fancy_git.execute_command('colors', 'on')
250
+ assert colors_enable_result is True
251
+
252
+ # Execute command
253
+ status_result = self.fancy_git.execute_command('status')
254
+
255
+ # Verify output coloring was applied
256
+ assert status_result is True
257
+ mock_colorize.assert_called_once()
258
+
259
+ @patch('src.git_insights.GitInsights.generate_insights_report')
260
+ @patch('src.git_runner.GitRunner.run_git_command')
261
+ def test_insights_workflow(self, mock_run_git, mock_generate):
262
+ """Test insights generation workflow"""
263
+ mock_run_git.return_value = (0, "On branch main", "")
264
+ mock_generate.return_value = {
265
+ 'generated_at': '2024-01-01T12:00:00Z',
266
+ 'analysis_period_days': 30,
267
+ 'summary': {'total_commits': 10, 'active_contributors': 2, 'active_branches': 3, 'total_branches': 5, 'total_files_changed': 15},
268
+ 'commit_frequency': {'Alice': 6, 'Bob': 4},
269
+ 'branch_analysis': {
270
+ 'main': {'type': 'local', 'status': 'active', 'days_inactive': 0},
271
+ 'feature-branch': {'type': 'local', 'status': 'active', 'days_inactive': 5},
272
+ 'old-branch': {'type': 'local', 'status': 'stale', 'days_inactive': 30}
273
+ },
274
+ 'file_hotspots': {'src/main.py': 8, 'README.md': 4, 'tests/test_main.py': 3},
275
+ 'contributor_stats': {
276
+ 'Alice': {'commits': 6, 'lines_added': 150, 'lines_removed': 50},
277
+ 'Bob': {'commits': 4, 'lines_added': 80, 'lines_removed': 30}
278
+ }
279
+ }
280
+
281
+ # Generate insights
282
+ insights_result = self.fancy_git.execute_command('insights', '--days=30')
283
+
284
+ # Verify insights generation
285
+ assert insights_result is True
286
+ mock_generate.assert_called_once_with(30)
287
+
288
+ @patch('src.mermaid_export.MermaidExporter.export_all')
289
+ @patch('src.git_runner.GitRunner.run_git_command')
290
+ @patch('webbrowser.open')
291
+ def test_visualization_workflow(self, mock_browser, mock_run_git, mock_export):
292
+ """Test repository visualization workflow"""
293
+ mock_run_git.return_value = (0, "On branch main", "")
294
+ mock_export.return_value = {
295
+ 'status_mmd': '.fancygit/status.mmd',
296
+ 'graph_mmd': '.fancygit/graph.mmd',
297
+ 'tree_mmd': '.fancygit/tree.mmd',
298
+ 'deps_mmd': '.fancygit/deps.mmd',
299
+ 'html': '.fancygit/repo_visualization.html'
300
+ }
301
+
302
+ # Generate visualization
303
+ with patch.object(self.fancy_git, 'get_repo_state', return_value={'branch': 'main', 'clean': True}):
304
+ viz_result = self.fancy_git.execute_command('visualize', '--max-commits=20')
305
+
306
+ # Verify visualization generation
307
+ assert viz_result is True
308
+ mock_export.assert_called_once()
309
+ mock_browser.assert_called_once()
310
+
311
+ @patch('builtins.input')
312
+ @patch('src.git_runner.GitRunner.run_git_command')
313
+ def test_confirmation_workflow(self, mock_run_git, mock_input):
314
+ """Test confirmation workflow"""
315
+ mock_run_git.return_value = (0, "On branch main", "")
316
+ mock_input.return_value = 'y' # User confirms the command
317
+
318
+ # Enable confirmation for this test
319
+ self.fancy_git.confirmation_enabled = True
320
+
321
+ # Execute command with confirmation
322
+ status_result = self.fancy_git.execute_command('status')
323
+
324
+ # Verify confirmation was requested and command executed
325
+ assert status_result is True
326
+ mock_input.assert_called_once()
327
+ mock_run_git.assert_called_once_with(['status'])
328
+
329
+ @patch('builtins.input')
330
+ @patch('src.git_runner.GitRunner.run_git_command')
331
+ def test_confirmation_cancel_workflow(self, mock_run_git, mock_input):
332
+ """Test confirmation cancellation workflow"""
333
+ mock_input.return_value = 'n' # User cancels the command
334
+
335
+ # Enable confirmation for this test
336
+ self.fancy_git.confirmation_enabled = True
337
+
338
+ # Execute command but cancel confirmation
339
+ status_result = self.fancy_git.execute_command('status')
340
+
341
+ # Verify confirmation was requested and command was cancelled
342
+ assert status_result is False
343
+ mock_input.assert_called_once()
344
+ mock_run_git.assert_not_called()
345
+
346
+ @pytest.mark.integration
347
+ class TestFancyGitErrorRecoveryWorkflows:
348
+ """Integration tests for error recovery and edge cases"""
349
+
350
+ def setup_method(self):
351
+ """Setup method called before each test"""
352
+ with patch.object(FancyGit, '_load_commands', return_value=['add', 'commit', 'push', 'pull', 'status']):
353
+ with patch.object(FancyGit, '_load_confirmation_state', return_value=False):
354
+ with patch.object(FancyGit, '_load_ai_analysis_state', return_value=False):
355
+ with patch.object(FancyGit, '_load_output_coloring_state', return_value=False):
356
+ with patch.object(FancyGit, '_load_animation_type', return_value='dots'):
357
+ self.fancy_git = FancyGit()
358
+
359
+ @patch('src.git_runner.GitRunner.run_git_command')
360
+ def test_network_error_recovery(self, mock_run_git):
361
+ """Test recovery from network errors"""
362
+ # First call fails with network error, second succeeds
363
+ mock_run_git.side_effect = [
364
+ (1, "", "fatal: unable to access 'https://github.com/user/repo.git/': Could not resolve host: github.com"),
365
+ (0, "Enumerating objects: 3, done.\nTo github.com:user/repo.git\n 1234567..abcdefg main -> main", "")
366
+ ]
367
+
368
+ # First push fails
369
+ push_fail_result = self.fancy_git.execute_command('push', 'origin', 'main')
370
+ assert push_fail_result is False
371
+
372
+ # Second push succeeds
373
+ push_success_result = self.fancy_git.execute_command('push', 'origin', 'main')
374
+ assert push_success_result is True
375
+
376
+ # Verify both calls were made
377
+ assert mock_run_git.call_count == 2
378
+
379
+ @patch('src.git_runner.GitRunner.run_git_command')
380
+ def test_permission_error_handling(self, mock_run_git):
381
+ """Test handling of permission errors"""
382
+ mock_run_git.return_value = (1, "", "error: unable to create file '.git/index.lock': Permission denied")
383
+
384
+ # Try to execute command that requires permissions
385
+ status_result = self.fancy_git.execute_command('status')
386
+
387
+ # Verify error was handled
388
+ assert status_result is False
389
+ mock_run_git.assert_called_once_with(['status'])
390
+
391
+ @patch('src.git_runner.GitRunner.run_git_command')
392
+ def test_repository_not_initialized(self, mock_run_git):
393
+ """Test handling of non-git repository"""
394
+ mock_run_git.return_value = (1, "", "fatal: not a git repository (or any of the parent directories): .git")
395
+
396
+ # Try to execute git command
397
+ status_result = self.fancy_git.execute_command('status')
398
+
399
+ # Verify error was handled
400
+ assert status_result is False
401
+ mock_run_git.assert_called_once_with(['status'])
402
+
403
+ @patch('src.git_runner.GitRunner.run_git_command')
404
+ def test_subprocess_exception_handling(self, mock_run_git):
405
+ """Test handling of subprocess exceptions"""
406
+ mock_run_git.return_value = (-1, "", "git command not found")
407
+
408
+ # Try to execute command when git is not available
409
+ status_result = self.fancy_git.execute_command('status')
410
+
411
+ # Verify exception was handled
412
+ assert isinstance(status_result, bool) # Should return False on exception
413
+ mock_run_git.assert_called_once_with(['status'])
414
+
415
+ @patch('src.git_runner.GitRunner.run_git_command')
416
+ def test_empty_repository_workflow(self, mock_run_git):
417
+ """Test workflow with empty/new repository"""
418
+ mock_run_git.side_effect = [
419
+ (0, "On branch main\nNo commits yet", ""), # git status
420
+ (0, "", ""), # git add .
421
+ (0, "[main (root-commit) 1234567] Initial commit\n 1 file changed, 1 insertion(+)", ""), # git commit
422
+ (0, "Enumerating objects: 3, done.\nTo github.com:user/repo.git\n * [new branch] main -> main", ""), # git push
423
+ ]
424
+
425
+ # Execute workflow in empty repository
426
+ status_result = self.fancy_git.execute_command('status')
427
+ add_result = self.fancy_git.execute_command('add', '.')
428
+ commit_result = self.fancy_git.execute_command('commit', '-m', 'Initial commit')
429
+ push_result = self.fancy_git.execute_command('push', '-u', 'origin', 'main')
430
+
431
+ # Verify all commands succeeded
432
+ assert all(result is True for result in [status_result, add_result, commit_result, push_result])
433
+
434
+ # Verify the correct sequence of calls
435
+ expected_calls = [
436
+ call(['status']),
437
+ call(['add', '.']),
438
+ call(['commit', '-m', 'Initial commit']),
439
+ call(['push', '-u', 'origin', 'main'])
440
+ ]
441
+ mock_run_git.assert_has_calls(expected_calls)
@@ -0,0 +1,74 @@
1
+ import pytest
2
+ from src.git_error import GitError
3
+
4
+ @pytest.mark.unit
5
+ class TestGitError:
6
+ """Test cases for GitError dataclass"""
7
+
8
+ def test_git_error_creation_minimal(self):
9
+ """Test creating GitError with minimal required fields"""
10
+ error = GitError(source="stdout", message="error: test message")
11
+
12
+ assert error.source == "stdout"
13
+ assert error.message == "error: test message"
14
+ assert error.severity == "unknown"
15
+ assert error.type is None
16
+ assert error.file is None
17
+ assert error.line is None
18
+
19
+ def test_git_error_creation_full(self):
20
+ """Test creating GitError with all fields"""
21
+ error = GitError(
22
+ source="stderr",
23
+ message="fatal: merge conflict",
24
+ severity="error",
25
+ type="MERGE_CONFLICT",
26
+ file="README.md",
27
+ line=42
28
+ )
29
+
30
+ assert error.source == "stderr"
31
+ assert error.message == "fatal: merge conflict"
32
+ assert error.severity == "error"
33
+ assert error.type == "MERGE_CONFLICT"
34
+ assert error.file == "README.md"
35
+ assert error.line == 42
36
+
37
+ def test_git_error_immutability(self):
38
+ """Test that GitError is immutable"""
39
+ error = GitError(source="stdout", message="test")
40
+
41
+ with pytest.raises(AttributeError):
42
+ error.source = "stderr"
43
+
44
+ with pytest.raises(AttributeError):
45
+ error.message = "changed"
46
+
47
+ def test_git_error_str_representation(self):
48
+ """Test string representation of GitError"""
49
+ error = GitError(
50
+ source="stderr",
51
+ message="error: test message",
52
+ severity="error",
53
+ type="TEST_ERROR",
54
+ file="test.py",
55
+ line=10
56
+ )
57
+
58
+ str_repr = str(error)
59
+ assert "type: TEST_ERROR" in str_repr
60
+ assert "source: stderr" in str_repr
61
+ assert "message: error: test message" in str_repr
62
+ assert "severity: error" in str_repr
63
+ assert "file: test.py" in str_repr
64
+ assert "line: 10" in str_repr
65
+ assert "============================================================" in str_repr
66
+
67
+ def test_git_error_equality(self):
68
+ """Test GitError equality comparison"""
69
+ error1 = GitError(source="stdout", message="test", severity="error")
70
+ error2 = GitError(source="stdout", message="test", severity="error")
71
+ error3 = GitError(source="stderr", message="test", severity="error")
72
+
73
+ assert error1 == error2
74
+ assert error1 != error3
@@ -0,0 +1,129 @@
1
+ import pytest
2
+ from src.git_error_parser import GitErrorParser
3
+ from src.git_error import GitError
4
+
5
+ @pytest.mark.unit
6
+ class TestGitErrorParser:
7
+ """Test cases for GitErrorParser class"""
8
+
9
+ def setup_method(self):
10
+ """Setup method called before each test"""
11
+ self.parser = GitErrorParser()
12
+
13
+ def test_detect_no_errors(self):
14
+ """Test parsing clean git output"""
15
+ stdout = "On branch main\nnothing to commit, working tree clean\n"
16
+ stderr = ""
17
+
18
+ errors = self.parser.detect_warnings_errors(stdout, stderr)
19
+
20
+ assert len(errors) == 0
21
+
22
+ def test_detect_error_patterns(self):
23
+ """Test detection of various error patterns"""
24
+ test_cases = [
25
+ ("error: pathspec 'test.txt' did not match any files", "error", 1),
26
+ ("fatal: not a git repository", "error", 1),
27
+ ("failed to push some refs", "error", 1),
28
+ ("rejected: non-fast-forward", "error", 1),
29
+ ("conflict in README.md", "error", 1),
30
+ ("merge conflict detected", "error", 2), # "conflict" and "merge conflict" patterns match
31
+ ("unable to checkout", "error", 1)
32
+ ]
33
+
34
+ for message, expected_severity, expected_count in test_cases:
35
+ errors = self.parser.detect_warnings_errors(message, "")
36
+ assert len(errors) == expected_count
37
+ assert all(error.severity == expected_severity for error in errors)
38
+ # Check that at least one error matches the full message
39
+ assert any(error.message == message for error in errors)
40
+ assert all(error.source == "stdout" for error in errors)
41
+
42
+ def test_detect_warning_patterns(self):
43
+ """Test detection of various warning patterns"""
44
+ test_cases = [
45
+ ("warning: LF will be replaced by CRLF", "warning", 2), # Both "warning:" and "WARNING:" patterns match
46
+ ("WARNING: file is too large", "warning", 2), # Both patterns match
47
+ ("Your branch is behind by 2 commits", "warning", 1), # Only "behind" pattern matches
48
+ ("Your branch is ahead by 1 commit", "warning", 1), # Only "ahead" pattern matches
49
+ ("branches have diverged", "warning", 1) # Only "diverged" pattern matches
50
+ ]
51
+
52
+ for message, expected_severity, expected_count in test_cases:
53
+ errors = self.parser.detect_warnings_errors(message, "")
54
+ assert len(errors) == expected_count
55
+ assert all(error.severity == expected_severity for error in errors)
56
+ # Check that the message contains the expected pattern
57
+ assert all(error.message in message or message in error.message for error in errors)
58
+ assert all(error.source == "stdout" for error in errors)
59
+
60
+ def test_detect_stderr_errors(self):
61
+ """Test error detection in stderr"""
62
+ stderr = "error: merge conflict in README.md\n"
63
+
64
+ errors = self.parser.detect_warnings_errors("", stderr)
65
+
66
+ assert len(errors) == 3 # "error:", "conflict", and "merge conflict" patterns match
67
+ assert all(error.severity == "error" for error in errors)
68
+ assert all(error.source == "stderr" for error in errors)
69
+ # Check that at least one error contains the full message
70
+ assert any("error: merge conflict in README.md" in error.message for error in errors)
71
+
72
+ def test_multiple_errors_same_output(self):
73
+ """Test detection of multiple errors in same output"""
74
+ output = "error: first issue\nwarning: some warning\nfatal: critical error\n"
75
+
76
+ errors = self.parser.detect_warnings_errors(output, "")
77
+
78
+ assert len(errors) == 4 # warning matches both "warning:" and "WARNING:" patterns
79
+ error_severities = [error.severity for error in errors]
80
+ assert error_severities.count("error") == 2
81
+ assert error_severities.count("warning") == 2
82
+
83
+ def test_case_insensitive_matching(self):
84
+ """Test that pattern matching is case insensitive"""
85
+ test_cases = [
86
+ ("ERROR: uppercase error", 1),
87
+ ("Error: Title case error", 1),
88
+ ("WARNING: uppercase warning", 2), # Both "warning:" and "WARNING:" patterns match
89
+ ("Warning: Title case warning", 2) # Both patterns match
90
+ ]
91
+
92
+ for message, expected_count in test_cases:
93
+ errors = self.parser.detect_warnings_errors(message, "")
94
+ assert len(errors) == expected_count
95
+ assert all(error.message == message for error in errors)
96
+
97
+ def test_partial_word_matching(self):
98
+ """Test that patterns match partial words correctly"""
99
+ output = "This command failed because it was unable to complete\n"
100
+
101
+ errors = self.parser.detect_warnings_errors(output, "")
102
+
103
+ assert len(errors) == 2
104
+ error_messages = [error.message for error in errors]
105
+ assert any("failed" in msg for msg in error_messages)
106
+ assert any("unable to" in msg for msg in error_messages)
107
+
108
+ def test_empty_outputs(self):
109
+ """Test handling of empty outputs"""
110
+ errors = self.parser.detect_warnings_errors("", "")
111
+ assert len(errors) == 0
112
+
113
+ def test_complex_git_output(self, sample_git_outputs):
114
+ """Test parsing realistic git command outputs"""
115
+ # Test clean output
116
+ clean_errors = self.parser.detect_warnings_errors(
117
+ sample_git_outputs["clean_output"]["stdout"],
118
+ sample_git_outputs["clean_output"]["stderr"]
119
+ )
120
+ assert len(clean_errors) == 0
121
+
122
+ # Test error output
123
+ error_errors = self.parser.detect_warnings_errors(
124
+ sample_git_outputs["error_output"]["stdout"],
125
+ sample_git_outputs["error_output"]["stderr"]
126
+ )
127
+ assert len(error_errors) == 1
128
+ assert error_errors[0].severity == "error"
129
+ assert "pathspec" in error_errors[0].message