KiCad PCB EDA Suite
Loading...
Searching...
No Matches
api_e2e_utils.h
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 along
17 * with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20#ifndef QA_API_E2E_UTILS_H
21#define QA_API_E2E_UTILS_H
22
23#include <chrono>
24#include <functional>
25#include <string>
26#include <vector>
27
28#include <google/protobuf/any.pb.h>
29#include <magic_enum.hpp>
30#include <wx/filename.h>
31#include <wx/process.h>
32#include <wx/stream.h>
33#include <wx/utils.h>
34
35#ifdef __UNIX__
36#include <signal.h>
37#include <sys/wait.h>
38#include <unistd.h>
39#endif
40
41#include <nng/nng.h>
42#include <nng/protocol/reqrep0/req.h>
43
44#include <import_export.h>
45#include <api/api_utils.h>
46#include <api/common/envelope.pb.h>
47#include <api/common/commands/base_commands.pb.h>
48#include <api/common/commands/editor_commands.pb.h>
49#include <api/common/commands/project_commands.pb.h>
50#include <api/common/types/base_types.pb.h>
51#include <api/common/types/enums.pb.h>
52#include <api/common/types/jobs.pb.h>
53#include <footprint.h>
56#include <wx/log.h>
57
58
59#ifndef QA_KICAD_CLI_PATH
60#define QA_KICAD_CLI_PATH "kicad-cli"
61#endif
62
63
64class API_SERVER_PROCESS : public wxProcess
65{
66public:
67 API_SERVER_PROCESS( std::function<void( int, const wxString&, const wxString& )> aCallback ) :
68 wxProcess(),
69 m_callback( std::move( aCallback ) )
70 {
71 }
72
73 void OnTerminate( int aPid, int aStatus ) override
74 {
75 wxString output;
76 wxString error;
77
78 drainStream( GetInputStream(), output );
79 drainStream( GetErrorStream(), error );
80
81 if( m_callback )
82 m_callback( aStatus, output, error );
83 }
84
85 static void drainStream( wxInputStream* aStream, wxString& aDest )
86 {
87 if( !aStream || !aStream->CanRead() )
88 return;
89
90 constexpr size_t chunkSize = 1024;
91 char buffer[chunkSize + 1] = { 0 };
92
93 while( aStream->CanRead() )
94 {
95 aStream->Read( buffer, chunkSize );
96 size_t bytesRead = aStream->LastRead();
97
98 if( bytesRead == 0 )
99 break;
100
101 buffer[bytesRead] = '\0';
102 aDest += wxString::FromUTF8( buffer );
103 }
104 }
105
106private:
107 std::function<void( int, const wxString&, const wxString& )> m_callback;
108};
109
110
112{
113public:
115 {
116 int ret = nng_req0_open( &m_socket );
117
118 if( ret == 0 )
119 {
120 m_isOpen = true;
121 nng_socket_set_ms( m_socket, NNG_OPT_RECVTIMEO, 10000 );
122 nng_socket_set_ms( m_socket, NNG_OPT_SENDTIMEO, 10000 );
123 }
124 else
125 {
127 wxString::Format( wxS( "nng_req0_open failed: %s" ), wxString::FromUTF8( nng_strerror( ret ) ) );
128 }
129 }
130
132 {
133 if( m_isOpen )
134 nng_close( m_socket );
135 }
136
137 bool Connect( const wxString& aSocketUrl )
138 {
139 if( !m_isOpen )
140 return false;
141
142 if( m_isConnected )
143 return true;
144
145 int ret = nng_dial( m_socket, aSocketUrl.ToStdString().c_str(), nullptr, 0 );
146
147 if( ret != 0 )
148 {
149 m_lastError = wxString::Format( wxS( "nng_dial failed: %s" ), wxString::FromUTF8( nng_strerror( ret ) ) );
150 return false;
151 }
152
153 m_isConnected = true;
154 return true;
155 }
156
158 {
159 if( m_isOpen )
160 {
161 nng_close( m_socket );
162 m_isOpen = false;
163 m_isConnected = false;
164
165 int ret = nng_req0_open( &m_socket );
166
167 if( ret == 0 )
168 {
169 m_isOpen = true;
170 nng_socket_set_ms( m_socket, NNG_OPT_RECVTIMEO, 10000 );
171 nng_socket_set_ms( m_socket, NNG_OPT_SENDTIMEO, 10000 );
172 }
173 }
174 }
175
176 const wxString& LastError() const { return m_lastError; }
177
178 bool Ping( kiapi::common::ApiStatusCode* aStatusOut = nullptr )
179 {
180 kiapi::common::commands::Ping ping;
181 kiapi::common::ApiResponse response;
182
183 if( !sendCommand( ping, &response ) )
184 return false;
185
186 if( aStatusOut )
187 *aStatusOut = response.status().status();
188
189 return true;
190 }
191
193 {
194 kiapi::common::commands::GetVersion request;
195 kiapi::common::ApiResponse response;
196
197 if( !sendCommand( request, &response ) )
198 return false;
199
200 if( response.status().status() != kiapi::common::AS_OK )
201 {
202 m_lastError = response.status().error_message();
203 return false;
204 }
205
206 kiapi::common::commands::GetVersionResponse version;
207
208 if( !response.message().UnpackTo( &version ) )
209 {
210 m_lastError = wxS( "Failed to unpack GetVersionResponse" );
211 return false;
212 }
213
214 return true;
215 }
216
217 bool OpenDocument( const wxString& aPath, kiapi::common::types::DocumentType aType,
218 kiapi::common::types::DocumentSpecifier* aDocument = nullptr )
219 {
220 kiapi::common::commands::OpenDocument request;
221 request.set_type( aType );
222 request.set_path( aPath.ToStdString() );
223
224 kiapi::common::ApiResponse response;
225
226 if( !sendCommand( request, &response ) )
227 return false;
228
229 if( response.status().status() != kiapi::common::AS_OK )
230 {
231 m_lastError = response.status().error_message();
232 return false;
233 }
234
235 kiapi::common::commands::OpenDocumentResponse openResponse;
236
237 if( !response.message().UnpackTo( &openResponse ) )
238 {
239 m_lastError = wxS( "Failed to unpack OpenDocumentResponse" );
240 return false;
241 }
242
243 if( aDocument )
244 *aDocument = openResponse.document();
245
246 return true;
247 }
248
249 // Convenience overload for PCB documents (backward compatible)
250 bool OpenDocument( const wxString& aPath, kiapi::common::types::DocumentSpecifier* aDocument = nullptr )
251 {
252 return OpenDocument( aPath, kiapi::common::types::DOCTYPE_PCB, aDocument );
253 }
254
255 bool GetItemsCount( const kiapi::common::types::DocumentSpecifier& aDocument,
256 kiapi::common::types::KiCadObjectType aType, int* aCount )
257 {
258 kiapi::common::commands::GetItems request;
259 *request.mutable_header()->mutable_document() = aDocument;
260 request.add_types( aType );
261
262 kiapi::common::ApiResponse response;
263
264 if( !sendCommand( request, &response ) )
265 return false;
266
267 if( response.status().status() != kiapi::common::AS_OK )
268 {
269 m_lastError = response.status().error_message();
270 return false;
271 }
272
273 kiapi::common::commands::GetItemsResponse itemsResponse;
274
275 if( !response.message().UnpackTo( &itemsResponse ) )
276 {
277 m_lastError = wxS( "Failed to unpack GetItemsResponse" );
278 return false;
279 }
280
281 if( itemsResponse.status() != kiapi::common::types::IRS_OK )
282 {
283 m_lastError = wxString::Format( wxS( "GetItems returned non-OK status: %d" ),
284 static_cast<int>( itemsResponse.status() ) );
285 return false;
286 }
287
288 *aCount = itemsResponse.items_size();
289 return true;
290 }
291
292 bool GetFirstFootprint( const kiapi::common::types::DocumentSpecifier& aDocument, FOOTPRINT* aFootprint )
293 {
294 wxCHECK( aFootprint, false );
295
296 kiapi::common::commands::GetItems request;
297 *request.mutable_header()->mutable_document() = aDocument;
298 request.add_types( kiapi::common::types::KOT_PCB_FOOTPRINT );
299
300 kiapi::common::ApiResponse response;
301
302 if( !sendCommand( request, &response ) )
303 {
304 m_lastError = wxS( "Failed to send command" );
305 return false;
306 }
307
308 if( response.status().status() != kiapi::common::AS_OK )
309 {
310 m_lastError = response.status().error_message();
311 return false;
312 }
313
314 kiapi::common::commands::GetItemsResponse itemsResponse;
315
316 if( !response.message().UnpackTo( &itemsResponse ) )
317 {
318 m_lastError = wxS( "Failed to unpack GetItemsResponse" );
319 return false;
320 }
321
322 if( itemsResponse.status() != kiapi::common::types::IRS_OK )
323 {
324 m_lastError = wxString::Format( wxS( "GetItems returned non-OK status: %s" ),
325 magic_enum::enum_name( itemsResponse.status() ) );
326 return false;
327 }
328
329 if( itemsResponse.items_size() == 0 )
330 {
331 m_lastError = wxS( "GetItems returned no footprints" );
332 return false;
333 }
334
335 if( !aFootprint->Deserialize( itemsResponse.items( 0 ) ) )
336 {
337 m_lastError = wxS( "Failed to deserialize first footprint from GetItems response" );
338 return false;
339 }
340
341 return true;
342 }
343
344 bool CloseDocument( const kiapi::common::types::DocumentSpecifier* aDocument,
345 kiapi::common::ApiStatusCode* aStatus = nullptr )
346 {
347 kiapi::common::commands::CloseDocument request;
348
349 if( aDocument )
350 *request.mutable_document() = *aDocument;
351
352 kiapi::common::ApiResponse response;
353
354 if( !sendCommand( request, &response ) )
355 {
356 m_lastError = wxS( "Failed to send command" );
357 return false;
358 }
359
360 if( aStatus )
361 *aStatus = response.status().status();
362
363 if( response.status().status() != kiapi::common::AS_OK )
364 m_lastError = response.status().error_message();
365
366 return true;
367 }
368
372 template <typename T>
373 bool RunJob( const T& aJobRequest, kiapi::common::types::RunJobResponse* aResponse )
374 {
375 kiapi::common::ApiResponse apiResponse;
376
377 if( !sendCommand( aJobRequest, &apiResponse ) )
378 return false;
379
380 if( apiResponse.status().status() != kiapi::common::AS_OK )
381 {
382 m_lastError = apiResponse.status().error_message();
383 return false;
384 }
385
386 if( !apiResponse.message().UnpackTo( aResponse ) )
387 {
388 m_lastError = wxS( "Failed to unpack RunJobResponse" );
389 return false;
390 }
391
392 return true;
393 }
394
395 template <typename T>
396 bool SendCommand( const T& aMessage, kiapi::common::ApiResponse* aResponse )
397 {
398 return sendCommand( aMessage, aResponse );
399 }
400
401private:
402 template <typename T>
403 bool sendCommand( const T& aMessage, kiapi::common::ApiResponse* aResponse )
404 {
405 if( !m_isConnected )
406 {
407 m_lastError = wxS( "API client is not connected" );
408 return false;
409 }
410
411 kiapi::common::ApiRequest request;
412 request.mutable_header()->set_client_name( "kicad.qa" );
413
414 if( !request.mutable_message()->PackFrom( aMessage ) )
415 {
416 m_lastError = wxS( "Failed to pack command into ApiRequest" );
417 return false;
418 }
419
420 std::string requestStr = request.SerializeAsString();
421 void* sendBuf = nng_alloc( requestStr.size() );
422
423 if( !sendBuf )
424 {
425 m_lastError = wxS( "nng_alloc failed" );
426 return false;
427 }
428
429 memcpy( sendBuf, requestStr.data(), requestStr.size() );
430
431 int ret = nng_send( m_socket, sendBuf, requestStr.size(), NNG_FLAG_ALLOC );
432
433 if( ret != 0 )
434 {
435 m_lastError = wxString::Format( wxS( "nng_send failed: %s" ), wxString::FromUTF8( nng_strerror( ret ) ) );
436 return false;
437 }
438
439 char* reply = nullptr;
440 size_t replySize = 0;
441
442 ret = nng_recv( m_socket, &reply, &replySize, NNG_FLAG_ALLOC );
443
444 if( ret != 0 )
445 {
446 m_lastError = wxString::Format( wxS( "nng_recv failed: %s" ), wxString::FromUTF8( nng_strerror( ret ) ) );
447 return false;
448 }
449
450 std::string responseStr( reply, replySize );
451 nng_free( reply, replySize );
452
453 if( !aResponse->ParseFromString( responseStr ) )
454 {
455 m_lastError = wxS( "Failed to parse reply from KiCad" );
456 return false;
457 }
458
459 return true;
460 }
461
462 nng_socket m_socket;
463 bool m_isOpen = false;
464 bool m_isConnected = false;
465 wxString m_lastError;
466};
467
468
481{
482public:
484 {
485 static API_SERVER_MANAGER s_instance;
486 return s_instance;
487 }
488
492 bool EnsureReady( const wxString& aCliPath, wxString* aError )
493 {
494 if( m_ready )
495 {
496 if( isProcessAlive() )
497 return true;
498
499 m_ready = false;
500 }
501
502 if( !startServerProcess( aCliPath, aError ) )
503 return false;
504
505 if( !connectAndWaitReady( aError ) )
506 return false;
507
508 m_ready = true;
509 return true;
510 }
511
513
517 void Reset()
518 {
519 m_ready = false;
520 m_client.Disconnect();
521
522 if( m_pid != 0 && isProcessAlive() )
523 {
524#ifdef __WXMAC__
525 wxProcess::Kill( m_pid, wxSIGKILL );
526#else
527 wxProcess::Kill( m_pid, wxSIGKILL, wxKILL_CHILDREN );
528#endif
529 }
530
531 m_pid = 0;
532 m_process = nullptr;
533
534 if( wxFileName::Exists( m_socketPath ) )
535 wxRemoveFile( m_socketPath );
536 }
537
538private:
540 {
541#ifdef __UNIX__
542 m_socketPath = wxString::Format( wxS( "/tmp/kicad-api-e2e-%ld.sock" ), wxGetProcessId() );
543#else
544 wxString tempPath = wxFileName::CreateTempFileName( wxS( "kicad-api-e2e" ) );
545
546 if( wxFileExists( tempPath ) )
547 wxRemoveFile( tempPath );
548
549 m_socketPath = tempPath + wxS( ".sock" );
550#endif
551
552 m_socketUrl = wxS( "ipc://" ) + m_socketPath;
553
554 if( wxFileName::Exists( m_socketPath ) )
555 wxRemoveFile( m_socketPath );
556 }
557
559 {
560 // Use POSIX primitives here because this destructor runs during static destruction
561 // when wxWidgets may already be partially torn down.
562#ifdef __UNIX__
563 if( m_pid != 0 )
564 {
565 kill( static_cast<pid_t>( m_pid ), SIGTERM );
566 usleep( 200000 );
567 kill( static_cast<pid_t>( m_pid ), SIGKILL );
568 }
569
570 if( !m_socketPath.IsEmpty() )
571 unlink( m_socketPath.ToUTF8().data() );
572#else
573 if( m_pid != 0 )
574 wxProcess::Kill( m_pid, wxSIGKILL, wxKILL_CHILDREN );
575
576 if( !m_socketPath.IsEmpty() && wxFileName::Exists( m_socketPath ) )
577 wxRemoveFile( m_socketPath );
578#endif
579 }
580
583
584 bool startServerProcess( const wxString& aCliPath, wxString* aError )
585 {
586 if( m_pid != 0 )
587 return true;
588
589 if( !wxFileExists( aCliPath ) )
590 {
591 if( aError )
592 *aError = wxS( "kicad-cli path does not exist: " ) + aCliPath;
593
594 return false;
595 }
596
597 m_stdout.clear();
598 m_stderr.clear();
599 m_exitCode = 0;
600
602 [this]( int aStatus, const wxString& aOutput, const wxString& aErrOutput )
603 {
604 m_exitCode = aStatus;
605 m_stdout += aOutput;
606 m_stderr += aErrOutput;
607 m_pid = 0;
608 m_process = nullptr;
609 m_ready = false;
610 } );
611
612 m_process->Redirect();
613
614 std::vector<const wchar_t*> args = { aCliPath.wc_str(), wxS( "api-server" ), wxS( "--socket" ),
615 m_socketPath.wc_str(), nullptr };
616
617 m_pid = wxExecute( args.data(), wxEXEC_ASYNC, m_process );
618
619 if( m_pid == 0 )
620 {
621 delete m_process;
622 m_process = nullptr;
623
624 if( aError )
625 *aError = wxS( "Failed to launch kicad-cli api-server" );
626
627 return false;
628 }
629
630 return true;
631 }
632
633 bool connectAndWaitReady( wxString* aError )
634 {
635 auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds( 15 );
636
637 while( std::chrono::steady_clock::now() < deadline )
638 {
639 wxMilliSleep( 100 );
641
642 if( !isProcessAlive() )
643 {
644 if( aError )
645 *aError = wxS( "kicad-cli process exited before ready" );
646
647 return false;
648 }
649
650 if( !m_client.Connect( m_socketUrl ) )
651 {
652 wxLogTrace( traceApi, "kicad-cli connect attempt failed, will retry" );
653 continue;
654 }
655
656 kiapi::common::ApiStatusCode pingStatus = kiapi::common::AS_UNKNOWN;
657
658 if( m_client.Ping( &pingStatus ) )
659 {
660 if( pingStatus == kiapi::common::AS_OK )
661 return true;
662
663 if( pingStatus != kiapi::common::AS_NOT_READY )
664 {
665 if( aError )
666 {
667 *aError = wxString::Format( wxS( "Ping returned unexpected status: %s" ),
668 magic_enum::enum_name( pingStatus ) );
669 }
670
671 return false;
672 }
673
674 wxLogTrace( traceApi, "kicad-cli connected but returned AS_NOT_READY, will retry" );
675 }
676 else
677 {
678 wxLogTrace( traceApi, "kicad-cli ping attempt failed, will retry" );
679 }
680 }
681
682 if( aError )
683 *aError = wxS( "Timed out waiting for kicad-cli to start up" );
684
685 return false;
686 }
687
689 {
690 if( !m_process )
691 return;
692
695 }
696
697 bool isProcessAlive() const
698 {
699 if( m_pid == 0 )
700 return false;
701
702#ifdef __UNIX__
703 int status = 0;
704 pid_t result = waitpid( static_cast<pid_t>( m_pid ), &status, WNOHANG );
705
706 if( result == 0 )
707 return true;
708
709 if( result > 0 )
710 {
711 if( WIFSIGNALED( status ) )
712 {
713 BOOST_TEST_MESSAGE( wxString::Format( "api-server killed by signal %d (%s)",
714 WTERMSIG( status ),
715 strsignal( WTERMSIG( status ) ) ) );
716 }
717 else if( WIFEXITED( status ) )
718 {
719 BOOST_TEST_MESSAGE( wxString::Format( "api-server exited with code %d",
720 WEXITSTATUS( status ) ) );
721 }
722
723 return false;
724 }
725#endif
726
727 return wxProcess::Exists( m_pid );
728 }
729
730 bool m_ready = false;
733 long m_pid = 0;
734 wxString m_socketPath;
735 wxString m_socketUrl;
736 wxString m_stdout;
737 wxString m_stderr;
738 mutable int m_exitCode = 0;
739};
740
741
752{
753public:
755 {
756 m_cliPath = wxString::FromUTF8( QA_KICAD_CLI_PATH );
757 }
758
760
761 bool Start( const wxString& aCliPathOverride = wxString() )
762 {
763 wxString cliPath = aCliPathOverride.IsEmpty() ? m_cliPath : aCliPathOverride;
764
766
767 if( manager.EnsureReady( cliPath, &m_lastError ) )
768 return true;
769
770 // First attempt failed. The server may have crashed during a previous test.
771 manager.Reset();
772
773 return manager.EnsureReady( cliPath, &m_lastError );
774 }
775
777
778 wxString CliPath() const { return m_cliPath; }
779
780 const wxString& LastError() const { return m_lastError; }
781
782private:
783 wxString m_cliPath;
784 wxString m_lastError;
785};
786
787#endif // QA_API_E2E_UTILS_H
#define QA_KICAD_CLI_PATH
General utilities for PCB file IO for QA programs.
bool Start(const wxString &aCliPathOverride=wxString())
~API_SERVER_E2E_FIXTURE()=default
const wxString & LastError() const
API_TEST_CLIENT & Client()
wxString CliPath() const
Manages a single shared kicad-cli api-server process and NNG client across all E2E tests.
API_TEST_CLIENT & Client()
API_SERVER_PROCESS * m_process
bool connectAndWaitReady(wxString *aError)
API_SERVER_MANAGER & operator=(const API_SERVER_MANAGER &)=delete
static API_SERVER_MANAGER & Instance()
bool isProcessAlive() const
API_SERVER_MANAGER(const API_SERVER_MANAGER &)=delete
API_TEST_CLIENT m_client
bool startServerProcess(const wxString &aCliPath, wxString *aError)
void Reset()
Kill the current server and reset state so EnsureReady() will start fresh.
bool EnsureReady(const wxString &aCliPath, wxString *aError)
Ensure the api-server is running and the shared client is connected and responsive.
void OnTerminate(int aPid, int aStatus) override
API_SERVER_PROCESS(std::function< void(int, const wxString &, const wxString &)> aCallback)
std::function< void(int, const wxString &, const wxString &)> m_callback
static void drainStream(wxInputStream *aStream, wxString &aDest)
bool GetItemsCount(const kiapi::common::types::DocumentSpecifier &aDocument, kiapi::common::types::KiCadObjectType aType, int *aCount)
bool OpenDocument(const wxString &aPath, kiapi::common::types::DocumentType aType, kiapi::common::types::DocumentSpecifier *aDocument=nullptr)
bool RunJob(const T &aJobRequest, kiapi::common::types::RunJobResponse *aResponse)
Send an arbitrary job request and receive a RunJobResponse.
nng_socket m_socket
bool sendCommand(const T &aMessage, kiapi::common::ApiResponse *aResponse)
bool CloseDocument(const kiapi::common::types::DocumentSpecifier *aDocument, kiapi::common::ApiStatusCode *aStatus=nullptr)
bool Connect(const wxString &aSocketUrl)
const wxString & LastError() const
bool OpenDocument(const wxString &aPath, kiapi::common::types::DocumentSpecifier *aDocument=nullptr)
bool SendCommand(const T &aMessage, kiapi::common::ApiResponse *aResponse)
bool Ping(kiapi::common::ApiStatusCode *aStatusOut=nullptr)
bool GetFirstFootprint(const kiapi::common::types::DocumentSpecifier &aDocument, FOOTPRINT *aFootprint)
bool Deserialize(const google::protobuf::Any &aContainer) override
Deserializes the given protobuf message into this object.
const wxChar *const traceApi
Flag to enable debug output related to the IPC API and its plugin system.
Definition api_utils.cpp:29
STL namespace.
nlohmann::json output
BOOST_TEST_MESSAGE("\n=== Real-World Polygon PIP Benchmark ===\n"<< formatTable(table))
wxString result
Test unit parsing edge cases and error handling.