auto-coder 0.1.363__py3-none-any.whl → 0.1.365__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.

Potentially problematic release.


This version of auto-coder might be problematic. Click here for more details.

Files changed (39) hide show
  1. {auto_coder-0.1.363.dist-info → auto_coder-0.1.365.dist-info}/METADATA +2 -2
  2. {auto_coder-0.1.363.dist-info → auto_coder-0.1.365.dist-info}/RECORD +39 -23
  3. autocoder/agent/base_agentic/tools/execute_command_tool_resolver.py +1 -1
  4. autocoder/auto_coder.py +46 -2
  5. autocoder/auto_coder_runner.py +2 -0
  6. autocoder/common/__init__.py +5 -0
  7. autocoder/common/file_checkpoint/__init__.py +21 -0
  8. autocoder/common/file_checkpoint/backup.py +264 -0
  9. autocoder/common/file_checkpoint/conversation_checkpoint.py +182 -0
  10. autocoder/common/file_checkpoint/examples.py +217 -0
  11. autocoder/common/file_checkpoint/manager.py +611 -0
  12. autocoder/common/file_checkpoint/models.py +156 -0
  13. autocoder/common/file_checkpoint/store.py +383 -0
  14. autocoder/common/file_checkpoint/test_backup.py +242 -0
  15. autocoder/common/file_checkpoint/test_manager.py +570 -0
  16. autocoder/common/file_checkpoint/test_models.py +360 -0
  17. autocoder/common/file_checkpoint/test_store.py +327 -0
  18. autocoder/common/file_checkpoint/test_utils.py +297 -0
  19. autocoder/common/file_checkpoint/utils.py +119 -0
  20. autocoder/common/rulefiles/autocoderrules_utils.py +114 -55
  21. autocoder/common/save_formatted_log.py +76 -5
  22. autocoder/common/utils_code_auto_generate.py +2 -1
  23. autocoder/common/v2/agent/agentic_edit.py +545 -225
  24. autocoder/common/v2/agent/agentic_edit_tools/list_files_tool_resolver.py +83 -43
  25. autocoder/common/v2/agent/agentic_edit_tools/read_file_tool_resolver.py +116 -29
  26. autocoder/common/v2/agent/agentic_edit_tools/replace_in_file_tool_resolver.py +179 -48
  27. autocoder/common/v2/agent/agentic_edit_tools/search_files_tool_resolver.py +101 -56
  28. autocoder/common/v2/agent/agentic_edit_tools/test_write_to_file_tool_resolver.py +322 -0
  29. autocoder/common/v2/agent/agentic_edit_tools/write_to_file_tool_resolver.py +173 -132
  30. autocoder/common/v2/agent/agentic_edit_types.py +4 -0
  31. autocoder/compilers/normal_compiler.py +64 -0
  32. autocoder/events/event_manager_singleton.py +133 -4
  33. autocoder/linters/normal_linter.py +373 -0
  34. autocoder/linters/python_linter.py +4 -2
  35. autocoder/version.py +1 -1
  36. {auto_coder-0.1.363.dist-info → auto_coder-0.1.365.dist-info}/LICENSE +0 -0
  37. {auto_coder-0.1.363.dist-info → auto_coder-0.1.365.dist-info}/WHEEL +0 -0
  38. {auto_coder-0.1.363.dist-info → auto_coder-0.1.365.dist-info}/entry_points.txt +0 -0
  39. {auto_coder-0.1.363.dist-info → auto_coder-0.1.365.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,360 @@
1
+ import pytest
2
+ import json
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+
6
+ from autocoder.common.file_checkpoint.models import (
7
+ FileChange, ChangeRecord, DiffResult, ApplyResult, UndoResult
8
+ )
9
+
10
+ class TestFileChange:
11
+ """FileChange类的单元测试"""
12
+
13
+ def test_init(self):
14
+ """测试FileChange初始化"""
15
+ fc = FileChange(
16
+ file_path="test.py",
17
+ content="print('hello')",
18
+ is_new=True,
19
+ is_deletion=False
20
+ )
21
+
22
+ assert fc.file_path == "test.py"
23
+ assert fc.content == "print('hello')"
24
+ assert fc.is_new is True
25
+ assert fc.is_deletion is False
26
+
27
+ def test_from_dict(self):
28
+ """测试从字典创建FileChange对象"""
29
+ data = {
30
+ 'file_path': 'test.py',
31
+ 'content': "print('hello')",
32
+ 'is_new': True,
33
+ 'is_deletion': False
34
+ }
35
+
36
+ fc = FileChange.from_dict(data)
37
+
38
+ assert fc.file_path == "test.py"
39
+ assert fc.content == "print('hello')"
40
+ assert fc.is_new is True
41
+ assert fc.is_deletion is False
42
+
43
+ def test_to_dict(self):
44
+ """测试将FileChange对象转换为字典"""
45
+ fc = FileChange(
46
+ file_path="test.py",
47
+ content="print('hello')",
48
+ is_new=True,
49
+ is_deletion=False
50
+ )
51
+
52
+ data = fc.to_dict()
53
+
54
+ assert data['file_path'] == "test.py"
55
+ assert data['content'] == "print('hello')"
56
+ assert data['is_new'] is True
57
+ assert data['is_deletion'] is False
58
+
59
+ def test_default_values(self):
60
+ """测试默认值"""
61
+ fc = FileChange(
62
+ file_path="test.py",
63
+ content="print('hello')"
64
+ )
65
+
66
+ assert fc.is_new is False
67
+ assert fc.is_deletion is False
68
+
69
+ def test_from_dict_missing_optionals(self):
70
+ """测试从缺少可选字段的字典创建"""
71
+ data = {
72
+ 'file_path': 'test.py',
73
+ 'content': "print('hello')"
74
+ }
75
+
76
+ fc = FileChange.from_dict(data)
77
+
78
+ assert fc.file_path == "test.py"
79
+ assert fc.content == "print('hello')"
80
+ assert fc.is_new is False
81
+ assert fc.is_deletion is False
82
+
83
+
84
+ class TestChangeRecord:
85
+ """ChangeRecord类的单元测试"""
86
+
87
+ def test_init(self):
88
+ """测试ChangeRecord初始化"""
89
+ cr = ChangeRecord(
90
+ change_id="123",
91
+ timestamp=1234567890.0,
92
+ file_path="test.py",
93
+ backup_id="456",
94
+ is_new=True,
95
+ is_deletion=False,
96
+ group_id="789"
97
+ )
98
+
99
+ assert cr.change_id == "123"
100
+ assert cr.timestamp == 1234567890.0
101
+ assert cr.file_path == "test.py"
102
+ assert cr.backup_id == "456"
103
+ assert cr.is_new is True
104
+ assert cr.is_deletion is False
105
+ assert cr.group_id == "789"
106
+
107
+ def test_create(self):
108
+ """测试创建一个新的变更记录"""
109
+ before_time = datetime.now().timestamp()
110
+
111
+ cr = ChangeRecord.create(
112
+ file_path="test.py",
113
+ backup_id="456",
114
+ is_new=True,
115
+ is_deletion=False,
116
+ group_id="789"
117
+ )
118
+
119
+ after_time = datetime.now().timestamp()
120
+
121
+ assert cr.file_path == "test.py"
122
+ assert cr.backup_id == "456"
123
+ assert cr.is_new is True
124
+ assert cr.is_deletion is False
125
+ assert cr.group_id == "789"
126
+
127
+ # UUID和时间戳应该是自动生成的
128
+ assert cr.change_id is not None
129
+ assert len(cr.change_id) > 0
130
+ assert before_time <= cr.timestamp <= after_time
131
+
132
+ def test_from_dict(self):
133
+ """测试从字典创建ChangeRecord对象"""
134
+ data = {
135
+ 'change_id': '123',
136
+ 'timestamp': 1234567890.0,
137
+ 'file_path': 'test.py',
138
+ 'backup_id': '456',
139
+ 'is_new': True,
140
+ 'is_deletion': False,
141
+ 'group_id': '789'
142
+ }
143
+
144
+ cr = ChangeRecord.from_dict(data)
145
+
146
+ assert cr.change_id == "123"
147
+ assert cr.timestamp == 1234567890.0
148
+ assert cr.file_path == "test.py"
149
+ assert cr.backup_id == "456"
150
+ assert cr.is_new is True
151
+ assert cr.is_deletion is False
152
+ assert cr.group_id == "789"
153
+
154
+ def test_to_dict(self):
155
+ """测试将ChangeRecord对象转换为字典"""
156
+ cr = ChangeRecord(
157
+ change_id="123",
158
+ timestamp=1234567890.0,
159
+ file_path="test.py",
160
+ backup_id="456",
161
+ is_new=True,
162
+ is_deletion=False,
163
+ group_id="789"
164
+ )
165
+
166
+ data = cr.to_dict()
167
+
168
+ assert data['change_id'] == "123"
169
+ assert data['timestamp'] == 1234567890.0
170
+ assert data['file_path'] == "test.py"
171
+ assert data['backup_id'] == "456"
172
+ assert data['is_new'] is True
173
+ assert data['is_deletion'] is False
174
+ assert data['group_id'] == "789"
175
+
176
+ def test_default_values(self):
177
+ """测试默认值"""
178
+ cr = ChangeRecord(
179
+ change_id="123",
180
+ timestamp=1234567890.0,
181
+ file_path="test.py",
182
+ backup_id="456"
183
+ )
184
+
185
+ assert cr.is_new is False
186
+ assert cr.is_deletion is False
187
+ assert cr.group_id is None
188
+
189
+ def test_from_dict_missing_optionals(self):
190
+ """测试从缺少可选字段的字典创建"""
191
+ data = {
192
+ 'change_id': '123',
193
+ 'timestamp': 1234567890.0,
194
+ 'file_path': 'test.py',
195
+ 'backup_id': '456'
196
+ }
197
+
198
+ cr = ChangeRecord.from_dict(data)
199
+
200
+ assert cr.change_id == "123"
201
+ assert cr.timestamp == 1234567890.0
202
+ assert cr.file_path == "test.py"
203
+ assert cr.backup_id == "456"
204
+ assert cr.is_new is False
205
+ assert cr.is_deletion is False
206
+ assert cr.group_id is None
207
+
208
+
209
+ class TestDiffResult:
210
+ """DiffResult类的单元测试"""
211
+
212
+ def test_init(self):
213
+ """测试DiffResult初始化"""
214
+ dr = DiffResult(
215
+ file_path="test.py",
216
+ old_content="print('hello')",
217
+ new_content="print('world')",
218
+ is_new=False,
219
+ is_deletion=False
220
+ )
221
+
222
+ assert dr.file_path == "test.py"
223
+ assert dr.old_content == "print('hello')"
224
+ assert dr.new_content == "print('world')"
225
+ assert dr.is_new is False
226
+ assert dr.is_deletion is False
227
+
228
+ def test_default_values(self):
229
+ """测试默认值"""
230
+ dr = DiffResult(
231
+ file_path="test.py",
232
+ old_content="print('hello')",
233
+ new_content="print('world')"
234
+ )
235
+
236
+ assert dr.is_new is False
237
+ assert dr.is_deletion is False
238
+
239
+ def test_get_diff_summary_modify(self):
240
+ """测试获取修改文件的差异摘要"""
241
+ dr = DiffResult(
242
+ file_path="test.py",
243
+ old_content="line1\nline2\nline3",
244
+ new_content="line1\nline2\nline3\nline4"
245
+ )
246
+
247
+ summary = dr.get_diff_summary()
248
+
249
+ assert "修改文件" in summary
250
+ assert "test.py" in summary
251
+ assert "原始行数: 3" in summary
252
+ assert "新行数: 4" in summary
253
+
254
+ def test_get_diff_summary_new(self):
255
+ """测试获取新文件的差异摘要"""
256
+ dr = DiffResult(
257
+ file_path="test.py",
258
+ old_content=None,
259
+ new_content="print('hello')",
260
+ is_new=True
261
+ )
262
+
263
+ summary = dr.get_diff_summary()
264
+
265
+ assert "新文件" in summary
266
+ assert "test.py" in summary
267
+
268
+ def test_get_diff_summary_delete(self):
269
+ """测试获取删除文件的差异摘要"""
270
+ dr = DiffResult(
271
+ file_path="test.py",
272
+ old_content="print('hello')",
273
+ new_content="",
274
+ is_deletion=True
275
+ )
276
+
277
+ summary = dr.get_diff_summary()
278
+
279
+ assert "删除文件" in summary
280
+ assert "test.py" in summary
281
+
282
+
283
+ class TestApplyResult:
284
+ """ApplyResult类的单元测试"""
285
+
286
+ def test_init(self):
287
+ """测试ApplyResult初始化"""
288
+ ar = ApplyResult(success=True)
289
+
290
+ assert ar.success is True
291
+ assert ar.change_ids == []
292
+ assert ar.errors == {}
293
+ assert ar.has_errors is False
294
+
295
+ def test_add_error(self):
296
+ """测试添加错误信息"""
297
+ ar = ApplyResult(success=True)
298
+
299
+ assert ar.success is True
300
+ assert ar.has_errors is False
301
+
302
+ ar.add_error("test.py", "文件不存在")
303
+
304
+ assert ar.success is False
305
+ assert ar.has_errors is True
306
+ assert "test.py" in ar.errors
307
+ assert ar.errors["test.py"] == "文件不存在"
308
+
309
+ def test_add_change_id(self):
310
+ """测试添加成功应用的变更ID"""
311
+ ar = ApplyResult(success=True)
312
+
313
+ assert len(ar.change_ids) == 0
314
+
315
+ ar.add_change_id("123")
316
+ ar.add_change_id("456")
317
+
318
+ assert len(ar.change_ids) == 2
319
+ assert "123" in ar.change_ids
320
+ assert "456" in ar.change_ids
321
+
322
+
323
+ class TestUndoResult:
324
+ """UndoResult类的单元测试"""
325
+
326
+ def test_init(self):
327
+ """测试UndoResult初始化"""
328
+ ur = UndoResult(success=True)
329
+
330
+ assert ur.success is True
331
+ assert ur.restored_files == []
332
+ assert ur.errors == {}
333
+ assert ur.has_errors is False
334
+
335
+ def test_add_error(self):
336
+ """测试添加错误信息"""
337
+ ur = UndoResult(success=True)
338
+
339
+ assert ur.success is True
340
+ assert ur.has_errors is False
341
+
342
+ ur.add_error("test.py", "文件不存在")
343
+
344
+ assert ur.success is False
345
+ assert ur.has_errors is True
346
+ assert "test.py" in ur.errors
347
+ assert ur.errors["test.py"] == "文件不存在"
348
+
349
+ def test_add_restored_file(self):
350
+ """测试添加成功恢复的文件"""
351
+ ur = UndoResult(success=True)
352
+
353
+ assert len(ur.restored_files) == 0
354
+
355
+ ur.add_restored_file("test.py")
356
+ ur.add_restored_file("main.py")
357
+
358
+ assert len(ur.restored_files) == 2
359
+ assert "test.py" in ur.restored_files
360
+ assert "main.py" in ur.restored_files
@@ -0,0 +1,327 @@
1
+ import pytest
2
+ import os
3
+ import json
4
+ import sqlite3
5
+ import tempfile
6
+ import shutil
7
+ from datetime import datetime, timedelta
8
+ from pathlib import Path
9
+ from unittest.mock import patch, MagicMock
10
+
11
+ from autocoder.common.file_checkpoint.store import FileChangeStore
12
+ from autocoder.common.file_checkpoint.models import ChangeRecord
13
+
14
+ @pytest.fixture
15
+ def temp_test_dir():
16
+ """提供一个临时的测试目录"""
17
+ temp_dir = tempfile.mkdtemp()
18
+ yield temp_dir
19
+ shutil.rmtree(temp_dir)
20
+
21
+ @pytest.fixture
22
+ def temp_store_dir():
23
+ """提供一个临时的存储目录"""
24
+ temp_dir = tempfile.mkdtemp()
25
+ yield temp_dir
26
+ shutil.rmtree(temp_dir)
27
+
28
+ @pytest.fixture
29
+ def sample_change_record():
30
+ """创建一个用于测试的变更记录"""
31
+ return ChangeRecord.create(
32
+ file_path="test.py",
33
+ backup_id="backup123",
34
+ is_new=False,
35
+ is_deletion=False,
36
+ group_id="group456"
37
+ )
38
+
39
+ class TestFileChangeStore:
40
+ """FileChangeStore类的单元测试"""
41
+
42
+ def test_init_with_custom_dir(self, temp_store_dir):
43
+ """测试使用自定义目录初始化"""
44
+ store = FileChangeStore(store_dir=temp_store_dir)
45
+
46
+ assert store.store_dir == temp_store_dir
47
+ assert os.path.exists(temp_store_dir)
48
+ assert os.path.exists(os.path.join(temp_store_dir, "changes.db"))
49
+
50
+ def test_init_with_default_dir(self, monkeypatch):
51
+ """测试使用默认目录初始化"""
52
+ # 创建一个临时主目录
53
+ temp_home = tempfile.mkdtemp()
54
+ try:
55
+ # 模拟用户主目录
56
+ monkeypatch.setattr(os.path, 'expanduser', lambda path: temp_home)
57
+
58
+ store = FileChangeStore()
59
+
60
+ expected_dir = os.path.join(temp_home, ".autocoder", "changes")
61
+ assert store.store_dir == expected_dir
62
+ assert os.path.exists(expected_dir)
63
+ assert os.path.exists(os.path.join(expected_dir, "changes.db"))
64
+ finally:
65
+ shutil.rmtree(temp_home)
66
+
67
+ def test_save_change(self, temp_store_dir, sample_change_record):
68
+ """测试保存变更记录"""
69
+ store = FileChangeStore(store_dir=temp_store_dir)
70
+
71
+ # 保存变更记录
72
+ change_id = store.save_change(sample_change_record)
73
+
74
+ # 检查返回的变更ID
75
+ assert change_id == sample_change_record.change_id
76
+
77
+ # 检查数据库中是否存在该记录
78
+ conn = sqlite3.connect(os.path.join(temp_store_dir, "changes.db"))
79
+ cursor = conn.cursor()
80
+ cursor.execute("SELECT * FROM changes WHERE change_id = ?", (change_id,))
81
+ row = cursor.fetchone()
82
+ conn.close()
83
+
84
+ assert row is not None
85
+ assert row[0] == change_id # change_id
86
+ assert row[1] == sample_change_record.timestamp # timestamp
87
+ assert row[2] == sample_change_record.file_path # file_path
88
+ assert row[3] == sample_change_record.backup_id # backup_id
89
+ assert row[4] == 0 # is_new (转为int)
90
+ assert row[5] == 0 # is_deletion (转为int)
91
+ assert row[6] == sample_change_record.group_id # group_id
92
+
93
+ def test_get_change(self, temp_store_dir, sample_change_record):
94
+ """测试获取变更记录"""
95
+ store = FileChangeStore(store_dir=temp_store_dir)
96
+
97
+ # 保存变更记录
98
+ change_id = store.save_change(sample_change_record)
99
+
100
+ # 获取变更记录
101
+ retrieved_record = store.get_change(change_id)
102
+
103
+ # 检查获取的记录
104
+ assert retrieved_record is not None
105
+ assert retrieved_record.change_id == sample_change_record.change_id
106
+ assert retrieved_record.timestamp == sample_change_record.timestamp
107
+ assert retrieved_record.file_path == sample_change_record.file_path
108
+ assert retrieved_record.backup_id == sample_change_record.backup_id
109
+ assert retrieved_record.is_new == sample_change_record.is_new
110
+ assert retrieved_record.is_deletion == sample_change_record.is_deletion
111
+ assert retrieved_record.group_id == sample_change_record.group_id
112
+
113
+ def test_get_nonexistent_change(self, temp_store_dir):
114
+ """测试获取不存在的变更记录"""
115
+ store = FileChangeStore(store_dir=temp_store_dir)
116
+
117
+ # 尝试获取不存在的记录
118
+ retrieved_record = store.get_change("nonexistent_id")
119
+
120
+ # 应该返回None
121
+ assert retrieved_record is None
122
+
123
+ def test_get_changes_by_group(self, temp_store_dir):
124
+ """测试按组获取变更记录"""
125
+ store = FileChangeStore(store_dir=temp_store_dir)
126
+ group_id = "group123"
127
+
128
+ # 创建并保存三个同组的变更记录
129
+ for i in range(3):
130
+ record = ChangeRecord.create(
131
+ file_path=f"file{i}.py",
132
+ backup_id=f"backup{i}",
133
+ group_id=group_id
134
+ )
135
+ store.save_change(record)
136
+
137
+ # 创建一个不同组的变更记录
138
+ other_record = ChangeRecord.create(
139
+ file_path="other.py",
140
+ backup_id="other_backup",
141
+ group_id="other_group"
142
+ )
143
+ store.save_change(other_record)
144
+
145
+ # 获取同组的变更记录
146
+ group_records = store.get_changes_by_group(group_id)
147
+
148
+ # 检查记录数量和组ID
149
+ assert len(group_records) == 3
150
+ for record in group_records:
151
+ assert record.group_id == group_id
152
+
153
+ def test_get_latest_changes(self, temp_store_dir):
154
+ """测试获取最近的变更记录"""
155
+ store = FileChangeStore(store_dir=temp_store_dir)
156
+
157
+ # 创建并保存五个变更记录,时间戳递增
158
+ for i in range(5):
159
+ record = ChangeRecord.create(
160
+ file_path=f"file{i}.py",
161
+ backup_id=f"backup{i}"
162
+ )
163
+ # 手动设置时间戳,确保顺序
164
+ record.timestamp = 1000 + i
165
+ store.save_change(record)
166
+
167
+ # 获取最近的3个变更记录
168
+ latest_records = store.get_latest_changes(limit=3)
169
+
170
+ # 检查记录数量和顺序
171
+ assert len(latest_records) == 3
172
+ assert latest_records[0].timestamp > latest_records[1].timestamp
173
+ assert latest_records[1].timestamp > latest_records[2].timestamp
174
+
175
+ def test_get_changes_by_file(self, temp_store_dir):
176
+ """测试按文件获取变更记录"""
177
+ store = FileChangeStore(store_dir=temp_store_dir)
178
+ file_path = "test.py"
179
+
180
+ # 创建并保存同一文件的三个变更记录
181
+ for i in range(3):
182
+ record = ChangeRecord.create(
183
+ file_path=file_path,
184
+ backup_id=f"backup{i}"
185
+ )
186
+ store.save_change(record)
187
+
188
+ # 创建一个不同文件的变更记录
189
+ other_record = ChangeRecord.create(
190
+ file_path="other.py",
191
+ backup_id="other_backup"
192
+ )
193
+ store.save_change(other_record)
194
+
195
+ # 获取同一文件的变更记录
196
+ file_records = store.get_changes_by_file(file_path)
197
+
198
+ # 检查记录数量和文件路径
199
+ assert len(file_records) == 3
200
+ for record in file_records:
201
+ assert record.file_path == file_path
202
+
203
+ def test_delete_change(self, temp_store_dir, sample_change_record):
204
+ """测试删除变更记录"""
205
+ store = FileChangeStore(store_dir=temp_store_dir)
206
+
207
+ # 保存变更记录
208
+ change_id = store.save_change(sample_change_record)
209
+
210
+ # 确认记录存在
211
+ assert store.get_change(change_id) is not None
212
+
213
+ # 删除记录
214
+ success = store.delete_change(change_id)
215
+
216
+ # 检查删除结果
217
+ assert success is True
218
+ assert store.get_change(change_id) is None
219
+
220
+ # 检查文件系统中的JSON文件是否也被删除
221
+ json_file = os.path.join(temp_store_dir, f"{change_id}.json")
222
+ assert not os.path.exists(json_file)
223
+
224
+ def test_delete_nonexistent_change(self, temp_store_dir):
225
+ """测试删除不存在的变更记录"""
226
+ store = FileChangeStore(store_dir=temp_store_dir)
227
+
228
+ # 尝试删除不存在的记录
229
+ success = store.delete_change("nonexistent_id")
230
+
231
+ # 应该返回False
232
+ assert success is False
233
+
234
+ def test_get_change_groups(self, temp_store_dir):
235
+ """测试获取变更组列表"""
236
+ store = FileChangeStore(store_dir=temp_store_dir)
237
+
238
+ # 创建两个不同组的变更记录
239
+ group1_id = "group1"
240
+ group2_id = "group2"
241
+
242
+ # 为第一个组创建两个记录
243
+ for i in range(2):
244
+ record = ChangeRecord.create(
245
+ file_path=f"file{i}.py",
246
+ backup_id=f"backup{i}",
247
+ group_id=group1_id
248
+ )
249
+ store.save_change(record)
250
+
251
+ # 为第二个组创建一个记录
252
+ record = ChangeRecord.create(
253
+ file_path="otherfile.py",
254
+ backup_id="other_backup",
255
+ group_id=group2_id
256
+ )
257
+ store.save_change(record)
258
+
259
+ # 获取变更组列表
260
+ groups = store.get_change_groups()
261
+
262
+ # 检查组数量
263
+ assert len(groups) == 2
264
+
265
+ # 检查组信息
266
+ groups_dict = {g[0]: (g[1], g[2]) for g in groups}
267
+ assert group1_id in groups_dict
268
+ assert group2_id in groups_dict
269
+ assert groups_dict[group1_id][1] == 2 # 第一个组有2个记录
270
+ assert groups_dict[group2_id][1] == 1 # 第二个组有1个记录
271
+
272
+ def test_clean_old_history(self, temp_store_dir):
273
+ """测试清理旧历史记录"""
274
+ store = FileChangeStore(store_dir=temp_store_dir, max_history=5)
275
+
276
+ # 创建10个变更记录
277
+ for i in range(10):
278
+ record = ChangeRecord.create(
279
+ file_path=f"file{i}.py",
280
+ backup_id=f"backup{i}"
281
+ )
282
+ # 手动设置时间戳,确保顺序
283
+ record.timestamp = 1000 + i
284
+ store.save_change(record)
285
+
286
+ # 获取所有变更记录
287
+ conn = sqlite3.connect(os.path.join(temp_store_dir, "changes.db"))
288
+ cursor = conn.cursor()
289
+ cursor.execute("SELECT COUNT(*) FROM changes")
290
+ count = cursor.fetchone()[0]
291
+ conn.close()
292
+
293
+ # 检查记录数量,应该只保留max_history个
294
+ assert count == 5 # max_history
295
+
296
+ def test_thread_safety(self, temp_store_dir, sample_change_record):
297
+ """测试线程安全性"""
298
+ store = FileChangeStore(store_dir=temp_store_dir)
299
+
300
+ # 保存变更记录
301
+ change_id = store.save_change(sample_change_record)
302
+
303
+ # 模拟两个线程同时访问
304
+ with patch('threading.RLock') as mock_lock:
305
+ # 设置锁为MagicMock对象
306
+ mock_lock_instance = MagicMock()
307
+ mock_lock.return_value = mock_lock_instance
308
+
309
+ # 重新创建存储对象(会使用模拟的锁)
310
+ store = FileChangeStore(store_dir=temp_store_dir)
311
+
312
+ # 执行一些需要锁的操作
313
+ store.get_change(change_id)
314
+
315
+ # 创建一个新的记录对象(使用不同的ID)
316
+ new_record = ChangeRecord.create(
317
+ file_path=sample_change_record.file_path,
318
+ backup_id=sample_change_record.backup_id,
319
+ is_new=sample_change_record.is_new,
320
+ is_deletion=sample_change_record.is_deletion,
321
+ group_id=sample_change_record.group_id
322
+ )
323
+ store.save_change(new_record)
324
+
325
+ # 检查锁是否被使用
326
+ assert mock_lock_instance.__enter__.call_count >= 2
327
+ assert mock_lock_instance.__exit__.call_count >= 2