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{
175 MERGE_PLAN plan;
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{
244 MERGE_PLAN plan;
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
254// BuildConflictList ---------------------------------------------------------
255
256BOOST_AUTO_TEST_CASE( BuildConflictList_OnlyUnresolvedActions )
257{
258 // Plan has three actions, but only two are flagged unresolved.
259 const KIID_PATH idA = MakePath( 2000 );
260 const KIID_PATH idB = MakePath( 2001 );
261 const KIID_PATH idC = MakePath( 2002 );
262
263 MERGE_PLAN plan;
264
266 ITEM_RESOLUTION b; b.id = idB; b.kind = ITEM_RES::KEEP;
267 ITEM_RESOLUTION c; c.id = idC; c.kind = ITEM_RES::KEEP;
268 plan.actions.push_back( a );
269 plan.actions.push_back( b );
270 plan.actions.push_back( c );
271 plan.unresolved.push_back( idB );
272 plan.unresolved.push_back( idC );
273
274 auto entries = BuildConflictList( plan );
275 BOOST_REQUIRE_EQUAL( entries.size(), 2u );
276 BOOST_CHECK_EQUAL( entries[0].actionIndex, 1u );
277 BOOST_CHECK( entries[0].id == idB );
278 BOOST_CHECK_EQUAL( entries[1].actionIndex, 2u );
279 BOOST_CHECK( entries[1].id == idC );
280}
281
282
283BOOST_AUTO_TEST_CASE( BuildConflictList_LongLabelsAreTruncated )
284{
285 // KIID_PATH strings can get long for nested-sheet hierarchies. The
286 // dialog uses a 40-char display rule with right-truncation so the
287 // disambiguating segment stays visible.
288 KIID::SeedGenerator( 2100 );
289 KIID a, b, c, d;
290 const wxString longSegmentString = wxS( "/" ) + a.AsString() + wxS( "/" )
291 + b.AsString() + wxS( "/" )
292 + c.AsString() + wxS( "/" ) + d.AsString();
293 const KIID_PATH longPath( longSegmentString );
294
295 MERGE_PLAN plan;
297 r.id = longPath;
299 plan.actions.push_back( r );
300 plan.unresolved.push_back( longPath );
301
302 auto entries = BuildConflictList( plan );
303 BOOST_REQUIRE_EQUAL( entries.size(), 1u );
304 BOOST_CHECK_EQUAL( entries[0].label.Length(), 40u );
305 BOOST_CHECK( entries[0].label.StartsWith( wxS( "…" ) ) );
306 // Last 39 chars of the original must be at the end of the label.
307 BOOST_CHECK( entries[0].label.EndsWith( longPath.AsString().Right( 39 ) ) );
308}
309
310
311BOOST_AUTO_TEST_CASE( BuildConflictList_ShortLabelNotTruncated )
312{
313 const KIID_PATH shortPath = MakePath( 2200 );
314
315 MERGE_PLAN plan;
317 r.id = shortPath;
319 plan.actions.push_back( r );
320 plan.unresolved.push_back( shortPath );
321
322 auto entries = BuildConflictList( plan );
323 BOOST_REQUIRE_EQUAL( entries.size(), 1u );
324 BOOST_CHECK( entries[0].label == shortPath.AsString() );
325}
326
327
328BOOST_AUTO_TEST_CASE( BuildConflictList_EmptyPlan )
329{
330 MERGE_PLAN plan;
331 BOOST_CHECK( BuildConflictList( plan ).empty() );
332}
333
334
335// CollectUnresolvedConflicts -----------------------------------------------
336
337BOOST_AUTO_TEST_CASE( CollectUnresolved_AllConcreteChoices_Empty )
338{
339 const KIID_PATH idA = MakePath( 3000 );
340 const KIID_PATH idB = MakePath( 3001 );
341
342 MERGE_PLAN plan;
345 plan.actions.push_back( a );
346 plan.actions.push_back( b );
347
348 BOOST_CHECK( CollectUnresolvedConflicts( plan, { 0, 1 } ).empty() );
349}
350
351
352BOOST_AUTO_TEST_CASE( CollectUnresolved_KeepIsUnresolved )
353{
354 const KIID_PATH idA = MakePath( 3100 );
355
356 MERGE_PLAN plan;
357 ITEM_RESOLUTION a; a.id = idA; a.kind = ITEM_RES::KEEP;
358 plan.actions.push_back( a );
359
360 auto unresolved = CollectUnresolvedConflicts( plan, { 0 } );
361 BOOST_REQUIRE_EQUAL( unresolved.size(), 1u );
362 BOOST_CHECK( unresolved[0] == idA );
363}
364
365
366BOOST_AUTO_TEST_CASE( CollectUnresolved_MergePropsIsUnresolved )
367{
368 const KIID_PATH idA = MakePath( 3200 );
369
370 MERGE_PLAN plan;
372 plan.actions.push_back( a );
373
374 auto unresolved = CollectUnresolvedConflicts( plan, { 0 } );
375 BOOST_REQUIRE_EQUAL( unresolved.size(), 1u );
376 BOOST_CHECK( unresolved[0] == idA );
377}
378
379
380BOOST_AUTO_TEST_CASE( CollectUnresolved_DeleteIsUnresolved )
381{
382 // DELETE is also engine-internal — the user is supposed to actively
383 // pick a TAKE_* side, not accept the engine's default of "delete".
384 const KIID_PATH idA = MakePath( 3300 );
385
386 MERGE_PLAN plan;
388 a.id = idA;
390 plan.actions.push_back( a );
391
392 auto unresolved = CollectUnresolvedConflicts( plan, { 0 } );
393 BOOST_REQUIRE_EQUAL( unresolved.size(), 1u );
394}
395
396
397BOOST_AUTO_TEST_CASE( CollectUnresolved_OutOfRangeIndexSkipped )
398{
399 // CollectUnresolvedConflicts is public; a caller can pass a stale index.
400 // Pin that out-of-range indices are silently skipped rather than UB.
401 const KIID_PATH idA = MakePath( 3350 );
402
403 MERGE_PLAN plan;
404 ITEM_RESOLUTION a; a.id = idA; a.kind = ITEM_RES::KEEP;
405 plan.actions.push_back( a );
406
407 // Index 0 valid (KEEP -> unresolved), index 99 stale (skipped).
408 auto unresolved = CollectUnresolvedConflicts( plan, { 0, 99 } );
409 BOOST_REQUIRE_EQUAL( unresolved.size(), 1u );
410 BOOST_CHECK( unresolved[0] == idA );
411}
412
413
414BOOST_AUTO_TEST_CASE( Apply_ValidThenStaleIndex_NoMidLoopMutation )
415{
416 // The staged-write pattern in ApplyAutoResolutions must NOT mutate any
417 // action when a later index is stale. A naive implementation that
418 // assigned as it iterated would have mutated index 0 before hitting
419 // the bad index 99 — pin that the rollback property holds.
420 const KIID_PATH idA = MakePath( 3370 );
421 const KIID_PATH idB = MakePath( 3371 );
422
423 MERGE_PLAN plan = MakePlanWithConflicts( { idA, idB } );
424
425 std::vector<std::size_t> conflictIdx = { 0, 99 }; // first valid, second stale
426 std::map<wxString, ITEM_RES> autoMap = {
427 { idA.AsString(), ITEM_RES::TAKE_OURS },
429 };
430
431 auto result = ApplyAutoResolutions( plan, conflictIdx, autoMap );
432 BOOST_CHECK( result.status == APPLY_AUTO_RESOLUTIONS_STATUS::PARTIAL );
433
434 // Both actions must still be at their pre-apply kind. If the apply
435 // mutated index 0 mid-loop, plan.actions[0].kind would be TAKE_OURS
436 // even though the overall call returned PARTIAL.
437 BOOST_CHECK( plan.actions[0].kind == ITEM_RES::KEEP );
438 BOOST_CHECK( plan.actions[1].kind == ITEM_RES::KEEP );
439 BOOST_CHECK_EQUAL( plan.unresolved.size(), 2u );
440}
441
442
443BOOST_AUTO_TEST_CASE( Apply_OutOfRangeIndexBailsAsPartial )
444{
445 const KIID_PATH idA = MakePath( 3360 );
446
447 MERGE_PLAN plan = MakePlanWithConflicts( { idA } );
448
449 // Index 99 is stale.
450 std::vector<std::size_t> conflictIdx = { 99 };
451 std::map<wxString, ITEM_RES> autoMap = {
452 { idA.AsString(), ITEM_RES::TAKE_OURS },
453 };
454
455 auto result = ApplyAutoResolutions( plan, conflictIdx, autoMap );
456 BOOST_CHECK( result.status == APPLY_AUTO_RESOLUTIONS_STATUS::PARTIAL );
457 BOOST_CHECK( plan.actions[0].kind == ITEM_RES::KEEP ); // untouched
458}
459
460
461BOOST_AUTO_TEST_CASE( CollectUnresolved_MixedSet )
462{
463 const KIID_PATH idA = MakePath( 3400 );
464 const KIID_PATH idB = MakePath( 3401 );
465 const KIID_PATH idC = MakePath( 3402 );
466
467 MERGE_PLAN plan;
469 ITEM_RESOLUTION b; b.id = idB; b.kind = ITEM_RES::KEEP;
471 plan.actions.push_back( a );
472 plan.actions.push_back( b );
473 plan.actions.push_back( c );
474
475 auto unresolved = CollectUnresolvedConflicts( plan, { 0, 1, 2 } );
476 BOOST_REQUIRE_EQUAL( unresolved.size(), 1u );
477 BOOST_CHECK( unresolved[0] == idB );
478}
479
480
481// ResolveConflictBBox -------------------------------------------------------
482
483namespace
484{
485
486const BOX2I VALID_OURS ( VECTOR2I( 100, 100 ), VECTOR2I( 50, 50 ) );
487const BOX2I VALID_THEIRS ( VECTOR2I( 200, 200 ), VECTOR2I( 50, 50 ) );
488const BOX2I VALID_ANCESTOR( VECTOR2I( 300, 300 ), VECTOR2I( 50, 50 ) );
489const BOX2I VALID_PRIMARY ( VECTOR2I( 400, 400 ), VECTOR2I( 50, 50 ) );
490const BOX2I DEGENERATE ( VECTOR2I( 0, 0 ), VECTOR2I( 0, 0 ) );
491
492} // namespace
493
494
495BOOST_AUTO_TEST_CASE( BBox_PrimaryWinsWhenPresent )
496{
497 const KIID_PATH id = MakePath( 4000 );
498
499 std::map<KIID_PATH, BOX2I> primary = { { id, VALID_PRIMARY } };
500 std::map<KIID_PATH, BOX2I> ours = { { id, VALID_OURS } };
501 std::map<KIID_PATH, BOX2I> theirs = { { id, VALID_THEIRS } };
502 std::map<KIID_PATH, BOX2I> ancestor = { { id, VALID_ANCESTOR } };
503
504 auto bbox = ResolveConflictBBox( id, primary, ours, theirs, ancestor );
505 BOOST_REQUIRE( bbox.has_value() );
506 BOOST_CHECK( *bbox == VALID_PRIMARY );
507}
508
509
510BOOST_AUTO_TEST_CASE( BBox_PrimaryEmptyFallsThroughToOurs )
511{
512 // Caller signals "no preferred side" by passing an empty map for
513 // primary (the API no longer accepts nullptr).
514 const KIID_PATH id = MakePath( 4001 );
515
516 std::map<KIID_PATH, BOX2I> ours = { { id, VALID_OURS } };
517 std::map<KIID_PATH, BOX2I> theirs = { { id, VALID_THEIRS } };
518 std::map<KIID_PATH, BOX2I> ancestor = { { id, VALID_ANCESTOR } };
519
520 auto bbox = ResolveConflictBBox( id, std::map<KIID_PATH, BOX2I>{}, ours, theirs, ancestor );
521 BOOST_REQUIRE( bbox.has_value() );
522 BOOST_CHECK( *bbox == VALID_OURS );
523}
524
525
526BOOST_AUTO_TEST_CASE( BBox_FallsThroughOursMissingToTheirs )
527{
528 const KIID_PATH id = MakePath( 4002 );
529
530 std::map<KIID_PATH, BOX2I> primary; // doesn't contain id
531 std::map<KIID_PATH, BOX2I> ours;
532 std::map<KIID_PATH, BOX2I> theirs = { { id, VALID_THEIRS } };
533 std::map<KIID_PATH, BOX2I> ancestor = { { id, VALID_ANCESTOR } };
534
535 auto bbox = ResolveConflictBBox( id, primary, ours, theirs, ancestor );
536 BOOST_REQUIRE( bbox.has_value() );
537 BOOST_CHECK( *bbox == VALID_THEIRS );
538}
539
540
541BOOST_AUTO_TEST_CASE( BBox_FallsThroughToAncestor )
542{
543 const KIID_PATH id = MakePath( 4003 );
544
545 std::map<KIID_PATH, BOX2I> primary;
546 std::map<KIID_PATH, BOX2I> ours;
547 std::map<KIID_PATH, BOX2I> theirs;
548 std::map<KIID_PATH, BOX2I> ancestor = { { id, VALID_ANCESTOR } };
549
550 auto bbox = ResolveConflictBBox( id, primary, ours, theirs, ancestor );
551 BOOST_REQUIRE( bbox.has_value() );
552 BOOST_CHECK( *bbox == VALID_ANCESTOR );
553}
554
555
556BOOST_AUTO_TEST_CASE( BBox_DegenerateBBoxSkipped )
557{
558 // A side with a zero-area bbox for the id is treated the same as a
559 // side with no entry — the next map in the chain wins.
560 const KIID_PATH id = MakePath( 4004 );
561
562 std::map<KIID_PATH, BOX2I> primary = { { id, DEGENERATE } };
563 std::map<KIID_PATH, BOX2I> ours = { { id, VALID_OURS } };
564 std::map<KIID_PATH, BOX2I> theirs;
565 std::map<KIID_PATH, BOX2I> ancestor;
566
567 auto bbox = ResolveConflictBBox( id, primary, ours, theirs, ancestor );
568 BOOST_REQUIRE( bbox.has_value() );
569 BOOST_CHECK( *bbox == VALID_OURS );
570}
571
572
573BOOST_AUTO_TEST_CASE( BBox_AllMissingReturnsNullopt )
574{
575 const KIID_PATH id = MakePath( 4005 );
576 std::map<KIID_PATH, BOX2I> empty;
577
578 auto bbox = ResolveConflictBBox( id, std::map<KIID_PATH, BOX2I>{}, empty, empty, empty );
579 BOOST_CHECK( !bbox.has_value() );
580}
581
582
583BOOST_AUTO_TEST_CASE( BBox_FallbackOrderOursThenTheirsThenAncestor )
584{
585 // When primary is empty and ours has a degenerate entry, we should
586 // fall through to theirs. This pins the documented fallback order
587 // beyond just "first map-with-entry wins" — degenerate entries are
588 // equivalent to absence.
589 const KIID_PATH id = MakePath( 4006 );
590
591 std::map<KIID_PATH, BOX2I> ours = { { id, DEGENERATE } };
592 std::map<KIID_PATH, BOX2I> theirs = { { id, VALID_THEIRS } };
593 std::map<KIID_PATH, BOX2I> ancestor = { { id, VALID_ANCESTOR } };
594
595 auto bbox = ResolveConflictBBox( id, std::map<KIID_PATH, BOX2I>{}, ours, theirs, ancestor );
596 BOOST_REQUIRE( bbox.has_value() );
597 BOOST_CHECK( *bbox == VALID_THEIRS );
598}
599
600
601// BuildConflictDetailText --------------------------------------------------
602
603BOOST_AUTO_TEST_CASE( DetailText_ContainsIdAndKind )
604{
606 r.id = MakePath( 4100 );
608
609 wxString text = BuildConflictDetailText( r );
610 BOOST_CHECK( text.Contains( r.id.AsString() ) );
611
612 // Run the comparison strings through `_()` so the assertions stay
613 // locale-stable: if a translation is loaded, both the production
614 // text and these expected strings go through the same wx machinery
615 // and match each other. The label strings (positive + negative pins)
616 // identify the kind's labelled prefix without depending on the exact
617 // English wording surviving a translation.
618 BOOST_CHECK( text.Contains( _( "Take ours" ) ) );
619 BOOST_CHECK( !text.Contains( _( "Property-level merge" ) ) );
620 BOOST_CHECK( !text.Contains( _( "Keep (default)" ) ) );
621}
622
623
624BOOST_AUTO_TEST_CASE( DetailText_IncludesPropertyCount )
625{
627 r.id = MakePath( 4101 );
629
630 PROPERTY_RESOLUTION p1; p1.name = wxS( "A" ); r.props.push_back( p1 );
631 PROPERTY_RESOLUTION p2; p2.name = wxS( "B" ); r.props.push_back( p2 );
632
633 wxString text = BuildConflictDetailText( r );
634 BOOST_CHECK( text.Contains( wxS( "2" ) ) );
635}
636
637
638BOOST_AUTO_TEST_CASE( DetailText_KnownResolutionKindsProduceText )
639{
640 // Snapshot of the six ITEM_RES values at time of writing. The list
641 // is NOT compile-time exhaustive — adding a new enum value won't
642 // automatically extend the array (C++ can't enumerate enum values
643 // without external libs). If a new ITEM_RES is introduced, add it
644 // here and update `BuildConflictDetailText`'s switch in the same
645 // commit; otherwise the new kind silently produces a label-less
646 // detail block.
647 const ITEM_RES kinds[] = {
650 };
651
652 // Format the expected count line through the same `_()` machinery the
653 // production code uses so the assertion stays locale-stable. A
654 // translated harness ends up comparing translated against translated.
655 const wxString expectedCountLine = wxString::Format(
656 _( "%zu property edit(s) in this resolution.\n" ),
657 static_cast<std::size_t>( 0 ) );
658
659 for( ITEM_RES kind : kinds )
660 {
662 r.id = MakePath( 4200 );
663 r.kind = kind;
664
665 wxString text = BuildConflictDetailText( r );
666 BOOST_CHECK_MESSAGE( text.Length() > 30,
667 "Detail text too short for kind "
668 << static_cast<int>( kind ) );
669 BOOST_CHECK( text.Contains( expectedCountLine ) );
670 }
671}
672
673
BOX2< VECTOR2I > BOX2I
Definition box2.h:918
wxString AsString() const
Definition kiid.cpp:393
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
wxString AsString() const
Definition kiid.cpp:242
static bool empty(const wxTextEntryBase *aCtrl)
#define _(s)
nlohmann::json json
Definition gerbview.cpp:49
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 subset of conflict actions whose kind is NOT one of the concrete TAKE_OURS / TAKE_THEIRS /...
_OUT_STRING AsString(const std::string &aString)
Definition sexpr.h:131
std::vector< PROPERTY_RESOLUTION > props
Result of planning a 3-way merge.
std::vector< ITEM_RESOLUTION > actions
std::vector< KIID_PATH > unresolved
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()
BOOST_CHECK_MESSAGE(totalMismatches==0, std::to_string(totalMismatches)+" board(s) with strategy disagreements")
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683