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#include <sstream>
30
31#include <core/profile.h>
32
33#include <board.h>
35#include <footprint.h>
36#include <pad.h>
37#include <pcbexpr_evaluator.h>
38#include <pcb_field.h>
39#include <pcb_group.h>
40#include <pcb_shape.h>
41#include <pcb_track.h>
43#include <zone.h>
44
45
46using namespace KI_TEST;
47
48
52static std::string vecToString( std::span<const std::string> aVec )
53{
54 std::ostringstream ss;
55 ss << "[";
56 const char* sep = "";
57 for( const std::string& s : aVec )
58 {
59 ss << sep << s;
60 sep = ", ";
61 }
62 ss << "]";
63 return ss.str();
64}
65
66
81{
82public:
83 enum class ROLE
84 {
89 };
90
97 static SCALAR_CONSTRAINT FromJson( const nlohmann::json& aJson, ROLE aKind )
98 {
100 c.m_kind = aKind;
101
102 if( aJson.is_object() )
103 {
104 if( aJson.contains( "exact" ) )
105 {
106 auto [v, units] = parseScalar( aJson.at( "exact" ), aKind );
107 c.m_min = v;
108 c.m_max = v;
109 c.m_displayUnits = units;
110 }
111 else
112 {
113 if( aJson.contains( "min" ) )
114 {
115 auto [v, units] = parseScalar( aJson.at( "min" ), aKind );
116 c.m_min = v;
117 c.m_displayUnits = units;
118 }
119
120 if( aJson.contains( "max" ) )
121 {
122 auto [v, units] = parseScalar( aJson.at( "max" ), aKind );
123 c.m_max = v;
124
125 if( !c.m_displayUnits )
126 c.m_displayUnits = units;
127 }
128
129 if( !c.m_min && !c.m_max )
130 {
131 throw std::runtime_error( "Scalar constraint object must have"
132 " 'min', 'max', or 'exact': "
133 + aJson.dump() );
134 }
135 }
136 }
137 else
138 {
139 // Shorthand: bare value means exact match
140 auto [v, units] = parseScalar( aJson, aKind );
141 c.m_min = v;
142 c.m_max = v;
143 c.m_displayUnits = units;
144 }
145
146 return c;
147 }
148
149 static SCALAR_CONSTRAINT Exact( int aValue, ROLE aKind = ROLE::COUNT )
150 {
152 c.m_kind = aKind;
153 c.m_min = aValue;
154 c.m_max = aValue;
155 return c;
156 }
157
161 bool Match( int aValue ) const
162 {
163 if( m_min && aValue < *m_min )
164 return false;
165
166 if( m_max && aValue > *m_max )
167 return false;
168
169 return true;
170 }
171
172 bool IsExact() const { return m_min && m_max && *m_min == *m_max; }
173
174 std::optional<int> GetMin() const { return m_min; }
175 std::optional<int> GetMax() const { return m_max; }
176
184 std::string Format( int aValue ) const
185 {
186 if( m_kind == ROLE::COUNT )
187 return std::to_string( aValue );
188
189 EDA_UNITS units = m_displayUnits.value_or( EDA_UNITS::MM );
190 wxString userStr = EDA_UNIT_UTILS::UI::StringFromValue( pcbIUScale, units, aValue, true /* aAddUnitsText */ );
191
192 std::ostringstream ss;
193 ss << userStr.ToStdString() << " (" << aValue << " nm)";
194 return ss.str();
195 }
196
197 std::string Describe() const
198 {
199 if( m_min && m_max && *m_min == *m_max )
200 return "exactly " + Format( *m_min );
201
202 std::string desc;
203
204 if( m_min )
205 desc += "at least " + Format( *m_min );
206
207 if( m_max )
208 {
209 if( !desc.empty() )
210 desc += ", ";
211
212 desc += "at most " + Format( *m_max );
213 }
214
215 return desc;
216 }
217
218private:
220 {
221 int value;
222 std::optional<EDA_UNITS> units;
223 };
224
234 static PARSED_SCALAR parseScalar( const nlohmann::json& aJson, ROLE aKind )
235 {
236 if( aJson.is_number_integer() )
237 {
238 int raw = aJson.get<int>();
239
240 if( aKind == ROLE::DIMENSION )
241 return { pcbIUScale.mmToIU( raw ), EDA_UNITS::MM };
242 else
243 return { raw, std::nullopt };
244 }
245
246 if( aJson.is_number_float() )
247 {
248 if( aKind == ROLE::COUNT )
249 throw std::runtime_error( "Float value not valid for a count constraint" );
250
251 return { KiROUND( pcbIUScale.mmToIU( aJson.get<double>() ) ), EDA_UNITS::MM };
252 }
253
254 if( aJson.is_string() )
255 {
256 if( aKind == ROLE::COUNT )
257 {
258 throw std::runtime_error( "String value not valid for a count constraint: " + aJson.dump() );
259 }
260
261 const std::string& dimStr = aJson.get<std::string>();
263
264 // Detect which units were written in the string (e.g. "5 mil" -> MILS)
265 EDA_UNITS detectedUnits = EDA_UNITS::MM;
266 EDA_UNIT_UTILS::FetchUnitsFromString( wxString( dimStr ), detectedUnits );
267
268 return { KiROUND( dimIu ), detectedUnits };
269 }
270
271 throw std::runtime_error( "Invalid scalar constraint value: " + aJson.dump() );
272 }
273
274 std::optional<int> m_min;
275 std::optional<int> m_max;
276 std::optional<EDA_UNITS> m_displayUnits;
278};
279
280
287static void CheckConstraint( const SCALAR_CONSTRAINT& aConstraint, int aActual )
288{
289 if( aConstraint.IsExact() )
290 {
291 const int expected = *aConstraint.GetMin();
292 BOOST_TEST( aActual == expected,
293 "expected " << aConstraint.Format( expected ) << ", got " << aConstraint.Format( aActual ) );
294 }
295 else
296 {
297 if( aConstraint.GetMin() )
298 {
299 const int minVal = *aConstraint.GetMin();
300 BOOST_TEST( aActual >= minVal, "expected at least " << aConstraint.Format( minVal ) << ", got "
301 << aConstraint.Format( aActual ) );
302 }
303
304 if( aConstraint.GetMax() )
305 {
306 const int maxVal = *aConstraint.GetMax();
307 BOOST_TEST( aActual <= maxVal, "expected at most " << aConstraint.Format( maxVal ) << ", got "
308 << aConstraint.Format( aActual ) );
309 }
310 }
311}
312
313
318{
319public:
320 explicit STRING_PATTERN_MATCHER( const std::string& aPattern ) :
321 m_pattern( aPattern )
322 {
323 }
324
325 static bool matchPredicate( const std::string& aStr, const std::string& aPattern )
326 {
327 return wxString( aStr ).Matches( aPattern );
328 }
329
330 void Test( const std::string& aStr ) const { BOOST_CHECK_PREDICATE( matchPredicate, (aStr) ( m_pattern ) ); }
331
332private:
333 std::string m_pattern;
334};
335
336
338{
339public:
340 std::optional<SCALAR_CONSTRAINT> m_Count;
341 std::vector<std::string> m_NamePatterns;
342
343private:
344 static bool nameMatches( const std::string& aName, const std::string& aPattern )
345 {
346 return wxString( aName ).Matches( aPattern );
347 }
348
349 std::vector<const NETINFO_ITEM*> findMatchingNets( const BOARD& aBrd ) const
350 {
351 std::vector<const NETINFO_ITEM*> matches;
352
353 if( m_NamePatterns.empty() )
354 {
355 // No patterns = all nets
356 for( const NETINFO_ITEM* net : aBrd.GetNetInfo() )
357 {
358 matches.push_back( net );
359 }
360 return matches;
361 }
362
363 for( const NETINFO_ITEM* net : aBrd.GetNetInfo() )
364 {
365 for( const std::string& pattern : m_NamePatterns )
366 {
367 if( nameMatches( net->GetNetname().ToStdString(), pattern ) )
368 {
369 matches.push_back( net );
370 break;
371 }
372 }
373 }
374
375 return matches;
376 }
377
378 void doSimpleCountTest( const BOARD& aBrd ) const
379 {
380 wxASSERT( m_Count.has_value() );
381 int actualCount = aBrd.GetNetCount();
382
383 BOOST_TEST_CONTEXT( "Net count: " + m_Count->Describe() )
384 {
385 CheckConstraint( *m_Count, actualCount );
386 }
387 }
388
389 void RunTest( const BOARD& aBrd ) const override
390 {
391 if( !m_Count.has_value() && m_NamePatterns.empty() )
392 {
393 BOOST_FAIL( "Net expectation must have at least a count or a name pattern" );
394 }
395
396 // Optimisation - if we ONLY have a count, we have a simple test that doesn't require iterating
397 // all the nets
398 if( m_Count.has_value() && m_NamePatterns.empty() )
399 {
400 doSimpleCountTest( aBrd );
401 return;
402 }
403
404 std::vector<const NETINFO_ITEM*> matches = findMatchingNets( aBrd );
405
406 if( m_Count )
407 {
408 // We need to check the count of matching nets
409 BOOST_TEST_CONTEXT( "Net count: " + m_Count->Describe() )
410 {
411 CheckConstraint( *m_Count, static_cast<int>( matches.size() ) );
412 }
413 }
414 else
415 {
416 // No count: every pattern must match at least one net
417 for( const std::string& pattern : m_NamePatterns )
418 {
419 const auto& netMatchesPattern = [&]( const NETINFO_ITEM* n )
420 {
421 return nameMatches( n->GetNetname().ToStdString(), pattern );
422 };
423
424 bool found = std::any_of( matches.begin(), matches.end(), netMatchesPattern );
425
426 BOOST_TEST( found, "Expected net matching '" << pattern << "'" );
427 }
428 }
429 }
430
431 std::string GetName() const override
432 {
433 std::string desc = "Net";
434
435 if( m_NamePatterns.size() == 1 )
436 {
437 desc += " '" + m_NamePatterns[0] + "'";
438 }
439 else if( !m_NamePatterns.empty() )
440 {
441 desc += " " + vecToString( m_NamePatterns );
442 }
443
444 if( m_Count )
445 desc += " count: " + m_Count->Describe();
446 else
447 desc += " exists";
448
449 return desc;
450 }
451};
452
453
455{
456public:
457 std::vector<std::string> m_NetClassNames;
458 std::optional<SCALAR_CONSTRAINT> m_Count;
460 std::optional<SCALAR_CONSTRAINT> m_TrackWidth;
461 std::optional<SCALAR_CONSTRAINT> m_Clearance;
462 std::optional<SCALAR_CONSTRAINT> m_DpGap;
463 std::optional<SCALAR_CONSTRAINT> m_DpWidth;
464
466 std::optional<SCALAR_CONSTRAINT> m_MatchingNetCount;
467
468private:
469 void doSimpleCountTest( const BOARD& aBrd ) const
470 {
471 wxCHECK( m_Count.has_value(), /*void*/ );
472 const std::shared_ptr<NET_SETTINGS>& netSettings = aBrd.GetDesignSettings().m_NetSettings;
473 int actualCount = netSettings->GetNetclasses().size();
474
475 BOOST_TEST_CONTEXT( "Net class count: " + m_Count->Describe() )
476 {
477 CheckConstraint( *m_Count, actualCount );
478 }
479 }
480
481 static bool nameMatches( const wxString& aName, const std::string& aPattern )
482 {
483 return aName.Matches( aPattern );
484 }
485
486 std::vector<const NETCLASS*> findMatchingNetclasses( const BOARD& aBrd ) const
487 {
488 std::vector<const NETCLASS*> matches;
489 const auto netclasses = aBrd.GetDesignSettings().m_NetSettings->GetNetclasses();
490
491 if( m_NetClassNames.empty() )
492 {
493 // No patterns = all nets
494 for( const auto& [name, nc] : netclasses )
495 {
496 matches.push_back( nc.get() );
497 }
498 return matches;
499 }
500
501 for( const auto& [name, netclass] : netclasses )
502 {
503 for( const std::string& pattern : m_NetClassNames )
504 {
505 if( nameMatches( name, pattern ) )
506 {
507 matches.push_back( netclass.get() );
508 break;
509 }
510 }
511 }
512
513 return matches;
514 }
515
516 void RunTest( const BOARD& aBrd ) const override
517 {
518 // Optimisation - if we ONLY have a count, we have a simple test that doesn't require iterating
519 // all the nets
520 if( m_Count.has_value() && m_NetClassNames.empty() )
521 {
522 doSimpleCountTest( aBrd );
523 return;
524 }
525
526 std::vector<const NETCLASS*> matches = findMatchingNetclasses( aBrd );
527
528 if( m_Count )
529 {
530 // We need to check the count of matching nets
531 BOOST_TEST_CONTEXT( "Net class count: " + m_Count->Describe() )
532 {
533 CheckConstraint( *m_Count, static_cast<int>( matches.size() ) );
534 }
535 }
536 else
537 {
538 // No count: every pattern must match at least one netclass
539 for( const std::string& pattern : m_NetClassNames )
540 {
541 const auto& netclassMatchesPattern = [&]( const NETCLASS* nc )
542 {
543 return nameMatches( nc->GetName(), pattern );
544 };
545
546 bool found = std::any_of( matches.begin(), matches.end(), netclassMatchesPattern );
547
548 BOOST_TEST( found, "Expected netclass matching '" << pattern << "'" );
549 }
550 }
551
552 for( const NETCLASS* nc : matches )
553 {
554 BOOST_TEST_CONTEXT( "Netclass '" << nc->GetName() << "' values" )
555 {
556 if( m_TrackWidth )
557 {
558 BOOST_CHECK( nc->HasTrackWidth() );
559 BOOST_TEST_CONTEXT( "Track width: " + m_TrackWidth->Describe() )
560 {
561 CheckConstraint( *m_TrackWidth, nc->GetTrackWidth() );
562 }
563 }
564
565 if( m_Clearance )
566 {
567 BOOST_CHECK( nc->HasClearance() );
568 BOOST_TEST_CONTEXT( "Clearance: " + m_Clearance->Describe() )
569 {
570 CheckConstraint( *m_Clearance, nc->GetClearance() );
571 }
572 }
573
574 if( m_DpGap )
575 {
576 BOOST_CHECK( nc->HasDiffPairGap() );
577 BOOST_TEST_CONTEXT( "Diff pair gap: " + m_DpGap->Describe() )
578 {
579 CheckConstraint( *m_DpGap, nc->GetDiffPairGap() );
580 }
581 }
582
583 if( m_DpWidth )
584 {
585 BOOST_CHECK( nc->HasDiffPairWidth() );
586 BOOST_TEST_CONTEXT( "Diff pair width: " + m_DpWidth->Describe() )
587 {
588 CheckConstraint( *m_DpWidth, nc->GetDiffPairWidth() );
589 }
590 }
591 }
592 }
593
595 {
596 int matchingNetCount = 0;
597 for( const NETCLASS* nc : matches )
598 {
599 matchingNetCount += CountMatchingNets( aBrd, nc->GetName() );
600 }
601
602 BOOST_TEST_CONTEXT( "Matching net count: " + m_MatchingNetCount->Describe() )
603 {
604 CheckConstraint( *m_MatchingNetCount, matchingNetCount );
605 }
606 }
607 }
608
609 static int CountMatchingNets( const BOARD& aBrd, const wxString& aNetClassName )
610 {
611 int count = 0;
612
613 for( NETINFO_ITEM* net : aBrd.GetNetInfo() )
614 {
615 if( net->GetNetCode() <= 0 )
616 continue;
617
618 NETCLASS* nc = net->GetNetClass();
619
620 if( !nc )
621 continue;
622
623 if( nc->GetName() == aNetClassName )
624 count++;
625 }
626
627 return count;
628 }
629
630 std::string GetName() const override
631 {
632 std::string desc = "Netclasses";
633
634 if( !m_NetClassNames.empty() )
635 desc += " " + vecToString( m_NetClassNames );
636
637 if( m_Count )
638 desc += " count: " + m_Count->Describe();
639 else
640 desc += " exists";
641
642 return desc;
643 }
644};
645
646
648{
649public:
650 std::optional<SCALAR_CONSTRAINT> m_CuCount;
651 std::vector<std::string> m_CuNames;
652
653private:
654 void RunTest( const BOARD& aBrd ) const override
655 {
656 int actualCount = aBrd.GetCopperLayerCount();
657
658 if( m_CuCount.has_value() )
659 {
660 BOOST_TEST_CONTEXT( "Layer count: " + m_CuCount->Describe() )
661 {
662 CheckConstraint( *m_CuCount, actualCount );
663 }
664 }
665
666 if( !m_CuNames.empty() )
667 {
668 std::vector<std::string> actualNames;
669 const LSET cuLayers = aBrd.GetLayerSet() & LSET::AllCuMask();
670
671 for( const auto& layer : cuLayers )
672 {
673 actualNames.push_back( aBrd.GetLayerName( layer ).ToStdString() );
674 }
675
676 BOOST_REQUIRE( actualNames.size() == m_CuNames.size() );
677
678 for( size_t i = 0; i < m_CuNames.size(); ++i )
679 {
680 BOOST_TEST_CONTEXT( "Expecting Cu layer name: '" << m_CuNames[i] << "'" )
681 {
682 BOOST_TEST( actualNames[i] == m_CuNames[i] );
683 }
684 }
685 }
686 }
687
688 std::string GetName() const override
689 {
690 return std::string( "Layers: " ) + ( m_CuCount.has_value() ? m_CuCount->Describe() : "N/A" );
691 }
692};
693
694
696{
697public:
712
713 std::optional<wxString> m_Expression;
714 std::optional<ITEM_TYPE> m_ItemType;
715 std::optional<SCALAR_CONSTRAINT> m_ExpectedMatches;
716 std::optional<wxString> m_ParentExpr;
717
718 static void reportError( const wxString& aMessage, int aOffset )
719 {
720 BOOST_TEST_FAIL( "Expression error: " << aMessage.ToStdString() << " at offset " << aOffset );
721 }
722
723 void RunTest( const BOARD& aBrd ) const override
724 {
726 PCBEXPR_UCODE ucode, parentUcode;
727 bool ok = true;
728
729 const auto matchItem = []( const BOARD_ITEM& aItem, PCBEXPR_UCODE& aUcode ) -> bool
730 {
731 LSET itemLayers = aItem.GetLayerSet();
732
733 for( PCB_LAYER_ID layer : itemLayers.Seq() )
734 {
735 PCBEXPR_CONTEXT ctx( 0, layer );
737 ctx.SetItems( (BOARD_ITEM*) &aItem, nullptr );
738
739 const LIBEVAL::VALUE* result = aUcode.Run( &ctx );
740
741 if( result && result->AsDouble() != 0.0 )
742 {
743 return true;
744 }
745 }
746 return false;
747 };
748
749 if( m_Expression.has_value() )
750 {
751 PCBEXPR_CONTEXT preflightContext;
752 preflightContext.SetErrorCallback( reportError );
753 bool error = !compiler.Compile( *m_Expression, &ucode, &preflightContext );
754 BOOST_REQUIRE( !error );
755 }
756
757 if( m_ParentExpr.has_value() )
758 {
759 PCBEXPR_CONTEXT preflightContext;
760 preflightContext.SetErrorCallback( reportError );
761 bool parentError = !compiler.Compile( *m_ParentExpr, &parentUcode, &preflightContext );
762 BOOST_REQUIRE( !parentError );
763 }
764
765 std::vector<const BOARD_ITEM*> items;
766 {
767 PROF_TIMER collectTimer;
768
769 // Gather only the items we actually need because this can be quite slow on big boards
770 items = collectItemsOfType( aBrd, m_ItemType.value_or( ITEM_TYPE::ANY ) );
771
772 BOOST_TEST_MESSAGE( "Collected " << items.size() << " items to evaluate expression against in "
773 << collectTimer.msecs() << " ms" );
774 }
775
776 size_t matchCount = 0;
777
778 if( !m_Expression.has_value() )
779 {
780 matchCount = items.size();
781 }
782 else
783 {
784 PROF_TIMER matchTimer;
785
786 for( const BOARD_ITEM* item : items )
787 {
788 if( matchItem( *item, ucode ) )
789 {
790 matchCount++;
791
792 if( m_ParentExpr.has_value() )
793 {
794 BOOST_TEST_CONTEXT( "Checking parent expression: " << *m_ParentExpr )
795 {
796 const BOARD_ITEM* parentItem = item->GetParent();
797 BOOST_REQUIRE( parentItem );
798
799 BOOST_CHECK( matchItem( *parentItem, parentUcode ) );
800 }
801 }
802 }
803 }
804
805 BOOST_TEST_MESSAGE( "Expression '" << m_Expression->ToStdString() << "' matched " << matchCount
806 << " items in " << matchTimer.msecs() << " ms" );
807 }
808
809 if( m_ExpectedMatches.has_value() )
810 {
811 BOOST_TEST_CONTEXT( "Eval matches: " + m_ExpectedMatches->Describe() )
812 {
813 CheckConstraint( *m_ExpectedMatches, matchCount );
814 }
815 }
816 else
817 {
818 BOOST_TEST( matchCount > 0, "Expected expression to match at least one item, but it matched none" );
819 }
820 }
821
822 std::string GetName() const override
823 {
824 std::ostringstream ss;
825 ss << "Eval: " << m_Expression.value_or( "<no_expr>" );
826 if( m_ExpectedMatches.has_value() )
827 ss << ", expected matches: " << m_ExpectedMatches->Describe();
828 return ss.str();
829 }
830
831private:
832 std::vector<const BOARD_ITEM*> collectAllBoardItems( const BOARD& aBrd ) const
833 {
834 std::vector<const BOARD_ITEM*> items;
835
836 for( const PCB_TRACK* track : aBrd.Tracks() )
837 items.push_back( track );
838
839 for( const FOOTPRINT* fp : aBrd.Footprints() )
840 {
841 items.push_back( fp );
842
843 for( const PAD* pad : fp->Pads() )
844 items.push_back( pad );
845
846 for( const PCB_FIELD* field : fp->GetFields() )
847 items.push_back( field );
848
849 for( const BOARD_ITEM* gi : fp->GraphicalItems() )
850 items.push_back( gi );
851
852 for( const ZONE* zone : fp->Zones() )
853 items.push_back( zone );
854 }
855
856 for( const BOARD_ITEM* item : aBrd.Drawings() )
857 items.push_back( item );
858
859 for( const ZONE* zone : aBrd.Zones() )
860 items.push_back( zone );
861
862 return items;
863 }
864
865 std::vector<const BOARD_ITEM*> collectItemsOfType( const BOARD& aBrd, ITEM_TYPE aType ) const
866 {
867 std::vector<const BOARD_ITEM*> items;
868
869 switch( aType )
870 {
872 {
873 for( const PCB_TRACK* track : aBrd.Tracks() )
874 {
875 if( track->Type() != PCB_VIA_T )
876 items.push_back( track );
877 }
878 break;
879 }
881 {
882 for( const PCB_TRACK* track : aBrd.Tracks() )
883 {
884 if( track->Type() == PCB_VIA_T )
885 items.push_back( track );
886 }
887 break;
888 }
890 {
891 for( const FOOTPRINT* fp : aBrd.Footprints() )
892 items.push_back( fp );
893 break;
894 }
896 {
897 for( const BOARD_ITEM* item : aBrd.Drawings() )
898 {
899 if( item->Type() == PCB_SHAPE_T )
900 items.push_back( item );
901 }
902 break;
903 }
905 {
906 for( const BOARD_ITEM* item : aBrd.Groups() )
907 items.push_back( item );
908 break;
909 }
911 {
912 for( const ZONE* zone : aBrd.Zones() )
913 items.push_back( zone );
914 break;
915 }
917 {
918 for( const FOOTPRINT* fp : aBrd.Footprints() )
919 {
920 for( const PAD* pad : fp->Pads() )
921 items.push_back( pad );
922 }
923 break;
924 }
926 {
927 for( const FOOTPRINT* fp : aBrd.Footprints() )
928 {
929 for( const BOARD_ITEM* gi : fp->GraphicalItems() )
930 items.push_back( gi );
931 }
932 break;
933 }
935 {
936 for( const FOOTPRINT* fp : aBrd.Footprints() )
937 {
938 for( const PCB_FIELD* field : fp->GetFields() )
939 items.push_back( field );
940 }
941 break;
942 }
944 {
945 for( const FOOTPRINT* fp : aBrd.Footprints() )
946 {
947 for( const ZONE* zone : fp->Zones() )
948 items.push_back( zone );
949 }
950 break;
951 }
952 case ITEM_TYPE::ANY:
953 {
954 items = collectAllBoardItems( aBrd );
955 break;
956 }
957 }
958
959 return items;
960 }
961};
962
963
964static std::vector<std::string> getStringArray( const nlohmann::json& aJson )
965{
966 std::vector<std::string> result;
967
968 if( aJson.is_string() )
969 {
970 result.push_back( aJson );
971 }
972 else if( aJson.is_array() )
973 {
974 for( const auto& entry : aJson )
975 {
976 if( !entry.is_string() )
977 {
978 throw std::runtime_error( "Expected a string or an array of strings" );
979 }
980
981 result.push_back( entry );
982 }
983 }
984 else
985 {
986 throw std::runtime_error( "Expected a string or an array of strings" );
987 }
988
989 return result;
990}
991
992
993static std::unique_ptr<BOARD_EXPECTATION> createNetExpectation( const nlohmann::json& aExpectationEntry )
994{
995 auto netExpectation = std::make_unique<NET_EXPECTATION>();
996
997 if( aExpectationEntry.contains( "count" ) )
998 {
999 netExpectation->m_Count =
1000 SCALAR_CONSTRAINT::FromJson( aExpectationEntry.at( "count" ), SCALAR_CONSTRAINT::ROLE::COUNT );
1001 }
1002
1003 if( aExpectationEntry.contains( "name" ) )
1004 {
1005 const auto& expectedNetName = aExpectationEntry.at( "name" );
1006 netExpectation->m_NamePatterns = getStringArray( expectedNetName );
1007 }
1008
1009 return netExpectation;
1010}
1011
1012
1013static std::unique_ptr<BOARD_EXPECTATION> createNetClassExpectation( const nlohmann::json& aExpectationEntry )
1014{
1015 auto netClassExpectation = std::make_unique<NETCLASS_EXPECTATION>();
1016
1017 if( aExpectationEntry.contains( "count" ) )
1018 {
1019 netClassExpectation->m_Count =
1020 SCALAR_CONSTRAINT::FromJson( aExpectationEntry.at( "count" ), SCALAR_CONSTRAINT::ROLE::COUNT );
1021 }
1022
1023 if( aExpectationEntry.contains( "name" ) )
1024 {
1025 const auto& expectedNetClassName = aExpectationEntry.at( "name" );
1026 netClassExpectation->m_NetClassNames = getStringArray( expectedNetClassName );
1027 }
1028
1029 if( aExpectationEntry.contains( "trackWidth" ) )
1030 {
1031 netClassExpectation->m_TrackWidth =
1032 SCALAR_CONSTRAINT::FromJson( aExpectationEntry.at( "trackWidth" ), SCALAR_CONSTRAINT::ROLE::DIMENSION );
1033 }
1034
1035 if( aExpectationEntry.contains( "clearance" ) )
1036 {
1037 netClassExpectation->m_Clearance =
1038 SCALAR_CONSTRAINT::FromJson( aExpectationEntry.at( "clearance" ), SCALAR_CONSTRAINT::ROLE::DIMENSION );
1039 }
1040
1041 if( aExpectationEntry.contains( "dpGap" ) )
1042 {
1043 netClassExpectation->m_DpGap =
1044 SCALAR_CONSTRAINT::FromJson( aExpectationEntry.at( "dpGap" ), SCALAR_CONSTRAINT::ROLE::DIMENSION );
1045 }
1046
1047 if( aExpectationEntry.contains( "dpWidth" ) )
1048 {
1049 netClassExpectation->m_DpWidth =
1050 SCALAR_CONSTRAINT::FromJson( aExpectationEntry.at( "dpWidth" ), SCALAR_CONSTRAINT::ROLE::DIMENSION );
1051 }
1052
1053 if( aExpectationEntry.contains( "netCount" ) )
1054 {
1055 netClassExpectation->m_MatchingNetCount =
1056 SCALAR_CONSTRAINT::FromJson( aExpectationEntry.at( "netCount" ), SCALAR_CONSTRAINT::ROLE::COUNT );
1057 }
1058
1059 return netClassExpectation;
1060}
1061
1062
1063static std::unique_ptr<BOARD_EXPECTATION> createLayerExpectation( const nlohmann::json& aExpectationEntry )
1064{
1065 auto layerExpectation = std::make_unique<LAYER_EXPECTATION>();
1066
1067 if( aExpectationEntry.contains( "cuNames" ) )
1068 {
1069 const auto& cuNamesEntry = aExpectationEntry.at( "cuNames" );
1070 std::vector<std::string> cuNames = getStringArray( cuNamesEntry );
1071 layerExpectation->m_CuNames = std::move( cuNames );
1072 }
1073
1074 if( aExpectationEntry.contains( "cuCount" ) )
1075 {
1076 layerExpectation->m_CuCount =
1077 SCALAR_CONSTRAINT::FromJson( aExpectationEntry.at( "cuCount" ), SCALAR_CONSTRAINT::ROLE::COUNT );
1078 }
1079 else if( layerExpectation->m_CuNames.size() > 0 )
1080 {
1081 // If specific layer names are specified, we expect that many layers
1082 layerExpectation->m_CuCount =
1083 SCALAR_CONSTRAINT::Exact( static_cast<int>( layerExpectation->m_CuNames.size() ) );
1084 }
1085
1086 return layerExpectation;
1087}
1088
1089
1090static std::unique_ptr<BOARD_EXPECTATION> createItemExprExpectation( const nlohmann::json& aExpectationEntry )
1091{
1092 auto evalExpectation = std::make_unique<ITEM_EVAL_EXPECTATION>();
1093
1094 if( aExpectationEntry.contains( "count" ) )
1095 {
1096 evalExpectation->m_ExpectedMatches =
1097 SCALAR_CONSTRAINT::FromJson( aExpectationEntry.at( "count" ), SCALAR_CONSTRAINT::ROLE::COUNT );
1098 }
1099
1100 // No expression is OK - it means all of the items of the specified type should be counted
1101 if( aExpectationEntry.contains( "expr" ) )
1102 {
1103 evalExpectation->m_Expression = aExpectationEntry.at( "expr" ).get<std::string>();
1104 }
1105
1106 if( aExpectationEntry.contains( "itemType" ) )
1107 {
1108 if( !aExpectationEntry.at( "itemType" ).is_string() )
1109 {
1110 throw std::runtime_error( "Eval expectation 'itemType' field must be a string" );
1111 }
1112
1113 const std::string itemTypeStr = aExpectationEntry.at( "itemType" ).get<std::string>();
1114
1116 const static std::unordered_map<std::string, ITYPE> itemTypeMap = {
1117 { "track", ITYPE::BOARD_TRACK },
1118 { "via", ITYPE::BOARD_VIA },
1119 { "footprint", ITYPE::BOARD_FOOTPRINT },
1120 { "board_graphic", ITYPE::BOARD_GRAPHIC },
1121 { "board_zone", ITYPE::BOARD_ZONE },
1122 { "board_group", ITYPE::BOARD_GROUP },
1123 { "pad", ITYPE::FP_PAD },
1124 { "field", ITYPE::FP_FIELD },
1125 { "fp_graphic", ITYPE::FP_GRAPHIC },
1126 { "fp_zone", ITYPE::FP_ZONE },
1127 { "any", ITYPE::ANY },
1128 };
1129
1130 const auto it = itemTypeMap.find( itemTypeStr );
1131 if( it == itemTypeMap.end() )
1132 {
1133 throw std::runtime_error( "Unknown eval expectation item type: " + itemTypeStr );
1134 }
1135
1136 evalExpectation->m_ItemType = it->second;
1137 }
1138
1139 if( aExpectationEntry.contains( "parentExpr" ) )
1140 {
1141 evalExpectation->m_ParentExpr = aExpectationEntry.at( "parentExpr" ).get<std::string>();
1142 }
1143
1144 return evalExpectation;
1145}
1146
1147
1148std::unique_ptr<BOARD_EXPECTATION_TEST> BOARD_EXPECTATION_TEST::CreateFromJson( const std::string& aBrdName,
1149 const nlohmann::json& aBrdExpectation )
1150{
1151 using ExpectationFactoryFunc = std::unique_ptr<BOARD_EXPECTATION> ( * )( const nlohmann::json& );
1152
1153 // clang-format off
1154 static const std::unordered_map<std::string, ExpectationFactoryFunc> factoryMap = {
1155 { "item", createItemExprExpectation },
1156 { "net", createNetExpectation },
1157 { "netclass", createNetClassExpectation },
1158 { "layers", createLayerExpectation },
1159 };
1160 // clang-format on
1161
1162 std::unique_ptr<BOARD_EXPECTATION_TEST> test = std::make_unique<BOARD_EXPECTATION_TEST>( aBrdName );
1163
1164 if( !aBrdExpectation.is_object() )
1165 {
1166 throw std::runtime_error( "Expectation entry for board " + aBrdName + " is not a valid JSON object" );
1167 }
1168
1169 if( !aBrdExpectation.contains( "type" ) || !aBrdExpectation.at( "type" ).is_string() )
1170 {
1171 throw std::runtime_error( "Expectation entry for board " + aBrdName
1172 + " must have a string field named 'type'" );
1173 }
1174
1175 const std::string expectationType = aBrdExpectation.at( "type" ).get<std::string>();
1176
1177 auto it = factoryMap.find( expectationType );
1178 if( it == factoryMap.end() )
1179 {
1180 throw std::runtime_error( "Unsupported expectation type '" + expectationType + "' for board " + aBrdName );
1181 }
1182
1183 if( std::unique_ptr<BOARD_EXPECTATION> expectation = it->second( aBrdExpectation ) )
1184 {
1185 // Apply common fields
1186 if( aBrdExpectation.contains( "comment" ) && aBrdExpectation.at( "comment" ).is_string() )
1187 {
1188 expectation->SetComment( aBrdExpectation.at( "comment" ).get<std::string>() );
1189 }
1190
1191 if( aBrdExpectation.contains( "skip" ) && aBrdExpectation.at( "skip" ).is_boolean()
1192 && aBrdExpectation.at( "skip" ).get<bool>() )
1193 {
1194 test->m_skip = true;
1195 }
1196
1197 test->m_expectation = std::move( expectation );
1198 }
1199 else
1200 {
1201 throw std::runtime_error( "Failed to create expectation for board " + aBrdName );
1202 }
1203
1204 return test;
1205}
1206
1207
1208std::vector<BOARD_EXPECTATION_TEST::DESCRIPTOR>
1210{
1211 std::vector<DESCRIPTOR> tests;
1212
1213 if( !aJsonArray.is_array() )
1214 {
1215 throw std::runtime_error( "Board expectations JSON must be an array of expectations" );
1216 }
1217
1218 unsigned int index = 0;
1219 for( const auto& expectationEntry : aJsonArray )
1220 {
1221 if( !expectationEntry.is_object() )
1222 {
1223 throw std::runtime_error( "Expectation entry at index " + std::to_string( index )
1224 + " is not a valid JSON object" );
1225 }
1226
1227 std::string name;
1228 std::vector<std::string> tags;
1229
1230 if( expectationEntry.contains( "testName" ) )
1231 {
1232 if( !expectationEntry.at( "testName" ).is_string() )
1233 {
1234 throw std::runtime_error( "Expectation entry 'testName' field at index " + std::to_string( index )
1235 + " must be a string, " + " but is was "
1236 + expectationEntry.at( "testName" ).type_name() );
1237 }
1238
1239 name = expectationEntry.at( "testName" ).get<std::string>();
1240 }
1241 else
1242 {
1243 // No name - use index as identifier
1244 name = std::to_string( index );
1245 }
1246
1247 if( expectationEntry.contains( "tags" ) )
1248 {
1249 if( !expectationEntry.at( "tags" ).is_array() )
1250 {
1251 throw std::runtime_error( "Expectation entry 'tags' field at index " + std::to_string( index )
1252 + " must be an array of strings" );
1253 }
1254
1255 for( const auto& tagEntry : expectationEntry.at( "tags" ) )
1256 {
1257 if( !tagEntry.is_string() )
1258 {
1259 throw std::runtime_error( "Expectation entry 'tags' field at index " + std::to_string( index )
1260 + " must be an array of strings" );
1261 }
1262
1263 tags.push_back( tagEntry.get<std::string>() );
1264 }
1265 }
1266
1267 tests.emplace_back( name, tags, expectationEntry );
1268
1269 ++index;
1270 }
1271
1272 return tests;
1273}
1274
1275
1277{
1278 BOOST_TEST_CONTEXT( wxString::Format( "Checking expectation of type %s", m_expectation->GetName() ) )
1279 {
1280 const std::string& expectationComment = m_expectation->GetComment();
1281 if( !expectationComment.empty() )
1282 {
1283 BOOST_TEST_MESSAGE( "Expectation comment: " << expectationComment );
1284 }
1285
1286 if( m_skip )
1287 {
1288 BOOST_TEST_MESSAGE( "Expectation skipped" );
1289 return;
1290 }
1291
1292 m_expectation->RunTest( aBrd );
1293 }
1294}
1295
1296
1300void BOARD_EXPECTATION_TEST::RunFromRef( const std::string& aBrdName, const BOARD& aBoard,
1301 const DESCRIPTOR& aExpectationTestRef )
1302{
1303 BOOST_TEST_CONTEXT( "Running board expectation: " << aExpectationTestRef.m_TestName )
1304 {
1305 std::unique_ptr<BOARD_EXPECTATION_TEST> boardExpectationTest;
1306
1307 // Load board expectations from the JSON file and create expectation objects
1308 boardExpectationTest = BOARD_EXPECTATION_TEST::CreateFromJson( aBrdName, aExpectationTestRef.m_TestJson );
1309
1310 if( boardExpectationTest )
1311 {
1312 boardExpectationTest->RunTest( aBoard );
1313 }
1314 }
1315}
int index
const char * name
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:112
static std::unique_ptr< BOARD_EXPECTATION > createNetExpectation(const nlohmann::json &aExpectationEntry)
static std::unique_ptr< BOARD_EXPECTATION > createNetClassExpectation(const nlohmann::json &aExpectationEntry)
static std::unique_ptr< BOARD_EXPECTATION > createItemExprExpectation(const nlohmann::json &aExpectationEntry)
static std::string vecToString(std::span< const std::string > aVec)
Format a vector of strings for display, e.g.
static std::unique_ptr< BOARD_EXPECTATION > createLayerExpectation(const nlohmann::json &aExpectationEntry)
static std::vector< std::string > getStringArray(const nlohmann::json &aJson)
static void CheckConstraint(const SCALAR_CONSTRAINT &aConstraint, int aActual)
Assert that aActual satisfies aConstraint using Boost.Test macros.
constexpr BOX2I KiROUND(const BOX2D &aBoxD)
Definition box2.h:990
std::shared_ptr< NET_SETTINGS > m_NetSettings
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:84
virtual LSET GetLayerSet() const
Return a std::bitset of all layers on which the item physically resides.
Definition board_item.h:288
BOARD_ITEM_CONTAINER * GetParent() const
Definition board_item.h:234
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:323
const NETINFO_LIST & GetNetInfo() const
Definition board.h:1004
LSET GetLayerSet() const override
Return a std::bitset of all layers on which the item physically resides.
Definition board.h:688
const ZONES & Zones() const
Definition board.h:368
const GROUPS & Groups() const
The groups must maintain the following invariants.
Definition board.h:390
int GetCopperLayerCount() const
Definition board.cpp:928
const FOOTPRINTS & Footprints() const
Definition board.h:364
const TRACKS & Tracks() const
Definition board.h:362
const wxString GetLayerName(PCB_LAYER_ID aLayer) const
Return the name of a aLayer.
Definition board.cpp:737
BOARD_DESIGN_SETTINGS & GetDesignSettings() const
Definition board.cpp:1091
unsigned GetNetCount() const
Definition board.h:1033
const DRAWINGS & Drawings() const
Definition board.h:366
std::vector< const BOARD_ITEM * > collectAllBoardItems(const BOARD &aBrd) const
std::optional< wxString > m_Expression
void RunTest(const BOARD &aBrd) const override
static void reportError(const wxString &aMessage, int aOffset)
std::optional< wxString > m_ParentExpr
std::optional< ITEM_TYPE > m_ItemType
std::string GetName() const override
std::vector< const BOARD_ITEM * > collectItemsOfType(const BOARD &aBrd, ITEM_TYPE aType) const
std::optional< SCALAR_CONSTRAINT > m_ExpectedMatches
static std::unique_ptr< BOARD_EXPECTATION_TEST > CreateFromJson(const std::string &aBrdName, const nlohmann::json &aBrdExpectations)
static std::vector< DESCRIPTOR > ExtractExpectationTestsFromJson(const nlohmann::json &aExpectationArray)
Extracts expectation tests from the given JSON array and returns a list of test references that can b...
void RunTest(const BOARD &aBrd) const
Runs the test against the given board.
std::unique_ptr< BOARD_EXPECTATION > m_expectation
static void RunFromRef(const std::string &aBrdName, const BOARD &aBoard, const BOARD_EXPECTATION_TEST::DESCRIPTOR &aExpectationTestRef)
Constructs a BOARD_EXPECTATION_TEST from the given JSON definition, and runs it on the given board.
A single expectation about a board, which can be run as a test against a parsed BOARD.
std::optional< SCALAR_CONSTRAINT > m_CuCount
std::string GetName() const override
void RunTest(const BOARD &aBrd) const override
std::vector< std::string > m_CuNames
bool Compile(const wxString &aString, UCODE *aCode, CONTEXT *aPreflightContext)
void SetErrorCallback(std::function< void(const wxString &aMessage, int aOffset)> aCallback)
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
LSEQ Seq(const LSEQ &aSequence) const
Return an LSEQ from the union of this LSET and a desired sequence.
Definition lset.cpp:313
void RunTest(const BOARD &aBrd) const override
static bool nameMatches(const wxString &aName, const std::string &aPattern)
std::optional< SCALAR_CONSTRAINT > m_Count
std::optional< SCALAR_CONSTRAINT > m_MatchingNetCount
Expectation for number of nets matching the netclass patterns in aggregate.
std::string GetName() const override
static int CountMatchingNets(const BOARD &aBrd, const wxString &aNetClassName)
std::optional< SCALAR_CONSTRAINT > m_DpGap
std::vector< std::string > m_NetClassNames
std::optional< SCALAR_CONSTRAINT > m_DpWidth
void doSimpleCountTest(const BOARD &aBrd) const
std::optional< SCALAR_CONSTRAINT > m_Clearance
std::optional< SCALAR_CONSTRAINT > m_TrackWidth
Expectation for track width constraint for all matching netclasses.
std::vector< const NETCLASS * > findMatchingNetclasses(const BOARD &aBrd) const
A collection of nets and the parameters used to route or test these nets.
Definition netclass.h:42
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
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)
std::optional< SCALAR_CONSTRAINT > m_Count
const std::map< wxString, std::shared_ptr< NETCLASS > > & GetNetclasses() const
Gets all netclasses.
Definition pad.h:55
void SetItems(BOARD_ITEM *a, BOARD_ITEM *b=nullptr)
A small class to help profiling.
Definition profile.h:49
double msecs(bool aSinceLast=false)
Definition profile.h:149
A constraint on a scalar value (counts, dimensions, etc.), supporting exact, min, and max bounds.
bool Match(int aValue) const
Test if a value satisfies this constraint.
std::optional< int > m_max
static SCALAR_CONSTRAINT FromJson(const nlohmann::json &aJson, ROLE aKind)
Parse a scalar constraint from JSON.
std::optional< int > m_min
std::string Describe() const
std::optional< EDA_UNITS > m_displayUnits
Original units for display.
static SCALAR_CONSTRAINT Exact(int aValue, ROLE aKind=ROLE::COUNT)
static PARSED_SCALAR parseScalar(const nlohmann::json &aJson, ROLE aKind)
Parse a single bound value from JSON.
std::optional< int > GetMax() const
@ COUNT
A simple count of items (e.g. number of footprints, nets, etc.)
@ DIMENSION
A linear dimension (e.g. track width, clearance)
std::optional< int > GetMin() const
std::string Format(int aValue) const
Format a value for human-readable display.
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)
Handle a list of polygons defining a copper zone.
Definition zone.h:74
EDA_UNITS
Definition eda_units.h:48
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:60
KICOMMON_API wxString StringFromValue(const EDA_IU_SCALE &aIuScale, EDA_UNITS aUnits, double aValue, bool aAddUnitsText=false, EDA_DATA_TYPE aType=EDA_DATA_TYPE::DISTANCE)
Return the string from aValue according to aUnits (inch, mm ...) for display.
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.
KICOMMON_API bool FetchUnitsFromString(const wxString &aTextValue, EDA_UNITS &aUnits)
Write any unit info found in the string to aUnits.
Definition eda_units.cpp:88
Class to handle a set of BOARD_ITEMs.
Lightweight descriptor for a BOARD_EXPECTATION_TEST, which can be used to refer to the test unambiguo...
std::string m_TestName
If the test has a name, it's that, else an index - this is for naming the test for filtering.
const nlohmann::json & m_TestJson
Handy ref to the JSON entry for this expectations test, which saves looking it up again.
std::optional< EDA_UNITS > units
The units detected from the input, if any.
BOOST_TEST(contains==c.ExpectedContains)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
VECTOR3I expected(15, 30, 45)
BOOST_TEST_MESSAGE("\n=== Real-World Polygon PIP Benchmark ===\n"<< formatTable(table))
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:85
@ PCB_VIA_T
class PCB_VIA, a via (like a track segment on a copper layer)
Definition typeinfo.h:94