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
45bool textFilesMatch( const wxString& aGoldenPath, const wxString& aGeneratedPath, int aSkipLines )
46{
47 std::ifstream goldenStream( aGoldenPath.ToStdString() );
48 std::ifstream generatedStream( aGeneratedPath.ToStdString() );
49
50 if( !goldenStream.is_open() )
51 {
52 BOOST_TEST_MESSAGE( "Cannot open golden file: " + aGoldenPath );
53 return false;
54 }
55
56 if( !generatedStream.is_open() )
57 {
58 BOOST_TEST_MESSAGE( "Cannot open generated file: " + aGeneratedPath );
59 return false;
60 }
61
62 std::string goldenLine, generatedLine;
63
64 for( int i = 0; i < aSkipLines; ++i )
65 {
66 std::getline( goldenStream, goldenLine );
67 std::getline( generatedStream, generatedLine );
68 }
69
70 int lineNum = aSkipLines + 1;
71
72 while( std::getline( goldenStream, goldenLine ) )
73 {
74 if( !std::getline( generatedStream, generatedLine ) )
75 {
76 BOOST_TEST_MESSAGE( "Generated file is shorter than golden at line " << lineNum );
77 return false;
78 }
79
80 if( goldenLine != generatedLine )
81 {
82 BOOST_TEST_MESSAGE( "Mismatch at line " << lineNum << ":\n"
83 << " golden: " << goldenLine << "\n"
84 << " generated: " << generatedLine );
85 return false;
86 }
87
88 ++lineNum;
89 }
90
91 if( std::getline( generatedStream, generatedLine ) )
92 {
93 BOOST_TEST_MESSAGE( "Generated file is longer than golden after line " << lineNum );
94 return false;
95 }
96
97 return true;
98}
99
100
101static size_t countOccurrences( const std::string& aHaystack, const std::string& aNeedle )
102{
103 size_t count = 0;
104
105 for( size_t pos = aHaystack.find( aNeedle ); pos != std::string::npos;
106 pos = aHaystack.find( aNeedle, pos + aNeedle.size() ) )
107 {
108 ++count;
109 }
110
111 return count;
112}
113
114
118static std::set<std::string> collectColours( const std::string& aSvg )
119{
120 std::set<std::string> colours;
121
122 for( size_t pos = aSvg.find( '#' ); pos != std::string::npos; pos = aSvg.find( '#', pos + 1 ) )
123 {
124 std::string colour = aSvg.substr( pos, 7 );
125
126 if( colour.size() < 7 )
127 continue;
128
129 if( colour.find_first_not_of( "0123456789ABCDEFabcdef", 1 ) != std::string::npos )
130 continue;
131
132 colours.insert( colour );
133 }
134
135 return colours;
136}
137
138
139BOOST_AUTO_TEST_SUITE( ApiJobs )
140
141
143{
144 BOOST_REQUIRE_MESSAGE( Start(), LastError() );
145
146 wxString testDataDir =
147 wxString::FromUTF8( KI_TEST::GetTestDataRootDir() ) + wxS( "cli/artwork_generation_regressions/" );
148
149 wxFileName boardPath( testDataDir, wxS( "ZoneFill-4.0.7.kicad_pcb" ) );
150
151 kiapi::common::types::DocumentSpecifier document;
152
153 BOOST_REQUIRE_MESSAGE( Client().OpenDocument( boardPath.GetFullPath(), &document ),
154 "OpenDocument failed: " + Client().LastError() );
155
156
157 KI_TEST::SCOPED_TEMP_DIR tempDir( "api_job_svg" );
158 wxFileName outputPath( tempDir.ChildPathStr( "plot.svg" ) );
159
160 kiapi::board::jobs::RunBoardJobExportSvg request;
161 *request.mutable_job_settings()->mutable_document() = document;
162 request.mutable_job_settings()->set_output_path( outputPath.GetFullPath().ToUTF8().data() );
163
164 request.mutable_plot_settings()->add_layers( kiapi::board::types::BL_F_Cu );
165 request.mutable_plot_settings()->set_black_and_white( true );
166 request.mutable_plot_settings()->set_plot_drawing_sheet( false );
167 request.mutable_plot_settings()->set_drill_marks( kiapi::board::jobs::PDM_FULL );
168 request.set_page_mode( kiapi::board::jobs::BJPM_EACH_LAYER_OWN_FILE );
169 request.set_precision( 4 );
170
171 kiapi::common::types::RunJobResponse response;
172 BOOST_REQUIRE_MESSAGE( Client().RunJob( request, &response ), "RunJob failed: " + Client().LastError() );
173
174 BOOST_REQUIRE_MESSAGE( response.status() == kiapi::common::types::JS_SUCCESS,
175 "Job failed: " + wxString::FromUTF8( response.message() ) );
176
177 BOOST_REQUIRE_MESSAGE( response.output_path_size() > 0, "Job returned no output paths" );
178
179 wxString generatedPath = wxString::FromUTF8( response.output_path( 0 ) );
180 BOOST_REQUIRE_MESSAGE( wxFileName::FileExists( generatedPath ), "Generated SVG does not exist: " + generatedPath );
181
182 // Plot fidelity is covered by the kicad-cli SVG regression tests, which rasterize and compare
183 // against golden artwork. All this test has to establish is that the job honoured the request.
184 const std::string svg = KI_TEST::LoadStringData( generatedPath );
185
186 BOOST_REQUIRE_MESSAGE( !svg.empty(), "Generated SVG is empty or unreadable: " + generatedPath );
187 BOOST_CHECK( svg.find( "<svg" ) != std::string::npos );
188 BOOST_CHECK( svg.find( "</svg>" ) != std::string::npos );
189
190 // The board page is A4, and the four decimals are the requested precision
191 BOOST_CHECK( svg.find( "width=\"297.0022mm\" height=\"210.0072mm\"" ) != std::string::npos );
192
193 std::set<std::string> colours = collectColours( svg );
194 BOOST_REQUIRE_MESSAGE( !colours.empty(), "Plot declares no colours at all" );
195
196 for( const std::string& colour : colours )
197 {
198 BOOST_CHECK_MESSAGE( colour == "#000000" || colour == "#FFFFFF",
199 "black_and_white was requested but the plot contains " + colour );
200 }
201
202 // F.Cu of this board is dense; a near-empty plot means the layer selection was dropped
203 BOOST_CHECK_GT( countOccurrences( svg, "<path" ), 100u );
204}
205
206
207// Reads the pixel dimensions out of the IHDR chunk of a PNG file.
208// Returns true on success; on failure aWidth/aHeight are untouched.
209static bool pngDimensions( const wxString& aPath, uint32_t& aWidth, uint32_t& aHeight )
210{
211 std::ifstream stream( aPath.ToStdString(), std::ios::binary );
212
213 if( !stream.is_open() )
214 return false;
215
216 char header[8];
217
218 if( !stream.read( header, 8 ) || memcmp( header, "\x89PNG\r\n\x1a\n", 8 ) != 0 )
219 return false;
220
221 char ihdr[16];
222
223 if( !stream.read( ihdr, 16 ) )
224 return false;
225
226 auto readBE32 = []( const char* aBytes )
227 {
228 return ( static_cast<uint32_t>( static_cast<unsigned char>( aBytes[0] ) ) << 24 )
229 | ( static_cast<uint32_t>( static_cast<unsigned char>( aBytes[1] ) ) << 16 )
230 | ( static_cast<uint32_t>( static_cast<unsigned char>( aBytes[2] ) ) << 8 )
231 | ( static_cast<uint32_t>( static_cast<unsigned char>( aBytes[3] ) ) );
232 };
233
234 aWidth = readBE32( ihdr + 8 );
235 aHeight = readBE32( ihdr + 12 );
236 return true;
237}
238
239
241{
242 BOOST_REQUIRE_MESSAGE( Start(), LastError() );
243
244 wxString testDataDir = wxString::FromUTF8( KI_TEST::GetTestDataRootDir() ) + wxS( "cli/basic_test/" );
245
246 wxFileName schPath( testDataDir, wxS( "basic_test.kicad_sch" ) );
247
248 kiapi::common::types::DocumentSpecifier document;
249
250 BOOST_REQUIRE_MESSAGE(
251 Client().OpenDocument( schPath.GetFullPath(), kiapi::common::types::DOCTYPE_SCHEMATIC, &document ),
252 "OpenDocument failed: " + Client().LastError() );
253
254 // --- All-sheets mode: output path is a directory
255 KI_TEST::SCOPED_TEMP_DIR tempDir( "api_job_sch_png" );
256 wxString outputDir = tempDir.CreateChildDirStr( "all" ) + wxFileName::GetPathSeparator();
257
258 {
259 kiapi::schematic::jobs::RunSchematicJobExportPng request;
260 *request.mutable_job_settings()->mutable_document() = document;
261 request.mutable_job_settings()->set_output_path( outputDir.ToUTF8().data() );
262 request.mutable_plot_settings()->set_sheet_mode( kiapi::schematic::jobs::SJSM_ALL_SHEETS );
263 request.mutable_plot_settings()->set_black_and_white( true );
264 request.set_dpi( 150 );
265
266 kiapi::common::types::RunJobResponse response;
267 BOOST_REQUIRE_MESSAGE( Client().RunJob( request, &response ),
268 "RunJob failed: " + Client().LastError() );
269
270 BOOST_REQUIRE_MESSAGE( response.status() == kiapi::common::types::JS_SUCCESS,
271 "Job failed: " + wxString::FromUTF8( response.message() ) );
272 BOOST_REQUIRE_MESSAGE( response.output_path_size() > 0, "Job returned no output paths" );
273
274 wxString generatedPath = wxString::FromUTF8( response.output_path( 0 ) );
275 BOOST_REQUIRE_MESSAGE( wxFileName::FileExists( generatedPath ),
276 "Generated PNG does not exist: " + generatedPath );
277
278 // basic_test.kicad_sch is A4 landscape; 297mm at 150dpi = 1754px (+/- rounding)
279 uint32_t width = 0, height = 0;
280 BOOST_REQUIRE_MESSAGE( pngDimensions( generatedPath, width, height ),
281 "Generated file is not a valid PNG: " + generatedPath );
282 BOOST_CHECK_MESSAGE( width >= 1750 && width <= 1758,
283 "Unexpected PNG width " << width << " for 150dpi A4 landscape" );
284 BOOST_CHECK_MESSAGE( height >= 1236 && height <= 1244,
285 "Unexpected PNG height " << height << " for 150dpi A4 landscape" );
286 }
287
288 // --- Single-sheet mode: output path is a file name
289 wxFileName singlePath( tempDir.ChildPathStr( "single.png" ) );
290
291 {
292 kiapi::schematic::jobs::RunSchematicJobExportPng request;
293 *request.mutable_job_settings()->mutable_document() = document;
294 request.mutable_job_settings()->set_output_path( singlePath.GetFullPath().ToUTF8().data() );
295 request.mutable_plot_settings()->set_sheet_mode( kiapi::schematic::jobs::SJSM_SINGLE_SHEET );
296 request.mutable_plot_settings()->set_black_and_white( true );
297 request.set_dpi( 300 );
298
299 kiapi::common::types::RunJobResponse response;
300 BOOST_REQUIRE_MESSAGE( Client().RunJob( request, &response ),
301 "Single-sheet RunJob failed: " + Client().LastError() );
302
303 BOOST_REQUIRE_MESSAGE( response.status() == kiapi::common::types::JS_SUCCESS,
304 "Single-sheet job failed: " + wxString::FromUTF8( response.message() ) );
305 BOOST_REQUIRE_MESSAGE( response.output_path_size() > 0,
306 "Single-sheet job returned no output paths" );
307
308 wxString generatedPath = wxString::FromUTF8( response.output_path( 0 ) );
309 BOOST_REQUIRE_MESSAGE( wxFileName::FileExists( generatedPath ),
310 "Generated PNG does not exist: " + generatedPath );
311
312 // 297mm at 300dpi = 3508px (+/- rounding)
313 uint32_t width = 0, height = 0;
314 BOOST_REQUIRE_MESSAGE( pngDimensions( generatedPath, width, height ),
315 "Generated file is not a valid PNG: " + generatedPath );
316 BOOST_CHECK_MESSAGE( width >= 3500 && width <= 3516,
317 "Unexpected PNG width " << width << " for 300dpi A4 landscape" );
318
319 // The single-file output path must be honored exactly
320 BOOST_CHECK_MESSAGE( generatedPath == singlePath.GetFullPath(),
321 "Single-sheet output path not honored: " + generatedPath );
322 }
323
324 // --- Out-of-range dpi is rejected with a bad-request error
325 {
326 kiapi::schematic::jobs::RunSchematicJobExportPng request;
327 *request.mutable_job_settings()->mutable_document() = document;
328 request.mutable_job_settings()->set_output_path( outputDir.ToUTF8().data() );
329 request.mutable_plot_settings()->set_sheet_mode( kiapi::schematic::jobs::SJSM_ALL_SHEETS );
330 request.set_dpi( 10 );
331
332 kiapi::common::types::RunJobResponse response;
333 BOOST_CHECK_MESSAGE( !Client().RunJob( request, &response ),
334 "Out-of-range dpi was not rejected" );
335 }
336
337 // --- Omitted dpi falls back to the GUI default (300)
338 {
339 wxString defaultDir = tempDir.CreateChildDirStr( "default" ) + wxFileName::GetPathSeparator();
340
341 kiapi::schematic::jobs::RunSchematicJobExportPng request;
342 *request.mutable_job_settings()->mutable_document() = document;
343 request.mutable_job_settings()->set_output_path( defaultDir.ToUTF8().data() );
344 request.mutable_plot_settings()->set_sheet_mode( kiapi::schematic::jobs::SJSM_ALL_SHEETS );
345
346 kiapi::common::types::RunJobResponse response;
347 BOOST_REQUIRE_MESSAGE( Client().RunJob( request, &response ),
348 "Default-dpi RunJob failed: " + Client().LastError() );
349 BOOST_REQUIRE_MESSAGE( response.status() == kiapi::common::types::JS_SUCCESS,
350 "Default-dpi job failed: " + wxString::FromUTF8( response.message() ) );
351 BOOST_REQUIRE_MESSAGE( response.output_path_size() > 0,
352 "Default-dpi job returned no output paths" );
353
354 uint32_t width = 0, height = 0;
355 wxString generatedPath = wxString::FromUTF8( response.output_path( 0 ) );
356 BOOST_REQUIRE_MESSAGE( pngDimensions( generatedPath, width, height ),
357 "Generated file is not a valid PNG: " + generatedPath );
358 BOOST_CHECK_MESSAGE( width >= 3500 && width <= 3516,
359 "Default dpi should be 300; got width " << width );
360 }
361}
362
363
365{
366 BOOST_REQUIRE_MESSAGE( Start(), LastError() );
367
368 wxString testDataDir = wxString::FromUTF8( KI_TEST::GetTestDataRootDir() ) + wxS( "cli/basic_test/" );
369
370 wxFileName boardPath( testDataDir, wxS( "basic_test.kicad_pcb" ) );
371
372 kiapi::common::types::DocumentSpecifier document;
373
374 BOOST_REQUIRE_MESSAGE( Client().OpenDocument( boardPath.GetFullPath(), &document ),
375 "OpenDocument failed: " + Client().LastError() );
376
377 KI_TEST::SCOPED_TEMP_DIR tempDir( "api_job_drill" );
378 wxString outputDir = tempDir.CreateChildDirStr( "drill" ) + wxFileName::GetPathSeparator();
379
380 kiapi::board::jobs::RunBoardJobExportDrill request;
381 *request.mutable_job_settings()->mutable_document() = document;
382 request.mutable_job_settings()->set_output_path( outputDir.ToUTF8().data() );
383 request.set_format( kiapi::board::jobs::DF_EXCELLON );
384
385 kiapi::common::types::RunJobResponse response;
386 BOOST_REQUIRE_MESSAGE( Client().RunJob( request, &response ), "RunJob failed: " + Client().LastError() );
387
388 BOOST_REQUIRE_MESSAGE( response.status() == kiapi::common::types::JS_SUCCESS,
389 "Job failed: " + wxString::FromUTF8( response.message() ) );
390
391 BOOST_REQUIRE_MESSAGE( response.output_path_size() > 0, "Job returned no output paths" );
392 wxString generatedDrillPath;
393
394 for( int i = 0; i < response.output_path_size(); ++i )
395 {
396 wxString outputPath = wxString::FromUTF8( response.output_path( i ) );
397
398 if( outputPath.EndsWith( wxS( ".drl" ) ) )
399 {
400 generatedDrillPath = outputPath;
401 break;
402 }
403 }
404
405 BOOST_REQUIRE_MESSAGE( !generatedDrillPath.IsEmpty(), "No .drl file found in job output" );
406 BOOST_REQUIRE_MESSAGE( wxFileName::FileExists( generatedDrillPath ),
407 "Generated drill file does not exist: " + generatedDrillPath );
408
409 wxString goldenPath = testDataDir + wxS( "basic_test_excellon_inches.drl" );
410 BOOST_CHECK_MESSAGE( textFilesMatch( goldenPath, generatedDrillPath, 5 ),
411 "Drill output does not match golden file" );
412}
413
414
416{
417 BOOST_REQUIRE_MESSAGE( Start(), LastError() );
418
419 wxFileName boardPath( wxString::FromUTF8( KI_TEST::GetTestDataRootDir() ) + wxS( "pcbnew/issue24735/" ),
420 wxS( "issue24735.kicad_pcb" ) );
421
422 kiapi::common::types::DocumentSpecifier document;
423
424 BOOST_REQUIRE_MESSAGE( Client().OpenDocument( boardPath.GetFullPath(), &document ),
425 "OpenDocument failed: " + Client().LastError() );
426
427 KI_TEST::SCOPED_TEMP_DIR tempDir( "api_job_odb" );
428 wxFileName target( tempDir.CreateChildDirStr( "odb" ), wxS( "combined" ) );
429
430 kiapi::board::jobs::RunBoardJobExportODB request;
431 *request.mutable_job_settings()->mutable_document() = document;
432 request.mutable_job_settings()->set_output_path( target.GetFullPath().ToUTF8().data() );
433 request.set_units( kiapi::common::types::U_MM );
434 request.set_compression( kiapi::board::jobs::ODBC_NONE );
435 request.add_variant_names( "var1" );
436 request.add_variant_names( "var2" );
437 request.set_variant_packaging( kiapi::board::jobs::ODBVP_COMBINED );
438
439 kiapi::common::types::RunJobResponse response;
440 BOOST_REQUIRE_MESSAGE( Client().RunJob( request, &response ), "RunJob failed: " + Client().LastError() );
441 BOOST_REQUIRE_MESSAGE( response.status() == kiapi::common::types::JS_SUCCESS,
442 "Job failed: " + wxString::FromUTF8( response.message() ) );
443
444 auto hasLine = []( const wxString& aPackage, const wxString& aDir, const std::string& aLine )
445 {
446 wxFileName file( aPackage + wxFileName::GetPathSeparator() + aDir, wxS( "attrlist" ) );
447 std::ifstream input( ToFsPath( file.GetFullPath() ) );
448 std::string line;
449
450 while( std::getline( input, line ) )
451 {
452 if( line == aLine )
453 return true;
454 }
455
456 return false;
457 };
458
459 BOOST_REQUIRE_EQUAL( response.output_path_size(), 1 );
460 wxString combined = wxString::FromUTF8( response.output_path( 0 ) );
461 BOOST_REQUIRE_MESSAGE( wxFileName::DirExists( combined ), "Missing reported output " + combined );
462 BOOST_CHECK_MESSAGE( wxFileName::DirName( combined ).SameAs( wxFileName::DirName( target.GetFullPath() ) ),
463 "Combined output ignored the requested path: " + combined );
464 BOOST_CHECK( hasLine( combined, wxS( "misc" ), ".variant_list=var1:var2" ) );
465 BOOST_CHECK( hasLine( combined, wxS( "steps/pcb" ), ".current_variant=var1" ) );
466
467 // One package per variant proves every repeated variant name reached the job
468 wxFileName separateTarget( tempDir.CreateChildDirStr( "odb_separate" ), wxS( "separate" ) );
469 request.mutable_job_settings()->set_output_path( separateTarget.GetFullPath().ToUTF8().data() );
470 request.set_variant_packaging( kiapi::board::jobs::ODBVP_SEPARATE );
471 response.Clear();
472
473 BOOST_REQUIRE_MESSAGE( Client().RunJob( request, &response ), "RunJob failed: " + Client().LastError() );
474 BOOST_REQUIRE_MESSAGE( response.status() == kiapi::common::types::JS_SUCCESS,
475 "Separate job failed: " + wxString::FromUTF8( response.message() ) );
476 BOOST_REQUIRE_EQUAL( response.output_path_size(), 2 );
477 std::set<std::string> currentVariants;
478
479 for( int i = 0; i < response.output_path_size(); ++i )
480 {
481 wxString package = wxString::FromUTF8( response.output_path( i ) );
482 BOOST_REQUIRE_MESSAGE( wxFileName::DirExists( package ), "Missing reported output " + package );
483 BOOST_CHECK_MESSAGE( package.StartsWith( separateTarget.GetFullPath() + wxS( "-" ) ),
484 "Separate output ignored the requested path: " + package );
485
486 for( const std::string variant : { "var1", "var2" } )
487 {
488 if( hasLine( package, wxS( "steps/pcb" ), ".current_variant=" + variant ) )
489 currentVariants.insert( variant );
490 }
491 }
492
493 BOOST_CHECK( currentVariants == std::set<std::string>( { "var1", "var2" } ) );
494}
495
496
498{
499 BOOST_REQUIRE_MESSAGE( Start(), LastError() );
500
501 wxString testDataDir =
502 wxString::FromUTF8( KI_TEST::GetTestDataRootDir() ) + wxS( "cli/artwork_generation_regressions/" );
503
504 wxFileName boardPath( testDataDir, wxS( "ZoneFill-4.0.7.kicad_pcb" ) );
505
506 kiapi::common::types::DocumentSpecifier document;
507
508 BOOST_REQUIRE_MESSAGE( Client().OpenDocument( boardPath.GetFullPath(), &document ),
509 "OpenDocument failed: " + Client().LastError() );
510
511 KI_TEST::SCOPED_TEMP_DIR tempDir( "api_job_pcb_png" );
512
513 // --- MULTI mode: one file per layer into a directory
514 {
515 wxString outputDir = tempDir.CreateChildDirStr( "multi" ) + wxFileName::GetPathSeparator();
516
517 kiapi::board::jobs::RunBoardJobExportPng request;
518 *request.mutable_job_settings()->mutable_document() = document;
519 request.mutable_job_settings()->set_output_path( outputDir.ToUTF8().data() );
520 request.mutable_plot_settings()->add_layers( kiapi::board::types::BL_F_Cu );
521 request.mutable_plot_settings()->set_black_and_white( true );
522 request.set_page_mode( kiapi::board::jobs::BJPM_EACH_LAYER_OWN_FILE );
523 request.set_dpi( 150 );
524
525 kiapi::common::types::RunJobResponse response;
526 BOOST_REQUIRE_MESSAGE( Client().RunJob( request, &response ),
527 "RunJob failed: " + Client().LastError() );
528
529 BOOST_REQUIRE_MESSAGE( response.status() == kiapi::common::types::JS_SUCCESS,
530 "Job failed: " + wxString::FromUTF8( response.message() ) );
531 BOOST_REQUIRE_MESSAGE( response.output_path_size() > 0, "Job returned no output paths" );
532
533 wxString generatedPath = wxString::FromUTF8( response.output_path( 0 ) );
534 BOOST_REQUIRE_MESSAGE( wxFileName::FileExists( generatedPath ),
535 "Generated PNG does not exist: " + generatedPath );
536
537 uint32_t width = 0, height = 0;
538 BOOST_REQUIRE_MESSAGE( pngDimensions( generatedPath, width, height ),
539 "Generated file is not a valid PNG: " + generatedPath );
540 BOOST_CHECK_MESSAGE( width >= 1750 && width <= 1758,
541 "Unexpected PNG width " << width << " for 150dpi A4 landscape" );
542 }
543
544 // --- SINGLE mode: all layers composited into one file
545 {
546 wxFileName outputPath( tempDir.ChildPathStr( "single.png" ) );
547
548 kiapi::board::jobs::RunBoardJobExportPng request;
549 *request.mutable_job_settings()->mutable_document() = document;
550 request.mutable_job_settings()->set_output_path( outputPath.GetFullPath().ToUTF8().data() );
551 request.mutable_plot_settings()->add_layers( kiapi::board::types::BL_F_Cu );
552 request.mutable_plot_settings()->add_layers( kiapi::board::types::BL_F_SilkS );
553 request.mutable_plot_settings()->set_black_and_white( true );
554 request.set_page_mode( kiapi::board::jobs::BJPM_ALL_LAYERS_ONE_PAGE );
555 request.set_dpi( 300 );
556
557 kiapi::common::types::RunJobResponse response;
558 BOOST_REQUIRE_MESSAGE( Client().RunJob( request, &response ),
559 "Single-mode RunJob failed: " + Client().LastError() );
560
561 BOOST_REQUIRE_MESSAGE( response.status() == kiapi::common::types::JS_SUCCESS,
562 "Single-mode job failed: " + wxString::FromUTF8( response.message() ) );
563 BOOST_REQUIRE_MESSAGE( response.output_path_size() > 0,
564 "Single-mode job returned no output paths" );
565
566 wxString generatedPath = wxString::FromUTF8( response.output_path( 0 ) );
567 BOOST_REQUIRE_MESSAGE( wxFileName::FileExists( generatedPath ),
568 "Generated PNG does not exist: " + generatedPath );
569 BOOST_CHECK_MESSAGE( generatedPath == outputPath.GetFullPath(),
570 "Single-mode output path not honored: " + generatedPath );
571
572 uint32_t width = 0, height = 0;
573 BOOST_REQUIRE_MESSAGE( pngDimensions( generatedPath, width, height ),
574 "Generated file is not a valid PNG: " + generatedPath );
575 BOOST_CHECK_MESSAGE( width >= 3500 && width <= 3516,
576 "Unexpected PNG width " << width << " for 300dpi A4 landscape" );
577 }
578}
579
580
582{
583 BOOST_REQUIRE_MESSAGE( Start(), LastError() );
584
585 wxString testDataDir = wxString::FromUTF8( KI_TEST::GetTestDataRootDir() ) + wxS( "cli/basic_test/" );
586
587 wxFileName schPath( testDataDir, wxS( "basic_test.kicad_sch" ) );
588
589 kiapi::common::types::DocumentSpecifier document;
590
591 BOOST_REQUIRE_MESSAGE(
592 Client().OpenDocument( schPath.GetFullPath(), kiapi::common::types::DOCTYPE_SCHEMATIC, &document ),
593 "OpenDocument failed: " + Client().LastError() );
594
595 KI_TEST::SCOPED_TEMP_DIR tempDir( "api_job_netlist" );
596 wxFileName outputPath( tempDir.ChildPathStr( "netlist.cadstar" ) );
597
598 kiapi::schematic::jobs::RunSchematicJobExportNetlist request;
599 *request.mutable_job_settings()->mutable_document() = document;
600 request.mutable_job_settings()->set_output_path( outputPath.GetFullPath().ToUTF8().data() );
601 request.set_format( kiapi::schematic::jobs::SNF_CADSTAR );
602
603 kiapi::common::types::RunJobResponse response;
604 BOOST_REQUIRE_MESSAGE( Client().RunJob( request, &response ), "RunJob failed: " + Client().LastError() );
605
606 BOOST_REQUIRE_MESSAGE( response.status() == kiapi::common::types::JS_SUCCESS,
607 "Job failed: " + wxString::FromUTF8( response.message() ) );
608
609 BOOST_REQUIRE_MESSAGE( response.output_path_size() > 0, "Job returned no output paths" );
610
611 wxString generatedPath = wxString::FromUTF8( response.output_path( 0 ) );
612 BOOST_REQUIRE_MESSAGE( wxFileName::FileExists( generatedPath ),
613 "Generated netlist does not exist: " + generatedPath );
614
615 // 3 header lines to skip (contain timestamp/version)
616 wxString goldenPath = testDataDir + wxS( "basic_test.netlist.cadstar" );
617 BOOST_CHECK_MESSAGE( textFilesMatch( goldenPath, generatedPath, 3 ), "Netlist output does not match golden file" );
618}
619
620
622{
623 BOOST_REQUIRE_MESSAGE( Start(), LastError() );
624
625 wxString testDataDir = wxString::FromUTF8( KI_TEST::GetTestDataRootDir() ) + wxS( "cli/variants/" );
626
627 wxFileName schPath( testDataDir, wxS( "variants.kicad_sch" ) );
628
629 kiapi::common::types::DocumentSpecifier document;
630
631 BOOST_REQUIRE_MESSAGE(
632 Client().OpenDocument( schPath.GetFullPath(), kiapi::common::types::DOCTYPE_SCHEMATIC, &document ),
633 "OpenDocument failed: " + Client().LastError() );
634
635 KI_TEST::SCOPED_TEMP_DIR tempDir( "api_job_bom" );
636 wxFileName outputPath( tempDir.ChildPathStr( "bom.csv" ) );
637
638 kiapi::schematic::jobs::RunSchematicJobExportBOM request;
639 *request.mutable_job_settings()->mutable_document() = document;
640 request.mutable_job_settings()->set_output_path( outputPath.GetFullPath().ToUTF8().data() );
641 request.set_exclude_dnp( true );
642
643 request.mutable_format()->set_preset_name( "CSV" );
644
645 auto* refField = request.mutable_fields()->add_fields();
646 refField->set_name( "Reference" );
647 refField->set_label( "Refs" );
648 refField->set_group_by( false );
649
650 auto* valField = request.mutable_fields()->add_fields();
651 valField->set_name( "Value" );
652 valField->set_label( "Value" );
653 valField->set_group_by( false );
654
655 kiapi::common::types::RunJobResponse response;
656 BOOST_REQUIRE_MESSAGE( Client().RunJob( request, &response ), "RunJob failed: " + Client().LastError() );
657
658 BOOST_REQUIRE_MESSAGE( response.status() == kiapi::common::types::JS_SUCCESS,
659 "Job failed: " + wxString::FromUTF8( response.message() ) );
660
661 BOOST_REQUIRE_MESSAGE( response.output_path_size() > 0, "Job returned no output paths" );
662
663 wxString generatedPath = wxString::FromUTF8( response.output_path( 0 ) );
664 BOOST_REQUIRE_MESSAGE( wxFileName::FileExists( generatedPath ), "Generated BOM does not exist: " + generatedPath );
665
666 wxString goldenPath = testDataDir + wxS( "variants_default.bom.csv" );
667 BOOST_CHECK_MESSAGE( textFilesMatch( goldenPath, generatedPath, 0 ), "BOM output does not match golden file" );
668
669 request.mutable_format()->clear_preset_name();
670 request.mutable_format()->set_field_delimiter( "," );
671 request.mutable_format()->set_string_delimiter( "\"" );
672 request.mutable_format()->set_ref_delimiter( "," );
673 request.mutable_format()->set_ref_range_delimiter( "" );
674 request.mutable_format()->set_include_byte_order_mark( true );
675 response.Clear();
676
677 BOOST_REQUIRE_MESSAGE( Client().RunJob( request, &response ),
678 "BOM job with byte order mark failed: " + Client().LastError() );
679
680 BOOST_REQUIRE_MESSAGE( response.status() == kiapi::common::types::JS_SUCCESS,
681 "BOM job with byte order mark failed: " + wxString::FromUTF8( response.message() ) );
682 BOOST_REQUIRE_MESSAGE( response.output_path_size() > 0, "BOM job with byte order mark returned no output paths" );
683
684 generatedPath = wxString::FromUTF8( response.output_path( 0 ) );
685 std::ifstream generatedStream( generatedPath.ToStdString(), std::ios::binary );
686 std::ifstream goldenStream( goldenPath.ToStdString(), std::ios::binary );
687 BOOST_REQUIRE( generatedStream.is_open() );
688 BOOST_REQUIRE( goldenStream.is_open() );
689
690 std::ostringstream generatedContents;
691 std::ostringstream goldenContents;
692 generatedContents << generatedStream.rdbuf();
693 goldenContents << goldenStream.rdbuf();
694
695 const std::string utf8ByteOrderMark( "\xEF\xBB\xBF", 3 );
696 const std::string bom = generatedContents.str();
697 BOOST_REQUIRE_GE( bom.size(), utf8ByteOrderMark.size() );
698 BOOST_CHECK_EQUAL( bom.substr( 0, utf8ByteOrderMark.size() ), utf8ByteOrderMark );
699 BOOST_CHECK_EQUAL( bom.substr( utf8ByteOrderMark.size() ), goldenContents.str() );
700
701 generatedStream.close();
702 goldenStream.close();
703}
704
705
Per-test fixture for API E2E tests.
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:909
std::string GetTestDataRootDir()
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")