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>
36
37#include <board.h>
40#include <zone.h>
41
42
43namespace
44{
45
47const wxString F_CU_ZONE = wxS( "00000000-0000-0000-0000-00005c07d704" );
48const wxString B_CU_ZONE = wxS( "00000000-0000-0000-0000-00005c07d701" );
49const wxString IN1_CU_ZONE = wxS( "00000000-0000-0000-0000-00005c07d707" );
50const wxString IN2_CU_ZONE = wxS( "00000000-0000-0000-0000-00005c07d70a" );
51
52
53struct API_HANDLER_PCB_FIXTURE
54{
55 SETTINGS_MANAGER m_settingsManager;
56 std::unique_ptr<BOARD> m_board;
57 std::shared_ptr<HEADLESS_PCB_CONTEXT> m_context;
58
59 // The context takes ownership of the board; the returned raw pointer lets the test inspect
60 // zone state after the handler runs.
61 BOARD* loadBoard( const wxString& aRelPath )
62 {
63 KI_TEST::LoadBoard( m_settingsManager, aRelPath, m_board );
64
65 BOARD* board = m_board.get();
66 m_context = std::make_shared<HEADLESS_PCB_CONTEXT>( std::move( m_board ),
67 &m_settingsManager.Prj(), nullptr );
68 return board;
69 }
70
71 kiapi::common::ApiRequest makeRefillRequest( BOARD* aBoard, const std::vector<wxString>& aZoneIds ) const
72 {
73 kiapi::board::commands::RefillZones command;
74 command.mutable_board()->set_type( kiapi::common::types::DocumentType::DOCTYPE_PCB );
75 command.mutable_board()->set_board_filename(
76 wxFileName( aBoard->GetFileName() ).GetFullName().ToStdString() );
77
78 for( const wxString& id : aZoneIds )
79 command.add_zones()->set_value( id.ToStdString() );
80
81 kiapi::common::ApiRequest request;
82 request.mutable_header()->set_client_name( "kicad.qa" );
83 BOOST_REQUIRE( request.mutable_message()->PackFrom( command ) );
84
85 return request;
86 }
87
88 ZONE* zoneByUuid( BOARD* aBoard, const wxString& aUuid ) const
89 {
90 for( ZONE* zone : aBoard->Zones() )
91 {
92 if( zone->m_Uuid.AsString() == aUuid )
93 return zone;
94 }
95
96 return nullptr;
97 }
98
99 void unfillAll( BOARD* aBoard ) const
100 {
101 // Start from a clean slate so a positive IsFilled() result can only come from this fill
102 for( ZONE* zone : aBoard->Zones() )
103 {
104 zone->UnFill();
105 BOOST_REQUIRE( !zone->IsFilled() );
106 }
107 }
108};
109
110
111kiapi::common::ApiRequest makeBeginCommitRequest()
112{
113 // No header, so the pre-11.0 path assumes the PCB editor
114 kiapi::common::commands::BeginCommit command;
115
116 kiapi::common::ApiRequest request;
117 request.mutable_header()->set_client_name( "kicad.qa" );
118 request.mutable_message()->PackFrom( command );
119
120 return request;
121}
122
123
124kiapi::common::ApiRequest makeRevertRequest( BOARD* aBoard )
125{
126 kiapi::common::commands::RevertDocument command;
127 command.mutable_document()->set_type( kiapi::common::types::DocumentType::DOCTYPE_PCB );
128 command.mutable_document()->set_board_filename( wxFileName( aBoard->GetFileName() ).GetFullName().ToStdString() );
129
130 kiapi::common::ApiRequest request;
131 request.mutable_header()->set_client_name( "kicad.qa" );
132 request.mutable_message()->PackFrom( command );
133
134 return request;
135}
136
137} // namespace
138
139
140BOOST_FIXTURE_TEST_SUITE( ApiHandlerPcb, API_HANDLER_PCB_FIXTURE )
141
142
143BOOST_AUTO_TEST_CASE( RefillZonesSubset )
144{
145 BOARD* board = loadBoard( wxS( "issue5830" ) );
146
147 unfillAll( board );
148
149 API_HANDLER_PCB handler( m_context );
150 kiapi::common::ApiRequest request = makeRefillRequest( board, { F_CU_ZONE, IN1_CU_ZONE } );
151 API_RESULT result = handler.Handle( request );
152
153 if( !result.has_value() )
154 {
155 BOOST_FAIL( "RefillZones returned status " << result.error().status() << ": "
156 << result.error().error_message() );
157 }
158
159 BOOST_CHECK_EQUAL( result->status().status(), kiapi::common::ApiStatusCode::AS_OK );
160
161 ZONE* fCu = zoneByUuid( board, F_CU_ZONE );
162 ZONE* bCu = zoneByUuid( board, B_CU_ZONE );
163 ZONE* in1Cu = zoneByUuid( board, IN1_CU_ZONE );
164 ZONE* in2Cu = zoneByUuid( board, IN2_CU_ZONE );
165
166 BOOST_REQUIRE( fCu && bCu && in1Cu && in2Cu );
167
168 // Exactly the requested zones must be filled; the others must be untouched.
169 BOOST_CHECK( fCu->IsFilled() );
170 BOOST_CHECK( in1Cu->IsFilled() );
171 BOOST_CHECK( !bCu->IsFilled() );
172 BOOST_CHECK( !in2Cu->IsFilled() );
173}
174
175
176BOOST_AUTO_TEST_CASE( RefillZonesSubsetRebuildsConnectivity )
177{
178 BOARD* board = loadBoard( wxS( "issue5830" ) );
179
180 unfillAll( board );
181
182 // Baseline ratsnest with every zone empty; the GND planes are unfilled so their pads still
183 // ratsnest together.
184 board->BuildConnectivity();
185 const unsigned baseline = board->GetConnectivity()->GetUnconnectedCount( false );
186 BOOST_REQUIRE_MESSAGE( baseline > 0, "expected an unconnected baseline with zones empty" );
187
188 API_HANDLER_PCB handler( m_context );
189 kiapi::common::ApiRequest request = makeRefillRequest( board, { F_CU_ZONE, IN1_CU_ZONE } );
190 API_RESULT result = handler.Handle( request );
191
192 if( !result.has_value() )
193 {
194 BOOST_FAIL( "RefillZones returned status " << result.error().status() << ": "
195 << result.error().error_message() );
196 }
197
198 const unsigned afterFill = board->GetConnectivity()->GetUnconnectedCount( false );
199
200 // Filling the GND planes bridges GND pads that previously ratsnested, so the unconnected
201 // count drops. Push cleared the ratsnest, so if the handler skipped the connectivity
202 // rebuild this would read zero instead of the reduced-but-nonzero count.
203 BOOST_CHECK_MESSAGE( afterFill > 0, "connectivity was cleared, not rebuilt, after the fill" );
204 BOOST_CHECK_MESSAGE( afterFill < baseline,
205 "filling the GND planes should reduce the unconnected count ("
206 << afterFill << " vs baseline " << baseline << ")" );
207}
208
209
210BOOST_AUTO_TEST_CASE( RefillZonesAllHeadless )
211{
212 BOARD* board = loadBoard( wxS( "issue5830" ) );
213
214 unfillAll( board );
215
216 API_HANDLER_PCB handler( m_context );
217 kiapi::common::ApiRequest request = makeRefillRequest( board, {} );
218 API_RESULT result = handler.Handle( request );
219
220 if( !result.has_value() )
221 {
222 BOOST_FAIL( "RefillZones returned status " << result.error().status() << ": "
223 << result.error().error_message() );
224 }
225
226 BOOST_CHECK_EQUAL( result->status().status(), kiapi::common::ApiStatusCode::AS_OK );
227
228 // With no frame the empty-zones request must fill everything synchronously
229 for( ZONE* zone : board->Zones() )
230 BOOST_CHECK_MESSAGE( zone->IsFilled(), "zone " << zone->m_Uuid.AsStdString() << " not filled" );
231}
232
233
234BOOST_AUTO_TEST_CASE( RefillZonesUnknownIdRejected )
235{
236 BOARD* board = loadBoard( wxS( "issue5830" ) );
237
238 API_HANDLER_PCB handler( m_context );
239 kiapi::common::ApiRequest request =
240 makeRefillRequest( board, { wxS( "deadbeef-0000-0000-0000-000000000000" ) } );
241 API_RESULT result = handler.Handle( request );
242
243 BOOST_REQUIRE( !result.has_value() );
244 BOOST_CHECK_EQUAL( result.error().status(), kiapi::common::ApiStatusCode::AS_BAD_REQUEST );
245}
246
247
248// RevertDocument reloads the board, freeing every item an in-flight client commit points at.
249// The handler must refuse with AS_BUSY while a commit is open, or the pointers dangle
250// (use-after-free). Latent UAF found via #24803.
251BOOST_AUTO_TEST_CASE( RevertDocumentRejectedWithOpenCommit )
252{
253 BOARD* board = loadBoard( wxS( "issue5830" ) );
254
255 API_HANDLER_PCB handler( m_context );
256
257 // Open a client transaction, as a client staging edits would
258 kiapi::common::ApiRequest beginRequest = makeBeginCommitRequest();
259 BOOST_REQUIRE( handler.Handle( beginRequest ).has_value() );
260
261 kiapi::common::ApiRequest request = makeRevertRequest( board );
262 API_RESULT result = handler.Handle( request );
263
264 BOOST_REQUIRE( !result.has_value() );
265 BOOST_CHECK_EQUAL( result.error().status(), kiapi::common::ApiStatusCode::AS_BUSY );
266 BOOST_CHECK( result.error().error_message().find( "commit" ) != std::string::npos );
267}
268
269
270// With no open commit the guard passes; the reload then needs a running editor, so a headless
271// handler reports AS_UNIMPLEMENTED. This confirms the guard does not reject the normal path.
272BOOST_AUTO_TEST_CASE( RevertDocumentWithoutCommitPassesGuard )
273{
274 BOARD* board = loadBoard( wxS( "issue5830" ) );
275
276 API_HANDLER_PCB handler( m_context );
277 kiapi::common::ApiRequest request = makeRevertRequest( board );
278 API_RESULT result = handler.Handle( request );
279
280 BOOST_REQUIRE( !result.has_value() );
281 BOOST_CHECK_EQUAL( result.error().status(), kiapi::common::ApiStatusCode::AS_BAD_REQUEST );
282}
283
284
tl::expected< ApiResponse, ApiResponseStatus > API_RESULT
Definition api_handler.h:42
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:409
const ZONES & Zones() const
Definition board.h:467
const wxString & GetFileName() const
Definition board.h:452
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(RefillZonesSubset)
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")