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>
28
29#include <wx/file.h>
30#include <wx/filefn.h>
31#include <wx/filename.h>
32#include <wx/process.h>
33#include <wx/stdpaths.h>
34#include <wx/txtstrm.h>
35
36#include <array>
37#include <string>
38#include <vector>
39
40
41#ifndef QA_KICAD_CLI_PATH
42#define QA_KICAD_CLI_PATH "kicad-cli"
43#endif
44
45
46namespace
47{
48
49struct TEMP_DIR
50{
51 TEMP_DIR()
52 {
53 static int counter = 0;
54 m_path = wxFileName::GetTempDir() + wxFILE_SEP_PATH
55 + wxString::Format( wxS( "kicad_cli_visual_diff_%ld_%d" ),
56 static_cast<long>( wxGetProcessId() ), ++counter );
57
58 BOOST_REQUIRE( wxFileName::Mkdir( m_path, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
59 }
60
61 ~TEMP_DIR()
62 {
63 if( !m_path.IsEmpty() && wxFileName::DirExists( m_path ) )
64 wxFileName::Rmdir( m_path, wxPATH_RMDIR_RECURSIVE );
65 }
66
67 wxString Path( const wxString& aName ) const
68 {
69 return m_path + wxFILE_SEP_PATH + aName;
70 }
71
72 wxString m_path;
73};
74
75
76struct COMMAND_RESULT
77{
78 int exitCode = -1;
79 wxString output;
80 wxString error;
81};
82
83
84wxString readStream( wxInputStream* aStream )
85{
86 wxString output;
87
88 if( !aStream )
89 return output;
90
91 wxTextInputStream textStream( *aStream );
92
93 while( !aStream->Eof() )
94 {
95 wxString line = textStream.ReadLine();
96
97 if( !line.IsEmpty() || !aStream->Eof() )
98 {
99 if( !output.IsEmpty() )
100 output += wxS( "\n" );
101
102 output += line;
103 }
104 }
105
106 return output;
107}
108
109
110COMMAND_RESULT runCli( const std::vector<wxString>& aArgs )
111{
112 wxProcess process;
113 process.Redirect();
114
115 std::vector<const wchar_t*> argv;
116 argv.reserve( aArgs.size() + 2 );
117 argv.push_back( wxS( QA_KICAD_CLI_PATH ) );
118
119 for( const wxString& arg : aArgs )
120 argv.push_back( arg.wc_str() );
121
122 argv.push_back( nullptr );
123
124 COMMAND_RESULT result;
125 result.exitCode = static_cast<int>( wxExecute( const_cast<wchar_t**>( argv.data() ), wxEXEC_SYNC, &process ) );
126 result.output = readStream( process.GetInputStream() );
127 result.error = readStream( process.GetErrorStream() );
128
129 return result;
130}
131
132
133// The A/B fixtures live in qa/data; kicad-cli writes its diff output next to its inputs so
134// each run gets a scratch copy rather than touching the source tree.
135void copyFixture( const wxString& aName, const wxString& aDest )
136{
137 wxString src = wxString::FromUTF8( KI_TEST::GetTestDataRootDir() )
138 + wxS( "diff_merge/visual_diff/" ) + aName;
139
140 BOOST_REQUIRE_MESSAGE( wxCopyFile( src, aDest ), "Could not copy " << src );
141}
142
143
144std::string readFileBytes( const wxString& aPath )
145{
146 wxFile file( aPath );
147 BOOST_REQUIRE_MESSAGE( file.IsOpened(), "Could not open " << aPath );
148
149 const wxFileOffset len = file.Length();
150 BOOST_REQUIRE_GE( len, 0 );
151
152 std::string bytes( static_cast<size_t>( len ), '\0' );
153
154 if( len > 0 )
155 {
156 ssize_t read = file.Read( bytes.data(), static_cast<size_t>( len ) );
157 BOOST_REQUIRE_EQUAL( read, len );
158 }
159
160 return bytes;
161}
162
163
164void expectCleanExit( const wxString& aName, const COMMAND_RESULT& aResult, int aExpectedExitCode )
165{
166 BOOST_TEST_CONTEXT( aName )
167 {
168 BOOST_CHECK_EQUAL( aResult.exitCode, aExpectedExitCode );
169 BOOST_CHECK_MESSAGE( aResult.output.IsEmpty(), "Unexpected stdout: " << aResult.output );
170 BOOST_CHECK_MESSAGE( aResult.error.IsEmpty(), "Unexpected stderr: " << aResult.error );
171 }
172}
173
174
175void expectInvalidExit( const wxString& aName, const COMMAND_RESULT& aResult, int aExpectedExitCode )
176{
177 BOOST_TEST_CONTEXT( aName )
178 {
179 BOOST_CHECK_EQUAL( aResult.exitCode, aExpectedExitCode );
180 }
181}
182
183
184void expectSvg( const wxString& aName, const wxString& aPath )
185{
186 BOOST_TEST_CONTEXT( aName )
187 {
188 std::string bytes = readFileBytes( aPath );
189 BOOST_REQUIRE( !bytes.empty() );
190 BOOST_CHECK( bytes.find( "<svg" ) != std::string::npos );
191 }
192}
193
194
195void expectPng( const wxString& aName, const wxString& aPath )
196{
197 static constexpr std::array<unsigned char, 8> PNG_HEADER = { 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n' };
198
199 BOOST_TEST_CONTEXT( aName )
200 {
201 std::string bytes = readFileBytes( aPath );
202 BOOST_REQUIRE_GE( bytes.size(), PNG_HEADER.size() );
203
204 for( size_t i = 0; i < PNG_HEADER.size(); ++i )
205 BOOST_CHECK_EQUAL( static_cast<unsigned char>( bytes[i] ), PNG_HEADER[i] );
206 }
207}
208
209
210void expectFilesDiffer( const wxString& aName, const wxString& aPathA, const wxString& aPathB )
211{
212 BOOST_TEST_CONTEXT( aName )
213 {
214 BOOST_CHECK( readFileBytes( aPathA ) != readFileBytes( aPathB ) );
215 }
216}
217
218
219struct CLI_FIXTURES
220{
221 explicit CLI_FIXTURES( TEMP_DIR& aDir )
222 {
223 pcbA = aDir.Path( wxS( "pcb_a.kicad_pcb" ) );
224 pcbB = aDir.Path( wxS( "pcb_b.kicad_pcb" ) );
225 schA = aDir.Path( wxS( "sch_a.kicad_sch" ) );
226 schB = aDir.Path( wxS( "sch_b.kicad_sch" ) );
227 fpA = aDir.Path( wxS( "fp_a.pretty" ) );
228 fpB = aDir.Path( wxS( "fp_b.pretty" ) );
229 symA = aDir.Path( wxS( "sym_a.kicad_sym" ) );
230 symB = aDir.Path( wxS( "sym_b.kicad_sym" ) );
231
232 writePcbFixtures();
233 writeSchFixtures();
234 writeFpFixtures();
235 writeSymFixtures();
236 }
237
238 void writePcbFixtures()
239 {
240 copyFixture( wxS( "pcb_a.kicad_pcb" ), pcbA );
241 copyFixture( wxS( "pcb_b.kicad_pcb" ), pcbB );
242 }
243
244 void writeSchFixtures()
245 {
246 copyFixture( wxS( "sch_a.kicad_sch" ), schA );
247 copyFixture( wxS( "sch_b.kicad_sch" ), schB );
248 }
249
250 void writeFpFixtures()
251 {
252 BOOST_REQUIRE( wxFileName::Mkdir( fpA, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
253 BOOST_REQUIRE( wxFileName::Mkdir( fpB, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
254
255 // fp_a stays empty so the diff reports VisualDiff as added
256 copyFixture( wxS( "fp_b.pretty/VisualDiff.kicad_mod" ),
257 fpB + wxFILE_SEP_PATH + wxS( "VisualDiff.kicad_mod" ) );
258 }
259
260 void writeSymFixtures()
261 {
262 copyFixture( wxS( "sym_a.kicad_sym" ), symA );
263 copyFixture( wxS( "sym_b.kicad_sym" ), symB );
264 }
265
266 wxString pcbA;
267 wxString pcbB;
268 wxString schA;
269 wxString schB;
270 wxString fpA;
271 wxString fpB;
272 wxString symA;
273 wxString symB;
274};
275
276
277struct CLI_CASE
278{
279 wxString commandGroup;
280 wxString commandName;
281 wxString refPath;
282 wxString changedPath;
283};
284
285
286void expectVisualDifference( TEMP_DIR& aDir, const CLI_CASE& aCase, const wxString& aFormat )
287{
288 const wxString caseName = aCase.commandGroup + wxS( " " ) + aFormat;
289 const wxString sameOut = aDir.Path( aCase.commandGroup + wxS( "_same." ) + aFormat );
290 const wxString diffOut = aDir.Path( aCase.commandGroup + wxS( "_diff." ) + aFormat );
291
292 std::vector<wxString> sameArgs = { aCase.commandGroup, aCase.commandName, aCase.refPath, aCase.refPath,
293 wxS( "--format" ), aFormat, wxS( "--output" ), sameOut };
294 std::vector<wxString> diffArgs = { aCase.commandGroup, aCase.commandName, aCase.refPath, aCase.changedPath,
295 wxS( "--format" ), aFormat, wxS( "--output" ), diffOut };
296
297 expectCleanExit( caseName + wxS( " identical" ), runCli( sameArgs ), 0 );
298 expectCleanExit( caseName + wxS( " changed" ), runCli( diffArgs ), 5 );
299
300 if( aFormat == wxS( "svg" ) )
301 {
302 expectSvg( caseName + wxS( " identical" ), sameOut );
303 expectSvg( caseName + wxS( " changed" ), diffOut );
304 }
305 else
306 {
307 expectPng( caseName + wxS( " identical" ), sameOut );
308 expectPng( caseName + wxS( " changed" ), diffOut );
309 }
310
311 expectFilesDiffer( caseName, sameOut, diffOut );
312}
313
314} // namespace
315
316
317BOOST_AUTO_TEST_SUITE( DiffJobConfig )
318
319
320// The diff jobs once declared their own m_outputPath, shadowing JOB::m_outputPath and
321// registering a duplicate "output" JSON param alongside the base "output_filename" key.
322// Serialization must now expose exactly one output key, routed through the base member.
323BOOST_AUTO_TEST_CASE( DiffJobSerializesSingleOutputKey )
324{
325 JOB_PCB_DIFF job;
326 job.SetConfiguredOutputPath( wxS( "diff-out.json" ) );
327
328 BOOST_CHECK_EQUAL( job.GetConfiguredOutputPath(), wxString( wxS( "diff-out.json" ) ) );
329
330 nlohmann::json j;
331 job.ToJson( j );
332
333 BOOST_CHECK( j.contains( "output_filename" ) );
334 BOOST_CHECK_EQUAL( j["output_filename"].get<wxString>(), wxString( wxS( "diff-out.json" ) ) );
335 BOOST_CHECK( !j.contains( "output" ) );
336}
337
338
340
341
342BOOST_AUTO_TEST_SUITE( CliVisualDiff )
343
344
345BOOST_AUTO_TEST_CASE( RejectsInvalidVisualDiffArguments )
346{
347 TEMP_DIR dir;
348 CLI_FIXTURES fixtures( dir );
349
350 const std::vector<CLI_CASE> cases = {
351 { wxS( "pcb" ), wxS( "diff" ), fixtures.pcbA, fixtures.pcbB },
352 { wxS( "sch" ), wxS( "diff" ), fixtures.schA, fixtures.schB },
353 { wxS( "fp" ), wxS( "diff" ), fixtures.fpA, fixtures.fpB },
354 { wxS( "sym" ), wxS( "diff" ), fixtures.symA, fixtures.symB },
355 };
356
357 for( const CLI_CASE& c : cases )
358 {
359 BOOST_TEST_CONTEXT( c.commandGroup )
360 {
361 expectInvalidExit( wxS( "invalid format" ),
362 runCli( { c.commandGroup, c.commandName, c.refPath, c.refPath, wxS( "--format" ),
363 wxS( "bogus" ) } ),
364 1 );
365
366 expectInvalidExit( wxS( "svg requires output" ),
367 runCli( { c.commandGroup, c.commandName, c.refPath, c.refPath, wxS( "--format" ),
368 wxS( "svg" ) } ),
369 1 );
370
371 expectInvalidExit( wxS( "png requires output" ),
372 runCli( { c.commandGroup, c.commandName, c.refPath, c.refPath, wxS( "--format" ),
373 wxS( "png" ) } ),
374 1 );
375 }
376 }
377}
378
379
380BOOST_AUTO_TEST_CASE( RendersChangedPngAndSvgContentForEveryCommand )
381{
382 TEMP_DIR dir;
383 CLI_FIXTURES fixtures( dir );
384
385 const std::vector<CLI_CASE> cases = {
386 { wxS( "pcb" ), wxS( "diff" ), fixtures.pcbA, fixtures.pcbB },
387 { wxS( "sch" ), wxS( "diff" ), fixtures.schA, fixtures.schB },
388 { wxS( "fp" ), wxS( "diff" ), fixtures.fpA, fixtures.fpB },
389 { wxS( "sym" ), wxS( "diff" ), fixtures.symA, fixtures.symB },
390 };
391
392 for( const CLI_CASE& c : cases )
393 {
394 expectVisualDifference( dir, c, wxS( "svg" ) );
395 expectVisualDifference( dir, c, wxS( "png" ) );
396 }
397}
398
399
#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:157
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
std::string GetTestDataRootDir()
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_TEST_CONTEXT("Test Clearance")
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")