KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_api_handler_pcb.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 modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation, either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * 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 <memory>
21#include <vector>
22
23#include <boost/test/unit_test.hpp>
24
25#include <wx/filename.h>
26
29
30#include <api/api_handler_pcb.h>
32#include <api/board/board_commands.pb.h>
33#include <api/common/commands/editor_commands.pb.h>
34#include <api/common/envelope.pb.h>
35#include <api/common/types/base_types.pb.h>
37
38#include <board.h>
41#include <zone.h>
42
43
44namespace
45{
46
48const wxString F_CU_ZONE = wxS( "00000000-0000-0000-0000-00005c07d704" );
49const wxString B_CU_ZONE = wxS( "00000000-0000-0000-0000-00005c07d701" );
50const wxString IN1_CU_ZONE = wxS( "00000000-0000-0000-0000-00005c07d707" );
51const wxString IN2_CU_ZONE = wxS( "00000000-0000-0000-0000-00005c07d70a" );
52
53
54struct API_HANDLER_PCB_FIXTURE
55{
56 SETTINGS_MANAGER m_settingsManager;
57 std::unique_ptr<BOARD> m_board;
58 std::shared_ptr<HEADLESS_PCB_CONTEXT> m_context;
59
60 // The context takes ownership of the board; the returned raw pointer lets the test inspect
61 // zone state after the handler runs.
62 BOARD* loadBoard( const wxString& aRelPath )
63 {
64 KI_TEST::LoadBoard( m_settingsManager, aRelPath, m_board );
65
66 BOARD* board = m_board.get();
67 m_context = std::make_shared<HEADLESS_PCB_CONTEXT>( std::move( m_board ),
68 &m_settingsManager.Prj(), nullptr );
69 return board;
70 }
71
72 kiapi::common::ApiRequest makeRefillRequest( BOARD* aBoard, const std::vector<wxString>& aZoneIds ) const
73 {
74 kiapi::board::commands::RefillZones command;
75 command.mutable_board()->set_type( kiapi::common::types::DocumentType::DOCTYPE_PCB );
76 command.mutable_board()->set_board_filename(
77 wxFileName( aBoard->GetFileName() ).GetFullName().ToStdString() );
78
79 for( const wxString& id : aZoneIds )
80 command.add_zones()->set_value( id.ToStdString() );
81
82 kiapi::common::ApiRequest request;
83 request.mutable_header()->set_client_name( "kicad.qa" );
84 BOOST_REQUIRE( request.mutable_message()->PackFrom( command ) );
85
86 return request;
87 }
88
89 ZONE* zoneByUuid( BOARD* aBoard, const wxString& aUuid ) const
90 {
91 for( ZONE* zone : aBoard->Zones() )
92 {
93 if( zone->m_Uuid.AsString() == aUuid )
94 return zone;
95 }
96
97 return nullptr;
98 }
99
100 void unfillAll( BOARD* aBoard ) const
101 {
102 // Start from a clean slate so a positive IsFilled() result can only come from this fill
103 for( ZONE* zone : aBoard->Zones() )
104 {
105 zone->UnFill();
106 BOOST_REQUIRE( !zone->IsFilled() );
107 }
108 }
109};
110
111
112kiapi::common::ApiRequest makeBeginCommitRequest()
113{
114 // No header, so the pre-11.0 path assumes the PCB editor
115 kiapi::common::commands::BeginCommit command;
116
117 kiapi::common::ApiRequest request;
118 request.mutable_header()->set_client_name( "kicad.qa" );
119 request.mutable_message()->PackFrom( command );
120
121 return request;
122}
123
124
125kiapi::common::ApiRequest makeRevertRequest( BOARD* aBoard )
126{
127 kiapi::common::commands::RevertDocument command;
128 command.mutable_document()->set_type( kiapi::common::types::DocumentType::DOCTYPE_PCB );
129 command.mutable_document()->set_board_filename( wxFileName( aBoard->GetFileName() ).GetFullName().ToStdString() );
130
131 kiapi::common::ApiRequest request;
132 request.mutable_header()->set_client_name( "kicad.qa" );
133 request.mutable_message()->PackFrom( command );
134
135 return request;
136}
137
138} // namespace
139
140
141BOOST_FIXTURE_TEST_SUITE( ApiHandlerPcb, API_HANDLER_PCB_FIXTURE )
142
143
144BOOST_AUTO_TEST_CASE( OdbJobRequestMapsVariants )
145{
147 kiapi::board::jobs::RunBoardJobExportODB request;
148 request.add_variant_names( "var1" );
149 request.add_variant_names( "var2" );
150 request.set_variant( "legacy" );
151 request.set_variant_packaging( kiapi::board::jobs::ODBVP_COMBINED );
152 request.set_manufacturer_part_number_column( "MPN" );
154 ApplyOdbJobRequest( request, job );
155 BOOST_REQUIRE_EQUAL( job.m_variantNames.size(), 2u );
156 BOOST_CHECK_EQUAL( job.m_variantNames[0], wxS( "var1" ) );
157 BOOST_CHECK_EQUAL( job.m_variantNames[1], wxS( "var2" ) );
158 BOOST_CHECK( job.m_variantPackaging == PACKAGING::COMBINED );
159 BOOST_CHECK_EQUAL( job.m_colMfgPn, wxString( wxS( "MPN" ) ) );
160
161 kiapi::board::jobs::RunBoardJobExportODB legacyRequest;
162 legacyRequest.set_variant( "legacy" );
163 JOB_EXPORT_PCB_ODB legacyJob;
164 ApplyOdbJobRequest( legacyRequest, legacyJob );
165 BOOST_REQUIRE_EQUAL( legacyJob.m_variantNames.size(), 1u );
166 BOOST_CHECK_EQUAL( legacyJob.m_variantNames[0], wxS( "legacy" ) );
167 BOOST_CHECK( legacyJob.m_variantPackaging == PACKAGING::SEPARATE );
168}
169
170
171BOOST_AUTO_TEST_CASE( OdbJobRequestMapsExportOptions )
172{
173 kiapi::board::jobs::RunBoardJobExportODB request;
174 request.set_origin( kiapi::board::jobs::ODBO_GRID );
175 request.set_product_name( "board product" );
176 request.mutable_content()->set_data_set( kiapi::board::jobs::FDS_ASSEMBLY );
177 request.mutable_content()->set_sections( "KP" );
178 request.mutable_content()->set_net_names( kiapi::board::jobs::FNNP_ANONYMIZE );
179
181 ApplyOdbJobRequest( request, job );
182 BOOST_CHECK( job.m_origin == JOB_EXPORT_PCB_ODB::ORIGIN::GRID );
183 BOOST_CHECK_EQUAL( job.m_productName, wxString( wxS( "board product" ) ) );
186 BOOST_CHECK_EQUAL( *job.m_sections, wxString( wxS( "KP" ) ) );
188
189 kiapi::board::jobs::RunBoardJobExportODB legacy;
190 JOB_EXPORT_PCB_ODB defaultJob;
191 ApplyOdbJobRequest( legacy, defaultJob );
192 BOOST_CHECK( defaultJob.m_origin == JOB_EXPORT_PCB_ODB::ORIGIN::ABSOLUTE_COORDS );
193 BOOST_CHECK( defaultJob.m_dataSet == JOB_EXPORT_PCB_FAB::DATA_SET::USERDEF );
194 BOOST_CHECK( defaultJob.m_netNames == JOB_EXPORT_PCB_FAB::NET_NAMES::INCLUDE );
195}
196
197
198BOOST_AUTO_TEST_CASE( RefillZonesSubset )
199{
200 BOARD* board = loadBoard( wxS( "issue5830" ) );
201
202 unfillAll( board );
203
204 API_HANDLER_PCB handler( m_context );
205 kiapi::common::ApiRequest request = makeRefillRequest( board, { F_CU_ZONE, IN1_CU_ZONE } );
206 API_RESULT result = handler.Handle( request );
207
208 if( !result.has_value() )
209 {
210 BOOST_FAIL( "RefillZones returned status " << result.error().status() << ": "
211 << result.error().error_message() );
212 }
213
214 BOOST_CHECK_EQUAL( result->status().status(), kiapi::common::ApiStatusCode::AS_OK );
215
216 ZONE* fCu = zoneByUuid( board, F_CU_ZONE );
217 ZONE* bCu = zoneByUuid( board, B_CU_ZONE );
218 ZONE* in1Cu = zoneByUuid( board, IN1_CU_ZONE );
219 ZONE* in2Cu = zoneByUuid( board, IN2_CU_ZONE );
220
221 BOOST_REQUIRE( fCu && bCu && in1Cu && in2Cu );
222
223 // Exactly the requested zones must be filled; the others must be untouched.
224 BOOST_CHECK( fCu->IsFilled() );
225 BOOST_CHECK( in1Cu->IsFilled() );
226 BOOST_CHECK( !bCu->IsFilled() );
227 BOOST_CHECK( !in2Cu->IsFilled() );
228}
229
230
231BOOST_AUTO_TEST_CASE( RefillZonesSubsetRebuildsConnectivity )
232{
233 BOARD* board = loadBoard( wxS( "issue5830" ) );
234
235 unfillAll( board );
236
237 // Baseline ratsnest with every zone empty; the GND planes are unfilled so their pads still
238 // ratsnest together.
239 board->BuildConnectivity();
240 const unsigned baseline = board->GetConnectivity()->GetUnconnectedCount( false );
241 BOOST_REQUIRE_MESSAGE( baseline > 0, "expected an unconnected baseline with zones empty" );
242
243 API_HANDLER_PCB handler( m_context );
244 kiapi::common::ApiRequest request = makeRefillRequest( board, { F_CU_ZONE, IN1_CU_ZONE } );
245 API_RESULT result = handler.Handle( request );
246
247 if( !result.has_value() )
248 {
249 BOOST_FAIL( "RefillZones returned status " << result.error().status() << ": "
250 << result.error().error_message() );
251 }
252
253 const unsigned afterFill = board->GetConnectivity()->GetUnconnectedCount( false );
254
255 // Filling the GND planes bridges GND pads that previously ratsnested, so the unconnected
256 // count drops. Push cleared the ratsnest, so if the handler skipped the connectivity
257 // rebuild this would read zero instead of the reduced-but-nonzero count.
258 BOOST_CHECK_MESSAGE( afterFill > 0, "connectivity was cleared, not rebuilt, after the fill" );
259 BOOST_CHECK_MESSAGE( afterFill < baseline,
260 "filling the GND planes should reduce the unconnected count ("
261 << afterFill << " vs baseline " << baseline << ")" );
262}
263
264
265BOOST_AUTO_TEST_CASE( RefillZonesAllHeadless )
266{
267 BOARD* board = loadBoard( wxS( "issue5830" ) );
268
269 unfillAll( board );
270
271 API_HANDLER_PCB handler( m_context );
272 kiapi::common::ApiRequest request = makeRefillRequest( board, {} );
273 API_RESULT result = handler.Handle( request );
274
275 if( !result.has_value() )
276 {
277 BOOST_FAIL( "RefillZones returned status " << result.error().status() << ": "
278 << result.error().error_message() );
279 }
280
281 BOOST_CHECK_EQUAL( result->status().status(), kiapi::common::ApiStatusCode::AS_OK );
282
283 // With no frame the empty-zones request must fill everything synchronously
284 for( ZONE* zone : board->Zones() )
285 BOOST_CHECK_MESSAGE( zone->IsFilled(), "zone " << zone->m_Uuid.AsStdString() << " not filled" );
286}
287
288
289BOOST_AUTO_TEST_CASE( RefillZonesUnknownIdRejected )
290{
291 BOARD* board = loadBoard( wxS( "issue5830" ) );
292
293 API_HANDLER_PCB handler( m_context );
294 kiapi::common::ApiRequest request =
295 makeRefillRequest( board, { wxS( "deadbeef-0000-0000-0000-000000000000" ) } );
296 API_RESULT result = handler.Handle( request );
297
298 BOOST_REQUIRE( !result.has_value() );
299 BOOST_CHECK_EQUAL( result.error().status(), kiapi::common::ApiStatusCode::AS_BAD_REQUEST );
300}
301
302
303// RevertDocument reloads the board, freeing every item an in-flight client commit points at.
304// The handler must refuse with AS_BUSY while a commit is open, or the pointers dangle
305// (use-after-free). Latent UAF found via #24803.
306BOOST_AUTO_TEST_CASE( RevertDocumentRejectedWithOpenCommit )
307{
308 BOARD* board = loadBoard( wxS( "issue5830" ) );
309
310 API_HANDLER_PCB handler( m_context );
311
312 // Open a client transaction, as a client staging edits would
313 kiapi::common::ApiRequest beginRequest = makeBeginCommitRequest();
314 BOOST_REQUIRE( handler.Handle( beginRequest ).has_value() );
315
316 kiapi::common::ApiRequest request = makeRevertRequest( board );
317 API_RESULT result = handler.Handle( request );
318
319 BOOST_REQUIRE( !result.has_value() );
320 BOOST_CHECK_EQUAL( result.error().status(), kiapi::common::ApiStatusCode::AS_BUSY );
321 BOOST_CHECK( result.error().error_message().find( "commit" ) != std::string::npos );
322}
323
324
325// With no open commit the guard passes; the reload then needs a running editor, so a headless
326// handler reports AS_UNIMPLEMENTED. This confirms the guard does not reject the normal path.
327BOOST_AUTO_TEST_CASE( RevertDocumentWithoutCommitPassesGuard )
328{
329 BOARD* board = loadBoard( wxS( "issue5830" ) );
330
331 API_HANDLER_PCB handler( m_context );
332 kiapi::common::ApiRequest request = makeRevertRequest( board );
333 API_RESULT result = handler.Handle( request );
334
335 BOOST_REQUIRE( !result.has_value() );
336 BOOST_CHECK_EQUAL( result.error().status(), kiapi::common::ApiStatusCode::AS_BAD_REQUEST );
337}
338
339
tl::expected< ApiResponse, ApiResponseStatus > API_RESULT
Definition api_handler.h:44
void ApplyOdbJobRequest(const RunBoardJobExportODB &aRequest, JOB_EXPORT_PCB_ODB &aJob)
API_RESULT Handle(ApiRequest &aMsg)
Attempt to handle the given API request, if a handler exists in this class for the message.
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:410
const ZONES & Zones() const
Definition board.h:468
const wxString & GetFileName() const
Definition board.h:453
std::optional< wxString > m_sections
Section key that replaces the optional sections of the data set Unset lets the data set select them a...
std::vector< wxString > m_variantNames
VARIANT_PACKAGING m_variantPackaging
Handle a list of polygons defining a copper zone.
Definition zone.h:70
bool IsFilled() const
Definition zone.h:306
static std::string ToStdString(const wxString &aStr)
void LoadBoard(SETTINGS_MANAGER &aSettingsManager, const wxString &aRelPath, std::unique_ptr< BOARD > &aBoard)
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_CASE(OdbJobRequestMapsVariants)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")