20#ifndef QA_API_E2E_UTILS_H
21#define QA_API_E2E_UTILS_H
28#include <google/protobuf/any.pb.h>
29#include <magic_enum.hpp>
30#include <wx/filename.h>
31#include <wx/process.h>
36#include <nng/protocol/reqrep0/req.h>
40#include <api/common/envelope.pb.h>
41#include <api/common/commands/base_commands.pb.h>
42#include <api/common/commands/editor_commands.pb.h>
43#include <api/common/commands/project_commands.pb.h>
44#include <api/common/types/base_types.pb.h>
45#include <api/common/types/enums.pb.h>
46#include <api/common/types/jobs.pb.h>
53#ifndef QA_KICAD_CLI_PATH
54#define QA_KICAD_CLI_PATH "kicad-cli"
79 static void drainStream( wxInputStream* aStream, wxString& aDest )
81 if( !aStream || !aStream->CanRead() )
84 constexpr size_t chunkSize = 1024;
85 char buffer[chunkSize + 1] = { 0 };
87 while( aStream->CanRead() )
89 aStream->Read( buffer, chunkSize );
90 size_t bytesRead = aStream->LastRead();
95 buffer[bytesRead] =
'\0';
96 aDest += wxString::FromUTF8( buffer );
101 std::function<void(
int,
const wxString&,
const wxString& )>
m_callback;
110 int ret = nng_req0_open( &
m_socket );
115 nng_socket_set_ms(
m_socket, NNG_OPT_RECVTIMEO, 10000 );
116 nng_socket_set_ms(
m_socket, NNG_OPT_SENDTIMEO, 10000 );
121 wxString::Format( wxS(
"nng_req0_open failed: %s" ), wxString::FromUTF8( nng_strerror( ret ) ) );
139 int ret = nng_dial(
m_socket, aSocketUrl.ToStdString().c_str(),
nullptr, 0 );
143 m_lastError = wxString::Format( wxS(
"nng_dial failed: %s" ), wxString::FromUTF8( nng_strerror( ret ) ) );
153 bool Ping( kiapi::common::ApiStatusCode* aStatusOut =
nullptr )
155 kiapi::common::commands::Ping ping;
156 kiapi::common::ApiResponse response;
162 *aStatusOut = response.status().status();
169 kiapi::common::commands::GetVersion request;
170 kiapi::common::ApiResponse response;
175 if( response.status().status() != kiapi::common::AS_OK )
181 kiapi::common::commands::GetVersionResponse version;
183 if( !response.message().UnpackTo( &version ) )
185 m_lastError = wxS(
"Failed to unpack GetVersionResponse" );
192 bool OpenDocument(
const wxString& aPath, kiapi::common::types::DocumentType aType,
193 kiapi::common::types::DocumentSpecifier* aDocument =
nullptr )
195 kiapi::common::commands::OpenDocument request;
196 request.set_type( aType );
197 request.set_path( aPath.ToStdString() );
199 kiapi::common::ApiResponse response;
204 if( response.status().status() != kiapi::common::AS_OK )
210 kiapi::common::commands::OpenDocumentResponse openResponse;
212 if( !response.message().UnpackTo( &openResponse ) )
214 m_lastError = wxS(
"Failed to unpack OpenDocumentResponse" );
219 *aDocument = openResponse.document();
225 bool OpenDocument(
const wxString& aPath, kiapi::common::types::DocumentSpecifier* aDocument =
nullptr )
227 return OpenDocument( aPath, kiapi::common::types::DOCTYPE_PCB, aDocument );
230 bool GetItemsCount(
const kiapi::common::types::DocumentSpecifier& aDocument,
231 kiapi::common::types::KiCadObjectType aType,
int* aCount )
233 kiapi::common::commands::GetItems request;
234 *request.mutable_header()->mutable_document() = aDocument;
235 request.add_types( aType );
237 kiapi::common::ApiResponse response;
242 if( response.status().status() != kiapi::common::AS_OK )
248 kiapi::common::commands::GetItemsResponse itemsResponse;
250 if( !response.message().UnpackTo( &itemsResponse ) )
252 m_lastError = wxS(
"Failed to unpack GetItemsResponse" );
256 if( itemsResponse.status() != kiapi::common::types::IRS_OK )
258 m_lastError = wxString::Format( wxS(
"GetItems returned non-OK status: %d" ),
259 static_cast<int>( itemsResponse.status() ) );
263 *aCount = itemsResponse.items_size();
269 wxCHECK( aFootprint,
false );
271 kiapi::common::commands::GetItems request;
272 *request.mutable_header()->mutable_document() = aDocument;
273 request.add_types( kiapi::common::types::KOT_PCB_FOOTPRINT );
275 kiapi::common::ApiResponse response;
283 if( response.status().status() != kiapi::common::AS_OK )
289 kiapi::common::commands::GetItemsResponse itemsResponse;
291 if( !response.message().UnpackTo( &itemsResponse ) )
293 m_lastError = wxS(
"Failed to unpack GetItemsResponse" );
297 if( itemsResponse.status() != kiapi::common::types::IRS_OK )
299 m_lastError = wxString::Format( wxS(
"GetItems returned non-OK status: %s" ),
300 magic_enum::enum_name( itemsResponse.status() ) );
304 if( itemsResponse.items_size() == 0 )
306 m_lastError = wxS(
"GetItems returned no footprints" );
310 if( !aFootprint->
Deserialize( itemsResponse.items( 0 ) ) )
312 m_lastError = wxS(
"Failed to deserialize first footprint from GetItems response" );
319 bool CloseDocument(
const kiapi::common::types::DocumentSpecifier* aDocument,
320 kiapi::common::ApiStatusCode* aStatus =
nullptr )
322 kiapi::common::commands::CloseDocument request;
325 *request.mutable_document() = *aDocument;
327 kiapi::common::ApiResponse response;
336 *aStatus = response.status().status();
338 if( response.status().status() != kiapi::common::AS_OK )
347 template <
typename T>
348 bool RunJob(
const T& aJobRequest, kiapi::common::types::RunJobResponse* aResponse )
350 kiapi::common::ApiResponse apiResponse;
355 if( apiResponse.status().status() != kiapi::common::AS_OK )
357 m_lastError = apiResponse.status().error_message();
361 if( !apiResponse.message().UnpackTo( aResponse ) )
363 m_lastError = wxS(
"Failed to unpack RunJobResponse" );
370 template <
typename T>
371 bool SendCommand(
const T& aMessage, kiapi::common::ApiResponse* aResponse )
377 template <
typename T>
378 bool sendCommand(
const T& aMessage, kiapi::common::ApiResponse* aResponse )
382 m_lastError = wxS(
"API client is not connected" );
386 kiapi::common::ApiRequest request;
387 request.mutable_header()->set_client_name(
"kicad.qa" );
389 if( !request.mutable_message()->PackFrom( aMessage ) )
391 m_lastError = wxS(
"Failed to pack command into ApiRequest" );
395 std::string requestStr = request.SerializeAsString();
396 void* sendBuf = nng_alloc( requestStr.size() );
404 memcpy( sendBuf, requestStr.data(), requestStr.size() );
406 int ret = nng_send(
m_socket, sendBuf, requestStr.size(), NNG_FLAG_ALLOC );
410 m_lastError = wxString::Format( wxS(
"nng_send failed: %s" ), wxString::FromUTF8( nng_strerror( ret ) ) );
414 char* reply =
nullptr;
415 size_t replySize = 0;
417 ret = nng_recv(
m_socket, &reply, &replySize, NNG_FLAG_ALLOC );
421 m_lastError = wxString::Format( wxS(
"nng_recv failed: %s" ), wxString::FromUTF8( nng_strerror( ret ) ) );
425 std::string responseStr( reply, replySize );
426 nng_free( reply, replySize );
428 if( !aResponse->ParseFromString( responseStr ) )
430 m_lastError = wxS(
"Failed to parse reply from KiCad" );
452 m_socketPath = wxString::Format( wxS(
"/tmp/kicad-api-e2e-%ld.sock" ), wxGetProcessId() );
454 wxString tempPath = wxFileName::CreateTempFileName( wxS(
"kicad-api-e2e" ) );
456 if( wxFileExists( tempPath ) )
457 wxRemoveFile( tempPath );
474 bool Start(
const wxString& aCliPathOverride = wxString() )
479 auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds( 15 );
480 wxString lastProbeError;
482 while( std::chrono::steady_clock::now() < deadline )
489 m_lastError = wxS(
"kicad-cli process exited before ready" );
495 if( !
m_client.Connect( socketUrl ) )
497 wxLogTrace(
traceApi,
"kicad-cli connect attempt failed, will retry" );
501 kiapi::common::ApiStatusCode pingStatus = kiapi::common::AS_UNKNOWN;
505 if( pingStatus == kiapi::common::AS_OK )
508 if( pingStatus != kiapi::common::AS_NOT_READY )
510 m_lastError = wxString::Format( wxS(
"Ping returned unexpected status: %s" ),
511 magic_enum::enum_name( pingStatus ) );
515 wxLogTrace(
traceApi,
"kicad-cli connected but returned AS_NOT_READY, will retry" );
519 wxLogTrace(
traceApi,
"kicad-cli ping attempt failed, will retry" );
523 m_lastError = wxS(
"Timed out waiting for kicad-cli to start up" );
539 wxString cliPath = aCliPathOverride.IsEmpty() ?
m_cliPath : aCliPathOverride;
541 if( !wxFileExists( cliPath ) )
543 m_lastError = wxS(
"kicad-cli path does not exist: " ) + cliPath;
552 [
this](
int aStatus,
const wxString& aOutput,
const wxString& aError )
563 std::vector<const wchar_t*> args = { cliPath.wc_str(), wxS(
"api-server" ), wxS(
"--socket" ),
572 m_lastError = wxS(
"Failed to launch kicad-cli api-server" );
594 wxProcess::Kill(
m_pid, wxSIGTERM, wxKILL_CHILDREN );
596 auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds( 5 );
598 while(
isProcessAlive() && std::chrono::steady_clock::now() < deadline )
602 wxProcess::Kill(
m_pid, wxSIGKILL, wxKILL_CHILDREN );
615 return wxProcess::Exists(
m_pid );
#define QA_KICAD_CLI_PATH
General utilities for PCB file IO for QA programs.
void collectServerOutput()
bool Start(const wxString &aCliPathOverride=wxString())
~API_SERVER_E2E_FIXTURE()
bool isProcessAlive() const
const wxString & LastError() const
API_TEST_CLIENT & Client()
bool startServerProcess(const wxString &aCliPathOverride)
API_SERVER_PROCESS * m_process
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.
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)
const wxChar *const traceApi
Flag to enable debug output related to the IPC API and its plugin system.