KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_drc_unconnected_items_exclusion_loss.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 2
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, see <https://www.gnu.org/licenses/>.
18 */
19
20#include <filesystem>
21#include <iostream>
22#include <string>
23
25#include <drc/drc_engine.h>
26#include <board.h>
27#include <boost/test/unit_test.hpp>
28#include <boost/uuid/uuid_generators.hpp>
29#include <boost/uuid/uuid_io.hpp>
30#include <boost/uuid/uuid.hpp>
31#include <drc/drc_engine.h>
32#include <drc/drc_item.h>
33#include <footprint.h>
34#include <pad.h>
35#include <pcb_edit_frame.h>
37#include <pcb_io/pcb_io_mgr.h>
38#include <pcb_io/pcb_io.h>
39#include <pcb_marker.h>
40#include <api/board/board_rules.pb.h>
41#include <pcb_track.h>
44#include <project.h>
48#include <tool/tool_manager.h>
49#include <wx/string.h>
50
52{
53 std::vector<wxString> m_files_to_delete;
54
55 FileCleaner() = default;
57 {
58 for( const auto& f_path : m_files_to_delete )
59 {
60 if( wxFileName::Exists( f_path ) )
61 {
62 if( !wxRemoveFile( f_path ) )
63 {
64 BOOST_TEST_MESSAGE( "Warning: Failed to delete temporary file " << f_path );
65 }
66 }
67 }
68 }
69
70 void AddFile( const wxString& f_path ) { m_files_to_delete.push_back( f_path ); }
71};
72
74{
76 {
77 }
78
79 std::string generate_uuid();
80 bool SaveBoardToFile( BOARD* board, const wxString& filename );
81 void loadBoardAndVerifyInitialExclusions( const wxString& aBoardNameStem, int aExpectedInitialExclusions );
83 int createAndVerifyAdditionalUnconnectedExclusions( int aAdditionalExclusions, int aInitialExclusions );
84 void runDrcOnBoard();
85 void saveBoardAndProjectToTempFiles( const wxString& aBoardNameStem, FileCleaner& aCleaner,
86 wxString& aTempBoardFullPath, wxString& aTempProjectFullPath,
87 wxString& aTempBoardStemName, bool aCopyBoardVerbatim = false );
88 void reloadBoardAndVerifyExclusions( const wxString& aTempBoardStemName, int aExpectedExclusions );
89
90
92 std::unique_ptr<BOARD> m_board;
93};
94
96{
101};
102
103
105{
108 {
109 m_board = std::make_unique<BOARD>();
110 }
111};
112
114{
115 boost::uuids::uuid uuid = boost::uuids::random_generator()();
116 return boost::uuids::to_string( uuid );
117}
118
119void DRC_BASE_FIXTURE::loadBoardAndVerifyInitialExclusions( const wxString& aBoardNameStem,
120 int aExpectedInitialExclusions )
121{
122 KI_TEST::LoadBoard( m_settingsManager, aBoardNameStem, m_board );
123 BOOST_REQUIRE_MESSAGE( m_board,
124 "Could not load board " + aBoardNameStem ); // Ensure board loaded from test data directory
125 PROJECT* pcb_project = m_board->GetProject();
126 BOOST_REQUIRE_MESSAGE( pcb_project, "Get project pointer after initial loading." );
127
128 // Board test file comes with initial exclusions, check if they are preserved after loading
129 const BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
130 size_t initialExclusionsCount = bds.m_DrcExclusions.size();
131 BOOST_TEST_MESSAGE( "Initial DRC exclusions count: " << initialExclusionsCount );
132 BOOST_CHECK_EQUAL( initialExclusionsCount, (size_t) aExpectedInitialExclusions );
133}
134
136{
137 const BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
138 std::vector<PCB_MARKER*> markers;
139
140 for( const DRC_EXCLUSION& exclusion : bds.m_DrcExclusions )
141 {
142 PCB_MARKER* marker = PCB_MARKER::FromProto( exclusion.ToProto().marker() );
143
144 if( marker )
145 {
146 marker->SetExcluded( true, exclusion.GetComment() );
147 markers.push_back( marker );
148 m_board->Add( marker );
149 }
150 }
151 size_t actualExclusionsCount = bds.m_DrcExclusions.size();
152 size_t initialExclusionsCount = markers.size();
153 BOOST_CHECK_EQUAL( actualExclusionsCount, initialExclusionsCount );
154 BOOST_TEST_MESSAGE( std::string( "Actual DRC exclusions count: " ) + std::to_string( actualExclusionsCount )
155 + " after adding initial markers." );
156}
157
159 int aInitialExclusions )
160{
161 for( int i = 0; i < aAdditionalExclusions; ++i )
162 {
163 std::shared_ptr<DRC_ITEM> drcItem = DRC_ITEM::Create( DRCE_UNCONNECTED_ITEMS );
164 wxString id1 = wxString( generate_uuid() );
165 wxString id2 = wxString( generate_uuid() );
166 drcItem->SetItems( KIID( id1 ), KIID( id2 ) );
167
168 PCB_MARKER* marker = new PCB_MARKER( drcItem, VECTOR2I( 1000 * i, 1000 * i ) );
169 m_board->Add( marker );
170
171 // Exclude odd-numbered markers
172 if( i % 2 == 1 )
173 {
174 marker->SetExcluded( true, wxString::Format( "Exclusion %d", i ) );
175 }
176 }
177
178 // Store the new exclusion markers in the board
179 m_board->RecordDRCExclusions();
180
181 // Verify the number of exclusions after adding unconnected items
182 const BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
183 const int expectedExclusions =
184 aInitialExclusions + aAdditionalExclusions / 2; // Only odd-numbered markers are excluded
185 size_t newActualExclusionsCount = bds.m_DrcExclusions.size();
186 BOOST_TEST_MESSAGE( std::string( "New actual DRC exclusions count: " ) + std::to_string( newActualExclusionsCount )
187 + " after adding unconnected items." );
188 BOOST_CHECK_EQUAL( newActualExclusionsCount, (size_t) expectedExclusions );
189 return expectedExclusions;
190}
191
193{
194 BOOST_TEST_MESSAGE( "Running DRC on board." );
195 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
196 bds.m_DRCEngine->InitEngine( wxFileName() );
197 m_board->RecordDRCExclusions();
198 bool runDRC = true;
199 bool runDRCOnAllLayers = true;
200 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, runDRC, runDRCOnAllLayers );
201 m_board->ResolveDRCExclusions( false );
202 BOOST_TEST_MESSAGE( "DRC done." );
203}
204
205void DRC_BASE_FIXTURE::saveBoardAndProjectToTempFiles( const wxString& aBoardNameStem, FileCleaner& aCleaner,
206 wxString& aTempBoardFullPath, wxString& aTempProjectFullPath,
207 wxString& aTempBoardStemName, bool aCopyBoardVerbatim )
208{
209 wxString tempPrefix = "tmp_test_drc_";
210 aTempBoardStemName = tempPrefix + aBoardNameStem.ToStdString();
211 aTempBoardFullPath = KI_TEST::GetPcbnewTestDataDir() + aTempBoardStemName + ".kicad_pcb";
212 aCleaner.AddFile( aTempBoardFullPath );
213 wxString tempProjectStemName = tempPrefix + aBoardNameStem.ToStdString();
214 aTempProjectFullPath = KI_TEST::GetPcbnewTestDataDir() + aTempBoardStemName + ".kicad_pro";
215 aCleaner.AddFile( aTempProjectFullPath );
216
217 bool boardSaved;
218
219 if( aCopyBoardVerbatim )
220 {
221 // Re-saving perturbs geometry slightly, which moves violation positions
222 boardSaved = wxCopyFile( KI_TEST::GetPcbnewTestDataDir() + aBoardNameStem + ".kicad_pcb", aTempBoardFullPath );
223 }
224 else
225 {
226 boardSaved = SaveBoardToFile( m_board->GetBoard(), aTempBoardFullPath );
227 }
228
229 BOOST_REQUIRE_MESSAGE( boardSaved, "Save board to temporary file: " << aTempBoardFullPath );
230
231 m_settingsManager.SaveProjectAs( aTempProjectFullPath, m_board->GetProject() );
232 BOOST_REQUIRE_MESSAGE( wxFileName::Exists( aTempProjectFullPath ),
233 "Save project to temporary file: " << aTempProjectFullPath );
234}
235
236void DRC_BASE_FIXTURE::reloadBoardAndVerifyExclusions( const wxString& aTempBoardStemName, int aExpectedExclusions )
237{
238 // clear the current board to ensure a fresh load
239 m_board.reset();
240
241 KI_TEST::LoadBoard( m_settingsManager, aTempBoardStemName, m_board );
242 BOOST_REQUIRE_MESSAGE( m_board, "Could not load board from tempfile:"
243 + aTempBoardStemName ); // Ensure board loaded from test data directory
244 PROJECT* pcb_project = m_board->GetProject();
245 BOOST_REQUIRE_MESSAGE( pcb_project, "Get project pointer after initial loading." );
246
247 BOARD_DESIGN_SETTINGS& reloaded_bds = m_board->GetDesignSettings();
248 size_t reloadedExclusionsCount = reloaded_bds.m_DrcExclusions.size();
249 BOOST_TEST_MESSAGE( "Reloaded DRC exclusions count: " << reloadedExclusionsCount );
250 BOOST_CHECK_EQUAL( reloadedExclusionsCount, aExpectedExclusions );
251}
252
253bool DRC_BASE_FIXTURE::SaveBoardToFile( BOARD* board, const wxString& filename )
254{
255 try
256 {
258 pi->SaveBoard( filename, *board, nullptr );
259 return true;
260 }
261 catch( const IO_ERROR& error )
262 {
263 BOOST_TEST_MESSAGE( wxString::Format( "Save board to %s: %s", filename, error.What() ) );
264 return false;
265 }
266}
268{
269 // Test that unconnected item exclusions are not lost after multiple DRC runs.
270 // This test is expected to fail if the bug (issue17429) is present.
271
272 std::vector<std::pair<wxString, int>> tests = {
273 { "issue17429", 10 }, // board name stem, expected initial exclusions
274 };
275
276 const int NUM_DRC_RUNS = 2;
277
278 for( const std::pair<wxString, int>& test_params : tests )
279 {
280 wxString boardNameStem = test_params.first;
281 int expectedInitialExclusions = test_params.second;
282
283 loadBoardAndVerifyInitialExclusions( boardNameStem, expectedInitialExclusions );
284 createAndVerifyInitialExclusionMarkers();
285 const int additionalExclusions = 5;
286 int expectedExclusions =
287 createAndVerifyAdditionalUnconnectedExclusions( additionalExclusions, expectedInitialExclusions );
288
289 runDrcOnBoard();
290
291 const BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
292 BOOST_TEST_MESSAGE( std::string( "DRC exclusions count after DRC run: " ) + std::to_string( expectedExclusions )
293 + " after adding unconnected items." );
294 BOOST_CHECK_EQUAL( bds.m_DrcExclusions.size(), expectedExclusions );
295 }
296}
297
298BOOST_FIXTURE_TEST_CASE( DRCUnconnectedItemsExclusionsSaveLoad, DRC_REGRESSION_TEST_FIXTURE )
299{
300 namespace fs = std::filesystem;
301
302 // Test that unconnected item exclusions are not lost during save/load.
303 // This test is expected to fail if the bug (issue17429) is present.
304
305 std::vector<std::pair<wxString, int>> tests = {
306 { "issue17429", 10 }, // board name stem, expected initial exclusions
307 };
308
309
310 for( const std::pair<wxString, int>& test_params : tests )
311 {
312 FileCleaner tempFileCleaner;
313 wxString boardNameStem = test_params.first;
314 int expectedInitialExclusions = test_params.second;
315
316 loadBoardAndVerifyInitialExclusions( boardNameStem, expectedInitialExclusions );
317
318 wxString tempBoardFullPath, tempProjectFullPath, tempBoardStemName;
319 saveBoardAndProjectToTempFiles( boardNameStem, tempFileCleaner, tempBoardFullPath, tempProjectFullPath,
320 tempBoardStemName );
321
322 createAndVerifyInitialExclusionMarkers();
323
324 const int additionalExclusions = 5;
325 int expectedExclusions =
326 createAndVerifyAdditionalUnconnectedExclusions( additionalExclusions, expectedInitialExclusions );
327
328 bool boardSaved = SaveBoardToFile( m_board->GetBoard(), tempBoardFullPath );
329 BOOST_REQUIRE_MESSAGE( boardSaved, "Save board to temporary file: " << tempBoardFullPath );
330
331 m_settingsManager.SaveProjectAs( tempProjectFullPath, m_board->GetProject() );
332 BOOST_REQUIRE_MESSAGE( wxFileName::Exists( tempProjectFullPath ),
333 "Save project to temporary file: " << tempProjectFullPath );
334
335 reloadBoardAndVerifyExclusions( tempBoardStemName, expectedExclusions );
336 }
337}
338
339
340static void runDrcAndCreateMarkers( BOARD* aBoard )
341{
342 aBoard->DeleteMARKERs();
343
345
346 bds.m_DRCEngine->InitEngine( wxFileName() );
348 [aBoard]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I& aPos, int aLayer,
349 const std::function<void( PCB_MARKER* )>& aPathGenerator )
350 {
351 PCB_MARKER* marker = new PCB_MARKER( aItem, aPos, aLayer );
352
353 aPathGenerator( marker );
354 aBoard->Add( marker );
355 } );
356
357 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true, true );
359}
360
361
362// Courtyard overlaps are positioned by SHAPE_POLY_SET::Collide(), which walks a triangulation
363// whose triangle order is not reproducible between loads, so their keys move on their own. That
364// is a separate defect in the geometry layer, not in the exclusion bookkeeping under test here.
365static bool isCourtyardOverlap( PCB_MARKER* aMarker )
366{
367 return aMarker->GetRCItem()->GetErrorCode() == DRCE_OVERLAPPING_FOOTPRINTS;
368}
369
370
371BOOST_FIXTURE_TEST_CASE( DRCExclusionsSurviveProjectRoundTrip, DRC_REGRESSION_TEST_FIXTURE )
372{
373 // Exclude every violation on the board, write the exclusions to the project file, then load
374 // the project back and run DRC again. Nothing may be reported the second time round. The
375 // board file itself is copied rather than re-saved, so both runs see identical geometry.
376
377 FileCleaner tempFileCleaner;
378 wxString tempBoardPath, tempProjectPath, tempStem;
379
380 KI_TEST::LoadBoard( m_settingsManager, "issue25101", m_board );
381 BOOST_REQUIRE( m_board );
382
383 runDrcAndCreateMarkers( m_board.get() );
384
385 auto markerKey =
386 []( const PCB_MARKER* aMarker ) -> std::string
387 {
388 return nlohmann::json( DRC_EXCLUSION::FromMarker( *aMarker ) ).dump();
389 };
390
391 std::map<std::string, int> keyCounts;
392 std::map<std::string, int> expectedKeys;
393
394 for( PCB_MARKER* marker : m_board->Markers() )
395 {
396 std::string serialized = markerKey( marker );
397
398 keyCounts[serialized]++;
399
400 if( !isCourtyardOverlap( marker ) )
401 expectedKeys[serialized]++;
402 }
403
404 // A violation found on several copper layers is reported once per layer but serializes to a
405 // single key, so one stored exclusion has to cover every marker that shares it
406 bool sharedKeys = std::any_of( keyCounts.begin(), keyCounts.end(),
407 []( const std::pair<const std::string, int>& aEntry )
408 {
409 return aEntry.second > 1;
410 } );
411
412 BOOST_REQUIRE_MESSAGE( sharedKeys, "board no longer produces multi-layer violations" );
413 BOOST_TEST_MESSAGE( "Markers: " << m_board->Markers().size() << " distinct keys: " << keyCounts.size() );
414
415 for( PCB_MARKER* marker : m_board->Markers() )
416 marker->SetExcluded( true );
417
418 m_board->RecordDRCExclusions();
419 BOOST_CHECK_EQUAL( m_board->GetDesignSettings().m_DrcExclusions.size(), keyCounts.size() );
420
421 saveBoardAndProjectToTempFiles( "issue25101", tempFileCleaner, tempBoardPath, tempProjectPath, tempStem, true );
422
423 KI_TEST::LoadBoard( m_settingsManager, tempStem, m_board );
424 BOOST_REQUIRE( m_board );
425
426 BOARD_DESIGN_SETTINGS& reloadedBds = m_board->GetDesignSettings();
427 BOOST_REQUIRE_EQUAL( reloadedBds.m_DrcExclusions.size(), keyCounts.size() );
428
429 runDrcAndCreateMarkers( m_board.get() );
430
431 // Violations have to be reported against the same items, at the same place, and just as
432 // often, on a board that has just been reloaded, or no exclusion can survive the trip
433 std::map<std::string, int> reloadedKeys;
434
435 for( PCB_MARKER* marker : m_board->Markers() )
436 {
437 if( !isCourtyardOverlap( marker ) )
438 reloadedKeys[markerKey( marker )]++;
439 }
440
441 for( const std::pair<const std::string, int>& entry : expectedKeys )
442 BOOST_CHECK_MESSAGE( reloadedKeys[entry.first] == entry.second, "key not reproduced: " << entry.first );
443
444 for( const std::pair<const std::string, int>& entry : reloadedKeys )
445 BOOST_CHECK_MESSAGE( expectedKeys.count( entry.first ), "key appeared only after reload: " << entry.first );
446
447 m_board->ResolveDRCExclusions( false );
448
449 for( PCB_MARKER* marker : m_board->Markers() )
450 {
451 BOOST_CHECK_MESSAGE( marker->IsExcluded() || isCourtyardOverlap( marker ),
452 "exclusion lost for " << markerKey( marker ) );
453 }
454}
General utilities for PCB file IO for QA programs.
Container for design settings for a BOARD object.
std::shared_ptr< DRC_ENGINE > m_DRCEngine
std::set< DRC_EXCLUSION, DRC_EXCLUSION_COMPARE > m_DrcExclusions
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:409
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
Definition board.cpp:1497
BOARD_DESIGN_SETTINGS & GetDesignSettings() const
Definition board.cpp:1299
void DeleteMARKERs()
Delete all MARKERS from the board.
Definition board.cpp:2040
void RunTests(EDA_UNITS aUnits, bool aReportAllTrackErrors, bool aTestFootprints, BOARD_COMMIT *aCommit=nullptr)
Run the DRC tests.
void SetViolationHandler(DRC_VIOLATION_HANDLER aHandler)
Set an optional DRC violation handler (receives DRC_ITEMs and positions).
Definition drc_engine.h:164
void ClearViolationHandler()
Definition drc_engine.h:169
void InitEngine(const wxFileName &aRulePath)
Initialize the DRC engine.
Container for an DRC exclusion, which is a PCB_MARKER plus an optional comment.
wxString GetComment() const
static DRC_EXCLUSION FromMarker(const PCB_MARKER &aMarker)
const kiapi::board::DrcExclusion & ToProto() const
static std::shared_ptr< DRC_ITEM > Create(int aErrorCode)
Constructs a DRC_ITEM for the given error code.
Definition drc_item.cpp:444
Hold an error message and may be used when throwing exceptions containing meaningful error messages.
virtual const wxString What() const
A composite of Problem() and Where()
Definition kiid.h:46
std::shared_ptr< RC_ITEM > GetRCItem() const
void SetExcluded(bool aExcluded, const wxString &aComment=wxEmptyString)
Definition marker_base.h:90
@ KICAD_SEXP
S-expression Pcbnew file format.
Definition pcb_io_mgr.h:54
static PCB_IO * FindPlugin(PCB_FILE_T aFileType)
Return a #PLUGIN which the caller can use to import, export, save, or load design documents.
static PCB_MARKER * FromProto(const kiapi::board::DrcMarker &aMsg)
Container for project specific data.
Definition project.h:63
int GetErrorCode() const
Definition rc_item.h:158
@ DRCE_UNCONNECTED_ITEMS
Definition drc_item.h:37
@ DRCE_OVERLAPPING_FOOTPRINTS
Definition drc_item.h:68
std::unique_ptr< T > IO_RELEASER
Helper to hold and release an IO_BASE object when exceptions are thrown.
Definition io_mgr.h:33
std::string GetPcbnewTestDataDir()
Utility which returns a path to the data directory where the test board files are stored.
void LoadBoard(SETTINGS_MANAGER &aSettingsManager, const wxString &aRelPath, std::unique_ptr< BOARD > &aBoard)
int createAndVerifyAdditionalUnconnectedExclusions(int aAdditionalExclusions, int aInitialExclusions)
void reloadBoardAndVerifyExclusions(const wxString &aTempBoardStemName, int aExpectedExclusions)
void loadBoardAndVerifyInitialExclusions(const wxString &aBoardNameStem, int aExpectedInitialExclusions)
void saveBoardAndProjectToTempFiles(const wxString &aBoardNameStem, FileCleaner &aCleaner, wxString &aTempBoardFullPath, wxString &aTempProjectFullPath, wxString &aTempBoardStemName, bool aCopyBoardVerbatim=false)
bool SaveBoardToFile(BOARD *board, const wxString &filename)
void AddFile(const wxString &f_path)
std::vector< wxString > m_files_to_delete
FileCleaner()=default
BOOST_FIXTURE_TEST_CASE(DRCUnconnectedExclusionsLoss, DRC_UNCONNECTED_SAVE_FIXTURE)
static void runDrcAndCreateMarkers(BOARD *aBoard)
static bool isCourtyardOverlap(PCB_MARKER *aMarker)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683