KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_merge_engine.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 3
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, you may find one here:
18 * http://www.gnu.org/licenses/gpl-3.0.html
19 * or you may search the http://www.gnu.org website for the version 3 license,
20 * or you may write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 */
23
24#include <boost/test/unit_test.hpp>
25
28
29#include <nlohmann/json.hpp>
30
31
32using namespace KICAD_DIFF;
33
34
35namespace
36{
37
38ITEM_CHANGE makeAdded( const KIID& aUuid, const wxString& aType = wxS( "PCB_TRACK" ) )
39{
41 c.id.push_back( aUuid );
42 c.typeName = aType;
44 return c;
45}
46
47
48ITEM_CHANGE makeRemoved( const KIID& aUuid, const wxString& aType = wxS( "PCB_TRACK" ) )
49{
51 c.id.push_back( aUuid );
52 c.typeName = aType;
54 return c;
55}
56
57
58ITEM_CHANGE makeModifiedWidth( const KIID& aUuid, int aBefore, int aAfter )
59{
61 c.id.push_back( aUuid );
62 c.typeName = wxS( "PCB_TRACK" );
64
66 d.name = wxS( "Width" );
67 d.before = DIFF_VALUE::FromInt( aBefore );
68 d.after = DIFF_VALUE::FromInt( aAfter );
69 c.properties.push_back( std::move( d ) );
70
71 return c;
72}
73
74
75ITEM_CHANGE makeModifiedTwoProps( const KIID& aUuid )
76{
78 c.id.push_back( aUuid );
79 c.typeName = wxS( "FOOTPRINT" );
81
83 p1.name = wxS( "Position X" );
85 p1.after = DIFF_VALUE::FromInt( 1000 );
86 c.properties.push_back( p1 );
87
89 p2.name = wxS( "Reference" );
90 p2.before = DIFF_VALUE::FromString( wxS( "R1" ) );
91 p2.after = DIFF_VALUE::FromString( wxS( "R2" ) );
92 c.properties.push_back( p2 );
93
94 return c;
95}
96
97} // namespace
98
99
100BOOST_AUTO_TEST_SUITE( MergeEngine )
101
102
103BOOST_AUTO_TEST_CASE( OneSidedChangeAutoTakes )
104{
105 KIID::SeedGenerator( 200 );
106 KIID id;
107
108 DOCUMENT_DIFF ours;
109 DOCUMENT_DIFF theirs;
110 ours.changes.push_back( makeAdded( id ) );
111
112 KICAD_MERGE_ENGINE engine;
113 MERGE_PLAN plan = engine.Plan( ours, theirs );
114
115 BOOST_REQUIRE_EQUAL( plan.actions.size(), 1u );
116 BOOST_CHECK( plan.actions[0].kind == ITEM_RES::TAKE_OURS );
117 BOOST_CHECK( plan.Resolved() );
118}
119
120
121BOOST_AUTO_TEST_CASE( BothSidesAddSameIdConflicts )
122{
123 KIID::SeedGenerator( 201 );
124 KIID id;
125
126 DOCUMENT_DIFF ours;
127 DOCUMENT_DIFF theirs;
128 ours.changes.push_back( makeAdded( id ) );
129 theirs.changes.push_back( makeAdded( id ) );
130
131 KICAD_MERGE_ENGINE engine;
132 MERGE_PLAN plan = engine.Plan( ours, theirs );
133
134 BOOST_REQUIRE_EQUAL( plan.actions.size(), 1u );
135 BOOST_CHECK( !plan.Resolved() );
136 BOOST_CHECK_EQUAL( plan.ConflictCount(), 1u );
137}
138
139
140BOOST_AUTO_TEST_CASE( BothSidesDeleteSameIdAutoTakes )
141{
142 KIID::SeedGenerator( 202 );
143 KIID id;
144
145 DOCUMENT_DIFF ours;
146 DOCUMENT_DIFF theirs;
147 ours.changes.push_back( makeRemoved( id ) );
148 theirs.changes.push_back( makeRemoved( id ) );
149
150 KICAD_MERGE_ENGINE engine;
151 MERGE_PLAN plan = engine.Plan( ours, theirs );
152
153 BOOST_REQUIRE_EQUAL( plan.actions.size(), 1u );
154 BOOST_CHECK( plan.actions[0].kind == ITEM_RES::DELETE_ITEM );
155 BOOST_CHECK( plan.Resolved() );
156}
157
158
159BOOST_AUTO_TEST_CASE( DeleteOnOneSideModifyOnOtherConflicts )
160{
161 KIID::SeedGenerator( 203 );
162 KIID id;
163
164 DOCUMENT_DIFF ours;
165 DOCUMENT_DIFF theirs;
166 ours.changes.push_back( makeRemoved( id ) );
167 theirs.changes.push_back( makeModifiedWidth( id, 100, 200 ) );
168
169 KICAD_MERGE_ENGINE engine;
170 MERGE_PLAN plan = engine.Plan( ours, theirs );
171
172 BOOST_REQUIRE_EQUAL( plan.actions.size(), 1u );
173 BOOST_CHECK( !plan.Resolved() );
174}
175
176
177BOOST_AUTO_TEST_CASE( OrthogonalPropertyEditsAutoMerge )
178{
179 KIID::SeedGenerator( 204 );
180 KIID id;
181
182 // Ours changes only "Position X"; Theirs changes only "Reference".
183 // The two edits don't touch the same property — auto-merge.
184 DOCUMENT_DIFF ours;
185 DOCUMENT_DIFF theirs;
186
187 ITEM_CHANGE ourChange;
188 ourChange.id.push_back( id );
189 ourChange.typeName = wxS( "FOOTPRINT" );
190 ourChange.kind = CHANGE_KIND::MODIFIED;
192 p1.name = wxS( "Position X" );
193 p1.before = DIFF_VALUE::FromInt( 0 );
194 p1.after = DIFF_VALUE::FromInt( 1000 );
195 ourChange.properties.push_back( p1 );
196 ours.changes.push_back( ourChange );
197
198 ITEM_CHANGE theirChange;
199 theirChange.id.push_back( id );
200 theirChange.typeName = wxS( "FOOTPRINT" );
201 theirChange.kind = CHANGE_KIND::MODIFIED;
203 p2.name = wxS( "Reference" );
204 p2.before = DIFF_VALUE::FromString( wxS( "R1" ) );
205 p2.after = DIFF_VALUE::FromString( wxS( "R2" ) );
206 theirChange.properties.push_back( p2 );
207 theirs.changes.push_back( theirChange );
208
209 KICAD_MERGE_ENGINE engine;
210 MERGE_PLAN plan = engine.Plan( ours, theirs );
211
212 BOOST_REQUIRE_EQUAL( plan.actions.size(), 1u );
213 BOOST_CHECK( plan.actions[0].kind == ITEM_RES::MERGE_PROPS );
214 BOOST_CHECK( plan.Resolved() );
215
216 // Two property resolutions: Position X -> OURS, Reference -> THEIRS.
217 BOOST_REQUIRE_EQUAL( plan.actions[0].props.size(), 2u );
218
219 bool foundOurs = false, foundTheirs = false;
220
221 for( const PROPERTY_RESOLUTION& p : plan.actions[0].props )
222 {
223 if( p.name == wxS( "Position X" ) && p.kind == PROP_RES::OURS )
224 foundOurs = true;
225
226 if( p.name == wxS( "Reference" ) && p.kind == PROP_RES::THEIRS )
227 foundTheirs = true;
228 }
229
230 BOOST_CHECK( foundOurs );
231 BOOST_CHECK( foundTheirs );
232}
233
234
235BOOST_AUTO_TEST_CASE( SamePropertyDifferentValuesConflicts )
236{
237 KIID::SeedGenerator( 205 );
238 KIID id;
239
240 DOCUMENT_DIFF ours;
241 DOCUMENT_DIFF theirs;
242 ours.changes.push_back( makeModifiedWidth( id, 100, 200 ) );
243 theirs.changes.push_back( makeModifiedWidth( id, 100, 300 ) );
244
245 KICAD_MERGE_ENGINE engine;
246 MERGE_PLAN plan = engine.Plan( ours, theirs );
247
248 BOOST_REQUIRE_EQUAL( plan.actions.size(), 1u );
249 BOOST_CHECK( !plan.Resolved() );
250 BOOST_CHECK( plan.actions[0].kind == ITEM_RES::MERGE_PROPS );
251}
252
253
254BOOST_AUTO_TEST_CASE( SamePropertyEqualValuesAutoMerges )
255{
256 KIID::SeedGenerator( 206 );
257 KIID id;
258
259 DOCUMENT_DIFF ours;
260 DOCUMENT_DIFF theirs;
261 ours.changes.push_back( makeModifiedWidth( id, 100, 250 ) );
262 theirs.changes.push_back( makeModifiedWidth( id, 100, 250 ) );
263
264 KICAD_MERGE_ENGINE engine;
265 MERGE_PLAN plan = engine.Plan( ours, theirs );
266
267 BOOST_CHECK( plan.Resolved() );
268}
269
270
271BOOST_AUTO_TEST_CASE( ZoneEditMarksRequiresZoneRefill )
272{
273 KIID::SeedGenerator( 207 );
274 KIID id;
275
276 DOCUMENT_DIFF ours;
277 DOCUMENT_DIFF theirs;
278
279 ITEM_CHANGE z;
280 z.id.push_back( id );
281 z.typeName = wxS( "ZONE" );
283 ours.changes.push_back( z );
284
285 KICAD_MERGE_ENGINE engine;
286 MERGE_PLAN plan = engine.Plan( ours, theirs );
287
288 BOOST_CHECK( plan.requiresZoneRefill );
289}
290
291
292BOOST_AUTO_TEST_CASE( TrackEditMarksConnectivityRebuild )
293{
294 KIID::SeedGenerator( 208 );
295 KIID id;
296
297 DOCUMENT_DIFF ours;
298 DOCUMENT_DIFF theirs;
299
300 ITEM_CHANGE t;
301 t.id.push_back( id );
302 t.typeName = wxS( "PCB_TRACK" );
304 ours.changes.push_back( t );
305
306 KICAD_MERGE_ENGINE engine;
307 MERGE_PLAN plan = engine.Plan( ours, theirs );
308
309 BOOST_CHECK( plan.requiresConnectivityRebuild );
310}
311
312
313BOOST_AUTO_TEST_CASE( DuplicateUuidIsAlwaysConflict )
314{
315 KIID::SeedGenerator( 209 );
316 KIID id;
317
318 DOCUMENT_DIFF ours;
319 DOCUMENT_DIFF theirs;
320
321 ITEM_CHANGE dup;
322 dup.id.push_back( id );
323 dup.typeName = wxS( "PCB_TRACK" );
325 ours.changes.push_back( dup );
326
327 KICAD_MERGE_ENGINE engine;
328 MERGE_PLAN plan = engine.Plan( ours, theirs );
329
330 BOOST_CHECK( !plan.Resolved() );
331}
332
333
334BOOST_AUTO_TEST_CASE( MergePlanJsonRoundTrip )
335{
336 KIID::SeedGenerator( 210 );
337 KIID id;
338
339 DOCUMENT_DIFF ours;
340 DOCUMENT_DIFF theirs;
341 ours.changes.push_back( makeAdded( id ) );
342 theirs.changes.push_back( makeModifiedWidth( id, 100, 200 ) );
343
344 KICAD_MERGE_ENGINE engine;
345 MERGE_PLAN plan = engine.Plan( ours, theirs );
346
347 nlohmann::json j = plan.ToJson();
349
350 // Sizes alone could miss a swap of actions <-> unresolved or a content
351 // drop — pin actual id and kind values.
352 BOOST_REQUIRE_EQUAL( back.actions.size(), plan.actions.size() );
353 for( std::size_t i = 0; i < plan.actions.size(); ++i )
354 {
355 BOOST_CHECK( back.actions[i].id == plan.actions[i].id );
356 BOOST_CHECK( back.actions[i].kind == plan.actions[i].kind );
357 }
358
359 BOOST_REQUIRE_EQUAL( back.unresolved.size(), plan.unresolved.size() );
360 for( std::size_t i = 0; i < plan.unresolved.size(); ++i )
361 BOOST_CHECK( back.unresolved[i] == plan.unresolved[i] );
362
365}
366
367
368BOOST_AUTO_TEST_CASE( MergePlanFromJsonMissingRequiresFieldsDefaultsFalse )
369{
370 // The FromJson contract uses `.value("...", false)` for the two
371 // requires* fields so plans serialized before those flags existed
372 // load cleanly with conservative defaults. Pin that contract.
373 nlohmann::json j;
374 j["actions"] = nlohmann::json::array();
375 j["unresolved"] = nlohmann::json::array();
376
378 BOOST_CHECK( plan.actions.empty() );
379 BOOST_CHECK( plan.unresolved.empty() );
380 BOOST_CHECK( !plan.requiresZoneRefill );
381 BOOST_CHECK( !plan.requiresConnectivityRebuild );
382}
383
384
385BOOST_AUTO_TEST_CASE( ItemResolutionJsonRoundTripAllKinds )
386{
387 // Every ITEM_RES enum value must round-trip via JSON. A regression in
388 // ItemResToString / ItemResFromString would only show up here.
389 KIID::SeedGenerator( 211 );
390 KIID id;
391
392 const ITEM_RES kinds[] = {
395 };
396
397 for( ITEM_RES kind : kinds )
398 {
400 r.id = KIID_PATH( id.AsString() );
401 r.kind = kind;
402
404 BOOST_CHECK( back == r );
405 }
406}
407
408
409BOOST_AUTO_TEST_CASE( ItemResolutionJsonRoundTripCarriesProps )
410{
411 // MERGE_PROPS resolutions carry a `props` vector of PROPERTY_RESOLUTIONs
412 // that needs to round-trip nested. A serializer that wrote `props`
413 // but a reader that ignored them would still pass the kind-only round
414 // trip above — pin the nested path here.
415 KIID::SeedGenerator( 212 );
416 KIID id;
417
419 r.id = KIID_PATH( id.AsString() );
421
422 PROPERTY_RESOLUTION pickOurs;
423 pickOurs.name = wxS( "Width" );
424 pickOurs.kind = PROP_RES::OURS;
425 r.props.push_back( pickOurs );
426
427 PROPERTY_RESOLUTION custom;
428 custom.name = wxS( "Layer" );
429 custom.kind = PROP_RES::CUSTOM;
430 custom.customValue = DIFF_VALUE::FromInt( 42 );
431 r.props.push_back( custom );
432
434 BOOST_CHECK( back == r );
435 BOOST_REQUIRE_EQUAL( back.props.size(), 2u );
436 BOOST_CHECK( back.props[0] == pickOurs );
437 BOOST_CHECK( back.props[1] == custom );
438 // Pin the customValue payload directly. The `==` checks above rely on
439 // PROPERTY_RESOLUTION::operator== — if equality stopped comparing
440 // customValue, the nested CUSTOM payload could silently drop and the
441 // round-trip would still appear to pass.
442 BOOST_CHECK( back.props[1].customValue == DIFF_VALUE::FromInt( 42 ) );
443}
444
445
446BOOST_AUTO_TEST_CASE( PropertyResolutionJsonRoundTripAllKinds )
447{
448 // Every PROP_RES enum value round-trips AND pins its literal kind
449 // string in the JSON. Round-trip-only symmetry (FromJson(ToJson()) ==)
450 // misses paired serializer/parser regressions where both ends flip
451 // to a new spelling — pin each literal explicitly.
452 struct CASE
453 {
454 PROP_RES kind;
455 const char* literal;
456 };
457
458 const CASE kinds[] = {
459 { PROP_RES::OURS, "ours" },
460 { PROP_RES::THEIRS, "theirs" },
461 { PROP_RES::ANCESTOR, "ancestor" },
462 { PROP_RES::CUSTOM, "custom" },
463 };
464
465 for( const CASE& c : kinds )
466 {
467 BOOST_TEST_CONTEXT( c.literal )
468 {
470 r.name = wxS( "Width" );
471 r.kind = c.kind;
472
473 if( c.kind == PROP_RES::CUSTOM )
475
476 nlohmann::json j = r.ToJson();
477 BOOST_CHECK_EQUAL( j.at( "kind" ).get<std::string>(), c.literal );
478 BOOST_CHECK( PROPERTY_RESOLUTION::FromJson( j ) == r );
479
480 if( c.kind == PROP_RES::CUSTOM )
481 {
482 BOOST_CHECK( PROPERTY_RESOLUTION::FromJson( j ).customValue
483 == DIFF_VALUE::FromInt( 42 ) );
484 }
485 }
486 }
487}
488
489
490BOOST_AUTO_TEST_CASE( EmptyDiffsProduceEmptyPlan )
491{
492 DOCUMENT_DIFF ours;
493 DOCUMENT_DIFF theirs;
494
495 KICAD_MERGE_ENGINE engine;
496 MERGE_PLAN plan = engine.Plan( ours, theirs );
497
498 BOOST_CHECK( plan.actions.empty() );
499 BOOST_CHECK( plan.Resolved() );
500 BOOST_CHECK( !plan.requiresZoneRefill );
501 BOOST_CHECK( !plan.requiresConnectivityRebuild );
502}
503
504
505BOOST_AUTO_TEST_CASE( ChildChangesAreIndexedRecursively )
506{
507 KIID::SeedGenerator( 220 );
508 KIID parentId, childId;
509
510 // Parent footprint MODIFIED with one child PAD also MODIFIED on ours.
511 // Theirs MODIFIEs the same child PAD — without recursive indexing the
512 // engine would only see the parent collision and miss the child conflict.
513 DOCUMENT_DIFF ours;
514 DOCUMENT_DIFF theirs;
515
516 ITEM_CHANGE ourParent;
517 ourParent.id.push_back( parentId );
518 ourParent.typeName = wxS( "FOOTPRINT" );
519 ourParent.kind = CHANGE_KIND::MODIFIED;
520
521 ITEM_CHANGE ourChild;
522 ourChild.id.push_back( parentId );
523 ourChild.id.push_back( childId );
524 ourChild.typeName = wxS( "PAD" );
525 ourChild.kind = CHANGE_KIND::MODIFIED;
527 d1.name = wxS( "Number" );
528 d1.before = DIFF_VALUE::FromString( wxS( "1" ) );
529 d1.after = DIFF_VALUE::FromString( wxS( "A" ) );
530 ourChild.properties.push_back( d1 );
531 ourParent.children.push_back( ourChild );
532
533 ours.changes.push_back( ourParent );
534
535 ITEM_CHANGE theirParent = ourParent;
536 theirParent.children.clear();
537 ITEM_CHANGE theirChild = ourChild;
538 theirChild.properties.clear();
540 d2.name = wxS( "Number" );
541 d2.before = DIFF_VALUE::FromString( wxS( "1" ) );
542 d2.after = DIFF_VALUE::FromString( wxS( "B" ) );
543 theirChild.properties.push_back( d2 );
544 theirParent.children.push_back( theirChild );
545
546 theirs.changes.push_back( theirParent );
547
548 KICAD_MERGE_ENGINE engine;
549 MERGE_PLAN plan = engine.Plan( ours, theirs );
550
551 // The child's KIID_PATH should appear in plan.actions because the engine
552 // indexed nested children. The actual property values differ, so it should
553 // be flagged unresolved.
554 bool foundChild = false;
555
556 for( const ITEM_RESOLUTION& r : plan.actions )
557 {
558 if( r.id.size() == 2 && r.id.back() == childId )
559 foundChild = true;
560 }
561
562 BOOST_CHECK( foundChild );
563 BOOST_CHECK( !plan.Resolved() );
564}
565
566
567BOOST_AUTO_TEST_CASE( PreferAutoMergeFalseConflictsOrthogonalEdits )
568{
569 KIID::SeedGenerator( 221 );
570 KIID id;
571
572 DOCUMENT_DIFF ours;
573 DOCUMENT_DIFF theirs;
574
575 // Orthogonal property edits — would auto-merge by default, but
576 // preferAutoMerge=false should treat them as conflicts.
577 ITEM_CHANGE o;
578 o.id.push_back( id );
579 o.typeName = wxS( "FOOTPRINT" );
582 d1.name = wxS( "Position X" );
583 d1.before = DIFF_VALUE::FromInt( 0 );
584 d1.after = DIFF_VALUE::FromInt( 1000 );
585 o.properties.push_back( d1 );
586 ours.changes.push_back( o );
587
588 ITEM_CHANGE t;
589 t.id.push_back( id );
590 t.typeName = wxS( "FOOTPRINT" );
593 d2.name = wxS( "Reference" );
594 d2.before = DIFF_VALUE::FromString( wxS( "R1" ) );
595 d2.after = DIFF_VALUE::FromString( wxS( "R2" ) );
596 t.properties.push_back( d2 );
597 theirs.changes.push_back( t );
598
600 opts.preferAutoMerge = false;
601 KICAD_MERGE_ENGINE engine( opts );
602
603 MERGE_PLAN plan = engine.Plan( ours, theirs );
604 BOOST_CHECK( !plan.Resolved() );
605}
606
607
608BOOST_AUTO_TEST_CASE( EmptyPropertyDeltasOnBothSidesConflicts )
609{
610 KIID::SeedGenerator( 222 );
611 KIID id;
612
613 DOCUMENT_DIFF ours;
614 DOCUMENT_DIFF theirs;
615
616 // MODIFIED records without any property deltas — change came from
617 // operator== alone. Engine must NOT silently auto-resolve.
618 ITEM_CHANGE c1;
619 c1.id.push_back( id );
620 c1.typeName = wxS( "PCB_TRACK" );
622 ours.changes.push_back( c1 );
623
624 ITEM_CHANGE c2 = c1;
625 theirs.changes.push_back( c2 );
626
627 KICAD_MERGE_ENGINE engine;
628 MERGE_PLAN plan = engine.Plan( ours, theirs );
629
630 BOOST_CHECK( !plan.Resolved() );
631}
632
633
634// ---------------------------------------------------------------------------
635// ResolvePropertyConflict
636// ---------------------------------------------------------------------------
637
638namespace
639{
640
641PROPERTY_DELTA MakeDelta( const wxString& aName, int aBefore, int aAfter )
642{
644 d.name = aName;
645 d.before = DIFF_VALUE::FromInt( aBefore );
646 d.after = DIFF_VALUE::FromInt( aAfter );
647 return d;
648}
649
650
651KICAD_MERGE_ENGINE::OPTIONS DefaultOptions()
652{
654 return o; // preferAutoMerge=true, autoResolveEqualValues=true
655}
656
657
658KICAD_MERGE_ENGINE::OPTIONS StrictOptions()
659{
661 o.preferAutoMerge = false;
662 o.autoResolveEqualValues = false;
663 return o;
664}
665
666} // namespace
667
668
669BOOST_AUTO_TEST_CASE( ResolveProperty_OnlyOurs_ReturnsOursClean )
670{
671 PROPERTY_DELTA ours = MakeDelta( wxS( "Width" ), 10, 20 );
672 auto outcome = ResolvePropertyConflict( &ours, nullptr, DefaultOptions() );
673 BOOST_CHECK( outcome.kind == PROP_RES::OURS );
674 BOOST_CHECK( !outcome.isUnresolved );
675}
676
677
678BOOST_AUTO_TEST_CASE( ResolveProperty_OnlyTheirs_ReturnsTheirsClean )
679{
680 PROPERTY_DELTA theirs = MakeDelta( wxS( "Width" ), 10, 30 );
681 auto outcome = ResolvePropertyConflict( nullptr, &theirs, DefaultOptions() );
682 BOOST_CHECK( outcome.kind == PROP_RES::THEIRS );
683 BOOST_CHECK( !outcome.isUnresolved );
684}
685
686
687BOOST_AUTO_TEST_CASE( ResolveProperty_BothNull_PreconditionViolation )
688{
689 // Both sides null is a precondition violation. CHECK_WX_ASSERT keys off
690 // wxDEBUG_LEVEL — when assertions are compiled in (the normal QA build)
691 // it expands to BOOST_CHECK_THROW( ..., WX_ASSERT_ERROR ); when stripped
692 // (wxDEBUG_LEVEL == 0) it expands to nothing. In the latter case we
693 // still want to pin the deterministic OURS fallback so the contract is
694 // verified end-to-end regardless of build configuration.
695 CHECK_WX_ASSERT( ResolvePropertyConflict( nullptr, nullptr, DefaultOptions() ) );
696
697#if wxDEBUG_LEVEL == 0
698 auto outcome = ResolvePropertyConflict( nullptr, nullptr, DefaultOptions() );
699 BOOST_CHECK( outcome.kind == PROP_RES::OURS );
700 BOOST_CHECK( !outcome.isUnresolved );
701#endif
702}
703
704
705BOOST_AUTO_TEST_CASE( ResolveProperty_BothSameAfter_AutoResolvesEqual )
706{
707 // Both sides changed to the same final value — autoResolveEqualValues
708 // picks OURS without flagging unresolved.
709 PROPERTY_DELTA ours = MakeDelta( wxS( "Width" ), 10, 25 );
710 PROPERTY_DELTA theirs = MakeDelta( wxS( "Width" ), 10, 25 );
711
712 auto outcome = ResolvePropertyConflict( &ours, &theirs, DefaultOptions() );
713 BOOST_CHECK( outcome.kind == PROP_RES::OURS );
714 BOOST_CHECK( !outcome.isUnresolved );
715}
716
717
718BOOST_AUTO_TEST_CASE( ResolveProperty_BothSameAfter_StrictReportsConflict )
719{
720 // With autoResolveEqualValues=false, equal end values still conflict.
721 PROPERTY_DELTA ours = MakeDelta( wxS( "Width" ), 10, 25 );
722 PROPERTY_DELTA theirs = MakeDelta( wxS( "Width" ), 10, 25 );
723
724 auto outcome = ResolvePropertyConflict( &ours, &theirs, StrictOptions() );
725 BOOST_CHECK( outcome.kind == PROP_RES::OURS );
726 BOOST_CHECK( outcome.isUnresolved );
727}
728
729
730BOOST_AUTO_TEST_CASE( ResolveProperty_OursNoOp_TakesTheirs )
731{
732 // Ours' "after" equals its "before" — a no-op edit. With preferAutoMerge,
733 // theirs (which actually changed) wins.
734 PROPERTY_DELTA ours = MakeDelta( wxS( "Width" ), 10, 10 );
735 PROPERTY_DELTA theirs = MakeDelta( wxS( "Width" ), 10, 30 );
736
737 auto outcome = ResolvePropertyConflict( &ours, &theirs, DefaultOptions() );
738 BOOST_CHECK( outcome.kind == PROP_RES::THEIRS );
739 BOOST_CHECK( !outcome.isUnresolved );
740}
741
742
743BOOST_AUTO_TEST_CASE( ResolveProperty_TheirsNoOp_TakesOurs )
744{
745 PROPERTY_DELTA ours = MakeDelta( wxS( "Width" ), 10, 30 );
746 PROPERTY_DELTA theirs = MakeDelta( wxS( "Width" ), 10, 10 );
747
748 auto outcome = ResolvePropertyConflict( &ours, &theirs, DefaultOptions() );
749 BOOST_CHECK( outcome.kind == PROP_RES::OURS );
750 BOOST_CHECK( !outcome.isUnresolved );
751}
752
753
754BOOST_AUTO_TEST_CASE( ResolveProperty_NoOpDetectionRequiresPreferAutoMerge )
755{
756 // Even when ours is a no-op, strict options should report a conflict.
757 PROPERTY_DELTA ours = MakeDelta( wxS( "Width" ), 10, 10 );
758 PROPERTY_DELTA theirs = MakeDelta( wxS( "Width" ), 10, 30 );
759
760 auto outcome = ResolvePropertyConflict( &ours, &theirs, StrictOptions() );
761 BOOST_CHECK( outcome.kind == PROP_RES::OURS );
762 BOOST_CHECK( outcome.isUnresolved );
763}
764
765
766BOOST_AUTO_TEST_CASE( ResolveProperty_TheirsNoOpRequiresMatchingBefore )
767{
768 // Mirror of OursNoOpRequiresMatchingBefore — theirs is a no-op against a
769 // different baseline. Without the matching-before check on the theirs-
770 // no-op branch, this case would silently take OURS (a stale baseline on
771 // theirs would override a real edit on ours).
772 PROPERTY_DELTA ours = MakeDelta( wxS( "Width" ), 10, 30 ); // changed
773 PROPERTY_DELTA theirs = MakeDelta( wxS( "Width" ), 5, 5 ); // no-op, stale before
774
775 auto outcome = ResolvePropertyConflict( &ours, &theirs, DefaultOptions() );
776 BOOST_CHECK( outcome.isUnresolved );
777}
778
779
780BOOST_AUTO_TEST_CASE( ResolveProperty_OursNoOpRequiresMatchingBefore )
781{
782 // Ours' before/after are equal (no-op), but ours' before differs from
783 // theirs' before — the "ours didn't really change" branch is gated on
784 // matching before values, so this falls through to the conflict path.
785 PROPERTY_DELTA ours = MakeDelta( wxS( "Width" ), 5, 5 ); // no-op
786 PROPERTY_DELTA theirs = MakeDelta( wxS( "Width" ), 10, 30 ); // changed
787
788 auto outcome = ResolvePropertyConflict( &ours, &theirs, DefaultOptions() );
789 BOOST_CHECK( outcome.isUnresolved );
790}
791
792
793BOOST_AUTO_TEST_CASE( ResolveProperty_BothDivergent_UnresolvedConflict )
794{
795 PROPERTY_DELTA ours = MakeDelta( wxS( "Width" ), 10, 20 );
796 PROPERTY_DELTA theirs = MakeDelta( wxS( "Width" ), 10, 30 );
797
798 auto outcome = ResolvePropertyConflict( &ours, &theirs, DefaultOptions() );
799 BOOST_CHECK( outcome.kind == PROP_RES::OURS );
800 BOOST_CHECK( outcome.isUnresolved );
801}
802
803
static DIFF_VALUE FromInt(int aValue)
static DIFF_VALUE FromString(const wxString &aValue)
Three-way merge plan generator.
MERGE_PLAN Plan(const DOCUMENT_DIFF &aAncestorOurs, const DOCUMENT_DIFF &aAncestorTheirs) const
Plan the merge given the canonical pair of diffs.
Definition kiid.h:44
static void SeedGenerator(unsigned int aSeed)
Re-initialize the UUID generator with a given seed (for testing or QA purposes)
Definition kiid.cpp:331
ITEM_RES
Resolution kind for a whole item.
PROPERTY_RESOLUTION_OUTCOME ResolvePropertyConflict(const PROPERTY_DELTA *aOurs, const PROPERTY_DELTA *aTheirs, const KICAD_MERGE_ENGINE::OPTIONS &aOptions)
Decide how to resolve a single property edit between two sides.
PROP_RES
Resolution kind for a single property of a single item.
The full set of changes between two parsed documents of one type.
std::vector< ITEM_CHANGE > changes
One change record on a single item.
std::vector< PROPERTY_DELTA > properties
std::vector< ITEM_CHANGE > children
std::vector< PROPERTY_RESOLUTION > props
static ITEM_RESOLUTION FromJson(const nlohmann::json &aJson)
bool preferAutoMerge
When true, property-orthogonal edits auto-merge silently.
bool autoResolveEqualValues
When true and a property edit conflicts but the side values are equal, the resolution is automaticall...
Result of planning a 3-way merge.
std::size_t ConflictCount() const
std::vector< ITEM_RESOLUTION > actions
nlohmann::json ToJson() const
std::vector< KIID_PATH > unresolved
static MERGE_PLAN FromJson(const nlohmann::json &aJson)
Single (name, before, after) triple for one mutated property on an item.
static PROPERTY_RESOLUTION FromJson(const nlohmann::json &aJson)
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_CASE(OneSidedChangeAutoTakes)
BOOST_TEST_CONTEXT("Test Clearance")
BOOST_CHECK_EQUAL(result, "25.4")
#define CHECK_WX_ASSERT(STATEMENT)
A test macro to check a wxASSERT is thrown.