KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_diff_tree_grouping.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
29using namespace KICAD_DIFF;
30
31
32namespace
33{
34
35ITEM_CHANGE MakeChange( const wxString& aType, CHANGE_KIND aKind, std::optional<wxString> aRefdes = std::nullopt )
36{
38 c.id = KIID_PATH( wxS( "/" ) + KIID().AsString() );
39 c.typeName = aType;
40 c.kind = aKind;
41 c.refdes = std::move( aRefdes );
42 return c;
43}
44
45
46std::array<bool, CATEGORY_COUNT> AllVisible()
47{
48 std::array<bool, CATEGORY_COUNT> out{};
49 out.fill( true );
50 return out;
51}
52
53} // namespace
54
55
56BOOST_AUTO_TEST_SUITE( DiffTreeGrouping )
57
58
59// ChangeKindLabel ----------------------------------------------------------
60
61BOOST_AUTO_TEST_CASE( ChangeKindLabel_AllKinds )
62{
63 // Pin that every CHANGE_KIND produces a non-empty label. Locale-dependent
64 // text content can't be asserted strictly but emptiness CAN.
65 BOOST_CHECK( !ChangeKindLabel( CHANGE_KIND::ADDED ).IsEmpty() );
66 BOOST_CHECK( !ChangeKindLabel( CHANGE_KIND::REMOVED ).IsEmpty() );
67 BOOST_CHECK( !ChangeKindLabel( CHANGE_KIND::MODIFIED ).IsEmpty() );
68 BOOST_CHECK( !ChangeKindLabel( CHANGE_KIND::COLLISION ).IsEmpty() );
69 BOOST_CHECK( !ChangeKindLabel( CHANGE_KIND::DUPLICATE_UUID ).IsEmpty() );
70}
71
72
73// ChangeMatchesSearchFilter ------------------------------------------------
74
75BOOST_AUTO_TEST_CASE( Filter_EmptyMatchesEverything )
76{
77 ITEM_CHANGE c = MakeChange( wxS( "PCB_TRACK" ), CHANGE_KIND::MODIFIED );
78 BOOST_CHECK( ChangeMatchesSearchFilter( c, wxS( "" ) ) );
79}
80
81
82BOOST_AUTO_TEST_CASE( Filter_TypeNameSubstring )
83{
84 ITEM_CHANGE c = MakeChange( wxS( "PCB_TRACK" ), CHANGE_KIND::MODIFIED );
85 BOOST_CHECK( ChangeMatchesSearchFilter( c, wxS( "track" ) ) );
86 BOOST_CHECK( ChangeMatchesSearchFilter( c, wxS( "pcb" ) ) );
87 BOOST_CHECK( !ChangeMatchesSearchFilter( c, wxS( "via" ) ) );
88}
89
90
91BOOST_AUTO_TEST_CASE( Filter_RefdesSubstring )
92{
93 ITEM_CHANGE c = MakeChange( wxS( "FOOTPRINT" ), CHANGE_KIND::MODIFIED, wxS( "R12" ) );
94 BOOST_CHECK( ChangeMatchesSearchFilter( c, wxS( "r12" ) ) );
95 BOOST_CHECK( ChangeMatchesSearchFilter( c, wxS( "r" ) ) );
96 BOOST_CHECK( !ChangeMatchesSearchFilter( c, wxS( "c4" ) ) );
97}
98
99
100BOOST_AUTO_TEST_CASE( Filter_AbsentRefdesNoMatch )
101{
102 // No refdes -> only typeName can match. A filter that would match a
103 // hypothetical refdes must not accidentally hit on a missing optional.
104 ITEM_CHANGE c = MakeChange( wxS( "ZONE" ), CHANGE_KIND::ADDED );
105 BOOST_CHECK( !ChangeMatchesSearchFilter( c, wxS( "u1" ) ) );
106}
107
108
109BOOST_AUTO_TEST_CASE( Filter_AssumesLowercaseInput )
110{
111 // The function explicitly requires the caller to pre-lowercase the
112 // filter (otherwise repeated calls would re-lowercase the same string
113 // for every item). Pin that contract: uppercase filter does NOT match.
114 ITEM_CHANGE c = MakeChange( wxS( "PCB_TRACK" ), CHANGE_KIND::MODIFIED );
115 BOOST_CHECK( !ChangeMatchesSearchFilter( c, wxS( "TRACK" ) ) );
116}
117
118
119// BuildChangeTreeGroups ----------------------------------------------------
120
121BOOST_AUTO_TEST_CASE( Groups_EmptyDiffProducesEmptyResult )
122{
123 DOCUMENT_DIFF diff;
124 auto groups = BuildChangeTreeGroups( diff, wxS( "" ), AllVisible() );
125 BOOST_CHECK( groups.empty() );
126}
127
128
129BOOST_AUTO_TEST_CASE( Groups_BucketsByKind )
130{
131 DOCUMENT_DIFF diff;
132 diff.changes.push_back( MakeChange( wxS( "PCB_TRACK" ), CHANGE_KIND::ADDED ) );
133 diff.changes.push_back( MakeChange( wxS( "PCB_VIA" ), CHANGE_KIND::ADDED ) );
134 diff.changes.push_back( MakeChange( wxS( "ZONE" ), CHANGE_KIND::REMOVED ) );
135
136 auto groups = BuildChangeTreeGroups( diff, wxS( "" ), AllVisible() );
137 BOOST_REQUIRE_EQUAL( groups.size(), 2u );
138
139 // std::map iterates in key (enum value) order; ADDED < REMOVED.
140 BOOST_CHECK( groups[0].kind == CHANGE_KIND::ADDED );
141 BOOST_CHECK_EQUAL( groups[0].entries.size(), 2u );
142 BOOST_CHECK( groups[1].kind == CHANGE_KIND::REMOVED );
143 BOOST_CHECK_EQUAL( groups[1].entries.size(), 1u );
144}
145
146
147BOOST_AUTO_TEST_CASE( Groups_RecursesIntoChildren )
148{
149 // Footprint with two child pad changes; each child must land in its
150 // own bucket entry so per-pad highlighting works in the dialog.
151 DOCUMENT_DIFF diff;
152 ITEM_CHANGE fp = MakeChange( wxS( "FOOTPRINT" ), CHANGE_KIND::MODIFIED );
153 fp.children.push_back( MakeChange( wxS( "PAD" ), CHANGE_KIND::MODIFIED ) );
154 fp.children.push_back( MakeChange( wxS( "PAD" ), CHANGE_KIND::ADDED ) );
155 diff.changes.push_back( std::move( fp ) );
156
157 auto groups = BuildChangeTreeGroups( diff, wxS( "" ), AllVisible() );
158 BOOST_REQUIRE_EQUAL( groups.size(), 2u );
159
160 // ADDED bucket: the added-pad child.
161 BOOST_CHECK( groups[0].kind == CHANGE_KIND::ADDED );
162 BOOST_CHECK_EQUAL( groups[0].entries.size(), 1u );
163
164 // MODIFIED bucket: the parent footprint + the modified-pad child.
165 BOOST_CHECK( groups[1].kind == CHANGE_KIND::MODIFIED );
166 BOOST_CHECK_EQUAL( groups[1].entries.size(), 2u );
167}
168
169
170BOOST_AUTO_TEST_CASE( Groups_CollapsesSameNetRoutingChanges )
171{
172 DOCUMENT_DIFF diff;
173
174 diff.changes.push_back( MakeChange( wxS( "PCB_TRACK" ), CHANGE_KIND::MODIFIED, wxS( "Net-(U1-Pad1)" ) ) );
175 diff.changes.push_back( MakeChange( wxS( "PCB_VIA" ), CHANGE_KIND::MODIFIED, wxS( "Net-(U1-Pad1)" ) ) );
176 diff.changes.push_back( MakeChange( wxS( "PCB_TRACK" ), CHANGE_KIND::MODIFIED, wxS( "GND" ) ) );
177
178 auto groups = BuildChangeTreeGroups( diff, wxS( "" ), AllVisible() );
179 BOOST_REQUIRE_EQUAL( groups.size(), 1u );
180 BOOST_REQUIRE_EQUAL( groups[0].entries.size(), 2u );
181 BOOST_CHECK( groups[0].entries[0].itemLabel == wxS( "NET [GND]" ) );
182 BOOST_CHECK( groups[0].entries[1].itemLabel == wxS( "NET [Net-(U1-Pad1)]" ) );
183}
184
185
186BOOST_AUTO_TEST_CASE( Groups_LabelIncludesRefdes )
187{
188 DOCUMENT_DIFF diff;
189 diff.changes.push_back( MakeChange( wxS( "FOOTPRINT" ), CHANGE_KIND::ADDED, wxS( "U7" ) ) );
190
191 auto groups = BuildChangeTreeGroups( diff, wxS( "" ), AllVisible() );
192 BOOST_REQUIRE_EQUAL( groups.size(), 1u );
193 BOOST_REQUIRE_EQUAL( groups[0].entries.size(), 1u );
194 BOOST_CHECK( groups[0].entries[0].itemLabel == wxS( "FOOTPRINT [U7]" ) );
195}
196
197
198BOOST_AUTO_TEST_CASE( Groups_LabelBaseWhenRefdesAbsent )
199{
200 DOCUMENT_DIFF diff;
201 diff.changes.push_back( MakeChange( wxS( "ZONE" ), CHANGE_KIND::ADDED ) );
202
203 auto groups = BuildChangeTreeGroups( diff, wxS( "" ), AllVisible() );
204 BOOST_REQUIRE_EQUAL( groups.size(), 1u );
205 BOOST_CHECK( groups[0].entries[0].itemLabel == wxS( "ZONE" ) );
206}
207
208
209BOOST_AUTO_TEST_CASE( Groups_HiddenCategoryDropsBucket )
210{
211 // ADDED maps to CATEGORY::ADDED. Hide it; the bucket must vanish.
212 DOCUMENT_DIFF diff;
213 diff.changes.push_back( MakeChange( wxS( "ZONE" ), CHANGE_KIND::ADDED ) );
214 diff.changes.push_back( MakeChange( wxS( "ZONE" ), CHANGE_KIND::REMOVED ) );
215
216 std::array<bool, CATEGORY_COUNT> visible{};
217 visible.fill( true );
218 visible[static_cast<std::size_t>( CATEGORY::ADDED )] = false;
219
220 auto groups = BuildChangeTreeGroups( diff, wxS( "" ), visible );
221 BOOST_REQUIRE_EQUAL( groups.size(), 1u );
222 BOOST_CHECK( groups[0].kind == CHANGE_KIND::REMOVED );
223}
224
225
226BOOST_AUTO_TEST_CASE( Groups_CollisionAndDuplicateBothMapToConflictCategory )
227{
228 // Hiding CATEGORY::CONFLICT must drop both kinds at once.
229 DOCUMENT_DIFF diff;
230 diff.changes.push_back( MakeChange( wxS( "ZONE" ), CHANGE_KIND::COLLISION ) );
231 diff.changes.push_back( MakeChange( wxS( "ZONE" ), CHANGE_KIND::DUPLICATE_UUID ) );
232 diff.changes.push_back( MakeChange( wxS( "ZONE" ), CHANGE_KIND::MODIFIED ) );
233
234 std::array<bool, CATEGORY_COUNT> visible{};
235 visible.fill( true );
236 visible[static_cast<std::size_t>( CATEGORY::CONFLICT )] = false;
237
238 auto groups = BuildChangeTreeGroups( diff, wxS( "" ), visible );
239 BOOST_REQUIRE_EQUAL( groups.size(), 1u );
240 BOOST_CHECK( groups[0].kind == CHANGE_KIND::MODIFIED );
241}
242
243
244BOOST_AUTO_TEST_CASE( Groups_SearchFilterDropsNonMatching )
245{
246 DOCUMENT_DIFF diff;
247 diff.changes.push_back( MakeChange( wxS( "PCB_TRACK" ), CHANGE_KIND::MODIFIED ) );
248 diff.changes.push_back( MakeChange( wxS( "PCB_VIA" ), CHANGE_KIND::MODIFIED ) );
249 diff.changes.push_back( MakeChange( wxS( "ZONE" ), CHANGE_KIND::MODIFIED ) );
250
251 // Filter for "track" — only PCB_TRACK matches.
252 auto groups = BuildChangeTreeGroups( diff, wxS( "track" ), AllVisible() );
253 BOOST_REQUIRE_EQUAL( groups.size(), 1u );
254 BOOST_REQUIRE_EQUAL( groups[0].entries.size(), 1u );
255 BOOST_CHECK( groups[0].entries[0].change->typeName == wxS( "PCB_TRACK" ) );
256}
257
258
259BOOST_AUTO_TEST_CASE( Groups_FilterIsCaseInsensitive )
260{
261 // User input doesn't have to be lowercased — `BuildChangeTreeGroups`
262 // lowercases internally. This is the contract distinguishing the
263 // higher-level function from `ChangeMatchesSearchFilter` (which
264 // requires pre-lowercased input).
265 DOCUMENT_DIFF diff;
266 diff.changes.push_back( MakeChange( wxS( "PCB_TRACK" ), CHANGE_KIND::ADDED ) );
267
268 auto groups = BuildChangeTreeGroups( diff, wxS( "TRACK" ), AllVisible() );
269 BOOST_REQUIRE_EQUAL( groups.size(), 1u );
270 BOOST_CHECK_EQUAL( groups[0].entries.size(), 1u );
271}
272
273
274BOOST_AUTO_TEST_CASE( Groups_GroupLabelShowsTotal_NoFilter )
275{
276 DOCUMENT_DIFF diff;
277 diff.changes.push_back( MakeChange( wxS( "ZONE" ), CHANGE_KIND::ADDED ) );
278 diff.changes.push_back( MakeChange( wxS( "ZONE" ), CHANGE_KIND::ADDED ) );
279 diff.changes.push_back( MakeChange( wxS( "ZONE" ), CHANGE_KIND::ADDED ) );
280
281 auto groups = BuildChangeTreeGroups( diff, wxS( "" ), AllVisible() );
282 BOOST_REQUIRE_EQUAL( groups.size(), 1u );
283 // "Kind (3)" format when there's no filter.
284 BOOST_CHECK( groups[0].groupLabel.EndsWith( wxS( "(3)" ) ) );
285}
286
287
288BOOST_AUTO_TEST_CASE( Groups_GroupLabelShowsVisibleSlashTotal_WithFilter )
289{
290 DOCUMENT_DIFF diff;
291 diff.changes.push_back( MakeChange( wxS( "PCB_TRACK" ), CHANGE_KIND::ADDED ) );
292 diff.changes.push_back( MakeChange( wxS( "PCB_VIA" ), CHANGE_KIND::ADDED ) );
293 diff.changes.push_back( MakeChange( wxS( "ZONE" ), CHANGE_KIND::ADDED ) );
294
295 // Filter matches only PCB_TRACK -> 1 of 3 visible.
296 auto groups = BuildChangeTreeGroups( diff, wxS( "track" ), AllVisible() );
297 BOOST_REQUIRE_EQUAL( groups.size(), 1u );
298 BOOST_CHECK( groups[0].groupLabel.EndsWith( wxS( "(1/3)" ) ) );
299}
300
301
302BOOST_AUTO_TEST_CASE( Groups_GroupLabelOmitsSlashWhenFilterMatchesAll )
303{
304 // Filter matches every item in the group — the V/N form would just
305 // be N/N, which is noisy. The function falls back to (N) in that case.
306 DOCUMENT_DIFF diff;
307 diff.changes.push_back( MakeChange( wxS( "PCB_TRACK" ), CHANGE_KIND::ADDED ) );
308 diff.changes.push_back( MakeChange( wxS( "PCB_TRACK" ), CHANGE_KIND::ADDED ) );
309
310 auto groups = BuildChangeTreeGroups( diff, wxS( "track" ), AllVisible() );
311 BOOST_REQUIRE_EQUAL( groups.size(), 1u );
312 BOOST_CHECK( groups[0].groupLabel.EndsWith( wxS( "(2)" ) ) );
313}
314
315
316BOOST_AUTO_TEST_CASE( Groups_EmptyBucketAfterFilterIsOmitted )
317{
318 DOCUMENT_DIFF diff;
319 diff.changes.push_back( MakeChange( wxS( "PCB_TRACK" ), CHANGE_KIND::ADDED ) );
320 diff.changes.push_back( MakeChange( wxS( "PCB_VIA" ), CHANGE_KIND::REMOVED ) );
321
322 // Filter matches only the ADDED bucket; the REMOVED bucket must
323 // disappear (UI doesn't show empty groups).
324 auto groups = BuildChangeTreeGroups( diff, wxS( "track" ), AllVisible() );
325 BOOST_REQUIRE_EQUAL( groups.size(), 1u );
326 BOOST_CHECK( groups[0].kind == CHANGE_KIND::ADDED );
327}
328
329
Definition kiid.h:44
CHANGE_KIND
Coarse classification of a single item-level change between two documents.
bool ChangeMatchesSearchFilter(const ITEM_CHANGE &aChange, const wxString &aLowercaseFilter)
Predicate: does this item change match the active search filter?
std::vector< CHANGE_TREE_GROUP > BuildChangeTreeGroups(const DOCUMENT_DIFF &aDiff, const wxString &aSearchFilter, const std::array< bool, CATEGORY_COUNT > &aVisibleCategories)
Group the changes in a DOCUMENT_DIFF by kind, apply category and search filters, and return the resul...
wxString ChangeKindLabel(CHANGE_KIND aKind)
Human-readable label for a CHANGE_KIND (e.g.
_OUT_STRING AsString(const std::string &aString)
Definition sexpr.h:131
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::optional< wxString > refdes
std::vector< ITEM_CHANGE > children
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_CASE(ChangeKindLabel_AllKinds)
BOOST_AUTO_TEST_SUITE_END()
BOOST_CHECK_EQUAL(result, "25.4")