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
89 std::unique_ptr<BOARD> board = KI_TEST::ReadBoardFromFileOrStream( boardPath.string() );
90 BOOST_REQUIRE( board );
91
92 return board;
93}
94
95
96std::unique_ptr<BOARD> loadComplexPadstackBoard()
97{
98 return loadBoard( "padstacks_complex.kicad_pcb" );
99}
100
101
103std::set<std::string> captureAll( const std::string& aText, const std::string& aPattern )
104{
105 std::set<std::string> found;
106 std::regex re( aPattern );
107
108 for( auto it = std::sregex_iterator( aText.begin(), aText.end(), re );
109 it != std::sregex_iterator(); ++it )
110 {
111 found.insert( ( *it )[1].str() );
112 }
113
114 return found;
115}
116} // namespace
117
118
119BOOST_AUTO_TEST_SUITE( ComplexPadstackExport )
120
121
122// GenCAD names one shape per layer in $PADS and references it per layer in $PADSTACKS
123BOOST_AUTO_TEST_CASE( GencadEmitsShapePerLayer )
124{
125 LOCALE_IO toggle;
126 std::unique_ptr<BOARD> board = loadComplexPadstackBoard();
127
128 TEMP_FILE out( wxT( "kicad_gencad_padstack_" ) );
129
130 GENCAD_EXPORTER exporter( board.get() );
131 BOOST_REQUIRE( exporter.WriteFile( out.WxStr() ) );
132
133 const std::string cad = slurp( out.Path() );
134
135 // Each unique padstack layer gets its own suffixed shape name
136 std::set<std::string> shapeLayers = captureAll( cad, R"(\nPAD P[0-9]+_([A-Za-z0-9.]+) )" );
137
138 BOOST_CHECK_MESSAGE( shapeLayers.count( "F.Cu" ), "expected an F.Cu-specific PAD shape" );
139 BOOST_CHECK_MESSAGE( shapeLayers.count( "In1.Cu" ), "expected an In1.Cu-specific PAD shape" );
140 BOOST_CHECK_MESSAGE( shapeLayers.count( "In2.Cu" ), "expected an In2.Cu-specific PAD shape" );
141 BOOST_CHECK_MESSAGE( shapeLayers.count( "B.Cu" ), "expected a B.Cu-specific PAD shape" );
142
143 // RECTANGLE is "x y width height", so the four square sizes give four distinct half-extents
144 std::set<std::string> rectSizes = captureAll( cad, R"(\nRECTANGLE [-0-9.]+ [-0-9.]+ ([0-9.]+))" );
145
146 BOOST_CHECK_MESSAGE( rectSizes.size() >= 4,
147 "flattened padstack: expected at least 4 distinct RECTANGLE sizes, got "
148 << rectSizes.size() );
149
150 // A padstack entry must be able to name a different shape on different layers
151 std::regex padstackBlock( R"(PADSTACK PAD[0-9]+ [^\n]*\n((?:PAD [^\n]*\n)+))" );
152 bool sawVaryingStack = false;
153
154 for( auto it = std::sregex_iterator( cad.begin(), cad.end(), padstackBlock );
155 it != std::sregex_iterator(); ++it )
156 {
157 if( captureAll( ( *it )[1].str(), R"(PAD (P[0-9_A-Za-z.]+) )" ).size() > 1 )
158 sawVaryingStack = true;
159 }
160
161 BOOST_CHECK_MESSAGE( sawVaryingStack,
162 "no PADSTACK entry referenced more than one shape across its layers" );
163
164 // Those references land on the right copper only if the names do. GenCAD numbers inner
165 // layers from the top at INNER1, and KiCad writes no $LAYERS section to remap them
166 std::set<std::string> stackLayers = captureAll( cad, R"(\nPAD P[0-9_A-Za-z.]+ ([A-Za-z0-9-]+) )" );
167
168 BOOST_CHECK_MESSAGE( stackLayers.count( "INNER1" ), "expected an INNER1 reference" );
169 BOOST_CHECK_MESSAGE( stackLayers.count( "INNER2" ), "expected an INNER2 reference" );
170
171 for( const std::string& layer : stackLayers )
172 {
173 BOOST_CHECK_MESSAGE( layer.find( '-' ) == std::string::npos,
174 "padstack references a layer GenCAD cannot name: " << layer );
175 }
176}
177
178
179// A Specctra padstack holds one (shape ...) per layer; its id must stay unique for a given
180// physical stack or two padstacks collide on one definition
181BOOST_AUTO_TEST_CASE( SpecctraEmitsShapePerLayer )
182{
183 LOCALE_IO toggle;
184 std::unique_ptr<BOARD> board = loadComplexPadstackBoard();
185
186 TEMP_FILE out( wxT( "kicad_specctra_padstack_" ) );
187 BOOST_REQUIRE_NO_THROW( DSN::ExportBoardToSpecctraFile( board.get(), out.WxStr() ) );
188
189 const std::string dsn = slurp( out.Path() );
190
191 // A stack whose copper varies is named for every layer it defines
192 BOOST_CHECK_MESSAGE( dsn.find( "Complex[" ) != std::string::npos,
193 "flattened padstack: no layer-by-layer padstack id was emitted" );
194
195 // Rect corners come out per layer, so the four sizes give four distinct extents
196 std::set<std::string> rectCorners = captureAll( dsn, R"(\‍(rect "[^"]+" ([-0-9]+) )" );
197
198 BOOST_CHECK_MESSAGE( rectCorners.size() >= 4,
199 "flattened padstack: expected at least 4 distinct rect extents, got "
200 << rectCorners.size() );
201}
202
203
204// A Hyperlynx PADSTACK lists ("<layer>", <shape>, <sx>, <sy>, ...) per layer. A uniform stack
205// collapses to the single "MDEF" default instead
206BOOST_AUTO_TEST_CASE( HyperlynxEmitsShapePerLayer )
207{
208 LOCALE_IO toggle;
209 std::unique_ptr<BOARD> board = loadComplexPadstackBoard();
210
211 TEMP_FILE out( wxT( "kicad_hyperlynx_padstack_" ) );
212 BOOST_REQUIRE( ExportBoardToHyperlynxFile( board.get(), out.WxStr() ) );
213
214 const std::string hyp = slurp( out.Path() );
215
216 std::set<std::string> padSizes = captureAll( hyp, R"RX(\‍("[^"]+", [0-9]+, ([0-9.]+),)RX" );
217
218 BOOST_CHECK_MESSAGE( padSizes.size() >= 4,
219 "flattened padstack: expected at least 4 distinct pad sizes, got "
220 << padSizes.size() );
221
222 // The four sizes must spread across layers, not repeat within one layer
223 std::set<std::string> layersWithSizes = captureAll( hyp, R"RX(\‍("([^"]+)", [0-9]+, [0-9.]+,)RX" );
224
225 BOOST_CHECK_MESSAGE( layersWithSizes.size() >= 4,
226 "expected per-layer entries rather than a single MDEF default" );
227}
228
229
230// The .HYP spec requires the PADSTACK drill parameter to be absent, not zero, on a hole-less
231// padstack
232BOOST_AUTO_TEST_CASE( HyperlynxOmitsDrillOnSmdPadstacks )
233{
234 LOCALE_IO toggle;
235
236 std::unique_ptr<BOARD> board = loadBoard( "api_kitchen_sink.kicad_pcb" );
237
238 TEMP_FILE out( wxT( "kicad_hyperlynx_smd_" ) );
239 BOOST_REQUIRE( ExportBoardToHyperlynxFile( board.get(), out.WxStr() ) );
240
241 const std::string hyp = slurp( out.Path() );
242
243 BOOST_CHECK_MESSAGE( hyp.find( "{PADSTACK=" ) != std::string::npos,
244 "board produced no padstacks to check" );
245
246 std::set<std::string> drills = captureAll( hyp, R"(\{PADSTACK=[0-9]+, ([0-9.]+))" );
247
248 BOOST_CHECK_MESSAGE( drills.count( "0.000000000" ) == 0,
249 "a hole-less padstack declared a zero drill instead of omitting it" );
250}
251
252
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)
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 > ReadBoardFromFileOrStream(const std::string &aFilename, std::istream &aFallback)
Read a board from a file, or another stream, as appropriate.
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()