gitmap-core 0.1.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.
- gitmap_core/README.md +46 -0
- gitmap_core/__init__.py +100 -0
- gitmap_core/communication.py +346 -0
- gitmap_core/compat.py +408 -0
- gitmap_core/connection.py +232 -0
- gitmap_core/context.py +709 -0
- gitmap_core/diff.py +283 -0
- gitmap_core/maps.py +385 -0
- gitmap_core/merge.py +449 -0
- gitmap_core/models.py +332 -0
- gitmap_core/py.typed +0 -0
- gitmap_core/pyproject.toml +48 -0
- gitmap_core/remote.py +728 -0
- gitmap_core/repository.py +1632 -0
- gitmap_core/tests/__init__.py +1 -0
- gitmap_core/tests/test_communication.py +695 -0
- gitmap_core/tests/test_compat.py +310 -0
- gitmap_core/tests/test_connection.py +314 -0
- gitmap_core/tests/test_context.py +814 -0
- gitmap_core/tests/test_diff.py +567 -0
- gitmap_core/tests/test_init.py +153 -0
- gitmap_core/tests/test_maps.py +642 -0
- gitmap_core/tests/test_merge.py +694 -0
- gitmap_core/tests/test_models.py +410 -0
- gitmap_core/tests/test_remote.py +3014 -0
- gitmap_core/tests/test_repository.py +1639 -0
- gitmap_core/tests/test_visualize.py +902 -0
- gitmap_core/visualize.py +1217 -0
- gitmap_core-0.1.0.dist-info/METADATA +961 -0
- gitmap_core-0.1.0.dist-info/RECORD +32 -0
- gitmap_core-0.1.0.dist-info/WHEEL +4 -0
- gitmap_core-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
"""Tests for gitmap_core.merge module.
|
|
2
|
+
|
|
3
|
+
Tests the layer-level merge logic for GitMap including:
|
|
4
|
+
- Basic merging without conflicts
|
|
5
|
+
- Three-way merge with conflict detection
|
|
6
|
+
- Conflict resolution
|
|
7
|
+
- Table merging
|
|
8
|
+
- Summary formatting
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from gitmap_core.merge import (
|
|
15
|
+
apply_resolution,
|
|
16
|
+
format_merge_summary,
|
|
17
|
+
merge_maps,
|
|
18
|
+
MergeConflict,
|
|
19
|
+
MergeResult,
|
|
20
|
+
resolve_conflict,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ---- Fixtures -----------------------------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture
|
|
28
|
+
def base_map() -> dict:
|
|
29
|
+
"""Base map state for three-way merges."""
|
|
30
|
+
return {
|
|
31
|
+
"operationalLayers": [
|
|
32
|
+
{"id": "layer1", "title": "Layer One", "url": "http://example.com/1"},
|
|
33
|
+
{"id": "layer2", "title": "Layer Two", "url": "http://example.com/2"},
|
|
34
|
+
],
|
|
35
|
+
"tables": [
|
|
36
|
+
{"id": "table1", "title": "Table One", "url": "http://example.com/t1"},
|
|
37
|
+
],
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.fixture
|
|
42
|
+
def our_map() -> dict:
|
|
43
|
+
"""Our modified map state."""
|
|
44
|
+
return {
|
|
45
|
+
"operationalLayers": [
|
|
46
|
+
{"id": "layer1", "title": "Layer One Modified", "url": "http://example.com/1"},
|
|
47
|
+
{"id": "layer2", "title": "Layer Two", "url": "http://example.com/2"},
|
|
48
|
+
{"id": "layer3", "title": "Layer Three (New)", "url": "http://example.com/3"},
|
|
49
|
+
],
|
|
50
|
+
"tables": [
|
|
51
|
+
{"id": "table1", "title": "Table One", "url": "http://example.com/t1"},
|
|
52
|
+
],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.fixture
|
|
57
|
+
def their_map() -> dict:
|
|
58
|
+
"""Their modified map state."""
|
|
59
|
+
return {
|
|
60
|
+
"operationalLayers": [
|
|
61
|
+
{"id": "layer1", "title": "Layer One", "url": "http://example.com/1"},
|
|
62
|
+
{"id": "layer2", "title": "Layer Two Updated", "url": "http://example.com/2-new"},
|
|
63
|
+
{"id": "layer4", "title": "Layer Four (New)", "url": "http://example.com/4"},
|
|
64
|
+
],
|
|
65
|
+
"tables": [
|
|
66
|
+
{"id": "table1", "title": "Table One Updated", "url": "http://example.com/t1-new"},
|
|
67
|
+
],
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ---- MergeConflict Tests ------------------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TestMergeConflict:
|
|
75
|
+
"""Tests for MergeConflict dataclass."""
|
|
76
|
+
|
|
77
|
+
def test_create_conflict(self):
|
|
78
|
+
"""Test basic conflict creation."""
|
|
79
|
+
conflict = MergeConflict(
|
|
80
|
+
layer_id="layer1",
|
|
81
|
+
layer_title="Test Layer",
|
|
82
|
+
ours={"id": "layer1", "title": "Ours"},
|
|
83
|
+
theirs={"id": "layer1", "title": "Theirs"},
|
|
84
|
+
)
|
|
85
|
+
assert conflict.layer_id == "layer1"
|
|
86
|
+
assert conflict.layer_title == "Test Layer"
|
|
87
|
+
assert conflict.ours == {"id": "layer1", "title": "Ours"}
|
|
88
|
+
assert conflict.theirs == {"id": "layer1", "title": "Theirs"}
|
|
89
|
+
assert conflict.base is None
|
|
90
|
+
|
|
91
|
+
def test_conflict_with_base(self):
|
|
92
|
+
"""Test conflict with base version."""
|
|
93
|
+
conflict = MergeConflict(
|
|
94
|
+
layer_id="layer1",
|
|
95
|
+
layer_title="Test Layer",
|
|
96
|
+
ours={"id": "layer1", "title": "Ours"},
|
|
97
|
+
theirs={"id": "layer1", "title": "Theirs"},
|
|
98
|
+
base={"id": "layer1", "title": "Base"},
|
|
99
|
+
)
|
|
100
|
+
assert conflict.base == {"id": "layer1", "title": "Base"}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---- MergeResult Tests --------------------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class TestMergeResult:
|
|
107
|
+
"""Tests for MergeResult dataclass."""
|
|
108
|
+
|
|
109
|
+
def test_default_result(self):
|
|
110
|
+
"""Test default merge result values."""
|
|
111
|
+
result = MergeResult()
|
|
112
|
+
assert result.success is True
|
|
113
|
+
assert result.merged_data == {}
|
|
114
|
+
assert result.conflicts == []
|
|
115
|
+
assert result.added_layers == []
|
|
116
|
+
assert result.removed_layers == []
|
|
117
|
+
assert result.modified_layers == []
|
|
118
|
+
|
|
119
|
+
def test_has_conflicts_false(self):
|
|
120
|
+
"""Test has_conflicts property when no conflicts."""
|
|
121
|
+
result = MergeResult()
|
|
122
|
+
assert result.has_conflicts is False
|
|
123
|
+
|
|
124
|
+
def test_has_conflicts_true(self):
|
|
125
|
+
"""Test has_conflicts property when conflicts exist."""
|
|
126
|
+
result = MergeResult()
|
|
127
|
+
result.conflicts.append(MergeConflict(
|
|
128
|
+
layer_id="layer1",
|
|
129
|
+
layer_title="Test",
|
|
130
|
+
ours={},
|
|
131
|
+
theirs={},
|
|
132
|
+
))
|
|
133
|
+
assert result.has_conflicts is True
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ---- merge_maps Tests ---------------------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class TestMergeMaps:
|
|
140
|
+
"""Tests for merge_maps function."""
|
|
141
|
+
|
|
142
|
+
def test_merge_identical_maps(self):
|
|
143
|
+
"""Test merging identical maps produces no conflicts."""
|
|
144
|
+
map_data = {
|
|
145
|
+
"operationalLayers": [
|
|
146
|
+
{"id": "layer1", "title": "Layer One"},
|
|
147
|
+
],
|
|
148
|
+
"tables": [],
|
|
149
|
+
}
|
|
150
|
+
result = merge_maps(map_data, map_data)
|
|
151
|
+
assert result.success is True
|
|
152
|
+
assert result.has_conflicts is False
|
|
153
|
+
assert len(result.merged_data["operationalLayers"]) == 1
|
|
154
|
+
|
|
155
|
+
def test_merge_empty_maps(self):
|
|
156
|
+
"""Test merging empty maps."""
|
|
157
|
+
result = merge_maps({}, {})
|
|
158
|
+
assert result.success is True
|
|
159
|
+
assert result.merged_data.get("operationalLayers", []) == []
|
|
160
|
+
assert result.merged_data.get("tables", []) == []
|
|
161
|
+
|
|
162
|
+
def test_merge_adds_their_new_layer(self):
|
|
163
|
+
"""Test merging adds layers only in theirs."""
|
|
164
|
+
ours = {
|
|
165
|
+
"operationalLayers": [
|
|
166
|
+
{"id": "layer1", "title": "Layer One"},
|
|
167
|
+
],
|
|
168
|
+
}
|
|
169
|
+
theirs = {
|
|
170
|
+
"operationalLayers": [
|
|
171
|
+
{"id": "layer1", "title": "Layer One"},
|
|
172
|
+
{"id": "layer2", "title": "Layer Two"},
|
|
173
|
+
],
|
|
174
|
+
}
|
|
175
|
+
result = merge_maps(ours, theirs)
|
|
176
|
+
assert result.success is True
|
|
177
|
+
assert len(result.merged_data["operationalLayers"]) == 2
|
|
178
|
+
assert "layer2" in result.added_layers
|
|
179
|
+
|
|
180
|
+
def test_merge_keeps_our_new_layer(self):
|
|
181
|
+
"""Test merging keeps layers only in ours."""
|
|
182
|
+
ours = {
|
|
183
|
+
"operationalLayers": [
|
|
184
|
+
{"id": "layer1", "title": "Layer One"},
|
|
185
|
+
{"id": "layer2", "title": "Layer Two"},
|
|
186
|
+
],
|
|
187
|
+
}
|
|
188
|
+
theirs = {
|
|
189
|
+
"operationalLayers": [
|
|
190
|
+
{"id": "layer1", "title": "Layer One"},
|
|
191
|
+
],
|
|
192
|
+
}
|
|
193
|
+
result = merge_maps(ours, theirs)
|
|
194
|
+
assert result.success is True
|
|
195
|
+
assert len(result.merged_data["operationalLayers"]) == 2
|
|
196
|
+
|
|
197
|
+
def test_three_way_merge_theirs_modified(self, base_map, our_map, their_map):
|
|
198
|
+
"""Test three-way merge when they modified a layer we didn't."""
|
|
199
|
+
# Revert our modification to layer1 to test "theirs wins"
|
|
200
|
+
our_map["operationalLayers"][0]["title"] = "Layer One" # Same as base
|
|
201
|
+
|
|
202
|
+
result = merge_maps(our_map, their_map, base_map)
|
|
203
|
+
|
|
204
|
+
# Layer2 should use their version since we didn't change it
|
|
205
|
+
layer2 = next(
|
|
206
|
+
(l for l in result.merged_data["operationalLayers"] if l["id"] == "layer2"),
|
|
207
|
+
None
|
|
208
|
+
)
|
|
209
|
+
assert layer2 is not None
|
|
210
|
+
assert layer2["title"] == "Layer Two Updated"
|
|
211
|
+
assert "layer2" in result.modified_layers
|
|
212
|
+
|
|
213
|
+
def test_three_way_merge_ours_modified(self, base_map):
|
|
214
|
+
"""Test three-way merge when we modified a layer they didn't."""
|
|
215
|
+
ours = {
|
|
216
|
+
"operationalLayers": [
|
|
217
|
+
{"id": "layer1", "title": "Layer One Modified", "url": "http://example.com/1"},
|
|
218
|
+
],
|
|
219
|
+
}
|
|
220
|
+
theirs = {
|
|
221
|
+
"operationalLayers": [
|
|
222
|
+
{"id": "layer1", "title": "Layer One", "url": "http://example.com/1"},
|
|
223
|
+
],
|
|
224
|
+
}
|
|
225
|
+
base = {
|
|
226
|
+
"operationalLayers": [
|
|
227
|
+
{"id": "layer1", "title": "Layer One", "url": "http://example.com/1"},
|
|
228
|
+
],
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
result = merge_maps(ours, theirs, base)
|
|
232
|
+
|
|
233
|
+
# Our modification should be kept
|
|
234
|
+
layer1 = result.merged_data["operationalLayers"][0]
|
|
235
|
+
assert layer1["title"] == "Layer One Modified"
|
|
236
|
+
assert result.success is True
|
|
237
|
+
|
|
238
|
+
def test_three_way_merge_both_modified_conflict(self, base_map):
|
|
239
|
+
"""Test three-way merge creates conflict when both modify same layer."""
|
|
240
|
+
ours = {
|
|
241
|
+
"operationalLayers": [
|
|
242
|
+
{"id": "layer1", "title": "Layer One - Ours", "url": "http://example.com/1"},
|
|
243
|
+
],
|
|
244
|
+
}
|
|
245
|
+
theirs = {
|
|
246
|
+
"operationalLayers": [
|
|
247
|
+
{"id": "layer1", "title": "Layer One - Theirs", "url": "http://example.com/1"},
|
|
248
|
+
],
|
|
249
|
+
}
|
|
250
|
+
base = {
|
|
251
|
+
"operationalLayers": [
|
|
252
|
+
{"id": "layer1", "title": "Layer One", "url": "http://example.com/1"},
|
|
253
|
+
],
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
result = merge_maps(ours, theirs, base)
|
|
257
|
+
|
|
258
|
+
assert result.success is False
|
|
259
|
+
assert result.has_conflicts is True
|
|
260
|
+
assert len(result.conflicts) == 1
|
|
261
|
+
assert result.conflicts[0].layer_id == "layer1"
|
|
262
|
+
|
|
263
|
+
def test_merge_conflict_without_base(self):
|
|
264
|
+
"""Test conflict when same layer differs without base."""
|
|
265
|
+
ours = {
|
|
266
|
+
"operationalLayers": [
|
|
267
|
+
{"id": "layer1", "title": "Layer One - Ours"},
|
|
268
|
+
],
|
|
269
|
+
}
|
|
270
|
+
theirs = {
|
|
271
|
+
"operationalLayers": [
|
|
272
|
+
{"id": "layer1", "title": "Layer One - Theirs"},
|
|
273
|
+
],
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
result = merge_maps(ours, theirs)
|
|
277
|
+
|
|
278
|
+
assert result.success is False
|
|
279
|
+
assert len(result.conflicts) == 1
|
|
280
|
+
|
|
281
|
+
def test_merge_they_deleted_we_kept(self, base_map):
|
|
282
|
+
"""Test merging when they deleted a layer we kept."""
|
|
283
|
+
ours = {
|
|
284
|
+
"operationalLayers": [
|
|
285
|
+
{"id": "layer1", "title": "Layer One", "url": "http://example.com/1"},
|
|
286
|
+
{"id": "layer2", "title": "Layer Two", "url": "http://example.com/2"},
|
|
287
|
+
],
|
|
288
|
+
}
|
|
289
|
+
theirs = {
|
|
290
|
+
"operationalLayers": [
|
|
291
|
+
{"id": "layer1", "title": "Layer One", "url": "http://example.com/1"},
|
|
292
|
+
# layer2 deleted
|
|
293
|
+
],
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
result = merge_maps(ours, theirs, base_map)
|
|
297
|
+
|
|
298
|
+
# We kept layer2, it should be in merged result
|
|
299
|
+
layer_ids = [l["id"] for l in result.merged_data["operationalLayers"]]
|
|
300
|
+
assert "layer2" in layer_ids
|
|
301
|
+
|
|
302
|
+
def test_merge_we_deleted_they_modified_conflict(self, base_map):
|
|
303
|
+
"""Test conflict when we deleted and they modified."""
|
|
304
|
+
ours = {
|
|
305
|
+
"operationalLayers": [
|
|
306
|
+
{"id": "layer1", "title": "Layer One", "url": "http://example.com/1"},
|
|
307
|
+
# layer2 deleted
|
|
308
|
+
],
|
|
309
|
+
}
|
|
310
|
+
theirs = {
|
|
311
|
+
"operationalLayers": [
|
|
312
|
+
{"id": "layer1", "title": "Layer One", "url": "http://example.com/1"},
|
|
313
|
+
{"id": "layer2", "title": "Layer Two Modified", "url": "http://example.com/2-new"},
|
|
314
|
+
],
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
result = merge_maps(ours, theirs, base_map)
|
|
318
|
+
|
|
319
|
+
# Conflict: we deleted, they modified
|
|
320
|
+
assert result.has_conflicts is True
|
|
321
|
+
conflict = result.conflicts[0]
|
|
322
|
+
assert conflict.layer_id == "layer2"
|
|
323
|
+
assert conflict.ours == {} # We deleted
|
|
324
|
+
|
|
325
|
+
def test_merge_tables_conflict(self, base_map):
|
|
326
|
+
"""Test table merge creates conflicts correctly."""
|
|
327
|
+
ours = {
|
|
328
|
+
"operationalLayers": [],
|
|
329
|
+
"tables": [
|
|
330
|
+
{"id": "table1", "title": "Table One - Ours", "url": "http://example.com/t1"},
|
|
331
|
+
],
|
|
332
|
+
}
|
|
333
|
+
theirs = {
|
|
334
|
+
"operationalLayers": [],
|
|
335
|
+
"tables": [
|
|
336
|
+
{"id": "table1", "title": "Table One - Theirs", "url": "http://example.com/t1"},
|
|
337
|
+
],
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
result = merge_maps(ours, theirs, base_map)
|
|
341
|
+
|
|
342
|
+
assert result.has_conflicts is True
|
|
343
|
+
conflict = result.conflicts[0]
|
|
344
|
+
assert conflict.layer_id == "table1"
|
|
345
|
+
|
|
346
|
+
def test_merge_adds_their_new_table(self):
|
|
347
|
+
"""Test merging adds tables only in theirs."""
|
|
348
|
+
ours = {
|
|
349
|
+
"operationalLayers": [],
|
|
350
|
+
"tables": [],
|
|
351
|
+
}
|
|
352
|
+
theirs = {
|
|
353
|
+
"operationalLayers": [],
|
|
354
|
+
"tables": [
|
|
355
|
+
{"id": "table1", "title": "New Table"},
|
|
356
|
+
],
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
result = merge_maps(ours, theirs)
|
|
360
|
+
|
|
361
|
+
assert len(result.merged_data["tables"]) == 1
|
|
362
|
+
assert "table1" in result.added_layers
|
|
363
|
+
|
|
364
|
+
def test_merge_tables_conflict_two_way(self):
|
|
365
|
+
"""Test two-way table merge (no base) creates conflict when both differ."""
|
|
366
|
+
ours = {
|
|
367
|
+
"operationalLayers": [],
|
|
368
|
+
"tables": [
|
|
369
|
+
{"id": "table1", "title": "Table One - Ours", "url": "http://example.com/t1-ours"},
|
|
370
|
+
],
|
|
371
|
+
}
|
|
372
|
+
theirs = {
|
|
373
|
+
"operationalLayers": [],
|
|
374
|
+
"tables": [
|
|
375
|
+
{"id": "table1", "title": "Table One - Theirs", "url": "http://example.com/t1-theirs"},
|
|
376
|
+
],
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
# Two-way merge: no base argument
|
|
380
|
+
result = merge_maps(ours, theirs)
|
|
381
|
+
|
|
382
|
+
assert result.has_conflicts is True
|
|
383
|
+
assert len(result.conflicts) == 1
|
|
384
|
+
conflict = result.conflicts[0]
|
|
385
|
+
assert conflict.layer_id == "table1"
|
|
386
|
+
assert conflict.layer_title == "Table One - Ours"
|
|
387
|
+
assert conflict.ours == ours["tables"][0]
|
|
388
|
+
assert conflict.theirs == theirs["tables"][0]
|
|
389
|
+
assert conflict.base is None # No base in two-way merge
|
|
390
|
+
|
|
391
|
+
def test_merge_they_modified_table_we_deleted(self, base_map):
|
|
392
|
+
"""Test conflict when they modify a table we deleted."""
|
|
393
|
+
# We deleted table1 (not in our map)
|
|
394
|
+
ours = {
|
|
395
|
+
"operationalLayers": base_map["operationalLayers"],
|
|
396
|
+
"tables": [], # We deleted table1
|
|
397
|
+
}
|
|
398
|
+
# They modified table1
|
|
399
|
+
theirs = {
|
|
400
|
+
"operationalLayers": base_map["operationalLayers"],
|
|
401
|
+
"tables": [
|
|
402
|
+
{"id": "table1", "title": "Table One Modified By Them", "url": "http://example.com/t1-modified"},
|
|
403
|
+
],
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
result = merge_maps(ours, theirs, base_map)
|
|
407
|
+
|
|
408
|
+
# Should create a conflict: they modified, we deleted
|
|
409
|
+
assert result.has_conflicts is True
|
|
410
|
+
conflict = next((c for c in result.conflicts if c.layer_id == "table1"), None)
|
|
411
|
+
assert conflict is not None
|
|
412
|
+
assert conflict.ours == {} # We deleted it
|
|
413
|
+
assert conflict.theirs["title"] == "Table One Modified By Them"
|
|
414
|
+
assert conflict.base == base_map["tables"][0]
|
|
415
|
+
|
|
416
|
+
def test_merge_they_kept_table_we_deleted_unchanged(self, base_map):
|
|
417
|
+
"""Test that when they don't change a table we deleted, we respect our deletion."""
|
|
418
|
+
# We deleted table1
|
|
419
|
+
ours = {
|
|
420
|
+
"operationalLayers": base_map["operationalLayers"],
|
|
421
|
+
"tables": [],
|
|
422
|
+
}
|
|
423
|
+
# They didn't modify table1 (same as base)
|
|
424
|
+
theirs = {
|
|
425
|
+
"operationalLayers": base_map["operationalLayers"],
|
|
426
|
+
"tables": base_map["tables"].copy(), # Same as base
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
result = merge_maps(ours, theirs, base_map)
|
|
430
|
+
|
|
431
|
+
# No conflict - we deleted, they didn't change, so deletion is respected
|
|
432
|
+
table_conflicts = [c for c in result.conflicts if c.layer_id == "table1"]
|
|
433
|
+
assert len(table_conflicts) == 0
|
|
434
|
+
# Table should not be in merged result
|
|
435
|
+
merged_table_ids = [t["id"] for t in result.merged_data.get("tables", [])]
|
|
436
|
+
assert "table1" not in merged_table_ids
|
|
437
|
+
|
|
438
|
+
def test_merge_preserves_non_layer_properties(self):
|
|
439
|
+
"""Test that merge preserves map properties outside layers."""
|
|
440
|
+
ours = {
|
|
441
|
+
"mapTitle": "My Map",
|
|
442
|
+
"basemap": "topo",
|
|
443
|
+
"operationalLayers": [],
|
|
444
|
+
}
|
|
445
|
+
theirs = {
|
|
446
|
+
"mapTitle": "Their Map",
|
|
447
|
+
"basemap": "satellite",
|
|
448
|
+
"operationalLayers": [],
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
result = merge_maps(ours, theirs)
|
|
452
|
+
|
|
453
|
+
# Should keep ours since we start with our map
|
|
454
|
+
assert result.merged_data["mapTitle"] == "My Map"
|
|
455
|
+
assert result.merged_data["basemap"] == "topo"
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
# ---- resolve_conflict Tests ---------------------------------------------------------------------------------
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
class TestResolveConflict:
|
|
462
|
+
"""Tests for resolve_conflict function."""
|
|
463
|
+
|
|
464
|
+
@pytest.fixture
|
|
465
|
+
def conflict(self):
|
|
466
|
+
"""Create a test conflict."""
|
|
467
|
+
return MergeConflict(
|
|
468
|
+
layer_id="layer1",
|
|
469
|
+
layer_title="Test Layer",
|
|
470
|
+
ours={"id": "layer1", "title": "Ours"},
|
|
471
|
+
theirs={"id": "layer1", "title": "Theirs"},
|
|
472
|
+
base={"id": "layer1", "title": "Base"},
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
def test_resolve_ours(self, conflict):
|
|
476
|
+
"""Test resolving conflict with 'ours'."""
|
|
477
|
+
result = resolve_conflict(conflict, "ours")
|
|
478
|
+
assert result == {"id": "layer1", "title": "Ours"}
|
|
479
|
+
|
|
480
|
+
def test_resolve_theirs(self, conflict):
|
|
481
|
+
"""Test resolving conflict with 'theirs'."""
|
|
482
|
+
result = resolve_conflict(conflict, "theirs")
|
|
483
|
+
assert result == {"id": "layer1", "title": "Theirs"}
|
|
484
|
+
|
|
485
|
+
def test_resolve_base(self, conflict):
|
|
486
|
+
"""Test resolving conflict with 'base'."""
|
|
487
|
+
result = resolve_conflict(conflict, "base")
|
|
488
|
+
assert result == {"id": "layer1", "title": "Base"}
|
|
489
|
+
|
|
490
|
+
def test_resolve_base_not_available(self):
|
|
491
|
+
"""Test error when resolving with base but no base exists."""
|
|
492
|
+
conflict = MergeConflict(
|
|
493
|
+
layer_id="layer1",
|
|
494
|
+
layer_title="Test",
|
|
495
|
+
ours={},
|
|
496
|
+
theirs={},
|
|
497
|
+
base=None,
|
|
498
|
+
)
|
|
499
|
+
with pytest.raises(ValueError, match="No base version available"):
|
|
500
|
+
resolve_conflict(conflict, "base")
|
|
501
|
+
|
|
502
|
+
def test_resolve_invalid_strategy(self, conflict):
|
|
503
|
+
"""Test error with invalid resolution strategy."""
|
|
504
|
+
with pytest.raises(ValueError, match="Invalid resolution strategy"):
|
|
505
|
+
resolve_conflict(conflict, "invalid")
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
# ---- apply_resolution Tests ---------------------------------------------------------------------------------
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
class TestApplyResolution:
|
|
512
|
+
"""Tests for apply_resolution function."""
|
|
513
|
+
|
|
514
|
+
def test_apply_layer_resolution(self):
|
|
515
|
+
"""Test applying resolution to a layer conflict."""
|
|
516
|
+
merge_result = MergeResult(
|
|
517
|
+
merged_data={
|
|
518
|
+
"operationalLayers": [
|
|
519
|
+
{"id": "layer1", "title": "Old"},
|
|
520
|
+
],
|
|
521
|
+
"tables": [],
|
|
522
|
+
},
|
|
523
|
+
conflicts=[
|
|
524
|
+
MergeConflict(
|
|
525
|
+
layer_id="layer1",
|
|
526
|
+
layer_title="Test",
|
|
527
|
+
ours={"id": "layer1", "title": "Old"},
|
|
528
|
+
theirs={"id": "layer1", "title": "New"},
|
|
529
|
+
),
|
|
530
|
+
],
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
result = apply_resolution(
|
|
534
|
+
merge_result,
|
|
535
|
+
"layer1",
|
|
536
|
+
{"id": "layer1", "title": "Resolved"},
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
assert len(result.conflicts) == 0
|
|
540
|
+
assert result.success is True
|
|
541
|
+
assert result.merged_data["operationalLayers"][0]["title"] == "Resolved"
|
|
542
|
+
|
|
543
|
+
def test_apply_table_resolution(self):
|
|
544
|
+
"""Test applying resolution to a table conflict."""
|
|
545
|
+
merge_result = MergeResult(
|
|
546
|
+
merged_data={
|
|
547
|
+
"operationalLayers": [],
|
|
548
|
+
"tables": [
|
|
549
|
+
{"id": "table1", "title": "Old"},
|
|
550
|
+
],
|
|
551
|
+
},
|
|
552
|
+
conflicts=[
|
|
553
|
+
MergeConflict(
|
|
554
|
+
layer_id="table1",
|
|
555
|
+
layer_title="Test",
|
|
556
|
+
ours={},
|
|
557
|
+
theirs={},
|
|
558
|
+
),
|
|
559
|
+
],
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
result = apply_resolution(
|
|
563
|
+
merge_result,
|
|
564
|
+
"table1",
|
|
565
|
+
{"id": "table1", "title": "Resolved"},
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
assert len(result.conflicts) == 0
|
|
569
|
+
assert result.merged_data["tables"][0]["title"] == "Resolved"
|
|
570
|
+
|
|
571
|
+
def test_apply_resolution_deletes_layer(self):
|
|
572
|
+
"""Test that empty resolution deletes the layer."""
|
|
573
|
+
merge_result = MergeResult(
|
|
574
|
+
merged_data={
|
|
575
|
+
"operationalLayers": [
|
|
576
|
+
{"id": "layer1", "title": "To Delete"},
|
|
577
|
+
],
|
|
578
|
+
"tables": [],
|
|
579
|
+
},
|
|
580
|
+
conflicts=[
|
|
581
|
+
MergeConflict(
|
|
582
|
+
layer_id="layer1",
|
|
583
|
+
layer_title="Test",
|
|
584
|
+
ours={},
|
|
585
|
+
theirs={},
|
|
586
|
+
),
|
|
587
|
+
],
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
result = apply_resolution(merge_result, "layer1", {})
|
|
591
|
+
|
|
592
|
+
assert len(result.merged_data["operationalLayers"]) == 0
|
|
593
|
+
|
|
594
|
+
def test_apply_resolution_adds_missing_layer(self):
|
|
595
|
+
"""Test adding a layer that wasn't in merged data."""
|
|
596
|
+
merge_result = MergeResult(
|
|
597
|
+
merged_data={
|
|
598
|
+
"operationalLayers": [],
|
|
599
|
+
"tables": [],
|
|
600
|
+
},
|
|
601
|
+
conflicts=[
|
|
602
|
+
MergeConflict(
|
|
603
|
+
layer_id="layer1",
|
|
604
|
+
layer_title="Test",
|
|
605
|
+
ours={},
|
|
606
|
+
theirs={"id": "layer1", "title": "New"},
|
|
607
|
+
),
|
|
608
|
+
],
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
result = apply_resolution(
|
|
612
|
+
merge_result,
|
|
613
|
+
"layer1",
|
|
614
|
+
{"id": "layer1", "title": "New"},
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
assert len(result.merged_data["operationalLayers"]) == 1
|
|
618
|
+
assert result.merged_data["operationalLayers"][0]["title"] == "New"
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
# ---- format_merge_summary Tests -----------------------------------------------------------------------------
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
class TestFormatMergeSummary:
|
|
625
|
+
"""Tests for format_merge_summary function."""
|
|
626
|
+
|
|
627
|
+
def test_format_successful_merge(self):
|
|
628
|
+
"""Test formatting a successful merge."""
|
|
629
|
+
result = MergeResult(success=True)
|
|
630
|
+
summary = format_merge_summary(result)
|
|
631
|
+
assert "Merge completed successfully" in summary
|
|
632
|
+
|
|
633
|
+
def test_format_merge_with_conflicts(self):
|
|
634
|
+
"""Test formatting a merge with conflicts."""
|
|
635
|
+
result = MergeResult(
|
|
636
|
+
success=False,
|
|
637
|
+
conflicts=[
|
|
638
|
+
MergeConflict(
|
|
639
|
+
layer_id="layer1",
|
|
640
|
+
layer_title="Problem Layer",
|
|
641
|
+
ours={},
|
|
642
|
+
theirs={},
|
|
643
|
+
),
|
|
644
|
+
],
|
|
645
|
+
)
|
|
646
|
+
summary = format_merge_summary(result)
|
|
647
|
+
assert "1 conflict" in summary
|
|
648
|
+
assert "Problem Layer" in summary
|
|
649
|
+
assert "layer1" in summary
|
|
650
|
+
|
|
651
|
+
def test_format_with_added_layers(self):
|
|
652
|
+
"""Test formatting shows added layers."""
|
|
653
|
+
result = MergeResult(
|
|
654
|
+
success=True,
|
|
655
|
+
added_layers=["layer1", "layer2"],
|
|
656
|
+
)
|
|
657
|
+
summary = format_merge_summary(result)
|
|
658
|
+
assert "Added layers: 2" in summary
|
|
659
|
+
assert "+ layer1" in summary
|
|
660
|
+
assert "+ layer2" in summary
|
|
661
|
+
|
|
662
|
+
def test_format_with_removed_layers(self):
|
|
663
|
+
"""Test formatting shows removed layers."""
|
|
664
|
+
result = MergeResult(
|
|
665
|
+
success=True,
|
|
666
|
+
removed_layers=["layer1"],
|
|
667
|
+
)
|
|
668
|
+
summary = format_merge_summary(result)
|
|
669
|
+
assert "Removed layers: 1" in summary
|
|
670
|
+
assert "- layer1" in summary
|
|
671
|
+
|
|
672
|
+
def test_format_with_modified_layers(self):
|
|
673
|
+
"""Test formatting shows modified layers."""
|
|
674
|
+
result = MergeResult(
|
|
675
|
+
success=True,
|
|
676
|
+
modified_layers=["layer1"],
|
|
677
|
+
)
|
|
678
|
+
summary = format_merge_summary(result)
|
|
679
|
+
assert "Modified layers: 1" in summary
|
|
680
|
+
assert "~ layer1" in summary
|
|
681
|
+
|
|
682
|
+
def test_format_with_tables(self):
|
|
683
|
+
"""Test formatting shows merged tables count."""
|
|
684
|
+
result = MergeResult(
|
|
685
|
+
success=True,
|
|
686
|
+
merged_data={
|
|
687
|
+
"tables": [
|
|
688
|
+
{"id": "table1"},
|
|
689
|
+
{"id": "table2"},
|
|
690
|
+
],
|
|
691
|
+
},
|
|
692
|
+
)
|
|
693
|
+
summary = format_merge_summary(result)
|
|
694
|
+
assert "Merged tables: 2" in summary
|