KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_specctra_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 <sstream>
26#include <string>
27#include <system_error>
28
29#include <board.h>
30#include <board_commit.h>
31#include <footprint.h>
32#include <layer_ids.h>
33#include <locale_io.h>
34#include <pcb_track.h>
38#include <tool/tool_manager.h>
39#include <reporter.h>
40#include <ranges>
41
42#include <wx/filename.h>
43#include <wx/string.h>
44
45using namespace DSN;
46
47
48namespace
49{
50// Unique per-test scratch file that survives BOOST_REQUIRE early-outs and cleans up on scope exit.
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 std::string Str() const { return m_path.string(); }
67 wxString WxStr() const { return wxString( m_path.string() ); }
68
69private:
70 std::filesystem::path m_path;
71};
72
73
74std::string slurp( const std::filesystem::path& aPath )
75{
76 std::ifstream in( aPath );
77 std::stringstream ss;
78 ss << in.rdbuf();
79 return ss.str();
80}
81
82
83std::unique_ptr<BOARD> loadTestBoard()
84{
85 const std::filesystem::path boardPath =
86 std::filesystem::path( KI_TEST::GetPcbnewTestDataDir() ) / "issue3812.kicad_pcb";
87
88 BOOST_REQUIRE( std::filesystem::exists( boardPath ) );
89
90 return KI_TEST::RequireBoardFromPcbnewQAData( "issue3812.kicad_pcb" );
91}
92
93
94int countTraces( BOARD* aBoard )
95{
96 int n = 0;
97
98 for( PCB_TRACK* track : aBoard->Tracks() )
99 {
100 if( track->Type() == PCB_TRACE_T )
101 ++n;
102 }
103
104 return n;
105}
106
107
108void importSession( BOARD* aBoard, const wxString& aSesPath )
109{
110 TOOL_MANAGER toolMgr;
111 toolMgr.SetEnvironment( aBoard, nullptr, nullptr, nullptr, nullptr );
112 toolMgr.RegisterTool( new KI_TEST::DUMMY_TOOL() );
113
114 BOARD_COMMIT commit( &toolMgr, true, false );
115 ImportSpecctraSession( aBoard, aSesPath, commit );
116 commit.Push( wxT( "Import Specctra Session" ), SKIP_UNDO );
117}
118
119
120void importSession( BOARD* aBoard, const wxString& aSesPath, WX_STRING_REPORTER* aReporter,
121 int* aSkipped )
122{
123 TOOL_MANAGER toolMgr;
124 toolMgr.SetEnvironment( aBoard, nullptr, nullptr, nullptr, nullptr );
125 toolMgr.RegisterTool( new KI_TEST::DUMMY_TOOL() );
126
127 BOARD_COMMIT commit( &toolMgr, true, false );
128 ImportSpecctraSession( aBoard, aSesPath, commit, aReporter, aSkipped );
129 commit.Push( wxT( "Import Specctra Session" ), SKIP_UNDO );
130}
131} // namespace
132
133
134/*
135 * A family of Specctra DSN quoting/naming defects that made otherwise-valid boards un-exportable
136 * or produced structurally invalid interchange files (#24946/7/8).
137 */
138
139
140// #24946: a field payload holding the string delimiter (an inch mark in the Value/PN) must not
141// terminate the quoted string early and desync a conforming reader.
142BOOST_AUTO_TEST_CASE( SpecctraExportQuoteInField )
143{
144 LOCALE_IO toggle;
145 std::unique_ptr<BOARD> board = loadTestBoard();
146
147 BOOST_REQUIRE( !board->Footprints().empty() );
148 board->Footprints().front()->SetValue( wxT( "2x20 pin 0.1\" female header" ) );
149
150 TEMP_FILE out( wxT( "kicad_specctra_24946_" ) );
151 BOOST_REQUIRE_NO_THROW( ExportBoardToSpecctraFile( board.get(), out.Str() ) );
152
153 const std::string dsn = slurp( out.Path() );
154
155 // The delimiter must no longer appear inside the payload, and the transformed form is emitted.
156 BOOST_CHECK( dsn.find( "0.1\" female" ) == std::string::npos );
157 BOOST_CHECK( dsn.find( "0.1'' female" ) != std::string::npos );
158}
159
160
161// #24947: duplicate and empty reference designators (unannotated REF** fiducials, intentional
162// duplicates, case-only differences) must not abort the export; they are uniquified on the fly,
163// case-insensitively to match the exported case_sensitive default.
164BOOST_AUTO_TEST_CASE( SpecctraExportDuplicateEmptyRefs )
165{
166 LOCALE_IO toggle;
167 std::unique_ptr<BOARD> board = loadTestBoard();
168
169 BOOST_REQUIRE_GE( board->Footprints().size(), 5u );
170
171 board->Footprints()[0]->SetReference( wxEmptyString );
172 board->Footprints()[1]->SetReference( wxEmptyString );
173 board->Footprints()[2]->SetReference( wxT( "U1" ) );
174 board->Footprints()[3]->SetReference( wxT( "U1" ) );
175 board->Footprints()[4]->SetReference( wxT( "u1" ) );
176
177 TEMP_FILE out( wxT( "kicad_specctra_24947_" ) );
178 BOOST_REQUIRE_NO_THROW( ExportBoardToSpecctraFile( board.get(), out.Str() ) );
179
180 const std::string dsn = slurp( out.Path() );
181
182 // Empty ids take a REF** base; case-insensitive collisions are all suffixed distinctly.
183 BOOST_CHECK( dsn.find( "(place REF**_1 " ) != std::string::npos );
184 BOOST_CHECK( dsn.find( "(place U1 " ) != std::string::npos );
185 BOOST_CHECK( dsn.find( "(place U1_1 " ) != std::string::npos );
186 BOOST_CHECK( dsn.find( "(place u1_2 " ) != std::string::npos );
187}
188
189
190// Allegro Specctra writes zone fills as (wire (poly ...)) — an abbreviation of (polygon).
191// Parsing must accept that token and skip the pour geometry (zones stay on the board).
192BOOST_AUTO_TEST_CASE( SpecctraImportAllegroPolyWire )
193{
194 LOCALE_IO toggle;
195 std::unique_ptr<BOARD> board = loadTestBoard();
196
197 TEMP_FILE ses( wxT( "kicad_specctra_allegro_poly_" ) );
198
199 const std::string validLayer = std::string( board->GetLayerName( F_Cu ).ToUTF8() );
200
201 std::ofstream out( ses.Path() );
202 out << "(session test.ses\n"
203 << " (base_design test.dsn)\n"
204 << " (routes\n"
205 << " (resolution um 10)\n"
206 << " (library_out )\n"
207 << " (network_out\n"
208 << " (net \"/ALLPST\"\n"
209 << " (wire (path \"" << validLayer << "\" 1600 0 0 1000 0)\n"
210 << " (type route))\n"
211 << " (wire (poly \"" << validLayer << "\" 0 0 0 1000 0 1000 1000 0 1000 0 0)\n"
212 << " (type normal))\n"
213 << " )\n"
214 << " )\n"
215 << " )\n"
216 << ")\n";
217 out.close();
218
219 BOOST_REQUIRE_NO_THROW( importSession( board.get(), ses.Str() ) );
220
221 // Path wire imported; poly wire ignored (zone fill), so exactly one trace.
222 BOOST_CHECK_EQUAL( countTraces( board.get() ), 1 );
223}
224
225
226// #24948: single unresolved session items (an unknown wire layer or a placement referencing a
227// uniquified id the board never had) must be skipped rather than sinking the whole import.
228BOOST_AUTO_TEST_CASE( SpecctraImportSkipsInvalidWireLayer )
229{
230 LOCALE_IO toggle;
231 std::unique_ptr<BOARD> board = loadTestBoard();
232
233 TEMP_FILE ses( wxT( "kicad_specctra_24948_" ) );
234
235 // The valid wire uses a layer name taken from the loaded board so this is a faithful
236 // router-output fragment; the placement id and the second wire's layer are deliberately absent.
237 const std::string validLayer = std::string( board->GetLayerName( F_Cu ).ToUTF8() );
238
239 std::ofstream out( ses.Path() );
240 out << "(session test.ses\n"
241 << " (base_design test.dsn)\n"
242 << " (placement\n"
243 << " (resolution um 10)\n"
244 << " (component test\n"
245 << " (place Nonexistent_Ref 0 0 front 0)\n"
246 << " )\n"
247 << " )\n"
248 << " (routes\n"
249 << " (resolution um 10)\n"
250 << " (library_out )\n"
251 << " (network_out\n"
252 << " (net \"/ALLPST\"\n"
253 << " (wire (path \"" << validLayer << "\" 1600 0 0 1000 0))\n"
254 << " (wire (path Bogus_Missing_Layer 1600 0 0 1000 1000))\n"
255 << " )\n"
256 << " )\n"
257 << " )\n"
258 << ")\n";
259 out.close();
260
261#ifdef __WXMAC__
262 // Don't convert log messages to throws, as they will trip up BOOST_REQUIRE_NO_THROW
263 wxLog::SetTimestamp( wxEmptyString );
264#endif
265
266 BOOST_REQUIRE_NO_THROW( importSession( board.get(), ses.Str() ) );
267
268 // The session replaces all unlocked traces; only the valid wire survives, the bogus wire and
269 // the unresolved placement are dropped rather than throwing.
270 BOOST_CHECK_EQUAL( countTraces( board.get() ), 1 );
271}
272
273
274/*
275 * DSN export must produce a file that the DSN parser can read back.
276 */
277BOOST_AUTO_TEST_CASE( SpecctraDsnExportParseable )
278{
279 LOCALE_IO toggle;
280 std::unique_ptr<BOARD> board = loadTestBoard();
281
282 TEMP_FILE out( wxT( "kicad_specctra_parseable_" ) );
283 BOOST_REQUIRE_NO_THROW( ExportBoardToSpecctraFile( board.get(), out.Str() ) );
284
285 SPECCTRA_DB db;
286 try
287 {
288 db.LoadPCB( out.WxStr() );
289 BOOST_CHECK( db.GetPCB() != nullptr );
290 }
291 catch( const IO_ERROR& ioe )
292 {
293 BOOST_FAIL( ioe.What().ToStdString() );
294 }
295}
296
297
298BOOST_AUTO_TEST_CASE( SpecctraDsnExportContent )
299{
300 LOCALE_IO toggle;
301 std::unique_ptr<BOARD> board = loadTestBoard();
302
303 BOOST_REQUIRE( !board->Footprints().empty() );
304
305 TEMP_FILE out( wxT( "kicad_specctra_content_" ) );
306 BOOST_REQUIRE_NO_THROW( ExportBoardToSpecctraFile( board.get(), out.Str() ) );
307
308 const std::string dsn = slurp( out.Path() );
309
310 // The unit declaration the exporter writes (mm-based resolution; um units).
311 BOOST_CHECK( dsn.find( "(unit um" ) != std::string::npos );
312
313 // At least one (net "<name>" header matches an existing net.
314 bool netHeaderFound = false;
315
316 for( const NETINFO_ITEM* net : board->GetNetInfo().NetsByNetcode() | std::views::values )
317 {
318 if( net->GetNetname().IsEmpty() )
319 continue;
320
321 if( dsn.find( "(net \"" + std::string( net->GetNetname().ToUTF8() ) + "\"" ) != std::string::npos )
322 netHeaderFound = true;
323 }
324
325 BOOST_CHECK( netHeaderFound );
326}
327
328
329BOOST_AUTO_TEST_CASE( SpecctraExportReporterCapturesOutlineWarning )
330{
331 LOCALE_IO toggle;
332
333 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
334
336 TEMP_FILE out( wxT( "kicad_specctra_reporter_" ) );
337 BOOST_REQUIRE_NO_THROW( ExportBoardToSpecctraFile( board.get(), out.WxStr(), &reporter ) );
338
339 BOOST_CHECK( reporter.GetMessages().Contains( wxT( "Board outline is malformed" ) ) );
340}
General utilities for PCB file IO for QA programs.
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:411
const TRACKS & Tracks() const
Definition board.h:463
A DSN data tree, usually coming from a DSN file.
Definition specctra.h:3621
void LoadPCB(const wxString &aFilename)
A recursive descent parser for a SPECCTRA DSN "design" file.
Definition specctra.cpp:246
Hold an error message and may be used when throwing exceptions containing meaningful error messages.
virtual const wxString What() const
A composite of Problem() and Where()
Instantiate the current locale within a scope in which you are expecting exceptions to be thrown.
Definition locale_io.h:37
Handle the data for a net.
Definition netinfo.h:50
Master controller class:
void RegisterTool(TOOL_BASE *aTool)
Add a tool to the manager set and sets it up.
void SetEnvironment(EDA_ITEM *aModel, KIGFX::VIEW *aView, KIGFX::VIEW_CONTROLS *aViewControls, APP_SETTINGS_BASE *aSettings, TOOLS_HOLDER *aFrame)
Set the work environment (model, view, view controls and the parent window).
A wrapper for reporting to a wxString object.
Definition reporter.h:242
@ F_Cu
Definition layer_ids.h:60
This source file implements export and import capabilities to the specctra dsn file format.
Definition specctra.cpp:60
void ExportBoardToSpecctraFile(BOARD *aBoard, const wxString &aFullFilename, REPORTER *aReporter=nullptr)
Helper method to export board to DSN file.
bool ImportSpecctraSession(BOARD *aBoard, const wxString &fullFileName, COMMIT &aCommit, REPORTER *aReporter=nullptr, int *aSkippedCount=nullptr)
Helper method to import SES file to a board.
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.
#define SKIP_UNDO
Definition sch_commit.h:38
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
IbisParser parser & reporter
BOOST_AUTO_TEST_CASE(SpecctraExportQuoteInField)
BOOST_CHECK_EQUAL(result, "25.4")
@ PCB_TRACE_T
class PCB_TRACK, a track segment (segment on a copper layer)
Definition typeinfo.h:88