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 <nlohmann/json.hpp>
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( SymbolFieldEditSurfacesAsProperty )
83{
84 // Find any symbol on the after-side and tweak a field value.
85 SCH_SHEET_LIST sheets = m_after->BuildSheetListSortedByPageNumbers();
86 SCH_SYMBOL* subject = nullptr;
87 SCH_SHEET_PATH subjectPath;
88
89 for( const SCH_SHEET_PATH& path : sheets )
90 {
91 SCH_SCREEN* screen = path.LastScreen();
92
93 for( SCH_ITEM* item : screen->Items().OfType( SCH_SYMBOL_T ) )
94 {
95 subject = static_cast<SCH_SYMBOL*>( item );
96 subjectPath = path;
97 break;
98 }
99
100 if( subject )
101 break;
102 }
103
104 BOOST_REQUIRE( subject );
105
106 // Move the symbol — every SCH_ITEM has a position that's enumerated by
107 // PROPERTY_MANAGER, so we can verify deltas without relying on a specific
108 // field shape that may differ across fixtures.
109 VECTOR2I origPos = subject->GetPosition();
110 subject->SetPosition( origPos + VECTOR2I( 1000, 0 ) );
111
112 SCH_DIFFER differ( m_before.get(), m_after.get() );
113 DOCUMENT_DIFF result = differ.Diff();
114
115 BOOST_REQUIRE_GE( result.changes.size(), 1u );
116
117 bool foundModifiedSymbol = false;
118
119 for( const ITEM_CHANGE& c : result.changes )
120 {
122 && c.id.back().AsString().ToStdString() == subject->m_Uuid.AsString().ToStdString() )
123 {
124 foundModifiedSymbol = true;
125 BOOST_CHECK( !c.properties.empty() );
126 }
127 }
128
129 BOOST_CHECK( foundModifiedSymbol );
130}
131
132
133BOOST_AUTO_TEST_CASE( DiffIsDeterministic )
134{
135 // Apply a deterministic mutation
136 SCH_SHEET_LIST sheets = m_after->BuildSheetListSortedByPageNumbers();
137 SCH_SYMBOL* subject = nullptr;
138
139 for( const SCH_SHEET_PATH& path : sheets )
140 {
141 for( SCH_ITEM* item : path.LastScreen()->Items().OfType( SCH_SYMBOL_T ) )
142 {
143 subject = static_cast<SCH_SYMBOL*>( item );
144 break;
145 }
146
147 if( subject )
148 break;
149 }
150
151 BOOST_REQUIRE( subject );
152 subject->SetPosition( subject->GetPosition() + VECTOR2I( 0, 1000 ) );
153
154 SCH_DIFFER differ1( m_before.get(), m_after.get() );
155 SCH_DIFFER differ2( m_before.get(), m_after.get() );
156
157 DOCUMENT_DIFF r1 = differ1.Diff();
158 DOCUMENT_DIFF r2 = differ2.Diff();
159
160 BOOST_CHECK_EQUAL( r1.ToJson().dump(), r2.ToJson().dump() );
161}
162
163
164BOOST_AUTO_TEST_CASE( DiffJsonRoundTrip )
165{
166 SCH_DIFFER differ( m_before.get(), m_after.get(), wxS( "test.kicad_sch" ) );
167 DOCUMENT_DIFF result = differ.Diff();
168
169 nlohmann::json j = result.ToJson();
171
172 BOOST_CHECK_EQUAL( back.path.ToStdString(), result.path.ToStdString() );
173 BOOST_CHECK_EQUAL( back.docType.ToStdString(), result.docType.ToStdString() );
174 BOOST_CHECK_EQUAL( back.changes.size(), result.changes.size() );
175}
176
177
178// Find the (root) document-level synthetic ITEM_CHANGE with an empty KIID_PATH
179// (modeled on the PCB DocLevelChange helper). Per-sheet doc-level deltas
180// would carry a non-empty KIID_PATH, so this returns only the root delta.
182{
183 for( const ITEM_CHANGE& c : aDiff.changes )
184 {
185 if( c.id.empty() )
186 return &c;
187 }
188
189 return nullptr;
190}
191
192
193static const PROPERTY_DELTA* findProperty( const ITEM_CHANGE& aChange, const wxString& aName )
194{
195 for( const PROPERTY_DELTA& p : aChange.properties )
196 {
197 if( p.name == aName )
198 return &p;
199 }
200
201 return nullptr;
202}
203
204
205// typeName on each emitted ITEM_CHANGE must come from the item's GetClass().
206// Pin the contract for SCH_SYMBOL specifically -- a regression that
207// substitutes a default empty string here breaks the rendering layer.
208BOOST_AUTO_TEST_CASE( ModifiedSymbolTypeNameIsSchSymbol )
209{
210 SCH_SHEET_LIST sheets = m_after->BuildSheetListSortedByPageNumbers();
211 SCH_SYMBOL* subject = nullptr;
212
213 for( const SCH_SHEET_PATH& path : sheets )
214 {
215 for( SCH_ITEM* item : path.LastScreen()->Items().OfType( SCH_SYMBOL_T ) )
216 {
217 subject = static_cast<SCH_SYMBOL*>( item );
218 break;
219 }
220
221 if( subject )
222 break;
223 }
224
225 BOOST_REQUIRE( subject );
226 subject->SetPosition( subject->GetPosition() + VECTOR2I( 2000, 0 ) );
227
228 SCH_DIFFER differ( m_before.get(), m_after.get() );
229 DOCUMENT_DIFF result = differ.Diff();
230
231 bool foundSymbolTypeName = false;
232
233 for( const ITEM_CHANGE& c : result.changes )
234 {
235 if( c.kind == CHANGE_KIND::MODIFIED && c.typeName.Find( wxS( "SCH_SYMBOL" ) ) != wxNOT_FOUND )
236 {
237 foundSymbolTypeName = true;
238 }
239 }
240
241 BOOST_CHECK( foundSymbolTypeName );
242}
243
244
245// Drawing-sheet file path lives in PROJECT_FILE::m_SchDrawingSheetFile.
246// Mutating it on one side must surface as DOC_PROP_DRAWING_SHEET in the
247// root doc-level delta. Skipped silently if the fixture doesn't ship a
248// project file (no GetProject() result).
249BOOST_AUTO_TEST_CASE( DrawingSheetFilePathEditEmitsDocLevelDelta )
250{
251 // SCHEMATIC_SETTINGS::m_SchDrawingSheetFileName is the SCH-side persisted
252 // drawing-sheet path; the diff accesses it via SCHEMATIC::Settings().
253 if( !m_after->IsValid() )
254 {
255 BOOST_TEST_MESSAGE( "Fixture is not valid (no project); skipping" );
256 return;
257 }
258
259 m_before->Settings().m_SchDrawingSheetFileName = wxEmptyString;
260 m_after->Settings().m_SchDrawingSheetFileName = wxS( "/some/sheet.kicad_wks" );
261
262 SCH_DIFFER differ( m_before.get(), m_after.get() );
263 DOCUMENT_DIFF result = differ.Diff();
264
265 const ITEM_CHANGE* docChange = findRootDocLevelChange( result );
266 BOOST_REQUIRE( docChange );
267
270 BOOST_CHECK( delta->before.ToDisplayString() != delta->after.ToDisplayString() );
271}
272
273
274// Multi-sheet output ordering must be deterministic. Apply a per-sheet
275// mutation across every sheet of the fixture and verify two Diff() runs
276// produce byte-identical JSON. Distinct from DiffIsDeterministic above
277// which only mutates a single symbol.
278BOOST_AUTO_TEST_CASE( MultiSheetDiffJsonIsDeterministic )
279{
280 SCH_SHEET_LIST sheets = m_after->BuildSheetListSortedByPageNumbers();
281
282 int n = 0;
283
284 for( const SCH_SHEET_PATH& path : sheets )
285 {
286 for( SCH_ITEM* item : path.LastScreen()->Items().OfType( SCH_SYMBOL_T ) )
287 {
288 SCH_SYMBOL* sym = static_cast<SCH_SYMBOL*>( item );
289 sym->SetPosition( sym->GetPosition() + VECTOR2I( 100 + n, 100 + n ) );
290 ++n;
291 }
292 }
293
294 SCH_DIFFER differ1( m_before.get(), m_after.get() );
295 SCH_DIFFER differ2( m_before.get(), m_after.get() );
296
297 DOCUMENT_DIFF r1 = differ1.Diff();
298 DOCUMENT_DIFF r2 = differ2.Diff();
299
300 BOOST_CHECK_EQUAL( r1.ToJson().dump(), r2.ToJson().dump() );
301}
302
303
304// Hierarchical instance data must survive the diff round-trip: a symbol on
305// a sub-sheet must show up with its full sheet path in KIID_PATH (not just
306// the symbol's own UUID). Pins the per-instance addressing contract.
307BOOST_AUTO_TEST_CASE( SubSheetSymbolKiidPathIncludesSheetPrefix )
308{
309 SCH_SHEET_LIST sheets = m_after->BuildSheetListSortedByPageNumbers();
310
311 if( sheets.size() < 2u )
312 {
313 BOOST_TEST_MESSAGE( "Fixture only has root sheet; skipping" );
314 return;
315 }
316
317 // Pick a symbol from a sub-sheet (non-root) so its KIID_PATH must have
318 // at least one prefix entry beyond the symbol UUID itself.
319 SCH_SYMBOL* subject = nullptr;
320 SCH_SHEET_PATH subjectPath;
321
322 for( const SCH_SHEET_PATH& path : sheets )
323 {
324 if( path.size() < 2u )
325 continue;
326
327 for( SCH_ITEM* item : path.LastScreen()->Items().OfType( SCH_SYMBOL_T ) )
328 {
329 subject = static_cast<SCH_SYMBOL*>( item );
330 subjectPath = path;
331 break;
332 }
333
334 if( subject )
335 break;
336 }
337
338 if( !subject )
339 {
340 BOOST_TEST_MESSAGE( "No sub-sheet symbol in fixture; skipping" );
341 return;
342 }
343
344 subject->SetPosition( subject->GetPosition() + VECTOR2I( 0, 500 ) );
345
346 SCH_DIFFER differ( m_before.get(), m_after.get() );
347 DOCUMENT_DIFF result = differ.Diff();
348
349 bool foundPathPrefix = false;
350
351 for( const ITEM_CHANGE& c : result.changes )
352 {
353 if( c.kind == CHANGE_KIND::MODIFIED && !c.id.empty() && c.id.back().AsString() == subject->m_Uuid.AsString() )
354 {
355 // KIID_PATH for a sub-sheet symbol must have at least 2 entries:
356 // sheet UUID(s) + symbol UUID.
357 BOOST_CHECK_GE( c.id.size(), 2u );
358 foundPathPrefix = true;
359 }
360 }
361
362 BOOST_CHECK( foundPathPrefix );
363}
364
365
366BOOST_AUTO_TEST_CASE( ExtractSchematicGeometryProducesDrawableContext )
367{
368 DOCUMENT_GEOMETRY geometry = ExtractSchematicGeometry( *m_after, KIGFX::COLOR4D( 0.38, 0.38, 0.38, 0.55 ) );
369
370 BOOST_CHECK( !geometry.Empty() );
371 BOOST_CHECK( BBoxFromGeometry( geometry ).has_value() );
372}
373
374
const KIID m_Uuid
Definition eda_item.h:531
EE_TYPE OfType(KICAD_T aType) const
Definition sch_rtree.h:221
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:393
wxString AsString() const
Definition kiid.cpp:242
Base class for any item which can be embedded within the SCHEMATIC container class,...
Definition sch_item.h:162
EE_RTREE & Items()
Get the full RTree, usually for iterating.
Definition sch_screen.h:115
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:69
void SetPosition(const VECTOR2I &aPosition) override
Definition sch_symbol.h:886
VECTOR2I GetPosition() const override
Definition sch_symbol.h:885
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...
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
BOOST_CHECK_MESSAGE(totalMismatches==0, std::to_string(totalMismatches)+" board(s) with strategy disagreements")
BOOST_TEST_MESSAGE("\n=== Real-World Polygon PIP Benchmark ===\n"<< formatTable(table))
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)
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
int delta
@ SCH_SYMBOL_T
Definition typeinfo.h:169
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683