KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_complex_padstack_export.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
20#include <boost/test/unit_test.hpp>
21
22#include <filesystem>
23#include <fstream>
24#include <memory>
25#include <regex>
26#include <set>
27#include <sstream>
28#include <string>
29#include <system_error>
30
31#include <board.h>
34#include <locale_io.h>
37
38#include <wx/filename.h>
39#include <wx/string.h>
40
41
42/*
43 * These formats all describe a padstack as a set of per-layer shapes. Pad "3" of
44 * padstacks_complex.kicad_pcb is MODE::CUSTOM with four distinct square sizes, one per copper
45 * layer, so an exporter that flattens the stack to F_Cu drops sizes instead of skewing them.
46 */
47
48namespace
49{
51class TEMP_FILE
52{
53public:
54 explicit TEMP_FILE( const wxString& aPrefix ) :
55 m_path( std::string( wxFileName::CreateTempFileName( aPrefix ).ToUTF8() ) )
56 {
57 }
58
59 ~TEMP_FILE()
60 {
61 std::error_code ec;
62 std::filesystem::remove( m_path, ec );
63 }
64
65 const std::filesystem::path& Path() const { return m_path; }
66 wxString WxStr() const { return wxString::FromUTF8( m_path.string() ); }
67
68private:
69 std::filesystem::path m_path;
70};
71
72
73std::string slurp( const std::filesystem::path& aPath )
74{
75 std::ifstream in( aPath );
76 std::stringstream ss;
77 ss << in.rdbuf();
78 return ss.str();
79}
80
81
82std::unique_ptr<BOARD> loadBoard( const char* aName )
83{
84 const std::filesystem::path boardPath =
85 std::filesystem::path( KI_TEST::GetPcbnewTestDataDir() ) / aName;
86
87 BOOST_REQUIRE( std::filesystem::exists( boardPath ) );
88
90}
91
92
93std::unique_ptr<BOARD> loadComplexPadstackBoard()
94{
95 return loadBoard( "padstacks_complex.kicad_pcb" );
96}
97
98
100std::set<std::string> captureAll( const std::string& aText, const std::string& aPattern )
101{
102 std::set<std::string> found;
103 std::regex re( aPattern );
104
105 for( auto it = std::sregex_iterator( aText.begin(), aText.end(), re );
106 it != std::sregex_iterator(); ++it )
107 {
108 found.insert( ( *it )[1].str() );
109 }
110
111 return found;
112}
113} // namespace
114
115
116BOOST_AUTO_TEST_SUITE( ComplexPadstackExport )
117
118
119// GenCAD names one shape per layer in $PADS and references it per layer in $PADSTACKS
120BOOST_AUTO_TEST_CASE( GencadEmitsShapePerLayer )
121{
122 LOCALE_IO toggle;
123 std::unique_ptr<BOARD> board = loadComplexPadstackBoard();
124
125 TEMP_FILE out( wxT( "kicad_gencad_padstack_" ) );
126
127 GENCAD_EXPORTER exporter( board.get() );
128 BOOST_REQUIRE( exporter.WriteFile( out.WxStr() ) );
129
130 const std::string cad = slurp( out.Path() );
131
132 // Each unique padstack layer gets its own suffixed shape name
133 std::set<std::string> shapeLayers = captureAll( cad, R"(\nPAD P[0-9]+_([A-Za-z0-9.]+) )" );
134
135 BOOST_CHECK_MESSAGE( shapeLayers.count( "F.Cu" ), "expected an F.Cu-specific PAD shape" );
136 BOOST_CHECK_MESSAGE( shapeLayers.count( "In1.Cu" ), "expected an In1.Cu-specific PAD shape" );
137 BOOST_CHECK_MESSAGE( shapeLayers.count( "In2.Cu" ), "expected an In2.Cu-specific PAD shape" );
138 BOOST_CHECK_MESSAGE( shapeLayers.count( "B.Cu" ), "expected a B.Cu-specific PAD shape" );
139
140 // RECTANGLE is "x y width height", so the four square sizes give four distinct half-extents
141 std::set<std::string> rectSizes = captureAll( cad, R"(\nRECTANGLE [-0-9.]+ [-0-9.]+ ([0-9.]+))" );
142
143 BOOST_CHECK_MESSAGE( rectSizes.size() >= 4,
144 "flattened padstack: expected at least 4 distinct RECTANGLE sizes, got "
145 << rectSizes.size() );
146
147 // A padstack entry must be able to name a different shape on different layers
148 std::regex padstackBlock( R"(PADSTACK PAD[0-9]+ [^\n]*\n((?:PAD [^\n]*\n)+))" );
149 bool sawVaryingStack = false;
150
151 for( auto it = std::sregex_iterator( cad.begin(), cad.end(), padstackBlock );
152 it != std::sregex_iterator(); ++it )
153 {
154 if( captureAll( ( *it )[1].str(), R"(PAD (P[0-9_A-Za-z.]+) )" ).size() > 1 )
155 sawVaryingStack = true;
156 }
157
158 BOOST_CHECK_MESSAGE( sawVaryingStack,
159 "no PADSTACK entry referenced more than one shape across its layers" );
160
161 // Those references land on the right copper only if the names do. GenCAD numbers inner
162 // layers from the top at INNER1, and KiCad writes no $LAYERS section to remap them
163 std::set<std::string> stackLayers = captureAll( cad, R"(\nPAD P[0-9_A-Za-z.]+ ([A-Za-z0-9-]+) )" );
164
165 BOOST_CHECK_MESSAGE( stackLayers.count( "INNER1" ), "expected an INNER1 reference" );
166 BOOST_CHECK_MESSAGE( stackLayers.count( "INNER2" ), "expected an INNER2 reference" );
167
168 for( const std::string& layer : stackLayers )
169 {
170 BOOST_CHECK_MESSAGE( layer.find( '-' ) == std::string::npos,
171 "padstack references a layer GenCAD cannot name: " << layer );
172 }
173}
174
175
176// A Specctra padstack holds one (shape ...) per layer; its id must stay unique for a given
177// physical stack or two padstacks collide on one definition
178BOOST_AUTO_TEST_CASE( SpecctraEmitsShapePerLayer )
179{
180 LOCALE_IO toggle;
181 std::unique_ptr<BOARD> board = loadComplexPadstackBoard();
182
183 TEMP_FILE out( wxT( "kicad_specctra_padstack_" ) );
184 BOOST_REQUIRE_NO_THROW( DSN::ExportBoardToSpecctraFile( board.get(), out.WxStr() ) );
185
186 const std::string dsn = slurp( out.Path() );
187
188 // A stack whose copper varies is named for every layer it defines
189 BOOST_CHECK_MESSAGE( dsn.find( "Complex[" ) != std::string::npos,
190 "flattened padstack: no layer-by-layer padstack id was emitted" );
191
192 // Rect corners come out per layer, so the four sizes give four distinct extents
193 std::set<std::string> rectCorners = captureAll( dsn, R"(\‍(rect "[^"]+" ([-0-9]+) )" );
194
195 BOOST_CHECK_MESSAGE( rectCorners.size() >= 4,
196 "flattened padstack: expected at least 4 distinct rect extents, got "
197 << rectCorners.size() );
198}
199
200
201// A Hyperlynx PADSTACK lists ("<layer>", <shape>, <sx>, <sy>, ...) per layer. A uniform stack
202// collapses to the single "MDEF" default instead
203BOOST_AUTO_TEST_CASE( HyperlynxEmitsShapePerLayer )
204{
205 LOCALE_IO toggle;
206 std::unique_ptr<BOARD> board = loadComplexPadstackBoard();
207
208 TEMP_FILE out( wxT( "kicad_hyperlynx_padstack_" ) );
209 BOOST_REQUIRE( ExportBoardToHyperlynxFile( board.get(), out.WxStr() ) );
210
211 const std::string hyp = slurp( out.Path() );
212
213 std::set<std::string> padSizes = captureAll( hyp, R"RX(\‍("[^"]+", [0-9]+, ([0-9.]+),)RX" );
214
215 BOOST_CHECK_MESSAGE( padSizes.size() >= 4,
216 "flattened padstack: expected at least 4 distinct pad sizes, got "
217 << padSizes.size() );
218
219 // The four sizes must spread across layers, not repeat within one layer
220 std::set<std::string> layersWithSizes = captureAll( hyp, R"RX(\‍("([^"]+)", [0-9]+, [0-9.]+,)RX" );
221
222 BOOST_CHECK_MESSAGE( layersWithSizes.size() >= 4,
223 "expected per-layer entries rather than a single MDEF default" );
224}
225
226
227// The .HYP spec requires the PADSTACK drill parameter to be absent, not zero, on a hole-less
228// padstack
229BOOST_AUTO_TEST_CASE( HyperlynxOmitsDrillOnSmdPadstacks )
230{
231 LOCALE_IO toggle;
232
233 std::unique_ptr<BOARD> board = loadBoard( "api_kitchen_sink.kicad_pcb" );
234
235 TEMP_FILE out( wxT( "kicad_hyperlynx_smd_" ) );
236 BOOST_REQUIRE( ExportBoardToHyperlynxFile( board.get(), out.WxStr() ) );
237
238 const std::string hyp = slurp( out.Path() );
239
240 BOOST_CHECK_MESSAGE( hyp.find( "{PADSTACK=" ) != std::string::npos,
241 "board produced no padstacks to check" );
242
243 std::set<std::string> drills = captureAll( hyp, R"(\{PADSTACK=[0-9]+, ([0-9.]+))" );
244
245 BOOST_CHECK_MESSAGE( drills.count( "0.000000000" ) == 0,
246 "a hole-less padstack declared a zero drill instead of omitting it" );
247}
248
249
General utilities for PCB file IO for QA programs.
Export board to GenCAD file format.
bool WriteFile(const wxString &aFullFileName)
Export a GenCAD file.
Instantiate the current locale within a scope in which you are expecting exceptions to be thrown.
Definition locale_io.h:37
bool ExportBoardToHyperlynxFile(BOARD *aBoard, const wxString &aFullFilename)
Write aBoard to aFullFilename in Hyperlynx .hyp format.
void ExportBoardToSpecctraFile(BOARD *aBoard, const wxString &aFullFilename, REPORTER *aReporter=nullptr)
Helper method to export board to DSN file.
std::string GetPcbnewTestDataDir()
Utility which returns a path to the data directory where the test board files are stored.
std::unique_ptr< BOARD > RequireBoardFromPcbnewQAData(const std::filesystem::path &aFilename)
Require a board from the path relative to the pcbnew QA data directory.
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_CASE(GencadEmitsShapePerLayer)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()