24#include <boost/test/unit_test.hpp>
29#include <wx/filename.h>
30#include <wx/process.h>
31#include <wx/stdpaths.h>
32#include <wx/txtstrm.h>
39#ifndef QA_KICAD_CLI_PATH
40#define QA_KICAD_CLI_PATH "kicad-cli"
51 static int counter = 0;
52 m_path = wxFileName::GetTempDir() + wxFILE_SEP_PATH
53 + wxString::Format( wxS(
"kicad_cli_visual_diff_%ld_%d" ),
54 static_cast<long>( wxGetProcessId() ), ++counter );
56 BOOST_REQUIRE( wxFileName::Mkdir( m_path, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
61 if( !m_path.IsEmpty() && wxFileName::DirExists( m_path ) )
62 wxFileName::Rmdir( m_path, wxPATH_RMDIR_RECURSIVE );
65 wxString Path(
const wxString& aName )
const
67 return m_path + wxFILE_SEP_PATH + aName;
82wxString readStream( wxInputStream* aStream )
89 wxTextInputStream textStream( *aStream );
91 while( !aStream->Eof() )
93 wxString line = textStream.ReadLine();
95 if( !line.IsEmpty() || !aStream->Eof() )
108COMMAND_RESULT runCli(
const std::vector<wxString>& aArgs )
113 std::vector<const wchar_t*> argv;
114 argv.reserve( aArgs.size() + 2 );
117 for(
const wxString& arg : aArgs )
118 argv.push_back( arg.wc_str() );
120 argv.push_back(
nullptr );
123 result.exitCode =
static_cast<int>( wxExecute(
const_cast<wchar_t**
>( argv.data() ), wxEXEC_SYNC, &
process ) );
131void writeTextFile(
const wxString& aPath,
const char* aContent )
134 BOOST_REQUIRE_MESSAGE( file.Create( aPath,
true ),
"Could not create " << aPath );
135 BOOST_REQUIRE( file.Write( wxString::FromUTF8( aContent ) ) );
140std::string readFileBytes(
const wxString& aPath )
142 wxFile file( aPath );
143 BOOST_REQUIRE_MESSAGE( file.IsOpened(),
"Could not open " << aPath );
145 const wxFileOffset len = file.Length();
146 BOOST_REQUIRE_GE( len, 0 );
148 std::string bytes(
static_cast<size_t>( len ),
'\0' );
152 ssize_t read = file.Read( bytes.data(),
static_cast<size_t>( len ) );
153 BOOST_REQUIRE_EQUAL( read, len );
160void expectCleanExit(
const wxString& aName,
const COMMAND_RESULT& aResult,
int aExpectedExitCode )
171void expectInvalidExit(
const wxString& aName,
const COMMAND_RESULT& aResult,
int aExpectedExitCode )
180void expectSvg(
const wxString& aName,
const wxString& aPath )
184 std::string bytes = readFileBytes( aPath );
186 BOOST_CHECK( bytes.find(
"<svg" ) != std::string::npos );
191void expectPng(
const wxString& aName,
const wxString& aPath )
193 static constexpr std::array<unsigned char, 8> PNG_HEADER = { 0x89,
'P',
'N',
'G',
'\r',
'\n', 0x1a,
'\n' };
197 std::string bytes = readFileBytes( aPath );
198 BOOST_REQUIRE_GE( bytes.size(), PNG_HEADER.size() );
200 for(
size_t i = 0; i < PNG_HEADER.size(); ++i )
206void expectFilesDiffer(
const wxString& aName,
const wxString& aPathA,
const wxString& aPathB )
210 BOOST_CHECK( readFileBytes( aPathA ) != readFileBytes( aPathB ) );
217 explicit CLI_FIXTURES( TEMP_DIR& aDir )
219 pcbA = aDir.Path( wxS(
"pcb_a.kicad_pcb" ) );
220 pcbB = aDir.Path( wxS(
"pcb_b.kicad_pcb" ) );
221 schA = aDir.Path( wxS(
"sch_a.kicad_sch" ) );
222 schB = aDir.Path( wxS(
"sch_b.kicad_sch" ) );
223 fpA = aDir.Path( wxS(
"fp_a.pretty" ) );
224 fpB = aDir.Path( wxS(
"fp_b.pretty" ) );
225 symA = aDir.Path( wxS(
"sym_a.kicad_sym" ) );
226 symB = aDir.Path( wxS(
"sym_b.kicad_sym" ) );
234 void writePcbFixtures()
236 writeTextFile( pcbA, R
"(
237(kicad_pcb (version 20241228) (generator "pcbnew") (generator_version "9.0")
238 (general (thickness 1.6))
243 (44 "Edge.Cuts" user)
248 writeTextFile( pcbB, R"(
249(kicad_pcb (version 20241228) (generator "pcbnew") (generator_version "9.0")
250 (general (thickness 1.6))
255 (44 "Edge.Cuts" user)
257 (gr_line (start 10 10) (end 40 10)
258 (stroke (width 0.15) (type solid)) (layer "Edge.Cuts") (uuid "11111111-1111-1111-1111-111111111111"))
263 void writeSchFixtures()
265 writeTextFile( schA, R
"(
266(kicad_sch (version 20230121) (generator "eeschema")
267 (uuid "00000000-0000-0000-0000-000000000001")
272 writeTextFile( schB, R"(
273(kicad_sch (version 20230121) (generator "eeschema")
274 (uuid "00000000-0000-0000-0000-000000000001")
276 (wire (pts (xy 25.4 25.4) (xy 50.8 25.4))
277 (stroke (width 0) (type default))
278 (uuid "11111111-1111-1111-1111-111111111111")
284 void writeFpFixtures()
286 BOOST_REQUIRE( wxFileName::Mkdir( fpA, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
287 BOOST_REQUIRE( wxFileName::Mkdir( fpB, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
289 writeTextFile( fpB + wxFILE_SEP_PATH + wxS(
"VisualDiff.kicad_mod" ), R
"(
290(footprint "VisualDiff"
297 (stroke (width 0.12) (type solid))
299 (uuid "11111111-1111-1111-1111-111111111111")
305 void writeSymFixtures()
307 writeTextFile( symA, R
"(
310 (generator "kicad_symbol_editor")
311 (generator_version "9.0")
315 writeTextFile( symB, R"(
318 (generator "kicad_symbol_editor")
319 (generator_version "9.0")
321 (exclude_from_sim no)
324 (property "Reference" "U" (at 0 2.54 0) (effects (font (size 1.27 1.27))))
325 (property "Value" "VisualDiff" (at 0 -2.54 0) (effects (font (size 1.27 1.27))))
326 (property "Footprint" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
327 (property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
328 (symbol "VisualDiff_1_1"
329 (rectangle (start -2.54 -2.54) (end 5.08 2.54) (stroke (width 0.254) (type default)) (fill (type none)))
349 wxString commandGroup;
350 wxString commandName;
352 wxString changedPath;
356void expectVisualDifference( TEMP_DIR& aDir,
const CLI_CASE& aCase,
const wxString& aFormat )
358 const wxString caseName = aCase.commandGroup + wxS(
" " ) + aFormat;
359 const wxString sameOut = aDir.Path( aCase.commandGroup + wxS(
"_same." ) + aFormat );
360 const wxString diffOut = aDir.Path( aCase.commandGroup + wxS(
"_diff." ) + aFormat );
362 std::vector<wxString> sameArgs = { aCase.commandGroup, aCase.commandName, aCase.refPath, aCase.refPath,
363 wxS(
"--format" ), aFormat, wxS(
"--output" ), sameOut };
364 std::vector<wxString> diffArgs = { aCase.commandGroup, aCase.commandName, aCase.refPath, aCase.changedPath,
365 wxS(
"--format" ), aFormat, wxS(
"--output" ), diffOut };
367 expectCleanExit( caseName + wxS(
" identical" ), runCli( sameArgs ), 0 );
368 expectCleanExit( caseName + wxS(
" changed" ), runCli( diffArgs ), 5 );
370 if( aFormat == wxS(
"svg" ) )
372 expectSvg( caseName + wxS(
" identical" ), sameOut );
373 expectSvg( caseName + wxS(
" changed" ), diffOut );
377 expectPng( caseName + wxS(
" identical" ), sameOut );
378 expectPng( caseName + wxS(
" changed" ), diffOut );
381 expectFilesDiffer( caseName, sameOut, diffOut );
403 BOOST_CHECK( j.contains(
"output_filename" ) );
404 BOOST_CHECK_EQUAL( j[
"output_filename"].get<wxString>(), wxString( wxS(
"diff-out.json" ) ) );
405 BOOST_CHECK( !j.contains(
"output" ) );
418 CLI_FIXTURES fixtures( dir );
420 const std::vector<CLI_CASE> cases = {
421 { wxS(
"pcb" ), wxS(
"diff" ), fixtures.pcbA, fixtures.pcbB },
422 { wxS(
"sch" ), wxS(
"diff" ), fixtures.schA, fixtures.schB },
423 { wxS(
"fp" ), wxS(
"diff" ), fixtures.fpA, fixtures.fpB },
424 { wxS(
"sym" ), wxS(
"diff" ), fixtures.symA, fixtures.symB },
427 for(
const CLI_CASE& c : cases )
431 expectInvalidExit( wxS(
"invalid format" ),
432 runCli( { c.commandGroup, c.commandName, c.refPath, c.refPath, wxS(
"--format" ),
436 expectInvalidExit( wxS(
"svg requires output" ),
437 runCli( { c.commandGroup, c.commandName, c.refPath, c.refPath, wxS(
"--format" ),
441 expectInvalidExit( wxS(
"png requires output" ),
442 runCli( { c.commandGroup, c.commandName, c.refPath, c.refPath, wxS(
"--format" ),
453 CLI_FIXTURES fixtures( dir );
455 const std::vector<CLI_CASE> cases = {
456 { wxS(
"pcb" ), wxS(
"diff" ), fixtures.pcbA, fixtures.pcbB },
457 { wxS(
"sch" ), wxS(
"diff" ), fixtures.schA, fixtures.schB },
458 { wxS(
"fp" ), wxS(
"diff" ), fixtures.fpA, fixtures.fpB },
459 { wxS(
"sym" ), wxS(
"diff" ), fixtures.symA, fixtures.symB },
462 for(
const CLI_CASE& c : cases )
464 expectVisualDifference( dir, c, wxS(
"svg" ) );
465 expectVisualDifference( dir, c, wxS(
"png" ) );
#define QA_KICAD_CLI_PATH
Job: diff two PCB files end-to-end via PCB_DIFFER.
void SetConfiguredOutputPath(const wxString &aPath)
Sets the configured output path for the job, this path is always saved to file.
wxString GetConfiguredOutputPath() const
Returns the configured output path for the job.
virtual void ToJson(nlohmann::json &j) const
static PGM_BASE * process
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_CASE(DiffJobSerializesSingleOutputKey)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
BOOST_CHECK_MESSAGE(totalMismatches==0, std::to_string(totalMismatches)+" board(s) with strategy disagreements")
BOOST_TEST_CONTEXT("Test Clearance")
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")