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
40#include <wx/filename.h>
41#include <wx/string.h>
42
43using namespace DSN;
44
45
46namespace
47{
48// Unique per-test scratch file that survives BOOST_REQUIRE early-outs and cleans up on scope exit.
49class TEMP_FILE
50{
51public:
52 explicit TEMP_FILE( const wxString& aPrefix ) :
53 m_path( std::string( wxFileName::CreateTempFileName( aPrefix ).ToUTF8() ) )
54 {
55 }
56
57 ~TEMP_FILE()
58 {
59 std::error_code ec;
60 std::filesystem::remove( m_path, ec );
61 }
62
63 const std::filesystem::path& Path() const { return m_path; }
64 std::string Str() const { return m_path.string(); }
65
66private:
67 std::filesystem::path m_path;
68};
69
70
71std::string slurp( const std::filesystem::path& aPath )
72{
73 std::ifstream in( aPath );
74 std::stringstream ss;
75 ss << in.rdbuf();
76 return ss.str();
77}
78
79
80std::unique_ptr<BOARD> loadTestBoard()
81{
82 const std::filesystem::path boardPath =
83 std::filesystem::path( KI_TEST::GetPcbnewTestDataDir() ) / "issue3812.kicad_pcb";
84
85 BOOST_REQUIRE( std::filesystem::exists( boardPath ) );
86
87 std::unique_ptr<BOARD> board = KI_TEST::ReadBoardFromFileOrStream( boardPath.string() );
88 BOOST_REQUIRE( board );
89
90 return board;
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} // namespace
119
120
121/*
122 * A family of Specctra DSN quoting/naming defects that made otherwise-valid boards un-exportable
123 * or produced structurally invalid interchange files (#24946/7/8).
124 */
125
126
127// #24946: a field payload holding the string delimiter (an inch mark in the Value/PN) must not
128// terminate the quoted string early and desync a conforming reader.
129BOOST_AUTO_TEST_CASE( SpecctraExportQuoteInField )
130{
131 LOCALE_IO toggle;
132 std::unique_ptr<BOARD> board = loadTestBoard();
133
134 BOOST_REQUIRE( !board->Footprints().empty() );
135 board->Footprints().front()->SetValue( wxT( "2x20 pin 0.1\" female header" ) );
136
137 TEMP_FILE out( wxT( "kicad_specctra_24946_" ) );
138 BOOST_REQUIRE_NO_THROW( ExportBoardToSpecctraFile( board.get(), out.Str() ) );
139
140 const std::string dsn = slurp( out.Path() );
141
142 // The delimiter must no longer appear inside the payload, and the transformed form is emitted.
143 BOOST_CHECK( dsn.find( "0.1\" female" ) == std::string::npos );
144 BOOST_CHECK( dsn.find( "0.1'' female" ) != std::string::npos );
145}
146
147
148// #24947: duplicate and empty reference designators (unannotated REF** fiducials, intentional
149// duplicates, case-only differences) must not abort the export; they are uniquified on the fly,
150// case-insensitively to match the exported case_sensitive default.
151BOOST_AUTO_TEST_CASE( SpecctraExportDuplicateEmptyRefs )
152{
153 LOCALE_IO toggle;
154 std::unique_ptr<BOARD> board = loadTestBoard();
155
156 BOOST_REQUIRE_GE( board->Footprints().size(), 5u );
157
158 board->Footprints()[0]->SetReference( wxEmptyString );
159 board->Footprints()[1]->SetReference( wxEmptyString );
160 board->Footprints()[2]->SetReference( wxT( "U1" ) );
161 board->Footprints()[3]->SetReference( wxT( "U1" ) );
162 board->Footprints()[4]->SetReference( wxT( "u1" ) );
163
164 TEMP_FILE out( wxT( "kicad_specctra_24947_" ) );
165 BOOST_REQUIRE_NO_THROW( ExportBoardToSpecctraFile( board.get(), out.Str() ) );
166
167 const std::string dsn = slurp( out.Path() );
168
169 // Empty ids take a REF** base; case-insensitive collisions are all suffixed distinctly.
170 BOOST_CHECK( dsn.find( "(place REF**_1 " ) != std::string::npos );
171 BOOST_CHECK( dsn.find( "(place U1 " ) != std::string::npos );
172 BOOST_CHECK( dsn.find( "(place U1_1 " ) != std::string::npos );
173 BOOST_CHECK( dsn.find( "(place u1_2 " ) != std::string::npos );
174}
175
176
177// Allegro Specctra writes zone fills as (wire (poly ...)) — an abbreviation of (polygon).
178// Parsing must accept that token and skip the pour geometry (zones stay on the board).
179BOOST_AUTO_TEST_CASE( SpecctraImportAllegroPolyWire )
180{
181 LOCALE_IO toggle;
182 std::unique_ptr<BOARD> board = loadTestBoard();
183
184 TEMP_FILE ses( wxT( "kicad_specctra_allegro_poly_" ) );
185
186 const std::string validLayer = std::string( board->GetLayerName( F_Cu ).ToUTF8() );
187
188 std::ofstream out( ses.Path() );
189 out << "(session test.ses\n"
190 << " (base_design test.dsn)\n"
191 << " (routes\n"
192 << " (resolution um 10)\n"
193 << " (library_out )\n"
194 << " (network_out\n"
195 << " (net \"/ALLPST\"\n"
196 << " (wire (path \"" << validLayer << "\" 1600 0 0 1000 0)\n"
197 << " (type route))\n"
198 << " (wire (poly \"" << validLayer << "\" 0 0 0 1000 0 1000 1000 0 1000 0 0)\n"
199 << " (type normal))\n"
200 << " )\n"
201 << " )\n"
202 << " )\n"
203 << ")\n";
204 out.close();
205
206 BOOST_REQUIRE_NO_THROW( importSession( board.get(), ses.Str() ) );
207
208 // Path wire imported; poly wire ignored (zone fill), so exactly one trace.
209 BOOST_CHECK_EQUAL( countTraces( board.get() ), 1 );
210}
211
212
213// #24948: single unresolved session items (an unknown wire layer or a placement referencing a
214// uniquified id the board never had) must be skipped rather than sinking the whole import.
215BOOST_AUTO_TEST_CASE( SpecctraImportSkipsInvalidWireLayer )
216{
217 LOCALE_IO toggle;
218 std::unique_ptr<BOARD> board = loadTestBoard();
219
220 TEMP_FILE ses( wxT( "kicad_specctra_24948_" ) );
221
222 // The valid wire uses a layer name taken from the loaded board so this is a faithful
223 // router-output fragment; the placement id and the second wire's layer are deliberately absent.
224 const std::string validLayer = std::string( board->GetLayerName( F_Cu ).ToUTF8() );
225
226 std::ofstream out( ses.Path() );
227 out << "(session test.ses\n"
228 << " (base_design test.dsn)\n"
229 << " (placement\n"
230 << " (resolution um 10)\n"
231 << " (component test\n"
232 << " (place Nonexistent_Ref 0 0 front 0)\n"
233 << " )\n"
234 << " )\n"
235 << " (routes\n"
236 << " (resolution um 10)\n"
237 << " (library_out )\n"
238 << " (network_out\n"
239 << " (net \"/ALLPST\"\n"
240 << " (wire (path \"" << validLayer << "\" 1600 0 0 1000 0))\n"
241 << " (wire (path Bogus_Missing_Layer 1600 0 0 1000 1000))\n"
242 << " )\n"
243 << " )\n"
244 << " )\n"
245 << ")\n";
246 out.close();
247
248 BOOST_REQUIRE_NO_THROW( importSession( board.get(), ses.Str() ) );
249
250 // The session replaces all unlocked traces; only the valid wire survives, the bogus wire and
251 // the unresolved placement are dropped rather than throwing.
252 BOOST_CHECK_EQUAL( countTraces( board.get() ), 1 );
253}
General utilities for PCB file IO for QA programs.
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:373
const TRACKS & Tracks() const
Definition board.h:419
Instantiate the current locale within a scope in which you are expecting exceptions to be thrown.
Definition locale_io.h:37
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).
@ 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)
Helper method to export board to DSN file.
bool ImportSpecctraSession(BOARD *aBoard, const wxString &fullFileName, COMMIT &aCommit)
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 > ReadBoardFromFileOrStream(const std::string &aFilename, std::istream &aFallback)
Read a board from a file, or another stream, as appropriate.
#define SKIP_UNDO
Definition sch_commit.h:36
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
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:89