KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_sch_differ.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
26
28
31
32#include <schematic.h>
33#include <schematic_settings.h>
34#include <erc/erc_settings.h>
35#include <sch_sheet_path.h>
36#include <sch_screen.h>
37#include <sch_symbol.h>
38#include <sch_field.h>
39#include <sch_sheet.h>
41#include <project.h>
43
44#include <json_common.h>
45
46
47using namespace KICAD_DIFF;
48
49
51{
53 {
54 KI_TEST::LoadSchematic( m_settingsA, "issue18606/issue18606", m_before );
55 KI_TEST::LoadSchematic( m_settingsB, "issue18606/issue18606", m_after );
58 }
59
62 std::unique_ptr<SCHEMATIC> m_before;
63 std::unique_ptr<SCHEMATIC> m_after;
64};
65
66
67BOOST_FIXTURE_TEST_SUITE( SchDiffer, SCH_DIFFER_FIXTURE )
68
69
70BOOST_AUTO_TEST_CASE( TwoFreshLoadsAreIdentical )
71{
72 SCH_DIFFER differ( m_before.get(), m_after.get(), wxS( "issue18606.kicad_sch" ) );
73 DOCUMENT_DIFF result = differ.Diff();
74
75 BOOST_CHECK_EQUAL( result.docType.ToStdString(), "kicad_sch" );
76 BOOST_CHECK_MESSAGE( result.Empty(), "Two fresh loads of the same fixture should diff to empty; "
77 "got " << result.changes.size()
78 << " changes" );
79}
80
81
82BOOST_AUTO_TEST_CASE( TitleBlockEditSurfacesAsDocumentChange )
83{
84 m_before->RootScreen()->GetTitleBlock().SetTitle( wxS( "Original" ) );
85 m_after->RootScreen()->GetTitleBlock().SetTitle( wxS( "Renamed" ) );
86
87 SCH_DIFFER differ( m_before.get(), m_after.get(), wxS( "issue18606.kicad_sch" ) );
88 DOCUMENT_DIFF result = differ.Diff();
89
90 BOOST_REQUIRE_EQUAL( result.changes.size(), 1u );
91 BOOST_CHECK( result.changes[0].id.empty() );
92 BOOST_CHECK( result.changes[0].kind == CHANGE_KIND::MODIFIED );
93
94 bool foundTitleDelta = false;
95
96 for( const PROPERTY_DELTA& d : result.changes[0].properties )
97 {
98 if( d.name == DOC_PROP_TITLE )
99 {
100 foundTitleDelta = true;
101 BOOST_CHECK_EQUAL( d.before.AsString().ToStdString(), "Original" );
102 BOOST_CHECK_EQUAL( d.after.AsString().ToStdString(), "Renamed" );
103 }
104 }
105
106 BOOST_CHECK_MESSAGE( foundTitleDelta, "Expected a title block delta after changing the sheet title" );
107}
108
109
110BOOST_AUTO_TEST_CASE( SymbolFieldEditSurfacesAsProperty )
111{
112 // Find any symbol on the after-side and tweak a field value.
113 SCH_SHEET_LIST sheets = m_after->BuildSheetListSortedByPageNumbers();
114 SCH_SYMBOL* subject = nullptr;
115 SCH_SHEET_PATH subjectPath;
116
117 for( const SCH_SHEET_PATH& path : sheets )
118 {
119 SCH_SCREEN* screen = path.LastScreen();
120
121 for( SCH_ITEM* item : screen->Items().OfType( SCH_SYMBOL_T ) )
122 {
123 subject = static_cast<SCH_SYMBOL*>( item );
124 subjectPath = path;
125 break;
126 }
127
128 if( subject )
129 break;
130 }
131
132 BOOST_REQUIRE( subject );
133
134 // Move the symbol — every SCH_ITEM has a position that's enumerated by
135 // PROPERTY_MANAGER, so we can verify deltas without relying on a specific
136 // field shape that may differ across fixtures.
137 VECTOR2I origPos = subject->GetPosition();
138 subject->SetPosition( origPos + VECTOR2I( 1000, 0 ) );
139
140 SCH_DIFFER differ( m_before.get(), m_after.get() );
141 DOCUMENT_DIFF result = differ.Diff();
142
143 BOOST_REQUIRE_GE( result.changes.size(), 1u );
144
145 bool foundModifiedSymbol = false;
146
147 for( const ITEM_CHANGE& c : result.changes )
148 {
150 && c.id.back().AsString().ToStdString() == subject->m_Uuid.AsString().ToStdString() )
151 {
152 foundModifiedSymbol = true;
153 BOOST_CHECK( !c.properties.empty() );
154 }
155 }
156
157 BOOST_CHECK( foundModifiedSymbol );
158}
159
160
161BOOST_AUTO_TEST_CASE( DiffIsDeterministic )
162{
163 // Apply a deterministic mutation
164 SCH_SHEET_LIST sheets = m_after->BuildSheetListSortedByPageNumbers();
165 SCH_SYMBOL* subject = nullptr;
166
167 for( const SCH_SHEET_PATH& path : sheets )
168 {
169 for( SCH_ITEM* item : path.LastScreen()->Items().OfType( SCH_SYMBOL_T ) )
170 {
171 subject = static_cast<SCH_SYMBOL*>( item );
172 break;
173 }
174
175 if( subject )
176 break;
177 }
178
179 BOOST_REQUIRE( subject );
180 subject->SetPosition( subject->GetPosition() + VECTOR2I( 0, 1000 ) );
181
182 SCH_DIFFER differ1( m_before.get(), m_after.get() );
183 SCH_DIFFER differ2( m_before.get(), m_after.get() );
184
185 DOCUMENT_DIFF r1 = differ1.Diff();
186 DOCUMENT_DIFF r2 = differ2.Diff();
187
188 BOOST_CHECK_EQUAL( r1.ToJson().dump(), r2.ToJson().dump() );
189}
190
191
192BOOST_AUTO_TEST_CASE( DiffJsonRoundTrip )
193{
194 SCH_DIFFER differ( m_before.get(), m_after.get(), wxS( "test.kicad_sch" ) );
195 DOCUMENT_DIFF result = differ.Diff();
196
197 nlohmann::json j = result.ToJson();
199
200 BOOST_CHECK_EQUAL( back.path.ToStdString(), result.path.ToStdString() );
201 BOOST_CHECK_EQUAL( back.docType.ToStdString(), result.docType.ToStdString() );
202 BOOST_CHECK_EQUAL( back.changes.size(), result.changes.size() );
203}
204
205
206// Find the (root) document-level synthetic ITEM_CHANGE with an empty KIID_PATH
207// (modeled on the PCB DocLevelChange helper). Per-sheet doc-level deltas
208// would carry a non-empty KIID_PATH, so this returns only the root delta.
210{
211 for( const ITEM_CHANGE& c : aDiff.changes )
212 {
213 if( c.id.empty() )
214 return &c;
215 }
216
217 return nullptr;
218}
219
220
221static const PROPERTY_DELTA* findProperty( const ITEM_CHANGE& aChange, const wxString& aName )
222{
223 for( const PROPERTY_DELTA& p : aChange.properties )
224 {
225 if( p.name == aName )
226 return &p;
227 }
228
229 return nullptr;
230}
231
232
233// typeName on each emitted ITEM_CHANGE must come from the item's GetClass().
234// Pin the contract for SCH_SYMBOL specifically -- a regression that
235// substitutes a default empty string here breaks the rendering layer.
236BOOST_AUTO_TEST_CASE( ModifiedSymbolTypeNameIsSchSymbol )
237{
238 SCH_SHEET_LIST sheets = m_after->BuildSheetListSortedByPageNumbers();
239 SCH_SYMBOL* subject = nullptr;
240
241 for( const SCH_SHEET_PATH& path : sheets )
242 {
243 for( SCH_ITEM* item : path.LastScreen()->Items().OfType( SCH_SYMBOL_T ) )
244 {
245 subject = static_cast<SCH_SYMBOL*>( item );
246 break;
247 }
248
249 if( subject )
250 break;
251 }
252
253 BOOST_REQUIRE( subject );
254 subject->SetPosition( subject->GetPosition() + VECTOR2I( 2000, 0 ) );
255
256 SCH_DIFFER differ( m_before.get(), m_after.get() );
257 DOCUMENT_DIFF result = differ.Diff();
258
259 bool foundSymbolTypeName = false;
260
261 for( const ITEM_CHANGE& c : result.changes )
262 {
263 if( c.kind == CHANGE_KIND::MODIFIED && c.typeName.Find( wxS( "SCH_SYMBOL" ) ) != wxNOT_FOUND )
264 {
265 foundSymbolTypeName = true;
266 }
267 }
268
269 BOOST_CHECK( foundSymbolTypeName );
270}
271
272
273// Drawing-sheet file path lives in PROJECT_FILE::m_SchDrawingSheetFile.
274// Mutating it on one side must surface as DOC_PROP_DRAWING_SHEET in the
275// root doc-level delta. Skipped silently if the fixture doesn't ship a
276// project file (no GetProject() result).
277BOOST_AUTO_TEST_CASE( DrawingSheetFilePathEditEmitsDocLevelDelta )
278{
279 // SCHEMATIC_SETTINGS::m_SchDrawingSheetFileName is the SCH-side persisted
280 // drawing-sheet path; the diff accesses it via SCHEMATIC::Settings().
281 if( !m_after->IsValid() )
282 {
283 BOOST_TEST_MESSAGE( "Fixture is not valid (no project); skipping" );
284 return;
285 }
286
287 m_before->Settings().m_SchDrawingSheetFileName = wxEmptyString;
288 m_after->Settings().m_SchDrawingSheetFileName = wxS( "/some/sheet.kicad_wks" );
289
290 SCH_DIFFER differ( m_before.get(), m_after.get() );
291 DOCUMENT_DIFF result = differ.Diff();
292
293 const ITEM_CHANGE* docChange = findRootDocLevelChange( result );
294 BOOST_REQUIRE( docChange );
295
298 BOOST_CHECK( delta->before.ToDisplayString() != delta->after.ToDisplayString() );
299}
300
301
302// Multi-sheet output ordering must be deterministic. Apply a per-sheet
303// mutation across every sheet of the fixture and verify two Diff() runs
304// produce byte-identical JSON. Distinct from DiffIsDeterministic above
305// which only mutates a single symbol.
306BOOST_AUTO_TEST_CASE( MultiSheetDiffJsonIsDeterministic )
307{
308 SCH_SHEET_LIST sheets = m_after->BuildSheetListSortedByPageNumbers();
309
310 int n = 0;
311
312 for( const SCH_SHEET_PATH& path : sheets )
313 {
314 for( SCH_ITEM* item : path.LastScreen()->Items().OfType( SCH_SYMBOL_T ) )
315 {
316 SCH_SYMBOL* sym = static_cast<SCH_SYMBOL*>( item );
317 sym->SetPosition( sym->GetPosition() + VECTOR2I( 100 + n, 100 + n ) );
318 ++n;
319 }
320 }
321
322 SCH_DIFFER differ1( m_before.get(), m_after.get() );
323 SCH_DIFFER differ2( m_before.get(), m_after.get() );
324
325 DOCUMENT_DIFF r1 = differ1.Diff();
326 DOCUMENT_DIFF r2 = differ2.Diff();
327
328 BOOST_CHECK_EQUAL( r1.ToJson().dump(), r2.ToJson().dump() );
329}
330
331
332// Hierarchical instance data must survive the diff round-trip: a symbol on
333// a sub-sheet must show up with its full sheet path in KIID_PATH (not just
334// the symbol's own UUID). Pins the per-instance addressing contract.
335BOOST_AUTO_TEST_CASE( SubSheetSymbolKiidPathIncludesSheetPrefix )
336{
337 SCH_SHEET_LIST sheets = m_after->BuildSheetListSortedByPageNumbers();
338
339 if( sheets.size() < 2u )
340 {
341 BOOST_TEST_MESSAGE( "Fixture only has root sheet; skipping" );
342 return;
343 }
344
345 // Pick a symbol from a sub-sheet (non-root) so its KIID_PATH must have
346 // at least one prefix entry beyond the symbol UUID itself.
347 SCH_SYMBOL* subject = nullptr;
348 SCH_SHEET_PATH subjectPath;
349
350 for( const SCH_SHEET_PATH& path : sheets )
351 {
352 if( path.size() < 2u )
353 continue;
354
355 for( SCH_ITEM* item : path.LastScreen()->Items().OfType( SCH_SYMBOL_T ) )
356 {
357 subject = static_cast<SCH_SYMBOL*>( item );
358 subjectPath = path;
359 break;
360 }
361
362 if( subject )
363 break;
364 }
365
366 if( !subject )
367 {
368 BOOST_TEST_MESSAGE( "No sub-sheet symbol in fixture; skipping" );
369 return;
370 }
371
372 subject->SetPosition( subject->GetPosition() + VECTOR2I( 0, 500 ) );
373
374 SCH_DIFFER differ( m_before.get(), m_after.get() );
375 DOCUMENT_DIFF result = differ.Diff();
376
377 bool foundPathPrefix = false;
378
379 for( const ITEM_CHANGE& c : result.changes )
380 {
381 if( c.kind == CHANGE_KIND::MODIFIED && !c.id.empty() && c.id.back().AsString() == subject->m_Uuid.AsString() )
382 {
383 // KIID_PATH for a sub-sheet symbol must have at least 2 entries:
384 // sheet UUID(s) + symbol UUID.
385 BOOST_CHECK_GE( c.id.size(), 2u );
386 foundPathPrefix = true;
387 }
388 }
389
390 BOOST_CHECK( foundPathPrefix );
391}
392
393
394BOOST_AUTO_TEST_CASE( ExtractSchematicGeometryProducesDrawableContext )
395{
396 DOCUMENT_GEOMETRY geometry = ExtractSchematicGeometry( *m_after, KIGFX::COLOR4D( 0.38, 0.38, 0.38, 0.55 ) );
397
398 BOOST_CHECK( !geometry.Empty() );
399 BOOST_CHECK( BBoxFromGeometry( geometry ).has_value() );
400}
401
402
const KIID m_Uuid
Definition eda_item.h:599
EE_TYPE OfType(KICAD_T aType) const
Definition sch_rtree.h:248
Diff two already-parsed SCHEMATICs and produce a DOCUMENT_DIFF.
Definition sch_differ.h:55
DOCUMENT_DIFF Diff() override
Produce a DOCUMENT_DIFF of the inputs the concrete differ was constructed with.
A color representation with 4 components: red, green, blue, alpha.
Definition color4d.h:101
wxString AsString() const
Definition kiid.cpp:423
wxString AsString() const
Definition kiid.cpp:264
Base class for any item which can be embedded within the SCHEMATIC container class,...
Definition sch_item.h:170
EE_RTREE & Items()
Get the full RTree, usually for iterating.
Definition sch_screen.h:122
A container for handling SCH_SHEET_PATH objects in a flattened hierarchy.
Handle access to a stack of flattened SCH_SHEET objects by way of a path for creating a flattened sch...
Schematic symbol object.
Definition sch_symbol.h:75
void SetPosition(const VECTOR2I &aPosition) override
Definition sch_symbol.h:909
VECTOR2I GetPosition() const override
Definition sch_symbol.h:908
DOCUMENT_GEOMETRY ExtractSchematicGeometry(const SCHEMATIC &aSchematic, const KIGFX::COLOR4D &aColor, const std::map< KIID, KIGFX::COLOR4D > &aOverrides, bool aOnlyOverrides)
Extract a coarse outline of a SCHEMATIC into a DOCUMENT_GEOMETRY for use as background context in DIF...
const wxString DOC_PROP_DRAWING_SHEET
std::optional< BOX2I > BBoxFromGeometry(const DOCUMENT_GEOMETRY &aGeometry)
Compute the tight bounding box of a DOCUMENT_GEOMETRY, inflating each primitive by half its stroke so...
const wxString DOC_PROP_TITLE
Title block fields. The comments are numbered from one, as the page settings dialog shows them.
void LoadSchematic(SETTINGS_MANAGER &aSettingsManager, const wxString &aRelPath, std::unique_ptr< SCHEMATIC > &aSchematic)
Definition of the SCH_SHEET_PATH and SCH_SHEET_LIST classes for Eeschema.
The full set of changes between two parsed documents of one type.
nlohmann::json ToJson() const
static DOCUMENT_DIFF FromJson(const nlohmann::json &aJson)
std::vector< ITEM_CHANGE > changes
Aggregate of background geometry extracted from one source document.
Definition diff_scene.h:163
One change record on a single item.
std::vector< PROPERTY_DELTA > properties
Single (name, before, after) triple for one mutated property on an item.
std::unique_ptr< SCHEMATIC > m_before
SETTINGS_MANAGER m_settingsB
std::unique_ptr< SCHEMATIC > m_after
SETTINGS_MANAGER m_settingsA
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
std::string path
static const PROPERTY_DELTA * findProperty(const ITEM_CHANGE &aChange, const wxString &aName)
BOOST_AUTO_TEST_CASE(TwoFreshLoadsAreIdentical)
static const ITEM_CHANGE * findRootDocLevelChange(const DOCUMENT_DIFF &aDiff)
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
int delta
@ SCH_SYMBOL_T
Definition typeinfo.h:168
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:708