KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_altium_pcb_import.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, you may find one here:
18 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
19 * or you may search the http://www.gnu.org website for the version 2 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
28
32
35
36#include <board.h>
39#include <common.h>
40#include <core/utf8.h>
41#include <eda_text.h>
42#include <netinfo.h>
43#include <netclass.h>
44#include <pcb_track.h>
45#include <project.h>
48#include <zone.h>
49
50#include <map>
51#include <string>
52#include <vector>
53
54
61
62
63BOOST_FIXTURE_TEST_SUITE( AltiumPcbImport, ALTIUM_PCB_IMPORT_FIXTURE )
64
65
66
70BOOST_AUTO_TEST_CASE( BoardLoadNoAssertions )
71{
72 std::string dataPath = KI_TEST::GetPcbnewTestDataDir()
73 + "plugins/altium/eDP_adapter_dvt1_source/eDP_adapter_dvt1.PcbDoc";
74
75 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
76
77 // Load the board - should not trigger any assertions
78 m_altiumPlugin.LoadBoard( dataPath, board.get(), nullptr );
79
80 BOOST_REQUIRE( board );
81
82 // Basic sanity checks
83 BOOST_CHECK( board->GetNetCount() > 0 );
84 BOOST_CHECK( board->Footprints().size() > 0 );
85}
86
87
95BOOST_AUTO_TEST_CASE( NetclassAssignment )
96{
97 // HiFive1.B01.PcbDoc has Altium netclass definitions
98 std::string dataPath = KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/HiFive/HiFive1.B01.PcbDoc";
99
100 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
101
102 m_altiumPlugin.LoadBoard( dataPath, board.get(), nullptr );
103
104 BOOST_REQUIRE( board );
105
106 // Get the net settings which contains pattern assignments
107 std::shared_ptr<NET_SETTINGS> netSettings = board->GetDesignSettings().m_NetSettings;
108
109 BOOST_REQUIRE( netSettings );
110
111 // Check if there are any pattern assignments in the board
112 auto& patternAssignments = netSettings->GetNetclassPatternAssignments();
113
114 // The HiFive board should have netclass definitions - require this for the test to be meaningful
115 BOOST_REQUIRE_MESSAGE( patternAssignments.size() > 0,
116 "Test file must have netclass pattern assignments" );
117
118 // For each net that has a pattern assignment, verify that the NETINFO_ITEM
119 // has a netclass directly assigned (not just through pattern resolution)
120 bool foundAssignedNet = false;
121
122 for( NETINFO_ITEM* net : board->GetNetInfo() )
123 {
124 if( net->GetNetCode() <= 0 )
125 continue;
126
127 // Get the netclass directly from the NETINFO_ITEM
128 NETCLASS* directNetclass = net->GetNetClass();
129
130 // Get the effective netclass from pattern resolution
131 std::shared_ptr<NETCLASS> effectiveNetclass =
132 netSettings->GetEffectiveNetClass( net->GetNetname() );
133
134 // If this net has a non-default effective netclass, the direct assignment
135 // should also be non-default (this is what the fix ensures)
136 if( effectiveNetclass && effectiveNetclass->GetName() != NETCLASS::Default )
137 {
139 directNetclass != nullptr,
140 wxString::Format( "Net '%s' should have a direct netclass assignment",
141 net->GetNetname() ) );
142
143 if( directNetclass )
144 {
145 foundAssignedNet = true;
146
147 // The direct netclass should match what effective resolution returns
148 // (or be part of the effective class for multi-netclass scenarios)
150 directNetclass->GetName() != NETCLASS::Default,
151 wxString::Format( "Net '%s' should not have default netclass, "
152 "expected effective class or component",
153 net->GetNetname() ) );
154 }
155 }
156 }
157
158 // If there were pattern assignments, we should have found at least one assigned net
159 BOOST_CHECK_MESSAGE( foundAssignedNet,
160 "At least one net should have a non-default netclass assigned" );
161}
162
163
170 const std::string& aRelativePath )
171{
172 std::string dataPath = KI_TEST::GetPcbnewTestDataDir() + aRelativePath;
173
174 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
175 aPlugin.LoadBoard( dataPath, board.get(), nullptr );
176
177 BOOST_REQUIRE( board );
178
179 int fillZoneCount = 0;
180 int fillZonesWithClearance = 0;
181
182 for( ZONE* zone : board->Zones() )
183 {
184 if( !zone->IsOnCopperLayer() || zone->GetIsRuleArea() || zone->IsTeardropArea() )
185 continue;
186
187 fillZoneCount++;
188
189 if( zone->GetLocalClearance().has_value() && zone->GetLocalClearance().value() > 0 )
190 fillZonesWithClearance++;
191 }
192
193 BOOST_CHECK_GT( fillZoneCount, 0 );
194
195 BOOST_CHECK_MESSAGE( fillZonesWithClearance == fillZoneCount,
196 wxString::Format( "%s: %d/%d copper fill zones have clearance set",
197 aRelativePath, fillZonesWithClearance,
198 fillZoneCount ) );
199}
200
201
202BOOST_AUTO_TEST_CASE( ZoneClearances_eDP )
203{
205 m_altiumPlugin, "plugins/altium/eDP_adapter_dvt1_source/eDP_adapter_dvt1.PcbDoc" );
206}
207
208
209BOOST_AUTO_TEST_CASE( ZoneClearances_HiFive )
210{
212 "plugins/altium/HiFive/HiFive1.B01.PcbDoc" );
213}
214
215
223BOOST_AUTO_TEST_CASE( ScopeExprMatchesPolygon )
224{
225 // Positive matches: expressions that reference polygons
226 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "InPolygon" ) ) );
227 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "InPoly" ) ) );
228 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "IsPolygon" ) ) );
229 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "IsPoly" ) ) );
230
231 // Case insensitivity
232 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "inpolygon" ) ) );
233 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "INPOLYGON" ) ) );
234 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "inPOLY" ) ) );
235
236 // Contained within longer expressions
237 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "InPolygon And InNet('GND')" ) ) );
238 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "(InPoly) Or IsVia" ) ) );
239
240 // Negative matches: expressions that don't reference polygons
241 BOOST_CHECK( !altiumScopeExprMatchesPolygon( wxT( "All" ) ) );
242 BOOST_CHECK( !altiumScopeExprMatchesPolygon( wxT( "IsVia" ) ) );
243 BOOST_CHECK( !altiumScopeExprMatchesPolygon( wxT( "IsTrack" ) ) );
244 BOOST_CHECK( !altiumScopeExprMatchesPolygon( wxT( "InNet('GND')" ) ) );
245 BOOST_CHECK( !altiumScopeExprMatchesPolygon( wxT( "InComponent('U1')" ) ) );
246 BOOST_CHECK( !altiumScopeExprMatchesPolygon( wxT( "" ) ) );
247}
248
249
255BOOST_AUTO_TEST_CASE( SelectAltiumPolygonRule_PriorityOrder )
256{
257 auto makeRule = []( int aPriority, const wxString& aScope1, const wxString& aScope2,
258 int aClearance )
259 {
260 ARULE6 rule;
261 rule.priority = aPriority;
262 rule.scope1expr = aScope1;
263 rule.scope2expr = aScope2;
264 rule.clearanceGap = aClearance;
265 return rule;
266 };
267
268 // Sorted by priority ascending, matching the order produced by ParseRules6Data.
269 std::vector<ARULE6> rules = {
270 makeRule( 1, wxT( "InPolygon And InNet('GND')" ), wxT( "All" ), 100 ),
271 makeRule( 2, wxT( "InPolygon" ), wxT( "All" ), 200 ),
272 makeRule( 3, wxT( "All" ), wxT( "All" ), 300 ),
273 makeRule( 4, wxT( "All" ), wxT( "All" ), 400 ),
274 };
275
276 const ARULE6* selected = selectAltiumPolygonRule( rules );
277 BOOST_REQUIRE( selected != nullptr );
278 BOOST_CHECK_EQUAL( selected->priority, 1 );
279 BOOST_CHECK_EQUAL( selected->clearanceGap, 100 );
280
281 rules.erase( rules.begin() );
282 selected = selectAltiumPolygonRule( rules );
283 BOOST_REQUIRE( selected != nullptr );
284 BOOST_CHECK_EQUAL( selected->priority, 2 );
285 BOOST_CHECK_EQUAL( selected->clearanceGap, 200 );
286
287 rules.erase( rules.begin() );
288 BOOST_CHECK( selectAltiumPolygonRule( rules ) == nullptr );
289
290 BOOST_CHECK( selectAltiumPolygonRule( {} ) == nullptr );
291}
292
293
304BOOST_AUTO_TEST_CASE( Via_HoleReferencedMaskTenting )
305{
306 std::string dataPath = KI_TEST::GetPcbnewTestDataDir()
307 + "plugins/altium/issue24456/Fastino_Ground_Isolator.PcbDoc";
308
309 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
310
311 m_altiumPlugin.LoadBoard( dataPath, board.get(), nullptr );
312
313 BOOST_REQUIRE( board );
314
315 int viaCount = 0;
316 int frontExposed = 0;
317 int backExposed = 0;
318
319 for( PCB_TRACK* track : board->Tracks() )
320 {
321 if( track->Type() != PCB_VIA_T )
322 continue;
323
324 const PCB_VIA* via = static_cast<const PCB_VIA*>( track );
325 viaCount++;
326
327 if( via->GetFrontTentingMode() != TENTING_MODE::TENTED )
328 frontExposed++;
329
330 if( via->GetBackTentingMode() != TENTING_MODE::TENTED )
331 backExposed++;
332 }
333
334 BOOST_REQUIRE_GT( viaCount, 0 );
335
336 // Every via on this board carries a hole-referenced mask opening narrower than its land, so the
337 // importer must tent both sides of all of them.
338 BOOST_CHECK_MESSAGE( frontExposed == 0,
339 wxString::Format( "%d of %d vias left front-exposed despite a "
340 "hole-referenced mask",
341 frontExposed, viaCount ) );
342 BOOST_CHECK_MESSAGE( backExposed == 0,
343 wxString::Format( "%d of %d vias left back-exposed despite a "
344 "hole-referenced mask",
345 backExposed, viaCount ) );
346
347 // Guard the tenting heuristic's boundary cases directly. A wide hole-referenced opening that
348 // clears the land must NOT tent, a land-referenced via must NOT be silently tented, and an
349 // explicit Altium tent flag must always tent regardless of expansion mode.
350 const uint32_t holeSize = 300000; // 0.3mm
351 const int landWidth = 600000; // 0.6mm
352
353 BOOST_CHECK( !altiumViaSideIsTented( /*tentFlag*/ false, /*manual*/ true, /*fromHole*/ true,
354 holeSize, /*expansion*/ 500000, landWidth ) );
355 BOOST_CHECK( !altiumViaSideIsTented( /*tentFlag*/ false, /*manual*/ true, /*fromHole*/ false,
356 holeSize, /*expansion*/ 30000, landWidth ) );
357 BOOST_CHECK( altiumViaSideIsTented( /*tentFlag*/ true, /*manual*/ false, /*fromHole*/ false,
358 holeSize, /*expansion*/ 0, landWidth ) );
359
360 // A narrow hole-referenced opening (hole + 2 * expansion <= land) tents the side.
361 BOOST_CHECK( altiumViaSideIsTented( /*tentFlag*/ false, /*manual*/ true, /*fromHole*/ true,
362 holeSize, /*expansion*/ 30000, landWidth ) );
363}
364
365
373BOOST_AUTO_TEST_CASE( StackupDielectricLossTangent )
374{
375 std::string dataPath = KI_TEST::GetPcbnewTestDataDir()
376 + "plugins/altium/issue24456/Fastino_Ground_Isolator.PcbDoc";
377
378 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
379
380 m_altiumPlugin.LoadBoard( dataPath, board.get(), nullptr );
381
382 BOOST_REQUIRE( board );
383
384 const BOARD_STACKUP& stackup = board->GetDesignSettings().GetStackupDescriptor();
385
386 int dielectricCount = 0;
387 int dielectricWithTangent = 0;
388
389 for( const BOARD_STACKUP_ITEM* item : stackup.GetList() )
390 {
391 if( item->GetType() != BS_ITEM_TYPE_DIELECTRIC )
392 continue;
393
394 for( int sub = 0; sub < item->GetSublayersCount(); sub++ )
395 {
396 // Only count dielectric sublayers that carry a real dielectric (non-zero thickness)
397 if( item->GetThickness( sub ) <= 0 )
398 continue;
399
400 dielectricCount++;
401
402 double tangent = item->GetLossTangent( sub );
403
404 if( tangent > 0. )
405 {
406 dielectricWithTangent++;
407
408 // Every prepreg/core dielectric in this board uses a 0.020 loss tangent.
409 BOOST_CHECK_CLOSE( tangent, 0.020, 1e-6 );
410 }
411 }
412 }
413
414 BOOST_REQUIRE_GT( dielectricCount, 0 );
415
416 // All of the board's substantive dielectrics carry a loss tangent in the Altium source, so
417 // every imported dielectric sublayer must receive it.
418 BOOST_CHECK_MESSAGE( dielectricWithTangent == dielectricCount,
419 wxString::Format( "Only %d of %d dielectric sublayers received a loss "
420 "tangent from the Altium stackup",
421 dielectricWithTangent, dielectricCount ) );
422}
423
424
431BOOST_AUTO_TEST_CASE( ProjectParametersToTextVars )
432{
433 std::string dataDir = KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/issue24456/";
434 std::string pcbDoc = dataDir + "Fastino_Ground_Isolator.PcbDoc";
435 std::string prjPcb = dataDir + "Fastino_Ground_Isolator.PrjPcb";
436
437 SETTINGS_MANAGER settingsManager;
438 settingsManager.LoadProject( "" );
439 PROJECT& project = settingsManager.Prj();
440
441 std::map<std::string, UTF8> props;
442 props["project_file"] = prjPcb;
443
444 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
445
446 m_altiumPlugin.LoadBoard( pcbDoc, board.get(), &props, &project );
447
448 const std::map<wxString, wxString>& textVars = project.GetTextVars();
449
450 // The parameter that the issue reports as broken must resolve to its value.
451 BOOST_REQUIRE( textVars.count( wxS( "PCB_REVISION" ) ) );
452 BOOST_CHECK_EQUAL( textVars.at( wxS( "PCB_REVISION" ) ), wxS( "A" ) );
453
454 // A representative sample of the remaining project parameters must all be present.
455 BOOST_CHECK_EQUAL( textVars.at( wxS( "COMPANY_NAME" ) ), wxS( "ETH Zurich" ) );
456 BOOST_CHECK_EQUAL( textVars.at( wxS( "PROJECT_NAME" ) ), wxS( "Fastino Ground Isolator" ) );
457 BOOST_CHECK_EQUAL( textVars.at( wxS( "REVISION_MAJOR" ) ), wxS( "1" ) );
458 BOOST_CHECK_EQUAL( textVars.at( wxS( "YEAR" ) ), wxS( "2026" ) );
459
460 // Board text referencing the special string now resolves through the project variable.
461 wxString resolved = ExpandTextVars( wxS( "${PCB_REVISION}" ), &project );
462 BOOST_CHECK_EQUAL( resolved, wxS( "A" ) );
463
464 // End-to-end: an actual imported board text that references ${PCB_REVISION} must render its
465 // value once the board is linked to the project carrying the variable. This guards against a
466 // regression in the Altium special-string conversion as well as the variable registration.
467 board->SetProject( &project, true /* reference only */ );
468
469 bool sawResolvedBoardText = false;
470
471 for( BOARD_ITEM* item : board->Drawings() )
472 {
473 const EDA_TEXT* text = dynamic_cast<const EDA_TEXT*>( item );
474
475 if( text && text->GetText().Contains( wxS( "${PCB_REVISION}" ) ) )
476 {
477 wxString shown = text->GetShownText( false );
478 BOOST_CHECK( !shown.Contains( wxS( "${PCB_REVISION}" ) ) );
479 BOOST_CHECK( shown.Contains( wxS( "A" ) ) );
480 sawResolvedBoardText = true;
481 }
482 }
483
484 BOOST_CHECK( sawResolvedBoardText );
485}
486
487
492BOOST_AUTO_TEST_CASE( ProjectParametersPreserveExisting )
493{
494 std::string dataDir = KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/issue24456/";
495 std::string pcbDoc = dataDir + "Fastino_Ground_Isolator.PcbDoc";
496 std::string prjPcb = dataDir + "Fastino_Ground_Isolator.PrjPcb";
497
498 SETTINGS_MANAGER settingsManager;
499 settingsManager.LoadProject( "" );
500 PROJECT& project = settingsManager.Prj();
501 project.GetTextVars()[wxS( "PCB_REVISION" )] = wxS( "user-set" );
502
503 std::map<std::string, UTF8> props;
504 props["project_file"] = prjPcb;
505
506 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
507
508 m_altiumPlugin.LoadBoard( pcbDoc, board.get(), &props, &project );
509
510 // A pre-existing variable wins over the imported parameter.
511 BOOST_CHECK_EQUAL( project.GetTextVars().at( wxS( "PCB_REVISION" ) ), wxS( "user-set" ) );
512
513 // Other parameters are still imported.
514 BOOST_CHECK_EQUAL( project.GetTextVars().at( wxS( "COMPANY_NAME" ) ), wxS( "ETH Zurich" ) );
515}
516
517
bool altiumScopeExprMatchesPolygon(const wxString &aExpr)
Return true if an Altium rule scope expression targets polygon pour primitives (matches InPolygon,...
const ARULE6 * selectAltiumPolygonRule(const std::vector< ARULE6 > &aRulesByPriorityAsc)
Select the highest Altium-priority rule whose scope references polygons.
bool altiumViaSideIsTented(bool aTentFlag, bool aManual, bool aFromHole, uint32_t aHoleSize, int32_t aMaskExpansion, int aLandDiameter)
Decide whether one side of an Altium via should be tented when imported into KiCad.
General utilities for PCB file IO for QA programs.
@ BS_ITEM_TYPE_DIELECTRIC
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:84
Manage one layer needed to make a physical board.
Manage layers needed to make a physical board.
const std::vector< BOARD_STACKUP_ITEM * > & GetList() const
A mix-in class (via multiple inheritance) that handles texts such as labels, parts,...
Definition eda_text.h:93
A collection of nets and the parameters used to route or test these nets.
Definition netclass.h:42
static const char Default[]
the name of the default NETCLASS
Definition netclass.h:44
const wxString GetName() const
Gets the name of this (maybe aggregate) netclass in a format for internal usage or for export to exte...
Definition netclass.cpp:328
Handle the data for a net.
Definition netinfo.h:50
BOARD * LoadBoard(const wxString &aFileName, BOARD *aAppendToMe, const std::map< std::string, UTF8 > *aProperties, PROJECT *aProject=nullptr) override
Load information from some input file format that this PCB_IO implementation knows about into either ...
Container for project specific data.
Definition project.h:66
bool LoadProject(const wxString &aFullPath, bool aSetActive=true)
Load a project or sets up a new project with a specified path.
PROJECT & Prj() const
A helper while we are not MDI-capable – return the one and only project.
Handle a list of polygons defining a copper zone.
Definition zone.h:74
wxString ExpandTextVars(const wxString &aSource, const PROJECT *aProject, int aFlags)
Definition common.cpp:63
The common library.
std::string GetPcbnewTestDataDir()
Utility which returns a path to the data directory where the test board files are stored.
ALTIUM_PCB_IMPORT_FIXTURE()=default
PCB_IO_ALTIUM_DESIGNER m_altiumPlugin
wxString scope1expr
wxString scope2expr
BOOST_AUTO_TEST_CASE(BoardLoadNoAssertions)
Test basic board loading - verifies that the Altium import doesn't trigger any assertions during the ...
static void checkAllCopperFillZonesHaveClearance(PCB_IO_ALTIUM_DESIGNER &aPlugin, const std::string &aRelativePath)
Verify that copper zones in imported Altium boards have non-zero local clearance values derived from ...
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
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")
BOOST_CHECK_EQUAL(result, "25.4")
@ PCB_VIA_T
class PCB_VIA, a via (like a track segment on a copper layer)
Definition typeinfo.h:94