KiCad PCB EDA Suite
Loading...
Searching...
No Matches
board_expectations.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
25
27
28#include <optional>
29
30#include <board.h>
31#include <pcb_shape.h>
32
33
34using namespace KI_TEST;
35
36
42{
43public:
44 static INT_MATCHER FromJson( const nlohmann::json& aJson )
45 {
46 INT_MATCHER matcher;
47
48 if( aJson.is_number() )
49 {
50 int v = aJson.get<int>();
51 matcher.m_min = v;
52 matcher.m_max = v;
53 }
54 else if( aJson.is_object() )
55 {
56 if( aJson.contains( "exact" ) )
57 {
58 int v = aJson["exact"];
59 matcher.m_min = v;
60 matcher.m_max = v;
61 }
62 else
63 {
64 if( aJson.contains( "min" ) )
65 matcher.m_min = aJson["min"];
66
67 if( aJson.contains( "max" ) )
68 matcher.m_max = aJson["max"];
69 }
70 }
71 else
72 {
73 throw std::runtime_error( "Invalid count expectation: " + aJson.dump() );
74 }
75
76 return matcher;
77 }
78
79 static INT_MATCHER Exact( int aValue )
80 {
81 INT_MATCHER matcher;
82 matcher.m_min = aValue;
83 matcher.m_max = aValue;
84 return matcher;
85 }
86
87 void Test( int aActual ) const
88 {
89 if( m_min )
90 BOOST_TEST( aActual >= *m_min );
91
92 if( m_max )
93 BOOST_TEST( aActual <= *m_max );
94 }
95
96 std::string Describe() const
97 {
98 if( m_min && m_max && *m_min == *m_max )
99 return "exactly " + std::to_string( *m_min );
100
101 std::string desc;
102
103 if( m_min )
104 desc += "at least " + std::to_string( *m_min );
105
106 if( m_max )
107 {
108 if( !desc.empty() )
109 desc += ", ";
110
111 desc += "at most " + std::to_string( *m_max );
112 }
113
114 return desc;
115 }
116
117private:
118 std::optional<int> m_min;
119 std::optional<int> m_max;
120};
121
122
127{
128public:
129 explicit STRING_PATTERN_MATCHER( const std::string& aPattern ) :
130 m_pattern( aPattern )
131 {
132 }
133
134 static bool matchPredicate( const std::string& aStr, const std::string& aPattern )
135 {
136 return wxString( aStr ).Matches( aPattern );
137 }
138
139 void Test( const std::string& aStr ) const { BOOST_CHECK_PREDICATE( matchPredicate, (aStr) ( m_pattern ) ); }
140
141private:
142 std::string m_pattern;
143};
144
145
147{
148public:
149 std::optional<INT_MATCHER> m_Count;
150
151private:
152 void RunTest( const BOARD& aBrd ) const override
153 {
154 int actualCount = aBrd.Footprints().size();
155
156 // TODO: filter footprints by layer, if layer filter is specified in the future
157
158 if( m_Count.has_value() )
159 {
160 BOOST_TEST_CONTEXT( "Footprint count: " + m_Count->Describe() )
161 {
162 m_Count->Test( actualCount );
163 }
164 }
165 }
166
167 std::string GetName() const override
168 {
169 return std::string( "Footprint: " ) + ( m_Count.has_value() ? m_Count->Describe() : "N/A" );
170 }
171};
172
173
175{
176public:
177 std::optional<INT_MATCHER> m_Count;
178 std::vector<std::string> m_NamePatterns;
179
180private:
181 static bool nameMatches( const std::string& aName, const std::string& aPattern )
182 {
183 return wxString( aName ).Matches( aPattern );
184 }
185
186 std::vector<const NETINFO_ITEM*> findMatchingNets( const BOARD& aBrd ) const
187 {
188 std::vector<const NETINFO_ITEM*> matches;
189
190 if( m_NamePatterns.empty() )
191 {
192 // No patterns = all nets
193 for( const NETINFO_ITEM* net : aBrd.GetNetInfo() )
194 {
195 matches.push_back( net );
196 }
197 return matches;
198 }
199
200 for( const NETINFO_ITEM* net : aBrd.GetNetInfo() )
201 {
202 for( const std::string& pattern : m_NamePatterns )
203 {
204 if( nameMatches( net->GetNetname().ToStdString(), pattern ) )
205 {
206 matches.push_back( net );
207 break;
208 }
209 }
210 }
211
212 return matches;
213 }
214
215 void doSimpleCountTest( const BOARD& aBrd ) const
216 {
217 wxASSERT( m_Count.has_value() );
218 int actualCount = aBrd.GetNetCount();
219
220 BOOST_TEST_CONTEXT( "Net count: " + m_Count->Describe() )
221 {
222 m_Count->Test( actualCount );
223 }
224 }
225
226 void RunTest( const BOARD& aBrd ) const override
227 {
228 // Optimisation - if we ONLY have a count, we have a simple test that doesn't require iterating
229 // all the nets
230 if( m_Count.has_value() && m_NamePatterns.empty() )
231 {
232 doSimpleCountTest( aBrd );
233 return;
234 }
235
236 std::vector<const NETINFO_ITEM*> matches = findMatchingNets( aBrd );
237
238 const NETINFO_LIST& nets = aBrd.GetNetInfo();
239
240 if( m_Count )
241 {
242 // We need to check the count of matching nets
243 BOOST_TEST_CONTEXT( "Net count: " + m_Count->Describe() )
244 {
245 m_Count->Test( static_cast<int>( matches.size() ) );
246 }
247 }
248 else
249 {
250 // No count: every pattern must match at least one net
251 for( const std::string& pattern : m_NamePatterns )
252 {
253 const auto& netMatchesPattern = [&]( const NETINFO_ITEM* n )
254 {
255 return nameMatches( n->GetNetname().ToStdString(), pattern );
256 };
257
258 bool found = std::any_of( matches.begin(), matches.end(), netMatchesPattern );
259
260 BOOST_TEST_CONTEXT( "Expected net matching '" << pattern << "'" )
261 {
262 BOOST_TEST( found );
263 }
264 }
265 }
266 }
267
268 std::string GetName() const override
269 {
270 std::string desc = "Net";
271
272 if( m_NamePatterns.size() == 1 )
273 {
274 desc += " '" + m_NamePatterns[0] + "'";
275 }
276 else if( !m_NamePatterns.empty() )
277 {
278 std::string joined;
279 for( size_t i = 0; i < m_NamePatterns.size(); ++i )
280 {
281 if( i > 0 )
282 joined += "', '";
283 joined += m_NamePatterns[i];
284 }
285 desc += " ['" + joined + "']";
286 }
287
288 if( m_Count )
289 desc += " count: " + m_Count->Describe();
290 else
291 desc += " exists";
292
293 return desc;
294 }
295};
296
297
299{
300public:
301 std::optional<INT_MATCHER> m_CuCount;
302 std::vector<std::string> m_CuNames;
303
304private:
305 void RunTest( const BOARD& aBrd ) const override
306 {
307 int actualCount = aBrd.GetCopperLayerCount();
308
309 if( m_CuCount.has_value() )
310 {
311 BOOST_TEST_CONTEXT( "Layer count: " + m_CuCount->Describe() )
312 {
313 m_CuCount->Test( actualCount );
314 }
315 }
316
317 if( !m_CuNames.empty() )
318 {
319 std::vector<std::string> actualNames;
320 const LSET cuLayers = aBrd.GetLayerSet() & LSET::AllCuMask();
321
322 for( const auto& layer : cuLayers )
323 {
324 actualNames.push_back( aBrd.GetLayerName( layer ).ToStdString() );
325 }
326
327 BOOST_REQUIRE( actualNames.size() == m_CuNames.size() );
328
329 for( size_t i = 0; i < m_CuNames.size(); ++i )
330 {
331 BOOST_TEST_CONTEXT( "Expecting Cu layer name: '" << m_CuNames[i] << "'" )
332 {
333 BOOST_TEST( actualNames[i] == m_CuNames[i] );
334 }
335 }
336 }
337 }
338
339 std::string GetName() const override
340 {
341 return std::string( "Layers: " ) + ( m_CuCount.has_value() ? m_CuCount->Describe() : "N/A" );
342 }
343};
344
345
347{
348public:
349 std::optional<VECTOR2I> m_Position;
350 std::optional<int> m_Radius;
351 std::optional<std::string> m_LayerName;
352
353 void RunTest( const BOARD& aBrd ) const override
354 {
355 bool found = false;
356
357 for( const auto& drawing : aBrd.Drawings() )
358 {
359 if( drawing->Type() != PCB_SHAPE_T )
360 continue;
361
362 const PCB_SHAPE& shape = static_cast<const PCB_SHAPE&>( *drawing );
363
364 if( shape.GetShape() == SHAPE_T::CIRCLE )
365 {
366 const VECTOR2I actualPos = shape.GetPosition();
367 const int actualRadius = shape.GetRadius();
368 const PCB_LAYER_ID actualLayer = shape.GetLayer();
369
370 bool layerMatches = true;
371 if( m_LayerName.has_value() && m_LayerName != "*" )
372 {
373 wxString actualLayerName = aBrd.GetLayerName( actualLayer );
374 layerMatches = ( actualLayerName == m_LayerName );
375 }
376
377 bool positionMatches = !m_Position.has_value() || ( actualPos == m_Position );
378 bool radiusMatches = !m_Radius.has_value() || ( actualRadius == m_Radius );
379
380 if( positionMatches && radiusMatches && layerMatches )
381 {
382 found = true;
383 break;
384 }
385 }
386 }
387
388 BOOST_TEST( found );
389 }
390
391 std::string GetName() const override
392 {
393 std::ostringstream ss;
394 ss << "Circle:";
395 if( m_Position.has_value() )
396 ss << " position (" << pcbIUScale.IUTomm( m_Position->x ) << " mm, " << pcbIUScale.IUTomm( m_Position->y )
397 << " mm),";
398
399 if( m_Radius.has_value() )
400 ss << " radius " << pcbIUScale.IUTomm( *m_Radius ) << " mm,";
401
402 if( m_LayerName.has_value() )
403 ss << " layer '" << *m_LayerName << "'";
404
405 return ss.str();
406 }
407};
408
409
410static std::unique_ptr<BOARD_EXPECTATION> createFootprintExpectation( const nlohmann::json& aExpectationEntry )
411{
412 auto footprintExpectation = std::make_unique<FOOTPRINT_EXPECTATION>();
413
414 if( aExpectationEntry.contains( "count" ) )
415 {
416 const auto& countEntry = aExpectationEntry["count"];
417 const INT_MATCHER countMatcher = INT_MATCHER::FromJson( countEntry );
418 footprintExpectation->m_Count = countMatcher;
419 }
420
421 return footprintExpectation;
422}
423
424
425static std::vector<std::string> getStringArray( const nlohmann::json& aJson )
426{
427 std::vector<std::string> result;
428
429 if( aJson.is_string() )
430 {
431 result.push_back( aJson );
432 }
433 else if( aJson.is_array() )
434 {
435 for( const auto& entry : aJson )
436 {
437 if( !entry.is_string() )
438 {
439 throw std::runtime_error( "Expected a string or an array of strings" );
440 }
441
442 result.push_back( entry );
443 }
444 }
445 else
446 {
447 throw std::runtime_error( "Expected a string or an array of strings" );
448 }
449
450 return result;
451}
452
453
454static std::unique_ptr<BOARD_EXPECTATION> createNetExpectation( const nlohmann::json& aExpectationEntry )
455{
456 auto netExpectation = std::make_unique<NET_EXPECTATION>();
457
458 if( aExpectationEntry.contains( "count" ) )
459 {
460 const auto& countEntry = aExpectationEntry["count"];
461 netExpectation->m_Count = INT_MATCHER::FromJson( countEntry );
462 }
463
464 if( aExpectationEntry.contains( "name" ) )
465 {
466 const auto& expectedNetName = aExpectationEntry["name"];
467 netExpectation->m_NamePatterns = getStringArray( expectedNetName );
468 }
469
470 return netExpectation;
471}
472
473
474static std::unique_ptr<BOARD_EXPECTATION> createLayerExpectation( const nlohmann::json& aExpectationEntry )
475{
476 auto layerExpectation = std::make_unique<LAYER_EXPECTATION>();
477
478 if( aExpectationEntry.contains( "cuNames" ) )
479 {
480 const auto& cuNamesEntry = aExpectationEntry["cuNames"];
481 std::vector<std::string> cuNames = getStringArray( cuNamesEntry );
482 layerExpectation->m_CuNames = std::move( cuNames );
483 }
484
485 if( aExpectationEntry.contains( "count" ) )
486 {
487 const auto& countEntry = aExpectationEntry["cuCount"];
488 layerExpectation->m_CuCount = INT_MATCHER::FromJson( countEntry );
489 }
490 else if( layerExpectation->m_CuNames.size() > 0 )
491 {
492 // If specific layer names are specified, we expect that many layers
493 layerExpectation->m_CuCount = INT_MATCHER::Exact( static_cast<int>( layerExpectation->m_CuNames.size() ) );
494 }
495
496 return layerExpectation;
497}
498
499
506static int parsePcbDim( const nlohmann::json& aJson )
507{
508 if( aJson.is_number_integer() )
509 {
510 return pcbIUScale.mmToIU( aJson.get<int>() );
511 }
512 else if( aJson.is_string() )
513 {
514 const std::string& dimStr = aJson.get<std::string>();
516 return KiROUND( dimIu );
517 }
518
519 throw std::runtime_error( "Expected dimension to be an integer or a string with units" );
520}
521
522
523static int parsePcbDim( const nlohmann::json& aJson, const std::string& aFieldName )
524{
525 if( !aJson.contains( aFieldName ) )
526 {
527 throw std::runtime_error( "Expectation entry must have a '" + aFieldName + "' field" );
528 }
529
530 return parsePcbDim( aJson[aFieldName] );
531}
532
533
534static VECTOR2I parsePosition( const nlohmann::json& aJson, const std::string& aFieldName )
535{
536 if( !aJson.contains( aFieldName ) )
537 {
538 throw std::runtime_error( "Expectation entry must have a '" + aFieldName + "' field" );
539 }
540
541 const auto& field = aJson[aFieldName];
542 if( !field.is_array() || field.size() != 2 )
543 {
544 throw std::runtime_error( "Expectation entry must have a '" + aFieldName
545 + "' field with an array of 2 entries" );
546 }
547
548 VECTOR2I pos;
549 pos.x = parsePcbDim( field[0] );
550 pos.y = parsePcbDim( field[1] );
551 return pos;
552}
553
554
555static std::unique_ptr<BOARD_EXPECTATION> createCircleExpectation( const nlohmann::json& aExpectationEntry )
556{
557 auto circleExpectation = std::make_unique<CIRCLE_EXPECTATION>();
558
559 circleExpectation->m_Position = parsePosition( aExpectationEntry, "position" );
560 circleExpectation->m_Radius = parsePcbDim( aExpectationEntry, "radius" );
561
562 if( !aExpectationEntry.contains( "layer" ) || !aExpectationEntry["layer"].is_string() )
563 {
564 throw std::runtime_error( "Circle expectation must have a 'layer' field with a string value" );
565 }
566
567 circleExpectation->m_LayerName = wxString( aExpectationEntry["layer"].get<std::string>() );
568
569 return circleExpectation;
570}
571
572
573static std::unique_ptr<BOARD_EXPECTATION> createGraphicExpectation( const nlohmann::json& aExpectationEntry )
574{
575 const auto& shapeEntry = aExpectationEntry["shape"];
576
577 if( !shapeEntry.is_string() )
578 {
579 throw std::runtime_error( "Graphic expectation must have a string 'shape' field" );
580 }
581
582 const std::string shape = shapeEntry.get<std::string>();
583
584 if( shape == "circle" )
585 {
586 return createCircleExpectation( aExpectationEntry );
587 }
588
589 throw std::runtime_error( "Unsupported graphic shape: " + shape );
590}
591
592
593std::unique_ptr<BOARD_EXPECTATION_TEST> BOARD_EXPECTATION_TEST::CreateFromJson( const std::string& aBrdName,
594 const nlohmann::json& aBrdExpectations )
595{
596 std::unique_ptr<BOARD_EXPECTATION_TEST> test = std::make_unique<BOARD_EXPECTATION_TEST>( aBrdName );
597
598 if( !aBrdExpectations.is_array() )
599 {
600 throw std::runtime_error( "Board expectations for board " + aBrdName + " are not a valid JSON object" );
601 }
602
603 for( const auto& expectationEntry : aBrdExpectations )
604 {
605 if( !expectationEntry.is_object() )
606 {
607 throw std::runtime_error( "Expectation entry for board " + aBrdName + " is not a valid JSON object" );
608 }
609
610 if( !expectationEntry.contains( "type" ) || !expectationEntry["type"].is_string() )
611 {
612 throw std::runtime_error( "Expectation entry for board " + aBrdName
613 + " must have a string field named 'type'" );
614 }
615
616 const std::string expectationType = expectationEntry["type"];
617
618 std::unique_ptr<BOARD_EXPECTATION> expectation;
619
620 if( expectationType == "footprint" )
621 {
622 expectation = createFootprintExpectation( expectationEntry );
623 }
624 else if( expectationType == "net" )
625 {
626 expectation = createNetExpectation( expectationEntry );
627 }
628 else if( expectationType == "layers" )
629 {
630 expectation = createLayerExpectation( expectationEntry );
631 }
632 else if( expectationType == "graphic" )
633 {
634 expectation = createGraphicExpectation( expectationEntry );
635 }
636 else
637 {
638 throw std::runtime_error( "Unsupported expectation type '" + expectationType + "' for board " + aBrdName );
639 }
640
641 if( expectation )
642 test->m_expectations.push_back( std::move( expectation ) );
643 }
644
645 return test;
646}
647
648
649void BOARD_EXPECTATION_TEST::RunTest( const BOARD& aBrd ) const
650{
651 for( const auto& expectation : m_expectations )
652 {
653 BOOST_TEST_CONTEXT( wxString::Format( "Checking expectation of type %s", expectation->GetName() ) )
654 {
655 expectation->RunTest( aBrd );
656 }
657 }
658}
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:112
static std::unique_ptr< BOARD_EXPECTATION > createFootprintExpectation(const nlohmann::json &aExpectationEntry)
static std::unique_ptr< BOARD_EXPECTATION > createCircleExpectation(const nlohmann::json &aExpectationEntry)
static std::unique_ptr< BOARD_EXPECTATION > createNetExpectation(const nlohmann::json &aExpectationEntry)
static VECTOR2I parsePosition(const nlohmann::json &aJson, const std::string &aFieldName)
static std::unique_ptr< BOARD_EXPECTATION > createLayerExpectation(const nlohmann::json &aExpectationEntry)
static int parsePcbDim(const nlohmann::json &aJson)
Parse a dimension from JSON, which can be either an integer (in mm) or a string with units (e....
static std::vector< std::string > getStringArray(const nlohmann::json &aJson)
static std::unique_ptr< BOARD_EXPECTATION > createGraphicExpectation(const nlohmann::json &aExpectationEntry)
constexpr BOX2I KiROUND(const BOX2D &aBoxD)
Definition box2.h:990
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:322
const NETINFO_LIST & GetNetInfo() const
Definition board.h:996
LSET GetLayerSet() const override
Return a std::bitset of all layers on which the item physically resides.
Definition board.h:680
int GetCopperLayerCount() const
Definition board.cpp:920
const FOOTPRINTS & Footprints() const
Definition board.h:363
const wxString GetLayerName(PCB_LAYER_ID aLayer) const
Return the name of a aLayer.
Definition board.cpp:729
unsigned GetNetCount() const
Definition board.h:1027
const DRAWINGS & Drawings() const
Definition board.h:365
std::optional< std::string > m_LayerName
void RunTest(const BOARD &aBrd) const override
std::optional< int > m_Radius
std::string GetName() const override
std::optional< VECTOR2I > m_Position
int GetRadius() const
SHAPE_T GetShape() const
Definition eda_shape.h:169
std::optional< INT_MATCHER > m_Count
void RunTest(const BOARD &aBrd) const override
std::string GetName() const override
Simple binary expectation that checks if an integer value meets the expectation (exact,...
static INT_MATCHER Exact(int aValue)
std::string Describe() const
std::optional< int > m_min
std::optional< int > m_max
static INT_MATCHER FromJson(const nlohmann::json &aJson)
void Test(int aActual) const
std::vector< std::unique_ptr< BOARD_EXPECTATION > > m_expectations
static std::unique_ptr< BOARD_EXPECTATION_TEST > CreateFromJson(const std::string &aBrdName, const nlohmann::json &aBrdExpectations)
void RunTest(const BOARD &aBrd) const
Runs the test against the given board.
A single expectation about a board, which can be run as a test against a parsed BOARD.
std::string GetName() const override
void RunTest(const BOARD &aBrd) const override
std::vector< std::string > m_CuNames
std::optional< INT_MATCHER > m_CuCount
LSET is a set of PCB_LAYER_IDs.
Definition lset.h:37
static const LSET & AllCuMask()
return AllCuMask( MAX_CU_LAYERS );
Definition lset.cpp:608
Handle the data for a net.
Definition netinfo.h:54
Container for NETINFO_ITEM elements, which are the nets.
Definition netinfo.h:212
std::optional< INT_MATCHER > m_Count
void RunTest(const BOARD &aBrd) const override
void doSimpleCountTest(const BOARD &aBrd) const
std::vector< std::string > m_NamePatterns
std::string GetName() const override
std::vector< const NETINFO_ITEM * > findMatchingNets(const BOARD &aBrd) const
static bool nameMatches(const std::string &aName, const std::string &aPattern)
VECTOR2I GetPosition() const override
Definition pcb_shape.h:79
PCB_LAYER_ID GetLayer() const override
Return the primary layer this item is on.
Definition pcb_shape.h:71
static bool matchPredicate(const std::string &aStr, const std::string &aPattern)
void Test(const std::string &aStr) const
STRING_PATTERN_MATCHER(const std::string &aPattern)
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:60
KICOMMON_API double DoubleValueFromString(const EDA_IU_SCALE &aIuScale, EDA_UNITS aUnits, const wxString &aTextValue, EDA_DATA_TYPE aType=EDA_DATA_TYPE::DISTANCE)
Convert aTextValue to a double.
BOOST_TEST(contains==c.ExpectedContains)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_CHECK_PREDICATE(ArePolylineEndPointsNearCircle,(chain)(c.m_geom.m_center_point)(radius)(accuracy+epsilon))
BOOST_TEST_CONTEXT("Test Clearance")
wxString result
Test unit parsing edge cases and error handling.
@ PCB_SHAPE_T
class PCB_SHAPE, a segment not on copper layers
Definition typeinfo.h:88
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:695