KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_auto_resolution.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
27
28#include <wx/intl.h>
29
30
31using namespace KICAD_DIFF;
32
33
34BOOST_AUTO_TEST_SUITE( AutoResolution )
35
36
37// ParseAutoResolutionJson ---------------------------------------------------
38
39BOOST_AUTO_TEST_CASE( Parse_ValidObject )
40{
41 const std::string json =
42 R"({"/abc": "take_ours", "/def": "take_theirs", "/ghi": "take_ancestor"})";
43
44 auto r = ParseAutoResolutionJson( json );
46 BOOST_REQUIRE_EQUAL( r.resolutions.size(), 3u );
47 BOOST_CHECK( r.resolutions.at( wxS( "/abc" ) ) == ITEM_RES::TAKE_OURS );
48 BOOST_CHECK( r.resolutions.at( wxS( "/def" ) ) == ITEM_RES::TAKE_THEIRS );
49 BOOST_CHECK( r.resolutions.at( wxS( "/ghi" ) ) == ITEM_RES::TAKE_ANCESTOR );
50}
51
52
53BOOST_AUTO_TEST_CASE( Parse_EmptyObject )
54{
55 auto r = ParseAutoResolutionJson( "{}" );
56 BOOST_CHECK( r.status == AUTO_RESOLUTION_STATUS::OK );
57 BOOST_CHECK( r.resolutions.empty() );
58}
59
60
61BOOST_AUTO_TEST_CASE( Parse_InvalidJson )
62{
63 auto r = ParseAutoResolutionJson( "this is not json {{{" );
64 BOOST_CHECK( r.status == AUTO_RESOLUTION_STATUS::INVALID_JSON );
65 BOOST_CHECK( !r.errorContext.IsEmpty() );
66}
67
68
69BOOST_AUTO_TEST_CASE( Parse_EmptyString )
70{
71 auto r = ParseAutoResolutionJson( "" );
72 BOOST_CHECK( r.status == AUTO_RESOLUTION_STATUS::INVALID_JSON );
73}
74
75
76BOOST_AUTO_TEST_CASE( Parse_NotObject )
77{
78 auto r = ParseAutoResolutionJson( "[1, 2, 3]" );
79 BOOST_CHECK( r.status == AUTO_RESOLUTION_STATUS::NOT_OBJECT );
80}
81
82
83BOOST_AUTO_TEST_CASE( Parse_NotObject_String )
84{
85 auto r = ParseAutoResolutionJson( R"("take_ours")" );
86 BOOST_CHECK( r.status == AUTO_RESOLUTION_STATUS::NOT_OBJECT );
87}
88
89
90BOOST_AUTO_TEST_CASE( Parse_BadValueType )
91{
92 // Value is a number, not a string. Errors out at the first bad entry.
93 auto r = ParseAutoResolutionJson( R"({"/abc": "take_ours", "/def": 42})" );
94 BOOST_CHECK( r.status == AUTO_RESOLUTION_STATUS::BAD_VALUE );
95 BOOST_CHECK( r.errorContext == wxS( "/def" ) );
96}
97
98
99BOOST_AUTO_TEST_CASE( Parse_UnknownKindString )
100{
101 auto r = ParseAutoResolutionJson( R"({"/abc": "take_neither"})" );
102 BOOST_CHECK( r.status == AUTO_RESOLUTION_STATUS::UNKNOWN_KIND );
103 BOOST_CHECK( r.errorContext == wxS( "/abc" ) );
104}
105
106
107BOOST_AUTO_TEST_CASE( Parse_EngineInternalKindRejected_Keep )
108{
109 auto r = ParseAutoResolutionJson( R"({"/abc": "keep"})" );
110 BOOST_CHECK( r.status == AUTO_RESOLUTION_STATUS::ENGINE_INTERNAL_KIND );
111 BOOST_CHECK( r.errorContext == wxS( "/abc" ) );
112}
113
114
115BOOST_AUTO_TEST_CASE( Parse_EngineInternalKindRejected_Delete )
116{
117 auto r = ParseAutoResolutionJson( R"({"/abc": "delete"})" );
118 BOOST_CHECK( r.status == AUTO_RESOLUTION_STATUS::ENGINE_INTERNAL_KIND );
119}
120
121
122BOOST_AUTO_TEST_CASE( Parse_EngineInternalKindRejected_MergeProps )
123{
124 auto r = ParseAutoResolutionJson( R"({"/abc": "merge_props"})" );
125 BOOST_CHECK( r.status == AUTO_RESOLUTION_STATUS::ENGINE_INTERNAL_KIND );
126}
127
128
129BOOST_AUTO_TEST_CASE( Parse_NonOkStatusClearsResolutions )
130{
131 // Header doc: "resolutions is populated only when status == OK". Pin
132 // that contract — a first-valid-then-bad entry must not leave the
133 // first entry in the map.
135 R"({"/abc": "take_ours", "/def": "not_a_kind"})" );
137 BOOST_CHECK( r.resolutions.empty() );
138
139 auto r2 = ParseAutoResolutionJson(
140 R"({"/abc": "take_ours", "/def": "keep"})" );
142 BOOST_CHECK( r2.resolutions.empty() );
143
144 auto r3 = ParseAutoResolutionJson(
145 R"({"/abc": "take_ours", "/def": 42})" );
147 BOOST_CHECK( r3.resolutions.empty() );
148}
149
150
151BOOST_AUTO_TEST_CASE( Parse_UnicodeKeys )
152{
153 // Non-ASCII keys must round-trip through UTF-8 — KIID_PATH strings are
154 // ASCII in practice but the parser shouldn't lose data on unicode input.
155 auto r = ParseAutoResolutionJson( "{\"/élément\": \"take_ours\"}" );
157 BOOST_CHECK( r.resolutions.count( wxString::FromUTF8( "/élément" ) ) == 1 );
158}
159
160
161// ApplyAutoResolutions ------------------------------------------------------
162
163namespace
164{
165
166KIID_PATH MakePath( int aSeed )
167{
168 KIID::SeedGenerator( aSeed );
169 return KIID_PATH( wxS( "/" ) + KIID().AsString() );
170}
171
172
173MERGE_PLAN MakePlanWithConflicts( const std::vector<KIID_PATH>& aConflictIds )
174{
176
177 for( const KIID_PATH& id : aConflictIds )
178 {
180 r.id = id;
182 plan.actions.push_back( r );
183 plan.unresolved.push_back( id );
184 }
185
186 return plan;
187}
188
189} // namespace
190
191
192BOOST_AUTO_TEST_CASE( Apply_AllConflictsCovered )
193{
194 const KIID_PATH idA = MakePath( 1000 );
195 const KIID_PATH idB = MakePath( 1001 );
196
197 MERGE_PLAN plan = MakePlanWithConflicts( { idA, idB } );
198
199 std::vector<std::size_t> conflictIdx = { 0, 1 };
200 std::map<wxString, ITEM_RES> autoMap = {
201 { idA.AsString(), ITEM_RES::TAKE_OURS },
203 };
204
205 auto result = ApplyAutoResolutions( plan, conflictIdx, autoMap );
206
207 BOOST_CHECK( result.status == APPLY_AUTO_RESOLUTIONS_STATUS::COMPLETE );
208 BOOST_CHECK_EQUAL( result.appliedCount, 2u );
209 BOOST_CHECK( plan.actions[0].kind == ITEM_RES::TAKE_OURS );
210 BOOST_CHECK( plan.actions[1].kind == ITEM_RES::TAKE_THEIRS );
211 BOOST_CHECK( plan.unresolved.empty() );
212}
213
214
215BOOST_AUTO_TEST_CASE( Apply_PartialCoverageLeavesPlanUntouched )
216{
217 // One conflict has no matching entry. The plan must NOT be mutated —
218 // the caller relies on observing the original plan for fallback
219 // reporting after a PARTIAL result.
220 const KIID_PATH idA = MakePath( 1002 );
221 const KIID_PATH idB = MakePath( 1003 );
222
223 MERGE_PLAN plan = MakePlanWithConflicts( { idA, idB } );
224
225 std::vector<std::size_t> conflictIdx = { 0, 1 };
226 std::map<wxString, ITEM_RES> autoMap = {
227 { idA.AsString(), ITEM_RES::TAKE_OURS },
228 // idB intentionally missing
229 };
230
231 auto result = ApplyAutoResolutions( plan, conflictIdx, autoMap );
232
233 BOOST_CHECK( result.status == APPLY_AUTO_RESOLUTIONS_STATUS::PARTIAL );
234 BOOST_CHECK( result.firstMissingId == idB );
235 // Both actions should still be at their pre-apply kind.
236 BOOST_CHECK( plan.actions[0].kind == ITEM_RES::KEEP );
237 BOOST_CHECK( plan.actions[1].kind == ITEM_RES::KEEP );
238 BOOST_CHECK_EQUAL( plan.unresolved.size(), 2u );
239}
240
241
242BOOST_AUTO_TEST_CASE( Apply_NoConflicts )
243{
245 std::vector<std::size_t> conflictIdx;
246 std::map<wxString, ITEM_RES> autoMap;
247
248 auto result = ApplyAutoResolutions( plan, conflictIdx, autoMap );
250 BOOST_CHECK_EQUAL( result.appliedCount, 0u );
251}
252
253
254BOOST_AUTO_TEST_CASE( Apply_UncoveredMergedEditKeepsEngineAnswer )
255{
256 const KIID_PATH idA = MakePath( 2200 );
257 const KIID_PATH idB = MakePath( 2201 );
258
260
262 a.id = idA;
265 b.id = idB;
267 plan.actions.push_back( a );
268 plan.actions.push_back( b );
269 plan.unresolved.push_back( idB );
270
271 std::vector<std::size_t> conflictIdx = { 0, 1 };
272 std::map<wxString, ITEM_RES> autoMap = { { idB.AsString(), ITEM_RES::TAKE_OURS } };
273
274 auto result = ApplyAutoResolutions( plan, conflictIdx, autoMap );
275
276 BOOST_CHECK( result.status == APPLY_AUTO_RESOLUTIONS_STATUS::COMPLETE );
277 BOOST_CHECK( plan.actions[0].kind == ITEM_RES::MERGE_PROPS );
278 BOOST_CHECK( plan.actions[1].kind == ITEM_RES::TAKE_OURS );
279 BOOST_CHECK( plan.unresolved.empty() );
280}
281
282
283// BuildConflictList ---------------------------------------------------------
284
285BOOST_AUTO_TEST_CASE( BuildConflictList_OnlyUnresolvedActions )
286{
287 // Plan has three actions, but only two are flagged unresolved.
288 const KIID_PATH idA = MakePath( 2000 );
289 const KIID_PATH idB = MakePath( 2001 );
290 const KIID_PATH idC = MakePath( 2002 );
291
293
295 ITEM_RESOLUTION b; b.id = idB; b.kind = ITEM_RES::KEEP;
296 ITEM_RESOLUTION c; c.id = idC; c.kind = ITEM_RES::KEEP;
297 plan.actions.push_back( a );
298 plan.actions.push_back( b );
299 plan.actions.push_back( c );
300 plan.unresolved.push_back( idB );
301 plan.unresolved.push_back( idC );
302
303 auto entries = BuildConflictList( plan );
304 BOOST_REQUIRE_EQUAL( entries.size(), 2u );
305 BOOST_CHECK_EQUAL( entries[0].actionIndex, 1u );
306 BOOST_CHECK( entries[0].id == idB );
307 BOOST_CHECK_EQUAL( entries[1].actionIndex, 2u );
308 BOOST_CHECK( entries[1].id == idC );
309}
310
311
312BOOST_AUTO_TEST_CASE( BuildConflictList_LongLabelsAreTruncated )
313{
314 // KIID_PATH strings can get long for nested-sheet hierarchies. The
315 // dialog uses a 40-char display rule with right-truncation so the
316 // disambiguating segment stays visible.
317 KIID::SeedGenerator( 2100 );
318 KIID a, b, c, d;
319 const wxString longSegmentString = wxS( "/" ) + a.AsString() + wxS( "/" )
320 + b.AsString() + wxS( "/" )
321 + c.AsString() + wxS( "/" ) + d.AsString();
322 const KIID_PATH longPath( longSegmentString );
323
326 r.id = longPath;
328 plan.actions.push_back( r );
329 plan.unresolved.push_back( longPath );
330
331 auto entries = BuildConflictList( plan );
332 BOOST_REQUIRE_EQUAL( entries.size(), 1u );
333 BOOST_CHECK_EQUAL( entries[0].label.Length(), 40u );
334 BOOST_CHECK( entries[0].label.StartsWith( wxS( "…" ) ) );
335 // Last 39 chars of the original must be at the end of the label.
336 BOOST_CHECK( entries[0].label.EndsWith( longPath.AsString().Right( 39 ) ) );
337}
338
339
340BOOST_AUTO_TEST_CASE( BuildConflictList_ShortLabelNotTruncated )
341{
342 const KIID_PATH shortPath = MakePath( 2200 );
343
346 r.id = shortPath;
348 plan.actions.push_back( r );
349 plan.unresolved.push_back( shortPath );
350
351 auto entries = BuildConflictList( plan );
352 BOOST_REQUIRE_EQUAL( entries.size(), 1u );
353 BOOST_CHECK( entries[0].label == shortPath.AsString() );
354}
355
356
357BOOST_AUTO_TEST_CASE( BuildConflictList_EmptyPlan )
358{
360 BOOST_CHECK( BuildConflictList( plan ).empty() );
361}
362
363
364BOOST_AUTO_TEST_CASE( BuildConflictList_MergedEditsAreListed )
365{
366 const KIID_PATH idA = MakePath( 2100 );
367 const KIID_PATH idB = MakePath( 2101 );
368
370
372 a.id = idA;
375 b.id = idB;
377 plan.actions.push_back( a );
378 plan.actions.push_back( b );
379
380 auto entries = BuildConflictList( plan );
381 BOOST_REQUIRE_EQUAL( entries.size(), 1u );
382 BOOST_CHECK_EQUAL( entries[0].actionIndex, 0u );
383 BOOST_CHECK( entries[0].id == idA );
384}
385
386
387// CollectUnresolvedConflicts -----------------------------------------------
388
389BOOST_AUTO_TEST_CASE( CollectUnresolved_AllConcreteChoices_Empty )
390{
391 const KIID_PATH idA = MakePath( 3000 );
392 const KIID_PATH idB = MakePath( 3001 );
393
397 plan.actions.push_back( a );
398 plan.actions.push_back( b );
399
400 BOOST_CHECK( CollectUnresolvedConflicts( plan, { 0, 1 } ).empty() );
401}
402
403
404BOOST_AUTO_TEST_CASE( CollectUnresolved_KeepIsUnresolved )
405{
406 const KIID_PATH idA = MakePath( 3100 );
407
409 ITEM_RESOLUTION a; a.id = idA; a.kind = ITEM_RES::KEEP;
410 plan.actions.push_back( a );
411
412 auto unresolved = CollectUnresolvedConflicts( plan, { 0 } );
413 BOOST_REQUIRE_EQUAL( unresolved.size(), 1u );
414 BOOST_CHECK( unresolved[0] == idA );
415}
416
417
418BOOST_AUTO_TEST_CASE( CollectUnresolved_MergePropsIsADecision )
419{
420 const KIID_PATH idA = MakePath( 3200 );
421
424 plan.actions.push_back( a );
425
426 BOOST_CHECK( CollectUnresolvedConflicts( plan, { 0 } ).empty() );
427}
428
429
430BOOST_AUTO_TEST_CASE( CollectUnresolved_DeleteIsUnresolved )
431{
432 // DELETE is also engine-internal — the user is supposed to actively
433 // pick a TAKE_* side, not accept the engine's default of "delete".
434 const KIID_PATH idA = MakePath( 3300 );
435
438 a.id = idA;
440 plan.actions.push_back( a );
441
442 auto unresolved = CollectUnresolvedConflicts( plan, { 0 } );
443 BOOST_REQUIRE_EQUAL( unresolved.size(), 1u );
444}
445
446
447BOOST_AUTO_TEST_CASE( CollectUnresolved_OutOfRangeIndexSkipped )
448{
449 // CollectUnresolvedConflicts is public; a caller can pass a stale index.
450 // Pin that out-of-range indices are silently skipped rather than UB.
451 const KIID_PATH idA = MakePath( 3350 );
452
454 ITEM_RESOLUTION a; a.id = idA; a.kind = ITEM_RES::KEEP;
455 plan.actions.push_back( a );
456
457 // Index 0 valid (KEEP -> unresolved), index 99 stale (skipped).
458 auto unresolved = CollectUnresolvedConflicts( plan, { 0, 99 } );
459 BOOST_REQUIRE_EQUAL( unresolved.size(), 1u );
460 BOOST_CHECK( unresolved[0] == idA );
461}
462
463
464BOOST_AUTO_TEST_CASE( Apply_ValidThenStaleIndex_NoMidLoopMutation )
465{
466 // The staged-write pattern in ApplyAutoResolutions must NOT mutate any
467 // action when a later index is stale. A naive implementation that
468 // assigned as it iterated would have mutated index 0 before hitting
469 // the bad index 99 — pin that the rollback property holds.
470 const KIID_PATH idA = MakePath( 3370 );
471 const KIID_PATH idB = MakePath( 3371 );
472
473 MERGE_PLAN plan = MakePlanWithConflicts( { idA, idB } );
474
475 std::vector<std::size_t> conflictIdx = { 0, 99 }; // first valid, second stale
476 std::map<wxString, ITEM_RES> autoMap = {
477 { idA.AsString(), ITEM_RES::TAKE_OURS },
479 };
480
481 auto result = ApplyAutoResolutions( plan, conflictIdx, autoMap );
482 BOOST_CHECK( result.status == APPLY_AUTO_RESOLUTIONS_STATUS::PARTIAL );
483
484 // Both actions must still be at their pre-apply kind. If the apply
485 // mutated index 0 mid-loop, plan.actions[0].kind would be TAKE_OURS
486 // even though the overall call returned PARTIAL.
487 BOOST_CHECK( plan.actions[0].kind == ITEM_RES::KEEP );
488 BOOST_CHECK( plan.actions[1].kind == ITEM_RES::KEEP );
489 BOOST_CHECK_EQUAL( plan.unresolved.size(), 2u );
490}
491
492
493BOOST_AUTO_TEST_CASE( Apply_OutOfRangeIndexBailsAsPartial )
494{
495 const KIID_PATH idA = MakePath( 3360 );
496
497 MERGE_PLAN plan = MakePlanWithConflicts( { idA } );
498
499 // Index 99 is stale.
500 std::vector<std::size_t> conflictIdx = { 99 };
501 std::map<wxString, ITEM_RES> autoMap = {
502 { idA.AsString(), ITEM_RES::TAKE_OURS },
503 };
504
505 auto result = ApplyAutoResolutions( plan, conflictIdx, autoMap );
506 BOOST_CHECK( result.status == APPLY_AUTO_RESOLUTIONS_STATUS::PARTIAL );
507 BOOST_CHECK( plan.actions[0].kind == ITEM_RES::KEEP ); // untouched
508}
509
510
511BOOST_AUTO_TEST_CASE( CollectUnresolved_MixedSet )
512{
513 const KIID_PATH idA = MakePath( 3400 );
514 const KIID_PATH idB = MakePath( 3401 );
515 const KIID_PATH idC = MakePath( 3402 );
516
519 ITEM_RESOLUTION b; b.id = idB; b.kind = ITEM_RES::KEEP;
521 plan.actions.push_back( a );
522 plan.actions.push_back( b );
523 plan.actions.push_back( c );
524
525 auto unresolved = CollectUnresolvedConflicts( plan, { 0, 1, 2 } );
526 BOOST_REQUIRE_EQUAL( unresolved.size(), 1u );
527 BOOST_CHECK( unresolved[0] == idB );
528}
529
530
531// ResolveConflictBBox -------------------------------------------------------
532
533namespace
534{
535
536const BOX2I VALID_OURS ( VECTOR2I( 100, 100 ), VECTOR2I( 50, 50 ) );
537const BOX2I VALID_THEIRS ( VECTOR2I( 200, 200 ), VECTOR2I( 50, 50 ) );
538const BOX2I VALID_ANCESTOR( VECTOR2I( 300, 300 ), VECTOR2I( 50, 50 ) );
539const BOX2I VALID_PRIMARY ( VECTOR2I( 400, 400 ), VECTOR2I( 50, 50 ) );
540const BOX2I DEGENERATE ( VECTOR2I( 0, 0 ), VECTOR2I( 0, 0 ) );
541
542} // namespace
543
544
545BOOST_AUTO_TEST_CASE( BBox_PrimaryWinsWhenPresent )
546{
547 const KIID_PATH id = MakePath( 4000 );
548
549 std::map<KIID_PATH, BOX2I> primary = { { id, VALID_PRIMARY } };
550 std::map<KIID_PATH, BOX2I> ours = { { id, VALID_OURS } };
551 std::map<KIID_PATH, BOX2I> theirs = { { id, VALID_THEIRS } };
552 std::map<KIID_PATH, BOX2I> ancestor = { { id, VALID_ANCESTOR } };
553
554 auto bbox = ResolveConflictBBox( id, primary, ours, theirs, ancestor );
555 BOOST_REQUIRE( bbox.has_value() );
556 BOOST_CHECK( *bbox == VALID_PRIMARY );
557}
558
559
560BOOST_AUTO_TEST_CASE( BBox_PrimaryEmptyFallsThroughToOurs )
561{
562 // Caller signals "no preferred side" by passing an empty map for
563 // primary (the API no longer accepts nullptr).
564 const KIID_PATH id = MakePath( 4001 );
565
566 std::map<KIID_PATH, BOX2I> ours = { { id, VALID_OURS } };
567 std::map<KIID_PATH, BOX2I> theirs = { { id, VALID_THEIRS } };
568 std::map<KIID_PATH, BOX2I> ancestor = { { id, VALID_ANCESTOR } };
569
570 auto bbox = ResolveConflictBBox( id, std::map<KIID_PATH, BOX2I>{}, ours, theirs, ancestor );
571 BOOST_REQUIRE( bbox.has_value() );
572 BOOST_CHECK( *bbox == VALID_OURS );
573}
574
575
576BOOST_AUTO_TEST_CASE( BBox_FallsThroughOursMissingToTheirs )
577{
578 const KIID_PATH id = MakePath( 4002 );
579
580 std::map<KIID_PATH, BOX2I> primary; // doesn't contain id
581 std::map<KIID_PATH, BOX2I> ours;
582 std::map<KIID_PATH, BOX2I> theirs = { { id, VALID_THEIRS } };
583 std::map<KIID_PATH, BOX2I> ancestor = { { id, VALID_ANCESTOR } };
584
585 auto bbox = ResolveConflictBBox( id, primary, ours, theirs, ancestor );
586 BOOST_REQUIRE( bbox.has_value() );
587 BOOST_CHECK( *bbox == VALID_THEIRS );
588}
589
590
591BOOST_AUTO_TEST_CASE( BBox_FallsThroughToAncestor )
592{
593 const KIID_PATH id = MakePath( 4003 );
594
595 std::map<KIID_PATH, BOX2I> primary;
596 std::map<KIID_PATH, BOX2I> ours;
597 std::map<KIID_PATH, BOX2I> theirs;
598 std::map<KIID_PATH, BOX2I> ancestor = { { id, VALID_ANCESTOR } };
599
600 auto bbox = ResolveConflictBBox( id, primary, ours, theirs, ancestor );
601 BOOST_REQUIRE( bbox.has_value() );
602 BOOST_CHECK( *bbox == VALID_ANCESTOR );
603}
604
605
606BOOST_AUTO_TEST_CASE( BBox_DegenerateBBoxSkipped )
607{
608 // A side with a zero-area bbox for the id is treated the same as a
609 // side with no entry — the next map in the chain wins.
610 const KIID_PATH id = MakePath( 4004 );
611
612 std::map<KIID_PATH, BOX2I> primary = { { id, DEGENERATE } };
613 std::map<KIID_PATH, BOX2I> ours = { { id, VALID_OURS } };
614 std::map<KIID_PATH, BOX2I> theirs;
615 std::map<KIID_PATH, BOX2I> ancestor;
616
617 auto bbox = ResolveConflictBBox( id, primary, ours, theirs, ancestor );
618 BOOST_REQUIRE( bbox.has_value() );
619 BOOST_CHECK( *bbox == VALID_OURS );
620}
621
622
623BOOST_AUTO_TEST_CASE( BBox_AllMissingReturnsNullopt )
624{
625 const KIID_PATH id = MakePath( 4005 );
626 std::map<KIID_PATH, BOX2I> empty;
627
628 auto bbox = ResolveConflictBBox( id, std::map<KIID_PATH, BOX2I>{}, empty, empty, empty );
629 BOOST_CHECK( !bbox.has_value() );
630}
631
632
633BOOST_AUTO_TEST_CASE( BBox_FallbackOrderOursThenTheirsThenAncestor )
634{
635 // When primary is empty and ours has a degenerate entry, we should
636 // fall through to theirs. This pins the documented fallback order
637 // beyond just "first map-with-entry wins" — degenerate entries are
638 // equivalent to absence.
639 const KIID_PATH id = MakePath( 4006 );
640
641 std::map<KIID_PATH, BOX2I> ours = { { id, DEGENERATE } };
642 std::map<KIID_PATH, BOX2I> theirs = { { id, VALID_THEIRS } };
643 std::map<KIID_PATH, BOX2I> ancestor = { { id, VALID_ANCESTOR } };
644
645 auto bbox = ResolveConflictBBox( id, std::map<KIID_PATH, BOX2I>{}, ours, theirs, ancestor );
646 BOOST_REQUIRE( bbox.has_value() );
647 BOOST_CHECK( *bbox == VALID_THEIRS );
648}
649
650
651// BuildConflictDetailText --------------------------------------------------
652
653BOOST_AUTO_TEST_CASE( DetailText_ContainsIdAndKind )
654{
656 r.id = MakePath( 4100 );
658
659 wxString text = BuildConflictDetailText( r );
660 BOOST_CHECK( text.Contains( r.id.AsString() ) );
661
662 // Run the comparison strings through `_()` so the assertions stay
663 // locale-stable: if a translation is loaded, both the production
664 // text and these expected strings go through the same wx machinery
665 // and match each other. The label strings (positive + negative pins)
666 // identify the kind's labelled prefix without depending on the exact
667 // English wording surviving a translation.
668 BOOST_CHECK( text.Contains( _( "Take ours" ) ) );
669 BOOST_CHECK( !text.Contains( _( "Property-level merge" ) ) );
670 BOOST_CHECK( !text.Contains( _( "Keep (default)" ) ) );
671}
672
673
674BOOST_AUTO_TEST_CASE( DetailText_IncludesPropertyCount )
675{
677 r.id = MakePath( 4101 );
679
680 PROPERTY_RESOLUTION p1; p1.name = wxS( "A" ); r.props.push_back( p1 );
681 PROPERTY_RESOLUTION p2; p2.name = wxS( "B" ); r.props.push_back( p2 );
682
683 wxString text = BuildConflictDetailText( r );
684 BOOST_CHECK( text.Contains( wxS( "2" ) ) );
685}
686
687
688BOOST_AUTO_TEST_CASE( DetailText_KnownResolutionKindsProduceText )
689{
690 // Snapshot of the six ITEM_RES values at time of writing. The list
691 // is NOT compile-time exhaustive — adding a new enum value won't
692 // automatically extend the array (C++ can't enumerate enum values
693 // without external libs). If a new ITEM_RES is introduced, add it
694 // here and update `BuildConflictDetailText`'s switch in the same
695 // commit; otherwise the new kind silently produces a label-less
696 // detail block.
697 const ITEM_RES kinds[] = {
700 };
701
702 // Format the expected count line through the same `_()` machinery the
703 // production code uses so the assertion stays locale-stable. A
704 // translated harness ends up comparing translated against translated.
705 const wxString expectedCountLine = wxString::Format(
706 _( "%zu property edit(s) in this resolution.\n" ),
707 static_cast<std::size_t>( 0 ) );
708
709 for( ITEM_RES kind : kinds )
710 {
712 r.id = MakePath( 4200 );
713 r.kind = kind;
714
715 wxString text = BuildConflictDetailText( r );
716 BOOST_CHECK_MESSAGE( text.Length() > 30,
717 "Detail text too short for kind "
718 << static_cast<int>( kind ) );
719 BOOST_CHECK( text.Contains( expectedCountLine ) );
720 }
721}
722
723
BOX2< VECTOR2I > BOX2I
Definition box2.h:914
wxString AsString() const
Definition kiid.cpp:423
Definition kiid.h:46
static void SeedGenerator(unsigned int aSeed)
Re-initialize the UUID generator with a given seed (for testing or QA purposes)
Definition kiid.cpp:361
wxString AsString() const
Definition kiid.cpp:264
static bool empty(const wxTextEntryBase *aCtrl)
#define _(s)
nlohmann::json json
Definition gerbview.cpp:50
static std::vector< PENDING_PROPERTY > plan(const EDA_ITEM &aSource, const EDA_ITEM &aTarget, const std::set< wxString > &aEnabledKeys)
Target properties to write, paired with values read off the source.
wxString BuildConflictDetailText(const ITEM_RESOLUTION &aResolution)
Build the human-readable detail text shown in the merge dialog's resolution panel for a given ITEM_RE...
AUTO_RESOLUTION_PARSE_RESULT ParseAutoResolutionJson(const std::string &aJsonContent)
Parse a KICAD_MERGETOOL_AUTO-format JSON object mapping item IDs (KIID_PATH strings) to ITEM_RES spel...
@ BAD_VALUE
at least one entry's value wasn't a string
@ NOT_OBJECT
JSON parsed but the root was not an object.
@ INVALID_JSON
input could not be parsed as JSON
@ UNKNOWN_KIND
at least one entry's string wasn't a known ITEM_RES
@ OK
parsed cleanly; resolutions is populated
@ ENGINE_INTERNAL_KIND
at least one entry asked for an engine-internal kind (KEEP / DELETE / MERGE_PROPS); only the TAKE_* f...
ITEM_RES
Resolution kind for a whole item.
std::vector< CONFLICT_LIST_ENTRY > BuildConflictList(const MERGE_PLAN &aPlan)
std::optional< BOX2I > ResolveConflictBBox(const KIID_PATH &aId, const std::map< KIID_PATH, BOX2I > &aPrimary, const std::map< KIID_PATH, BOX2I > &aOurs, const std::map< KIID_PATH, BOX2I > &aTheirs, const std::map< KIID_PATH, BOX2I > &aAncestor)
Resolve the best bounding box for a conflicted item across the three sides of a 3-way merge.
APPLY_AUTO_RESOLUTIONS_RESULT ApplyAutoResolutions(MERGE_PLAN &aPlan, const std::vector< std::size_t > &aConflictActionIndices, const std::map< wxString, ITEM_RES > &aResolutions)
Apply a parsed auto-resolution map to a MERGE_PLAN's conflicts.
std::vector< KIID_PATH > CollectUnresolvedConflicts(const MERGE_PLAN &aPlan, const std::vector< std::size_t > &aConflictActionIndices)
Return the conflict actions that still have no decision.
_OUT_STRING AsString(const std::string &aString)
Definition sexpr.h:131
std::vector< PROPERTY_RESOLUTION > props
Result of planning a 3-way merge.
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_CASE(Parse_ValidObject)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:708