KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_api_jobs.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 <fstream>
21#include <set>
22#include <sstream>
23#include <string>
24
25#include <wx/filename.h>
26
27#include <boost/test/unit_test.hpp>
28#include <gestfich.h>
29#include <qa_utils/file_utils.h>
31
32#include "api_e2e_utils.h"
33
34#include <api/board/board_jobs.pb.h>
35#include <api/board/board_types.pb.h>
36#include <api/schematic/schematic_jobs.pb.h>
37
38#include <api/board/board_commands.pb.h>
39
40#include <wx/file.h>
41
42#include <footprint.h>
43
50bool textFilesMatch( const wxString& aGoldenPath, const wxString& aGeneratedPath, int aSkipLines )
51{
52 std::ifstream goldenStream( aGoldenPath.ToStdString() );
53 std::ifstream generatedStream( aGeneratedPath.ToStdString() );
54
55 if( !goldenStream.is_open() )
56 {
57 BOOST_TEST_MESSAGE( "Cannot open golden file: " + aGoldenPath );
58 return false;
59 }
60
61 if( !generatedStream.is_open() )
62 {
63 BOOST_TEST_MESSAGE( "Cannot open generated file: " + aGeneratedPath );
64 return false;
65 }
66
67 std::string goldenLine, generatedLine;
68
69 for( int i = 0; i < aSkipLines; ++i )
70 {
71 std::getline( goldenStream, goldenLine );
72 std::getline( generatedStream, generatedLine );
73 }
74
75 int lineNum = aSkipLines + 1;
76
77 while( std::getline( goldenStream, goldenLine ) )
78 {
79 if( !std::getline( generatedStream, generatedLine ) )
80 {
81 BOOST_TEST_MESSAGE( "Generated file is shorter than golden at line " << lineNum );
82 return false;
83 }
84
85 if( goldenLine != generatedLine )
86 {
87 BOOST_TEST_MESSAGE( "Mismatch at line " << lineNum << ":\n"
88 << " golden: " << goldenLine << "\n"
89 << " generated: " << generatedLine );
90 return false;
91 }
92
93 ++lineNum;
94 }
95
96 if( std::getline( generatedStream, generatedLine ) )
97 {
98 BOOST_TEST_MESSAGE( "Generated file is longer than golden after line " << lineNum );
99 return false;
100 }
101
102 return true;
103}
104
105
106static size_t countOccurrences( const std::string& aHaystack, const std::string& aNeedle )
107{
108 size_t count = 0;
109
110 for( size_t pos = aHaystack.find( aNeedle ); pos != std::string::npos;
111 pos = aHaystack.find( aNeedle, pos + aNeedle.size() ) )
112 {
113 ++count;
114 }
115
116 return count;
117}
118
119
123static std::set<std::string> collectColours( const std::string& aSvg )
124{
125 std::set<std::string> colours;
126
127 for( size_t pos = aSvg.find( '#' ); pos != std::string::npos; pos = aSvg.find( '#', pos + 1 ) )
128 {
129 std::string colour = aSvg.substr( pos, 7 );
130
131 if( colour.size() < 7 )
132 continue;
133
134 if( colour.find_first_not_of( "0123456789ABCDEFabcdef", 1 ) != std::string::npos )
135 continue;
136
137 colours.insert( colour );
138 }
139
140 return colours;
141}
142
143
144BOOST_AUTO_TEST_SUITE( ApiJobs )
145
146
148{
149 BOOST_REQUIRE_MESSAGE( Start(), LastError() );
150
151 wxString testDataDir =
152 wxString::FromUTF8( KI_TEST::GetTestDataRootDir() ) + wxS( "cli/artwork_generation_regressions/" );
153
154 wxFileName boardPath( testDataDir, wxS( "ZoneFill-4.0.7.kicad_pcb" ) );
155
156 kiapi::common::types::DocumentSpecifier document;
157
158 BOOST_REQUIRE_MESSAGE( Client().OpenDocument( boardPath.GetFullPath(), &document ),
159 "OpenDocument failed: " + Client().LastError() );
160
161
162 KI_TEST::SCOPED_TEMP_DIR tempDir( "api_job_svg" );
163 wxFileName outputPath( tempDir.ChildPathStr( "plot.svg" ) );
164
165 kiapi::board::jobs::RunBoardJobExportSvg request;
166 *request.mutable_job_settings()->mutable_document() = document;
167 request.mutable_job_settings()->set_output_path( outputPath.GetFullPath().ToUTF8().data() );
168
169 request.mutable_plot_settings()->add_layers( kiapi::board::types::BL_F_Cu );
170 request.mutable_plot_settings()->set_black_and_white( true );
171 request.mutable_plot_settings()->set_plot_drawing_sheet( false );
172 request.mutable_plot_settings()->set_drill_marks( kiapi::board::jobs::PDM_FULL );
173 request.set_page_mode( kiapi::board::jobs::BJPM_EACH_LAYER_OWN_FILE );
174 request.set_precision( 4 );
175
176 kiapi::common::types::RunJobResponse response;
177 BOOST_REQUIRE_MESSAGE( Client().RunJob( request, &response ), "RunJob failed: " + Client().LastError() );
178
179 BOOST_REQUIRE_MESSAGE( response.status() == kiapi::common::types::JS_SUCCESS,
180 "Job failed: " + wxString::FromUTF8( response.message() ) );
181
182 BOOST_REQUIRE_MESSAGE( response.output_path_size() > 0, "Job returned no output paths" );
183
184 wxString generatedPath = wxString::FromUTF8( response.output_path( 0 ) );
185 BOOST_REQUIRE_MESSAGE( wxFileName::FileExists( generatedPath ), "Generated SVG does not exist: " + generatedPath );
186
187 // Plot fidelity is covered by the kicad-cli SVG regression tests, which rasterize and compare
188 // against golden artwork. All this test has to establish is that the job honoured the request.
189 const std::string svg = KI_TEST::LoadStringData( generatedPath );
190
191 BOOST_REQUIRE_MESSAGE( !svg.empty(), "Generated SVG is empty or unreadable: " + generatedPath );
192 BOOST_CHECK( svg.find( "<svg" ) != std::string::npos );
193 BOOST_CHECK( svg.find( "</svg>" ) != std::string::npos );
194
195 // The board page is A4, and the four decimals are the requested precision
196 BOOST_CHECK( svg.find( "width=\"297.0022mm\" height=\"210.0072mm\"" ) != std::string::npos );
197
198 std::set<std::string> colours = collectColours( svg );
199 BOOST_REQUIRE_MESSAGE( !colours.empty(), "Plot declares no colours at all" );
200
201 for( const std::string& colour : colours )
202 {
203 BOOST_CHECK_MESSAGE( colour == "#000000" || colour == "#FFFFFF",
204 "black_and_white was requested but the plot contains " + colour );
205 }
206
207 // F.Cu of this board is dense; a near-empty plot means the layer selection was dropped
208 BOOST_CHECK_GT( countOccurrences( svg, "<path" ), 100u );
209}
210
211
212// Reads the pixel dimensions out of the IHDR chunk of a PNG file.
213// Returns true on success; on failure aWidth/aHeight are untouched.
214static bool pngDimensions( const wxString& aPath, uint32_t& aWidth, uint32_t& aHeight )
215{
216 std::ifstream stream( aPath.ToStdString(), std::ios::binary );
217
218 if( !stream.is_open() )
219 return false;
220
221 char header[8];
222
223 if( !stream.read( header, 8 ) || memcmp( header, "\x89PNG\r\n\x1a\n", 8 ) != 0 )
224 return false;
225
226 char ihdr[16];
227
228 if( !stream.read( ihdr, 16 ) )
229 return false;
230
231 auto readBE32 = []( const char* aBytes )
232 {
233 return ( static_cast<uint32_t>( static_cast<unsigned char>( aBytes[0] ) ) << 24 )
234 | ( static_cast<uint32_t>( static_cast<unsigned char>( aBytes[1] ) ) << 16 )
235 | ( static_cast<uint32_t>( static_cast<unsigned char>( aBytes[2] ) ) << 8 )
236 | ( static_cast<uint32_t>( static_cast<unsigned char>( aBytes[3] ) ) );
237 };
238
239 aWidth = readBE32( ihdr + 8 );
240 aHeight = readBE32( ihdr + 12 );
241 return true;
242}
243
244
246{
247 BOOST_REQUIRE_MESSAGE( Start(), LastError() );
248
249 wxString testDataDir = wxString::FromUTF8( KI_TEST::GetTestDataRootDir() ) + wxS( "cli/basic_test/" );
250
251 wxFileName schPath( testDataDir, wxS( "basic_test.kicad_sch" ) );
252
253 kiapi::common::types::DocumentSpecifier document;
254
255 BOOST_REQUIRE_MESSAGE(
256 Client().OpenDocument( schPath.GetFullPath(), kiapi::common::types::DOCTYPE_SCHEMATIC, &document ),
257 "OpenDocument failed: " + Client().LastError() );
258
259 // --- All-sheets mode: output path is a directory
260 KI_TEST::SCOPED_TEMP_DIR tempDir( "api_job_sch_png" );
261 wxString outputDir = tempDir.CreateChildDirStr( "all" ) + wxFileName::GetPathSeparator();
262
263 {
264 kiapi::schematic::jobs::RunSchematicJobExportPng request;
265 *request.mutable_job_settings()->mutable_document() = document;
266 request.mutable_job_settings()->set_output_path( outputDir.ToUTF8().data() );
267 request.mutable_plot_settings()->set_sheet_mode( kiapi::schematic::jobs::SJSM_ALL_SHEETS );
268 request.mutable_plot_settings()->set_black_and_white( true );
269 request.set_dpi( 150 );
270
271 kiapi::common::types::RunJobResponse response;
272 BOOST_REQUIRE_MESSAGE( Client().RunJob( request, &response ),
273 "RunJob failed: " + Client().LastError() );
274
275 BOOST_REQUIRE_MESSAGE( response.status() == kiapi::common::types::JS_SUCCESS,
276 "Job failed: " + wxString::FromUTF8( response.message() ) );
277 BOOST_REQUIRE_MESSAGE( response.output_path_size() > 0, "Job returned no output paths" );
278
279 wxString generatedPath = wxString::FromUTF8( response.output_path( 0 ) );
280 BOOST_REQUIRE_MESSAGE( wxFileName::FileExists( generatedPath ),
281 "Generated PNG does not exist: " + generatedPath );
282
283 // basic_test.kicad_sch is A4 landscape; 297mm at 150dpi = 1754px (+/- rounding)
284 uint32_t width = 0, height = 0;
285 BOOST_REQUIRE_MESSAGE( pngDimensions( generatedPath, width, height ),
286 "Generated file is not a valid PNG: " + generatedPath );
287 BOOST_CHECK_MESSAGE( width >= 1750 && width <= 1758,
288 "Unexpected PNG width " << width << " for 150dpi A4 landscape" );
289 BOOST_CHECK_MESSAGE( height >= 1236 && height <= 1244,
290 "Unexpected PNG height " << height << " for 150dpi A4 landscape" );
291 }
292
293 // --- Single-sheet mode: output path is a file name
294 wxFileName singlePath( tempDir.ChildPathStr( "single.png" ) );
295
296 {
297 kiapi::schematic::jobs::RunSchematicJobExportPng request;
298 *request.mutable_job_settings()->mutable_document() = document;
299 request.mutable_job_settings()->set_output_path( singlePath.GetFullPath().ToUTF8().data() );
300 request.mutable_plot_settings()->set_sheet_mode( kiapi::schematic::jobs::SJSM_SINGLE_SHEET );
301 request.mutable_plot_settings()->set_black_and_white( true );
302 request.set_dpi( 300 );
303
304 kiapi::common::types::RunJobResponse response;
305 BOOST_REQUIRE_MESSAGE( Client().RunJob( request, &response ),
306 "Single-sheet RunJob failed: " + Client().LastError() );
307
308 BOOST_REQUIRE_MESSAGE( response.status() == kiapi::common::types::JS_SUCCESS,
309 "Single-sheet job failed: " + wxString::FromUTF8( response.message() ) );
310 BOOST_REQUIRE_MESSAGE( response.output_path_size() > 0,
311 "Single-sheet job returned no output paths" );
312
313 wxString generatedPath = wxString::FromUTF8( response.output_path( 0 ) );
314 BOOST_REQUIRE_MESSAGE( wxFileName::FileExists( generatedPath ),
315 "Generated PNG does not exist: " + generatedPath );
316
317 // 297mm at 300dpi = 3508px (+/- rounding)
318 uint32_t width = 0, height = 0;
319 BOOST_REQUIRE_MESSAGE( pngDimensions( generatedPath, width, height ),
320 "Generated file is not a valid PNG: " + generatedPath );
321 BOOST_CHECK_MESSAGE( width >= 3500 && width <= 3516,
322 "Unexpected PNG width " << width << " for 300dpi A4 landscape" );
323
324 // The single-file output path must be honored exactly
325 BOOST_CHECK_MESSAGE( generatedPath == singlePath.GetFullPath(),
326 "Single-sheet output path not honored: " + generatedPath );
327 }
328
329 // --- Out-of-range dpi is rejected with a bad-request error
330 {
331 kiapi::schematic::jobs::RunSchematicJobExportPng request;
332 *request.mutable_job_settings()->mutable_document() = document;
333 request.mutable_job_settings()->set_output_path( outputDir.ToUTF8().data() );
334 request.mutable_plot_settings()->set_sheet_mode( kiapi::schematic::jobs::SJSM_ALL_SHEETS );
335 request.set_dpi( 10 );
336
337 kiapi::common::types::RunJobResponse response;
338 BOOST_CHECK_MESSAGE( !Client().RunJob( request, &response ),
339 "Out-of-range dpi was not rejected" );
340 }
341
342 // --- Omitted dpi falls back to the GUI default (300)
343 {
344 wxString defaultDir = tempDir.CreateChildDirStr( "default" ) + wxFileName::GetPathSeparator();
345
346 kiapi::schematic::jobs::RunSchematicJobExportPng request;
347 *request.mutable_job_settings()->mutable_document() = document;
348 request.mutable_job_settings()->set_output_path( defaultDir.ToUTF8().data() );
349 request.mutable_plot_settings()->set_sheet_mode( kiapi::schematic::jobs::SJSM_ALL_SHEETS );
350
351 kiapi::common::types::RunJobResponse response;
352 BOOST_REQUIRE_MESSAGE( Client().RunJob( request, &response ),
353 "Default-dpi RunJob failed: " + Client().LastError() );
354 BOOST_REQUIRE_MESSAGE( response.status() == kiapi::common::types::JS_SUCCESS,
355 "Default-dpi job failed: " + wxString::FromUTF8( response.message() ) );
356 BOOST_REQUIRE_MESSAGE( response.output_path_size() > 0,
357 "Default-dpi job returned no output paths" );
358
359 uint32_t width = 0, height = 0;
360 wxString generatedPath = wxString::FromUTF8( response.output_path( 0 ) );
361 BOOST_REQUIRE_MESSAGE( pngDimensions( generatedPath, width, height ),
362 "Generated file is not a valid PNG: " + generatedPath );
363 BOOST_CHECK_MESSAGE( width >= 3500 && width <= 3516,
364 "Default dpi should be 300; got width " << width );
365 }
366}
367
368
370{
371 BOOST_REQUIRE_MESSAGE( Start(), LastError() );
372
373 wxString testDataDir = wxString::FromUTF8( KI_TEST::GetTestDataRootDir() ) + wxS( "cli/basic_test/" );
374
375 wxFileName boardPath( testDataDir, wxS( "basic_test.kicad_pcb" ) );
376
377 kiapi::common::types::DocumentSpecifier document;
378
379 BOOST_REQUIRE_MESSAGE( Client().OpenDocument( boardPath.GetFullPath(), &document ),
380 "OpenDocument failed: " + Client().LastError() );
381
382 KI_TEST::SCOPED_TEMP_DIR tempDir( "api_job_drill" );
383 wxString outputDir = tempDir.CreateChildDirStr( "drill" ) + wxFileName::GetPathSeparator();
384
385 kiapi::board::jobs::RunBoardJobExportDrill request;
386 *request.mutable_job_settings()->mutable_document() = document;
387 request.mutable_job_settings()->set_output_path( outputDir.ToUTF8().data() );
388 request.set_format( kiapi::board::jobs::DF_EXCELLON );
389
390 kiapi::common::types::RunJobResponse response;
391 BOOST_REQUIRE_MESSAGE( Client().RunJob( request, &response ), "RunJob failed: " + Client().LastError() );
392
393 BOOST_REQUIRE_MESSAGE( response.status() == kiapi::common::types::JS_SUCCESS,
394 "Job failed: " + wxString::FromUTF8( response.message() ) );
395
396 BOOST_REQUIRE_MESSAGE( response.output_path_size() > 0, "Job returned no output paths" );
397 wxString generatedDrillPath;
398
399 for( int i = 0; i < response.output_path_size(); ++i )
400 {
401 wxString outputPath = wxString::FromUTF8( response.output_path( i ) );
402
403 if( outputPath.EndsWith( wxS( ".drl" ) ) )
404 {
405 generatedDrillPath = outputPath;
406 break;
407 }
408 }
409
410 BOOST_REQUIRE_MESSAGE( !generatedDrillPath.IsEmpty(), "No .drl file found in job output" );
411 BOOST_REQUIRE_MESSAGE( wxFileName::FileExists( generatedDrillPath ),
412 "Generated drill file does not exist: " + generatedDrillPath );
413
414 wxString goldenPath = testDataDir + wxS( "basic_test_excellon_inches.drl" );
415 BOOST_CHECK_MESSAGE( textFilesMatch( goldenPath, generatedDrillPath, 5 ),
416 "Drill output does not match golden file" );
417}
418
419
421{
422 BOOST_REQUIRE_MESSAGE( Start(), LastError() );
423
424 wxFileName boardPath( wxString::FromUTF8( KI_TEST::GetTestDataRootDir() ) + wxS( "pcbnew/issue24735/" ),
425 wxS( "issue24735.kicad_pcb" ) );
426
427 kiapi::common::types::DocumentSpecifier document;
428
429 BOOST_REQUIRE_MESSAGE( Client().OpenDocument( boardPath.GetFullPath(), &document ),
430 "OpenDocument failed: " + Client().LastError() );
431
432 KI_TEST::SCOPED_TEMP_DIR tempDir( "api_job_odb" );
433 wxFileName target( tempDir.CreateChildDirStr( "odb" ), wxS( "combined" ) );
434
435 kiapi::board::jobs::RunBoardJobExportODB request;
436 *request.mutable_job_settings()->mutable_document() = document;
437 request.mutable_job_settings()->set_output_path( target.GetFullPath().ToUTF8().data() );
438 request.set_units( kiapi::common::types::U_MM );
439 request.set_compression( kiapi::board::jobs::ODBC_NONE );
440 request.add_variant_names( "var1" );
441 request.add_variant_names( "var2" );
442 request.set_variant_packaging( kiapi::board::jobs::ODBVP_COMBINED );
443
444 kiapi::common::types::RunJobResponse response;
445 BOOST_REQUIRE_MESSAGE( Client().RunJob( request, &response ), "RunJob failed: " + Client().LastError() );
446 BOOST_REQUIRE_MESSAGE( response.status() == kiapi::common::types::JS_SUCCESS,
447 "Job failed: " + wxString::FromUTF8( response.message() ) );
448
449 auto hasLine = []( const wxString& aPackage, const wxString& aDir, const std::string& aLine )
450 {
451 wxFileName file( aPackage + wxFileName::GetPathSeparator() + aDir, wxS( "attrlist" ) );
452 std::ifstream input( ToFsPath( file.GetFullPath() ) );
453 std::string line;
454
455 while( std::getline( input, line ) )
456 {
457 if( line == aLine )
458 return true;
459 }
460
461 return false;
462 };
463
464 BOOST_REQUIRE_EQUAL( response.output_path_size(), 1 );
465 wxString combined = wxString::FromUTF8( response.output_path( 0 ) );
466 BOOST_REQUIRE_MESSAGE( wxFileName::DirExists( combined ), "Missing reported output " + combined );
467 BOOST_CHECK_MESSAGE( wxFileName::DirName( combined ).SameAs( wxFileName::DirName( target.GetFullPath() ) ),
468 "Combined output ignored the requested path: " + combined );
469 BOOST_CHECK( hasLine( combined, wxS( "misc" ), ".variant_list=var1:var2" ) );
470 BOOST_CHECK( hasLine( combined, wxS( "steps/pcb" ), ".current_variant=var1" ) );
471
472 // One package per variant proves every repeated variant name reached the job
473 wxFileName separateTarget( tempDir.CreateChildDirStr( "odb_separate" ), wxS( "separate" ) );
474 request.mutable_job_settings()->set_output_path( separateTarget.GetFullPath().ToUTF8().data() );
475 request.set_variant_packaging( kiapi::board::jobs::ODBVP_SEPARATE );
476 response.Clear();
477
478 BOOST_REQUIRE_MESSAGE( Client().RunJob( request, &response ), "RunJob failed: " + Client().LastError() );
479 BOOST_REQUIRE_MESSAGE( response.status() == kiapi::common::types::JS_SUCCESS,
480 "Separate job failed: " + wxString::FromUTF8( response.message() ) );
481 BOOST_REQUIRE_EQUAL( response.output_path_size(), 2 );
482 std::set<std::string> currentVariants;
483
484 for( int i = 0; i < response.output_path_size(); ++i )
485 {
486 wxString package = wxString::FromUTF8( response.output_path( i ) );
487 BOOST_REQUIRE_MESSAGE( wxFileName::DirExists( package ), "Missing reported output " + package );
488 BOOST_CHECK_MESSAGE( package.StartsWith( separateTarget.GetFullPath() + wxS( "-" ) ),
489 "Separate output ignored the requested path: " + package );
490
491 for( const std::string variant : { "var1", "var2" } )
492 {
493 if( hasLine( package, wxS( "steps/pcb" ), ".current_variant=" + variant ) )
494 currentVariants.insert( variant );
495 }
496 }
497
498 BOOST_CHECK( currentVariants == std::set<std::string>( { "var1", "var2" } ) );
499}
500
501
503{
504 BOOST_REQUIRE_MESSAGE( Start(), LastError() );
505
506 wxString testDataDir =
507 wxString::FromUTF8( KI_TEST::GetTestDataRootDir() ) + wxS( "cli/artwork_generation_regressions/" );
508
509 wxFileName boardPath( testDataDir, wxS( "ZoneFill-4.0.7.kicad_pcb" ) );
510
511 kiapi::common::types::DocumentSpecifier document;
512
513 BOOST_REQUIRE_MESSAGE( Client().OpenDocument( boardPath.GetFullPath(), &document ),
514 "OpenDocument failed: " + Client().LastError() );
515
516 KI_TEST::SCOPED_TEMP_DIR tempDir( "api_job_pcb_png" );
517
518 // --- MULTI mode: one file per layer into a directory
519 {
520 wxString outputDir = tempDir.CreateChildDirStr( "multi" ) + wxFileName::GetPathSeparator();
521
522 kiapi::board::jobs::RunBoardJobExportPng request;
523 *request.mutable_job_settings()->mutable_document() = document;
524 request.mutable_job_settings()->set_output_path( outputDir.ToUTF8().data() );
525 request.mutable_plot_settings()->add_layers( kiapi::board::types::BL_F_Cu );
526 request.mutable_plot_settings()->set_black_and_white( true );
527 request.set_page_mode( kiapi::board::jobs::BJPM_EACH_LAYER_OWN_FILE );
528 request.set_dpi( 150 );
529
530 kiapi::common::types::RunJobResponse response;
531 BOOST_REQUIRE_MESSAGE( Client().RunJob( request, &response ),
532 "RunJob failed: " + Client().LastError() );
533
534 BOOST_REQUIRE_MESSAGE( response.status() == kiapi::common::types::JS_SUCCESS,
535 "Job failed: " + wxString::FromUTF8( response.message() ) );
536 BOOST_REQUIRE_MESSAGE( response.output_path_size() > 0, "Job returned no output paths" );
537
538 wxString generatedPath = wxString::FromUTF8( response.output_path( 0 ) );
539 BOOST_REQUIRE_MESSAGE( wxFileName::FileExists( generatedPath ),
540 "Generated PNG does not exist: " + generatedPath );
541
542 uint32_t width = 0, height = 0;
543 BOOST_REQUIRE_MESSAGE( pngDimensions( generatedPath, width, height ),
544 "Generated file is not a valid PNG: " + generatedPath );
545 BOOST_CHECK_MESSAGE( width >= 1750 && width <= 1758,
546 "Unexpected PNG width " << width << " for 150dpi A4 landscape" );
547 }
548
549 // --- SINGLE mode: all layers composited into one file
550 {
551 wxFileName outputPath( tempDir.ChildPathStr( "single.png" ) );
552
553 kiapi::board::jobs::RunBoardJobExportPng request;
554 *request.mutable_job_settings()->mutable_document() = document;
555 request.mutable_job_settings()->set_output_path( outputPath.GetFullPath().ToUTF8().data() );
556 request.mutable_plot_settings()->add_layers( kiapi::board::types::BL_F_Cu );
557 request.mutable_plot_settings()->add_layers( kiapi::board::types::BL_F_SilkS );
558 request.mutable_plot_settings()->set_black_and_white( true );
559 request.set_page_mode( kiapi::board::jobs::BJPM_ALL_LAYERS_ONE_PAGE );
560 request.set_dpi( 300 );
561
562 kiapi::common::types::RunJobResponse response;
563 BOOST_REQUIRE_MESSAGE( Client().RunJob( request, &response ),
564 "Single-mode RunJob failed: " + Client().LastError() );
565
566 BOOST_REQUIRE_MESSAGE( response.status() == kiapi::common::types::JS_SUCCESS,
567 "Single-mode job failed: " + wxString::FromUTF8( response.message() ) );
568 BOOST_REQUIRE_MESSAGE( response.output_path_size() > 0,
569 "Single-mode job returned no output paths" );
570
571 wxString generatedPath = wxString::FromUTF8( response.output_path( 0 ) );
572 BOOST_REQUIRE_MESSAGE( wxFileName::FileExists( generatedPath ),
573 "Generated PNG does not exist: " + generatedPath );
574 BOOST_CHECK_MESSAGE( generatedPath == outputPath.GetFullPath(),
575 "Single-mode output path not honored: " + generatedPath );
576
577 uint32_t width = 0, height = 0;
578 BOOST_REQUIRE_MESSAGE( pngDimensions( generatedPath, width, height ),
579 "Generated file is not a valid PNG: " + generatedPath );
580 BOOST_CHECK_MESSAGE( width >= 3500 && width <= 3516,
581 "Unexpected PNG width " << width << " for 300dpi A4 landscape" );
582 }
583}
584
585
587{
588 BOOST_REQUIRE_MESSAGE( Start(), LastError() );
589
590 wxString testDataDir = wxString::FromUTF8( KI_TEST::GetTestDataRootDir() ) + wxS( "cli/basic_test/" );
591
592 wxFileName schPath( testDataDir, wxS( "basic_test.kicad_sch" ) );
593
594 kiapi::common::types::DocumentSpecifier document;
595
596 BOOST_REQUIRE_MESSAGE(
597 Client().OpenDocument( schPath.GetFullPath(), kiapi::common::types::DOCTYPE_SCHEMATIC, &document ),
598 "OpenDocument failed: " + Client().LastError() );
599
600 KI_TEST::SCOPED_TEMP_DIR tempDir( "api_job_netlist" );
601 wxFileName outputPath( tempDir.ChildPathStr( "netlist.cadstar" ) );
602
603 kiapi::schematic::jobs::RunSchematicJobExportNetlist request;
604 *request.mutable_job_settings()->mutable_document() = document;
605 request.mutable_job_settings()->set_output_path( outputPath.GetFullPath().ToUTF8().data() );
606 request.set_format( kiapi::schematic::jobs::SNF_CADSTAR );
607
608 kiapi::common::types::RunJobResponse response;
609 BOOST_REQUIRE_MESSAGE( Client().RunJob( request, &response ), "RunJob failed: " + Client().LastError() );
610
611 BOOST_REQUIRE_MESSAGE( response.status() == kiapi::common::types::JS_SUCCESS,
612 "Job failed: " + wxString::FromUTF8( response.message() ) );
613
614 BOOST_REQUIRE_MESSAGE( response.output_path_size() > 0, "Job returned no output paths" );
615
616 wxString generatedPath = wxString::FromUTF8( response.output_path( 0 ) );
617 BOOST_REQUIRE_MESSAGE( wxFileName::FileExists( generatedPath ),
618 "Generated netlist does not exist: " + generatedPath );
619
620 // 3 header lines to skip (contain timestamp/version)
621 wxString goldenPath = testDataDir + wxS( "basic_test.netlist.cadstar" );
622 BOOST_CHECK_MESSAGE( textFilesMatch( goldenPath, generatedPath, 3 ), "Netlist output does not match golden file" );
623}
624
625
627{
628 BOOST_REQUIRE_MESSAGE( Start(), LastError() );
629
630 wxString testDataDir = wxString::FromUTF8( KI_TEST::GetTestDataRootDir() ) + wxS( "cli/variants/" );
631
632 wxFileName schPath( testDataDir, wxS( "variants.kicad_sch" ) );
633
634 kiapi::common::types::DocumentSpecifier document;
635
636 BOOST_REQUIRE_MESSAGE(
637 Client().OpenDocument( schPath.GetFullPath(), kiapi::common::types::DOCTYPE_SCHEMATIC, &document ),
638 "OpenDocument failed: " + Client().LastError() );
639
640 KI_TEST::SCOPED_TEMP_DIR tempDir( "api_job_bom" );
641 wxFileName outputPath( tempDir.ChildPathStr( "bom.csv" ) );
642
643 kiapi::schematic::jobs::RunSchematicJobExportBOM request;
644 *request.mutable_job_settings()->mutable_document() = document;
645 request.mutable_job_settings()->set_output_path( outputPath.GetFullPath().ToUTF8().data() );
646 request.set_exclude_dnp( true );
647
648 request.mutable_format()->set_preset_name( "CSV" );
649
650 auto* refField = request.mutable_fields()->add_fields();
651 refField->set_name( "Reference" );
652 refField->set_label( "Refs" );
653 refField->set_group_by( false );
654
655 auto* valField = request.mutable_fields()->add_fields();
656 valField->set_name( "Value" );
657 valField->set_label( "Value" );
658 valField->set_group_by( false );
659
660 kiapi::common::types::RunJobResponse response;
661 BOOST_REQUIRE_MESSAGE( Client().RunJob( request, &response ), "RunJob failed: " + Client().LastError() );
662
663 BOOST_REQUIRE_MESSAGE( response.status() == kiapi::common::types::JS_SUCCESS,
664 "Job failed: " + wxString::FromUTF8( response.message() ) );
665
666 BOOST_REQUIRE_MESSAGE( response.output_path_size() > 0, "Job returned no output paths" );
667
668 wxString generatedPath = wxString::FromUTF8( response.output_path( 0 ) );
669 BOOST_REQUIRE_MESSAGE( wxFileName::FileExists( generatedPath ), "Generated BOM does not exist: " + generatedPath );
670
671 wxString goldenPath = testDataDir + wxS( "variants_default.bom.csv" );
672 BOOST_CHECK_MESSAGE( textFilesMatch( goldenPath, generatedPath, 0 ), "BOM output does not match golden file" );
673
674 request.mutable_format()->clear_preset_name();
675 request.mutable_format()->set_field_delimiter( "," );
676 request.mutable_format()->set_string_delimiter( "\"" );
677 request.mutable_format()->set_ref_delimiter( "," );
678 request.mutable_format()->set_ref_range_delimiter( "" );
679 request.mutable_format()->set_include_byte_order_mark( true );
680 response.Clear();
681
682 BOOST_REQUIRE_MESSAGE( Client().RunJob( request, &response ),
683 "BOM job with byte order mark failed: " + Client().LastError() );
684
685 BOOST_REQUIRE_MESSAGE( response.status() == kiapi::common::types::JS_SUCCESS,
686 "BOM job with byte order mark failed: " + wxString::FromUTF8( response.message() ) );
687 BOOST_REQUIRE_MESSAGE( response.output_path_size() > 0, "BOM job with byte order mark returned no output paths" );
688
689 generatedPath = wxString::FromUTF8( response.output_path( 0 ) );
690 std::ifstream generatedStream( generatedPath.ToStdString(), std::ios::binary );
691 std::ifstream goldenStream( goldenPath.ToStdString(), std::ios::binary );
692 BOOST_REQUIRE( generatedStream.is_open() );
693 BOOST_REQUIRE( goldenStream.is_open() );
694
695 std::ostringstream generatedContents;
696 std::ostringstream goldenContents;
697 generatedContents << generatedStream.rdbuf();
698 goldenContents << goldenStream.rdbuf();
699
700 const std::string utf8ByteOrderMark( "\xEF\xBB\xBF", 3 );
701 const std::string bom = generatedContents.str();
702 BOOST_REQUIRE_GE( bom.size(), utf8ByteOrderMark.size() );
703 BOOST_CHECK_EQUAL( bom.substr( 0, utf8ByteOrderMark.size() ), utf8ByteOrderMark );
704 BOOST_CHECK_EQUAL( bom.substr( utf8ByteOrderMark.size() ), goldenContents.str() );
705
706 generatedStream.close();
707 goldenStream.close();
708}
709
710
712{
713 BOOST_REQUIRE_MESSAGE( Start(), LastError() );
714
715 wxString testDataDir = wxString::FromUTF8( KI_TEST::GetTestDataRootDir() ) + wxS( "cli/basic_test/" );
716
717 wxFileName boardPath( testDataDir, wxS( "basic_test.kicad_pcb" ) );
718
719 kiapi::common::types::DocumentSpecifier document;
720
721 BOOST_REQUIRE_MESSAGE( Client().OpenDocument( boardPath.GetFullPath(), &document ),
722 "OpenDocument failed: " + Client().LastError() );
723
724 KI_TEST::SCOPED_TEMP_DIR tempDir( "api_job_specctra_dsn" );
725 wxFileName outputPath( tempDir.ChildPathStr( "basic_test.dsn" ) );
726
727 kiapi::board::jobs::RunBoardJobExportSpecctraDsn request;
728 *request.mutable_job_settings()->mutable_document() = document;
729 request.mutable_job_settings()->set_output_path( outputPath.GetFullPath().ToUTF8().data() );
730
731 kiapi::common::types::RunJobResponse response;
732 BOOST_REQUIRE_MESSAGE( Client().RunJob( request, &response ), "RunJob failed: " + Client().LastError() );
733
734 BOOST_REQUIRE_MESSAGE( response.status() == kiapi::common::types::JS_SUCCESS,
735 "Job failed: " + wxString::FromUTF8( response.message() ) );
736 BOOST_REQUIRE_MESSAGE( response.output_path_size() > 0, "Job returned no output paths" );
737
738 wxString generatedPath = wxString::FromUTF8( response.output_path( 0 ) );
739 BOOST_REQUIRE_MESSAGE( wxFileName::FileExists( generatedPath ), "Generated DSN does not exist: " + generatedPath );
740 BOOST_CHECK_MESSAGE( generatedPath == outputPath.GetFullPath(), "Output path not honored: " + generatedPath );
741
742 const std::string dsn = KI_TEST::LoadStringData( generatedPath );
743 BOOST_CHECK( dsn.find( "(unit um)" ) != std::string::npos );
744 BOOST_CHECK( dsn.find( "(network" ) != std::string::npos );
745}
746
747
748BOOST_FIXTURE_TEST_CASE( ImportSpecctraSessionErrors, API_SERVER_E2E_FIXTURE )
749{
750 BOOST_REQUIRE_MESSAGE( Start(), LastError() );
751
752 wxString testDataDir = wxString::FromUTF8( KI_TEST::GetTestDataRootDir() ) + wxS( "cli/basic_test/" );
753
754 wxFileName boardPath( testDataDir, wxS( "basic_test.kicad_pcb" ) );
755
756 kiapi::common::types::DocumentSpecifier document;
757
758 BOOST_REQUIRE_MESSAGE( Client().OpenDocument( boardPath.GetFullPath(), &document ),
759 "OpenDocument failed: " + Client().LastError() );
760
761 // Missing session path -> AS_BAD_REQUEST
762 {
763 kiapi::board::commands::ImportSpecctraSession request;
764 *request.mutable_board() = document;
765 request.set_session_path( wxFileName( wxS( "/nonexistent/missing.ses" ) ).GetFullPath().ToUTF8().data() );
766
767 kiapi::common::ApiResponse response;
768 BOOST_CHECK_MESSAGE( Client().SendCommand( request, &response ),
769 "SendCommand failed: " + Client().LastError() );
770 BOOST_CHECK_EQUAL( response.status().status(), kiapi::common::AS_BAD_REQUEST );
771 }
772
773 // Malformed session -> AS_BAD_REQUEST
774 {
775 KI_TEST::SCOPED_TEMP_DIR tempDir( "api_job_specctra_ses_bad" );
776 wxFileName sesPath( tempDir.ChildPathStr( "malformed.ses" ) );
777
778 wxFile badFile;
779 BOOST_REQUIRE( badFile.Create( sesPath.GetFullPath(), true ) );
780 badFile.Write( "(session test.ses\n (base_design test.dsn)\n", 45 );
781 badFile.Close();
782
783 kiapi::board::commands::ImportSpecctraSession request;
784 *request.mutable_board() = document;
785 request.set_session_path( sesPath.GetFullPath().ToUTF8().data() );
786
787 kiapi::common::ApiResponse response;
788 BOOST_CHECK_MESSAGE( Client().SendCommand( request, &response ), "SendCommand failed" );
789 BOOST_CHECK_EQUAL( response.status().status(), kiapi::common::AS_BAD_REQUEST );
790 BOOST_CHECK( !response.status().error_message().empty() );
791 }
792}
793
794
796{
797 BOOST_REQUIRE_MESSAGE( Start(), LastError() );
798
799 wxString testDataDir = wxString::FromUTF8( KI_TEST::GetTestDataRootDir() ) + wxS( "cli/basic_test/" );
800
801 wxFileName boardPath( testDataDir, wxS( "basic_test.kicad_pcb" ) );
802
803 kiapi::common::types::DocumentSpecifier document;
804
805 BOOST_REQUIRE_MESSAGE( Client().OpenDocument( boardPath.GetFullPath(), &document ),
806 "OpenDocument failed: " + Client().LastError() );
807
808 FOOTPRINT footprint( nullptr );
809 BOOST_REQUIRE_MESSAGE( Client().GetFootprintByReference( document, wxS( "J1" ), &footprint ),
810 "GetFootprintByReference failed: " + Client().LastError() );
811
812 VECTOR2I positionBefore = footprint.GetPosition();
813
814 wxFileName sesPath( wxString::FromUTF8( KI_TEST::GetPcbnewTestDataDir() ) + wxString( wxS( "/specctra/" ) ),
815 wxS( "basic_test" ), wxS( "ses" ) );
816
817 BOOST_REQUIRE_MESSAGE( wxFileName::FileExists( sesPath.GetFullPath() ),
818 "Session fixture is missing: " + sesPath.GetFullPath() );
819
820 kiapi::board::commands::ImportSpecctraSession request;
821 *request.mutable_board() = document;
822 request.set_session_path( sesPath.GetFullPath().ToUTF8().data() );
823
824 kiapi::common::ApiResponse response;
825 BOOST_REQUIRE_MESSAGE( Client().SendCommand( request, &response ),
826 "Session import SendCommand failed: " + Client().LastError() );
827 BOOST_CHECK_EQUAL( response.status().status(), kiapi::common::AS_OK );
828
829 kiapi::board::commands::ImportSpecctraSessionResponse importResponse;
830 BOOST_REQUIRE_MESSAGE( response.message().UnpackTo( &importResponse ),
831 "Failed to unpack ImportSpecctraSessionResponse" );
832 BOOST_CHECK_EQUAL( importResponse.skipped_count(), 0 );
833 BOOST_CHECK_MESSAGE( importResponse.report().empty(), "Unexpected importer report: "
834 + importResponse.report() );
835
836 // The session replaces all unlocked board routes with the routed session contents
837 int traceCountAfter = 0;
838 BOOST_REQUIRE_MESSAGE( Client().GetItemsCount( document, kiapi::common::types::KOT_PCB_TRACE, &traceCountAfter ),
839 "GetItems (traces after import) failed: " + Client().LastError() );
840 BOOST_CHECK_EQUAL( traceCountAfter, 1 );
841
842 // The session places J1 at board origin (0, 0).
843 FOOTPRINT after( nullptr );
844 BOOST_REQUIRE_MESSAGE( Client().GetFootprintByReference( document, wxS( "J1" ), &after ),
845 "GetFootprintByReference after import failed: " + Client().LastError() );
846 BOOST_CHECK( after.GetPosition() != positionBefore );
847 BOOST_CHECK_EQUAL( after.GetPosition().x, 0 );
848 BOOST_CHECK_EQUAL( after.GetPosition().y, 0 );
849}
850
851
Per-test fixture for API E2E tests.
VECTOR2I GetPosition() const override
Definition footprint.h:379
wxString ChildPathStr(const wxString &aName) const
Get the path to a direct child of the temporary directory as a wxString, without creating the child.
wxString CreateChildDirStr(const wxString &aName) const
Create and return the path to a direct child directory as a wxString.
std::filesystem::path ToFsPath(const wxString &aPath)
Give aPath to std::filesystem without a detour through the C locale.
Definition gestfich.cpp:922
std::string GetTestDataRootDir()
std::string GetPcbnewTestDataDir()
Utility which returns a path to the data directory where the test board files are stored.
std::string LoadStringData(const wxString &aPath)
Load the contents of a file into a string.
BOOST_FIXTURE_TEST_CASE(ServerStartsAndResponds, API_SERVER_E2E_FIXTURE)
static std::set< std::string > collectColours(const std::string &aSvg)
Return every distinct #RRGGBB literal appearing in aSvg.
bool textFilesMatch(const wxString &aGoldenPath, const wxString &aGeneratedPath, int aSkipLines)
Compare two text files line-by-line, optionally skipping the first aSkipLines lines of each file (to ...
static bool pngDimensions(const wxString &aPath, uint32_t &aWidth, uint32_t &aHeight)
BOOST_FIXTURE_TEST_CASE(ExportBoardSvg, API_SERVER_E2E_FIXTURE)
static size_t countOccurrences(const std::string &aHaystack, const std::string &aNeedle)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:708