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