KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_cli_visual_diff.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,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU 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, you may find one here:
18 * http://www.gnu.org/licenses/gpl-3.0.html
19 * or you may search the http://www.gnu.org website for the version 3 license,
20 * or you may write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 */
23
24#include <boost/test/unit_test.hpp>
25
26#include <jobs/job_pcb_diff.h>
27
28#include <wx/file.h>
29#include <wx/filename.h>
30#include <wx/process.h>
31#include <wx/stdpaths.h>
32#include <wx/txtstrm.h>
33
34#include <array>
35#include <string>
36#include <vector>
37
38
39#ifndef QA_KICAD_CLI_PATH
40#define QA_KICAD_CLI_PATH "kicad-cli"
41#endif
42
43
44namespace
45{
46
47struct TEMP_DIR
48{
49 TEMP_DIR()
50 {
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 );
55
56 BOOST_REQUIRE( wxFileName::Mkdir( m_path, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
57 }
58
59 ~TEMP_DIR()
60 {
61 if( !m_path.IsEmpty() && wxFileName::DirExists( m_path ) )
62 wxFileName::Rmdir( m_path, wxPATH_RMDIR_RECURSIVE );
63 }
64
65 wxString Path( const wxString& aName ) const
66 {
67 return m_path + wxFILE_SEP_PATH + aName;
68 }
69
70 wxString m_path;
71};
72
73
74struct COMMAND_RESULT
75{
76 int exitCode = -1;
77 wxString output;
78 wxString error;
79};
80
81
82wxString readStream( wxInputStream* aStream )
83{
84 wxString output;
85
86 if( !aStream )
87 return output;
88
89 wxTextInputStream textStream( *aStream );
90
91 while( !aStream->Eof() )
92 {
93 wxString line = textStream.ReadLine();
94
95 if( !line.IsEmpty() || !aStream->Eof() )
96 {
97 if( !output.IsEmpty() )
98 output += wxS( "\n" );
99
100 output += line;
101 }
102 }
103
104 return output;
105}
106
107
108COMMAND_RESULT runCli( const std::vector<wxString>& aArgs )
109{
110 wxProcess process;
111 process.Redirect();
112
113 std::vector<const wchar_t*> argv;
114 argv.reserve( aArgs.size() + 2 );
115 argv.push_back( wxS( QA_KICAD_CLI_PATH ) );
116
117 for( const wxString& arg : aArgs )
118 argv.push_back( arg.wc_str() );
119
120 argv.push_back( nullptr );
121
122 COMMAND_RESULT result;
123 result.exitCode = static_cast<int>( wxExecute( const_cast<wchar_t**>( argv.data() ), wxEXEC_SYNC, &process ) );
124 result.output = readStream( process.GetInputStream() );
125 result.error = readStream( process.GetErrorStream() );
126
127 return result;
128}
129
130
131void writeTextFile( const wxString& aPath, const char* aContent )
132{
133 wxFile file;
134 BOOST_REQUIRE_MESSAGE( file.Create( aPath, true ), "Could not create " << aPath );
135 BOOST_REQUIRE( file.Write( wxString::FromUTF8( aContent ) ) );
136 file.Close();
137}
138
139
140std::string readFileBytes( const wxString& aPath )
141{
142 wxFile file( aPath );
143 BOOST_REQUIRE_MESSAGE( file.IsOpened(), "Could not open " << aPath );
144
145 const wxFileOffset len = file.Length();
146 BOOST_REQUIRE_GE( len, 0 );
147
148 std::string bytes( static_cast<size_t>( len ), '\0' );
149
150 if( len > 0 )
151 {
152 ssize_t read = file.Read( bytes.data(), static_cast<size_t>( len ) );
153 BOOST_REQUIRE_EQUAL( read, len );
154 }
155
156 return bytes;
157}
158
159
160void expectCleanExit( const wxString& aName, const COMMAND_RESULT& aResult, int aExpectedExitCode )
161{
162 BOOST_TEST_CONTEXT( aName )
163 {
164 BOOST_CHECK_EQUAL( aResult.exitCode, aExpectedExitCode );
165 BOOST_CHECK_MESSAGE( aResult.output.IsEmpty(), "Unexpected stdout: " << aResult.output );
166 BOOST_CHECK_MESSAGE( aResult.error.IsEmpty(), "Unexpected stderr: " << aResult.error );
167 }
168}
169
170
171void expectInvalidExit( const wxString& aName, const COMMAND_RESULT& aResult, int aExpectedExitCode )
172{
173 BOOST_TEST_CONTEXT( aName )
174 {
175 BOOST_CHECK_EQUAL( aResult.exitCode, aExpectedExitCode );
176 }
177}
178
179
180void expectSvg( const wxString& aName, const wxString& aPath )
181{
182 BOOST_TEST_CONTEXT( aName )
183 {
184 std::string bytes = readFileBytes( aPath );
185 BOOST_REQUIRE( !bytes.empty() );
186 BOOST_CHECK( bytes.find( "<svg" ) != std::string::npos );
187 }
188}
189
190
191void expectPng( const wxString& aName, const wxString& aPath )
192{
193 static constexpr std::array<unsigned char, 8> PNG_HEADER = { 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n' };
194
195 BOOST_TEST_CONTEXT( aName )
196 {
197 std::string bytes = readFileBytes( aPath );
198 BOOST_REQUIRE_GE( bytes.size(), PNG_HEADER.size() );
199
200 for( size_t i = 0; i < PNG_HEADER.size(); ++i )
201 BOOST_CHECK_EQUAL( static_cast<unsigned char>( bytes[i] ), PNG_HEADER[i] );
202 }
203}
204
205
206void expectFilesDiffer( const wxString& aName, const wxString& aPathA, const wxString& aPathB )
207{
208 BOOST_TEST_CONTEXT( aName )
209 {
210 BOOST_CHECK( readFileBytes( aPathA ) != readFileBytes( aPathB ) );
211 }
212}
213
214
215struct CLI_FIXTURES
216{
217 explicit CLI_FIXTURES( TEMP_DIR& aDir )
218 {
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" ) );
227
228 writePcbFixtures();
229 writeSchFixtures();
230 writeFpFixtures();
231 writeSymFixtures();
232 }
233
234 void writePcbFixtures()
235 {
236 writeTextFile( pcbA, R"(
237(kicad_pcb (version 20241228) (generator "pcbnew") (generator_version "9.0")
238 (general (thickness 1.6))
239 (paper "A4")
240 (layers
241 (0 "F.Cu" signal)
242 (31 "B.Cu" signal)
243 (44 "Edge.Cuts" user)
244 )
245)
246)" );
247
248 writeTextFile( pcbB, R"(
249(kicad_pcb (version 20241228) (generator "pcbnew") (generator_version "9.0")
250 (general (thickness 1.6))
251 (paper "A4")
252 (layers
253 (0 "F.Cu" signal)
254 (31 "B.Cu" signal)
255 (44 "Edge.Cuts" user)
256 )
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"))
259)
260)" );
261 }
262
263 void writeSchFixtures()
264 {
265 writeTextFile( schA, R"(
266(kicad_sch (version 20230121) (generator "eeschema")
267 (uuid "00000000-0000-0000-0000-000000000001")
268 (paper "A4")
269)
270)" );
271
272 writeTextFile( schB, R"(
273(kicad_sch (version 20230121) (generator "eeschema")
274 (uuid "00000000-0000-0000-0000-000000000001")
275 (paper "A4")
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")
279 )
280)
281)" );
282 }
283
284 void writeFpFixtures()
285 {
286 BOOST_REQUIRE( wxFileName::Mkdir( fpA, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
287 BOOST_REQUIRE( wxFileName::Mkdir( fpB, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
288
289 writeTextFile( fpB + wxFILE_SEP_PATH + wxS( "VisualDiff.kicad_mod" ), R"(
290(footprint "VisualDiff"
291 (version 20240108)
292 (generator "pcbnew")
293 (layer "F.Cu")
294 (fp_line
295 (start 0 0)
296 (end 2 0)
297 (stroke (width 0.12) (type solid))
298 (layer "F.SilkS")
299 (uuid "11111111-1111-1111-1111-111111111111")
300 )
301)
302)" );
303 }
304
305 void writeSymFixtures()
306 {
307 writeTextFile( symA, R"(
308(kicad_symbol_lib
309 (version 20241209)
310 (generator "kicad_symbol_editor")
311 (generator_version "9.0")
312)
313)" );
314
315 writeTextFile( symB, R"(
316(kicad_symbol_lib
317 (version 20241209)
318 (generator "kicad_symbol_editor")
319 (generator_version "9.0")
320 (symbol "VisualDiff"
321 (exclude_from_sim no)
322 (in_bom yes)
323 (on_board yes)
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)))
330 )
331 )
332)
333)" );
334 }
335
336 wxString pcbA;
337 wxString pcbB;
338 wxString schA;
339 wxString schB;
340 wxString fpA;
341 wxString fpB;
342 wxString symA;
343 wxString symB;
344};
345
346
347struct CLI_CASE
348{
349 wxString commandGroup;
350 wxString commandName;
351 wxString refPath;
352 wxString changedPath;
353};
354
355
356void expectVisualDifference( TEMP_DIR& aDir, const CLI_CASE& aCase, const wxString& aFormat )
357{
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 );
361
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 };
366
367 expectCleanExit( caseName + wxS( " identical" ), runCli( sameArgs ), 0 );
368 expectCleanExit( caseName + wxS( " changed" ), runCli( diffArgs ), 5 );
369
370 if( aFormat == wxS( "svg" ) )
371 {
372 expectSvg( caseName + wxS( " identical" ), sameOut );
373 expectSvg( caseName + wxS( " changed" ), diffOut );
374 }
375 else
376 {
377 expectPng( caseName + wxS( " identical" ), sameOut );
378 expectPng( caseName + wxS( " changed" ), diffOut );
379 }
380
381 expectFilesDiffer( caseName, sameOut, diffOut );
382}
383
384} // namespace
385
386
387BOOST_AUTO_TEST_SUITE( DiffJobConfig )
388
389
390// The diff jobs once declared their own m_outputPath, shadowing JOB::m_outputPath and
391// registering a duplicate "output" JSON param alongside the base "output_filename" key.
392// Serialization must now expose exactly one output key, routed through the base member.
393BOOST_AUTO_TEST_CASE( DiffJobSerializesSingleOutputKey )
394{
395 JOB_PCB_DIFF job;
396 job.SetConfiguredOutputPath( wxS( "diff-out.json" ) );
397
398 BOOST_CHECK_EQUAL( job.GetConfiguredOutputPath(), wxString( wxS( "diff-out.json" ) ) );
399
400 nlohmann::json j;
401 job.ToJson( j );
402
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" ) );
406}
407
408
410
411
412BOOST_AUTO_TEST_SUITE( CliVisualDiff )
413
414
415BOOST_AUTO_TEST_CASE( RejectsInvalidVisualDiffArguments )
416{
417 TEMP_DIR dir;
418 CLI_FIXTURES fixtures( dir );
419
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 },
425 };
426
427 for( const CLI_CASE& c : cases )
428 {
429 BOOST_TEST_CONTEXT( c.commandGroup )
430 {
431 expectInvalidExit( wxS( "invalid format" ),
432 runCli( { c.commandGroup, c.commandName, c.refPath, c.refPath, wxS( "--format" ),
433 wxS( "bogus" ) } ),
434 1 );
435
436 expectInvalidExit( wxS( "svg requires output" ),
437 runCli( { c.commandGroup, c.commandName, c.refPath, c.refPath, wxS( "--format" ),
438 wxS( "svg" ) } ),
439 1 );
440
441 expectInvalidExit( wxS( "png requires output" ),
442 runCli( { c.commandGroup, c.commandName, c.refPath, c.refPath, wxS( "--format" ),
443 wxS( "png" ) } ),
444 1 );
445 }
446 }
447}
448
449
450BOOST_AUTO_TEST_CASE( RendersChangedPngAndSvgContentForEveryCommand )
451{
452 TEMP_DIR dir;
453 CLI_FIXTURES fixtures( dir );
454
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 },
460 };
461
462 for( const CLI_CASE& c : cases )
463 {
464 expectVisualDifference( dir, c, wxS( "svg" ) );
465 expectVisualDifference( dir, c, wxS( "png" ) );
466 }
467}
468
469
#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.
Definition job.cpp:163
wxString GetConfiguredOutputPath() const
Returns the configured output path for the job.
Definition job.h:235
virtual void ToJson(nlohmann::json &j) const
Definition job.cpp:67
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()
nlohmann::json output
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")