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
17 * along with this program. If not, see <https://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 <api/api_client.h>
42#include <api/api_utils.h>
43#include <api/common/envelope.pb.h>
44#include <api/common/commands/base_commands.pb.h>
45#include <api/common/commands/editor_commands.pb.h>
46#include <api/common/commands/project_commands.pb.h>
47#include <api/common/types/base_types.pb.h>
48#include <api/common/types/enums.pb.h>
49#include <api/common/types/jobs.pb.h>
50#include <footprint.h>
53#include <wx/log.h>
54
55
56#ifndef QA_KICAD_CLI_PATH
57#define QA_KICAD_CLI_PATH "kicad-cli"
58#endif
59
60
61class API_SERVER_PROCESS : public wxProcess
62{
63public:
64 API_SERVER_PROCESS( std::function<void( int, const wxString&, const wxString& )> aCallback ) :
65 wxProcess(),
66 m_callback( std::move( aCallback ) )
67 {
68 }
69
70 void OnTerminate( int aPid, int aStatus ) override
71 {
72 wxString output;
73 wxString error;
74
75 drainStream( GetInputStream(), output );
76 drainStream( GetErrorStream(), error );
77
78 if( m_callback )
79 m_callback( aStatus, output, error );
80 }
81
82 static void drainStream( wxInputStream* aStream, wxString& aDest )
83 {
84 if( !aStream || !aStream->CanRead() )
85 return;
86
87 constexpr size_t chunkSize = 1024;
88 char buffer[chunkSize + 1] = { 0 };
89
90 while( aStream->CanRead() )
91 {
92 aStream->Read( buffer, chunkSize );
93 size_t bytesRead = aStream->LastRead();
94
95 if( bytesRead == 0 )
96 break;
97
98 buffer[bytesRead] = '\0';
99 aDest += wxString::FromUTF8( buffer );
100 }
101 }
102
103private:
104 std::function<void( int, const wxString&, const wxString& )> m_callback;
105};
106
107
109{
110public:
111 API_TEST_CLIENT() : m_client( 10000 ) {}
112
113 bool Connect( const wxString& aSocketUrl ) { return m_client.Connect( aSocketUrl ); }
114 void Disconnect() { m_client.Disconnect(); }
115
116 const wxString& LastError() const { return m_lastError; }
117
118 bool Ping( kiapi::common::ApiStatusCode* aStatusOut = nullptr )
119 {
120 kiapi::common::commands::Ping ping;
121 kiapi::common::ApiResponse response;
122
123 if( !send( ping, response ) )
124 return false;
125
126 if( aStatusOut )
127 *aStatusOut = response.status().status();
128
129 return true;
130 }
131
133 {
134 kiapi::common::commands::GetVersion request;
135 kiapi::common::ApiResponse response;
136
137 if( !send( request, response ) )
138 return false;
139
140 if( response.status().status() != kiapi::common::AS_OK )
141 {
142 m_lastError = response.status().error_message();
143 return false;
144 }
145
146 kiapi::common::commands::GetVersionResponse version;
147
148 if( !response.message().UnpackTo( &version ) )
149 {
150 m_lastError = wxS( "Failed to unpack GetVersionResponse" );
151 return false;
152 }
153
154 return true;
155 }
156
157 bool OpenDocument( const wxString& aPath, kiapi::common::types::DocumentType aType,
158 kiapi::common::types::DocumentSpecifier* aDocument = nullptr )
159 {
160 kiapi::common::commands::OpenDocument request;
161 request.set_type( aType );
162 request.set_path( aPath.ToStdString() );
163
164 kiapi::common::ApiResponse response;
165
166 if( !send( request, response ) )
167 return false;
168
169 if( response.status().status() != kiapi::common::AS_OK )
170 {
171 m_lastError = response.status().error_message();
172 return false;
173 }
174
175 kiapi::common::commands::OpenDocumentResponse openResponse;
176
177 if( !response.message().UnpackTo( &openResponse ) )
178 {
179 m_lastError = wxS( "Failed to unpack OpenDocumentResponse" );
180 return false;
181 }
182
183 if( aDocument )
184 *aDocument = openResponse.document();
185
186 return true;
187 }
188
189 bool CreateDocument( const wxString& aPath, kiapi::common::types::DocumentType aType,
190 kiapi::common::types::DocumentSpecifier* aDocument = nullptr )
191 {
192 kiapi::common::commands::CreateDocument request;
193 request.set_type( aType );
194 request.set_path( aPath.ToStdString() );
195
196 kiapi::common::ApiResponse response;
197
198 if( !send( request, response ) )
199 return false;
200
201 if( response.status().status() != kiapi::common::AS_OK )
202 {
203 m_lastError = response.status().error_message();
204 return false;
205 }
206
207 kiapi::common::commands::OpenDocumentResponse openResponse;
208
209 if( !response.message().UnpackTo( &openResponse ) )
210 {
211 m_lastError = wxS( "Failed to unpack OpenDocumentResponse" );
212 return false;
213 }
214
215 if( aDocument )
216 *aDocument = openResponse.document();
217
218 return true;
219 }
220
221 // Convenience overload for PCB documents (backward compatible)
222 bool OpenDocument( const wxString& aPath, kiapi::common::types::DocumentSpecifier* aDocument = nullptr )
223 {
224 return OpenDocument( aPath, kiapi::common::types::DOCTYPE_PCB, aDocument );
225 }
226
227 bool GetItemsCount( const kiapi::common::types::DocumentSpecifier& aDocument,
228 kiapi::common::types::KiCadObjectType aType, int* aCount )
229 {
230 kiapi::common::commands::GetItems request;
231 *request.mutable_header()->mutable_document() = aDocument;
232 request.add_types( aType );
233
234 kiapi::common::ApiResponse response;
235
236 if( !send( request, response ) )
237 return false;
238
239 if( response.status().status() != kiapi::common::AS_OK )
240 {
241 m_lastError = response.status().error_message();
242 return false;
243 }
244
245 kiapi::common::commands::GetItemsResponse itemsResponse;
246
247 if( !response.message().UnpackTo( &itemsResponse ) )
248 {
249 m_lastError = wxS( "Failed to unpack GetItemsResponse" );
250 return false;
251 }
252
253 if( itemsResponse.status() != kiapi::common::types::IRS_OK )
254 {
255 m_lastError = wxString::Format( wxS( "GetItems returned non-OK status: %d" ),
256 static_cast<int>( itemsResponse.status() ) );
257 return false;
258 }
259
260 *aCount = itemsResponse.items_size();
261 return true;
262 }
263
264 bool GetFirstFootprint( const kiapi::common::types::DocumentSpecifier& aDocument, FOOTPRINT* aFootprint )
265 {
266 wxCHECK( aFootprint, false );
267
268 kiapi::common::commands::GetItems request;
269 *request.mutable_header()->mutable_document() = aDocument;
270 request.add_types( kiapi::common::types::KOT_PCB_FOOTPRINT );
271
272 kiapi::common::ApiResponse response;
273
274 if( !send( request, response ) )
275 {
276 m_lastError = wxS( "Failed to send command" );
277 return false;
278 }
279
280 if( response.status().status() != kiapi::common::AS_OK )
281 {
282 m_lastError = response.status().error_message();
283 return false;
284 }
285
286 kiapi::common::commands::GetItemsResponse itemsResponse;
287
288 if( !response.message().UnpackTo( &itemsResponse ) )
289 {
290 m_lastError = wxS( "Failed to unpack GetItemsResponse" );
291 return false;
292 }
293
294 if( itemsResponse.status() != kiapi::common::types::IRS_OK )
295 {
296 m_lastError = wxString::Format( wxS( "GetItems returned non-OK status: %s" ),
297 magic_enum::enum_name( itemsResponse.status() ) );
298 return false;
299 }
300
301 if( itemsResponse.items_size() == 0 )
302 {
303 m_lastError = wxS( "GetItems returned no footprints" );
304 return false;
305 }
306
307 if( !aFootprint->Deserialize( itemsResponse.items( 0 ) ) )
308 {
309 m_lastError = wxS( "Failed to deserialize first footprint from GetItems response" );
310 return false;
311 }
312
313 return true;
314 }
315
320 bool GetFootprintByReference( const kiapi::common::types::DocumentSpecifier& aDocument, const wxString& aReference,
321 FOOTPRINT* aFootprint )
322 {
323 wxCHECK( aFootprint, false );
324
325 kiapi::common::commands::GetItems request;
326 *request.mutable_header()->mutable_document() = aDocument;
327 request.add_types( kiapi::common::types::KOT_PCB_FOOTPRINT );
328
329 kiapi::common::ApiResponse response;
330
331 if( !send( request, response ) )
332 {
333 m_lastError = wxS( "Failed to send command" );
334 return false;
335 }
336
337 if( response.status().status() != kiapi::common::AS_OK )
338 {
339 m_lastError = response.status().error_message();
340 return false;
341 }
342
343 kiapi::common::commands::GetItemsResponse itemsResponse;
344
345 if( !response.message().UnpackTo( &itemsResponse ) )
346 {
347 m_lastError = wxS( "Failed to unpack GetItemsResponse" );
348 return false;
349 }
350
351 if( itemsResponse.status() != kiapi::common::types::IRS_OK )
352 {
353 m_lastError = wxString::Format( wxS( "GetItems returned non-OK status: %s" ),
354 magic_enum::enum_name( itemsResponse.status() ) );
355 return false;
356 }
357
358 for( const google::protobuf::Any& item : itemsResponse.items() )
359 {
360 FOOTPRINT candidate( nullptr );
361
362 if( !candidate.Deserialize( item ) )
363 continue;
364
365 const PCB_FIELD* reference = std::as_const( candidate ).GetField( FIELD_T::REFERENCE );
366
367 if( reference && reference->GetText() == aReference )
368 {
369 *aFootprint = std::move( candidate );
370 return true;
371 }
372 }
373
374 m_lastError = wxS( "No footprint with reference " ) + aReference;
375 return false;
376 }
377
378 bool CloseDocument( const kiapi::common::types::DocumentSpecifier* aDocument,
379 kiapi::common::ApiStatusCode* aStatus = nullptr )
380 {
381 kiapi::common::commands::CloseDocument request;
382
383 if( aDocument )
384 *request.mutable_document() = *aDocument;
385
386 kiapi::common::ApiResponse response;
387
388 if( !send( request, response ) )
389 {
390 m_lastError = wxS( "Failed to send command" );
391 return false;
392 }
393
394 if( aStatus )
395 *aStatus = response.status().status();
396
397 if( response.status().status() != kiapi::common::AS_OK )
398 m_lastError = response.status().error_message();
399
400 return true;
401 }
402
404 {
405 kiapi::common::commands::CloseAllDocuments request;
406 kiapi::common::ApiResponse response;
407
408 if( !send( request, response ) )
409 return false;
410
411 if( response.status().status() != kiapi::common::AS_OK )
412 {
413 m_lastError = response.status().error_message();
414 return false;
415 }
416
417 return true;
418 }
419
423 template <typename T>
424 bool RunJob( const T& aJobRequest, kiapi::common::types::RunJobResponse* aResponse )
425 {
426 kiapi::common::ApiResponse apiResponse;
427
428 if( !send( aJobRequest, apiResponse ) )
429 return false;
430
431 if( apiResponse.status().status() != kiapi::common::AS_OK )
432 {
433 m_lastError = apiResponse.status().error_message();
434 return false;
435 }
436
437 if( !apiResponse.message().UnpackTo( aResponse ) )
438 {
439 m_lastError = wxS( "Failed to unpack RunJobResponse" );
440 return false;
441 }
442
443 return true;
444 }
445
446 template <typename T>
447 bool SendCommand( const T& aMessage, kiapi::common::ApiResponse* aResponse )
448 {
449 return send( aMessage, *aResponse );
450 }
451
452private:
453 bool send( const google::protobuf::Message& aRequest, kiapi::common::ApiResponse& aResponse )
454 {
455 if( !m_client.Send( aRequest, aResponse, "kicad.qa" ) )
456 {
457 m_lastError = m_client.GetLastError();
458 return false;
459 }
460
461 return true;
462 }
463
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 {
498 // A new testcase is starting up, make sure old things are closed
500 return true;
501 }
502
503 m_ready = false;
504 }
505
506 BOOST_TEST_MESSAGE( "Starting API server with CLI path: " << aCliPath );
507
508 if( !startServerProcess( aCliPath, aError ) )
509 return false;
510
511 if( !connectAndWaitReady( aError ) )
512 return false;
513
514 m_ready = true;
515 return true;
516 }
517
519
523 void Reset()
524 {
525 m_ready = false;
526 m_client.Disconnect();
527
528 if( m_pid != 0 && isProcessAlive() )
529 {
530#ifdef __WXMAC__
531 wxProcess::Kill( m_pid, wxSIGKILL );
532#else
533 wxProcess::Kill( m_pid, wxSIGKILL, wxKILL_CHILDREN );
534#endif
535 }
536
537 m_pid = 0;
538 m_process = nullptr;
539
540 if( wxFileName::Exists( m_socketPath ) )
541 wxRemoveFile( m_socketPath );
542 }
543
544private:
546 {
547#ifdef __UNIX__
548 m_socketPath = wxString::Format( wxS( "/tmp/kicad-api-e2e-%ld.sock" ), wxGetProcessId() );
549#else
550 wxString tempPath = wxFileName::CreateTempFileName( wxS( "kicad-api-e2e" ) );
551
552 if( wxFileExists( tempPath ) )
553 wxRemoveFile( tempPath );
554
555 m_socketPath = tempPath + wxS( ".sock" );
556#endif
557
558 m_socketUrl = wxS( "ipc://" ) + m_socketPath;
559
560 if( wxFileName::Exists( m_socketPath ) )
561 wxRemoveFile( m_socketPath );
562 }
563
565 {
566 // Use POSIX primitives here because this destructor runs during static destruction
567 // when wxWidgets may already be partially torn down.
568#ifdef __UNIX__
569 if( m_pid != 0 )
570 {
571 kill( static_cast<pid_t>( m_pid ), SIGTERM );
572 usleep( 200000 );
573 kill( static_cast<pid_t>( m_pid ), SIGKILL );
574 }
575
576 if( !m_socketPath.IsEmpty() )
577 unlink( m_socketPath.ToUTF8().data() );
578#else
579 if( m_pid != 0 )
580 wxProcess::Kill( m_pid, wxSIGKILL, wxKILL_CHILDREN );
581
582 if( !m_socketPath.IsEmpty() && wxFileName::Exists( m_socketPath ) )
583 wxRemoveFile( m_socketPath );
584#endif
585 }
586
589
590 bool startServerProcess( const wxString& aCliPath, wxString* aError )
591 {
592 if( m_pid != 0 )
593 return true;
594
595 if( !wxFileExists( aCliPath ) )
596 {
597 if( aError )
598 *aError = wxS( "kicad-cli path does not exist: " ) + aCliPath;
599
600 return false;
601 }
602
603 m_stdout.clear();
604 m_stderr.clear();
605 m_exitCode = 0;
606
608 [this]( int aStatus, const wxString& aOutput, const wxString& aErrOutput )
609 {
610 m_exitCode = aStatus;
611 m_stdout += aOutput;
612 m_stderr += aErrOutput;
613 m_pid = 0;
614 m_process = nullptr;
615 m_ready = false;
616 } );
617
618 m_process->Redirect();
619
620 std::vector<const wchar_t*> args = { aCliPath.wc_str(), wxS( "api-server" ), wxS( "--socket" ),
621 m_socketPath.wc_str(), nullptr };
622
623 m_pid = wxExecute( args.data(), wxEXEC_ASYNC, m_process );
624
625 if( m_pid == 0 )
626 {
627 delete m_process;
628 m_process = nullptr;
629
630 if( aError )
631 *aError = wxS( "Failed to launch kicad-cli api-server" );
632
633 return false;
634 }
635
636 return true;
637 }
638
639 bool connectAndWaitReady( wxString* aError )
640 {
641 auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds( 15 );
642
643 while( std::chrono::steady_clock::now() < deadline )
644 {
645 wxMilliSleep( 100 );
647
648 if( !isProcessAlive() )
649 {
650 if( aError )
651 *aError = wxS( "kicad-cli process exited before ready" );
652
653 return false;
654 }
655
656 if( !m_client.Connect( m_socketUrl ) )
657 {
658 wxLogTrace( traceApi, "kicad-cli connect attempt failed, will retry" );
659 continue;
660 }
661
662 kiapi::common::ApiStatusCode pingStatus = kiapi::common::AS_UNKNOWN;
663
664 if( m_client.Ping( &pingStatus ) )
665 {
666 if( pingStatus == kiapi::common::AS_OK )
667 return true;
668
669 if( pingStatus != kiapi::common::AS_NOT_READY )
670 {
671 if( aError )
672 {
673 *aError = wxString::Format( wxS( "Ping returned unexpected status: %s" ),
674 magic_enum::enum_name( pingStatus ) );
675 }
676
677 return false;
678 }
679
680 wxLogTrace( traceApi, "kicad-cli connected but returned AS_NOT_READY, will retry" );
681 }
682 else
683 {
684 wxLogTrace( traceApi, "kicad-cli ping attempt failed, will retry" );
685 }
686 }
687
688 if( aError )
689 *aError = wxS( "Timed out waiting for kicad-cli to start up" );
690
691 return false;
692 }
693
695 {
696 if( !m_process )
697 return;
698
701 }
702
703 bool isProcessAlive() const
704 {
705 if( m_pid == 0 )
706 return false;
707
708#ifdef __UNIX__
709 int status = 0;
710 pid_t result = waitpid( static_cast<pid_t>( m_pid ), &status, WNOHANG );
711
712 if( result == 0 )
713 return true;
714
715 if( result > 0 )
716 {
717 if( WIFSIGNALED( status ) )
718 {
719 BOOST_TEST_MESSAGE( wxString::Format( "api-server killed by signal %d (%s)",
720 WTERMSIG( status ),
721 strsignal( WTERMSIG( status ) ) ) );
722 }
723 else if( WIFEXITED( status ) )
724 {
725 BOOST_TEST_MESSAGE( wxString::Format( "api-server exited with code %d",
726 WEXITSTATUS( status ) ) );
727 }
728
729 return false;
730 }
731#endif
732
733 return wxProcess::Exists( m_pid );
734 }
735
736 bool m_ready = false;
739 long m_pid = 0;
740 wxString m_socketPath;
741 wxString m_socketUrl;
742 wxString m_stdout;
743 wxString m_stderr;
744 mutable int m_exitCode = 0;
745};
746
747
758{
759public:
761 {
762 m_cliPath = wxString::FromUTF8( QA_KICAD_CLI_PATH );
763 }
764
769
771 bool Start( const wxString& aCliPathOverride = wxString() )
772 {
773 wxString cliPath = aCliPathOverride.IsEmpty() ? m_cliPath : aCliPathOverride;
774
776
777 if( manager.EnsureReady( cliPath, &m_lastError ) )
778 return true;
779
780 // First attempt failed. The server may have crashed during a previous test.
781 manager.Reset();
782
783 return manager.EnsureReady( cliPath, &m_lastError );
784 }
785
787
788 wxString CliPath() const { return m_cliPath; }
789
790 const wxString& LastError() const { return m_lastError; }
791
792private:
793 wxString m_cliPath;
794 wxString m_lastError;
795};
796
797#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())
Starts the API server if not already running. If running, closes any open documents.
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 send(const google::protobuf::Message &aRequest, kiapi::common::ApiResponse &aResponse)
bool CreateDocument(const wxString &aPath, kiapi::common::types::DocumentType aType, kiapi::common::types::DocumentSpecifier *aDocument=nullptr)
bool GetFootprintByReference(const kiapi::common::types::DocumentSpecifier &aDocument, const wxString &aReference, FOOTPRINT *aFootprint)
Fetch all footprints and deserialize the one whose reference designator matches aReference.
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.
KICAD_API_CLIENT m_client
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)
virtual const wxString & GetText() const
Return the string associated with the text object.
Definition eda_text.h:118
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:33
STL namespace.
@ REFERENCE
Field Reference of part, i.e. "IC21".
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
wxString result
Test unit parsing edge cases and error handling.