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#include <qa_utils/file_utils.h>
29
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
144void expectCleanExit( const wxString& aName, const COMMAND_RESULT& aResult, int aExpectedExitCode )
145{
146 BOOST_TEST_CONTEXT( aName )
147 {
148 BOOST_CHECK_EQUAL( aResult.exitCode, aExpectedExitCode );
149 BOOST_CHECK_MESSAGE( aResult.output.IsEmpty(), "Unexpected stdout: " << aResult.output );
150 BOOST_CHECK_MESSAGE( aResult.error.IsEmpty(), "Unexpected stderr: " << aResult.error );
151 }
152}
153
154
155void expectInvalidExit( const wxString& aName, const COMMAND_RESULT& aResult, int aExpectedExitCode )
156{
157 BOOST_TEST_CONTEXT( aName )
158 {
159 BOOST_CHECK_EQUAL( aResult.exitCode, aExpectedExitCode );
160 }
161}
162
163
164void expectSvg( const wxString& aName, const wxString& aPath )
165{
166 BOOST_TEST_CONTEXT( aName )
167 {
168 std::string bytes = KI_TEST::LoadStringData( aPath );
169 BOOST_REQUIRE( !bytes.empty() );
170 BOOST_CHECK( bytes.find( "<svg" ) != std::string::npos );
171 }
172}
173
174
175void expectPng( const wxString& aName, const wxString& aPath )
176{
177 static constexpr std::array<unsigned char, 8> PNG_HEADER = { 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n' };
178
179 BOOST_TEST_CONTEXT( aName )
180 {
181 std::string bytes = KI_TEST::LoadStringData( aPath );
182 BOOST_REQUIRE_GE( bytes.size(), PNG_HEADER.size() );
183
184 for( size_t i = 0; i < PNG_HEADER.size(); ++i )
185 BOOST_CHECK_EQUAL( static_cast<unsigned char>( bytes[i] ), PNG_HEADER[i] );
186 }
187}
188
189
190void expectFilesDiffer( const wxString& aName, const wxString& aPathA, const wxString& aPathB )
191{
192 BOOST_TEST_CONTEXT( aName )
193 {
194 BOOST_CHECK( KI_TEST::LoadStringData( aPathA ) != KI_TEST::LoadStringData( aPathB ) );
195 }
196}
197
198
199struct CLI_FIXTURES
200{
201 explicit CLI_FIXTURES( TEMP_DIR& aDir )
202 {
203 pcbA = aDir.Path( wxS( "pcb_a.kicad_pcb" ) );
204 pcbB = aDir.Path( wxS( "pcb_b.kicad_pcb" ) );
205 schA = aDir.Path( wxS( "sch_a.kicad_sch" ) );
206 schB = aDir.Path( wxS( "sch_b.kicad_sch" ) );
207 fpA = aDir.Path( wxS( "fp_a.pretty" ) );
208 fpB = aDir.Path( wxS( "fp_b.pretty" ) );
209 symA = aDir.Path( wxS( "sym_a.kicad_sym" ) );
210 symB = aDir.Path( wxS( "sym_b.kicad_sym" ) );
211
212 writePcbFixtures();
213 writeSchFixtures();
214 writeFpFixtures();
215 writeSymFixtures();
216 }
217
218 void writePcbFixtures()
219 {
220 copyFixture( wxS( "pcb_a.kicad_pcb" ), pcbA );
221 copyFixture( wxS( "pcb_b.kicad_pcb" ), pcbB );
222 }
223
224 void writeSchFixtures()
225 {
226 copyFixture( wxS( "sch_a.kicad_sch" ), schA );
227 copyFixture( wxS( "sch_b.kicad_sch" ), schB );
228 }
229
230 void writeFpFixtures()
231 {
232 BOOST_REQUIRE( wxFileName::Mkdir( fpA, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
233 BOOST_REQUIRE( wxFileName::Mkdir( fpB, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
234
235 // fp_a stays empty so the diff reports VisualDiff as added
236 copyFixture( wxS( "fp_b.pretty/VisualDiff.kicad_mod" ),
237 fpB + wxFILE_SEP_PATH + wxS( "VisualDiff.kicad_mod" ) );
238 }
239
240 void writeSymFixtures()
241 {
242 copyFixture( wxS( "sym_a.kicad_sym" ), symA );
243 copyFixture( wxS( "sym_b.kicad_sym" ), symB );
244 }
245
246 wxString pcbA;
247 wxString pcbB;
248 wxString schA;
249 wxString schB;
250 wxString fpA;
251 wxString fpB;
252 wxString symA;
253 wxString symB;
254};
255
256
257struct CLI_CASE
258{
259 wxString commandGroup;
260 wxString commandName;
261 wxString refPath;
262 wxString changedPath;
263};
264
265
266void expectVisualDifference( TEMP_DIR& aDir, const CLI_CASE& aCase, const wxString& aFormat )
267{
268 const wxString caseName = aCase.commandGroup + wxS( " " ) + aFormat;
269 const wxString sameOut = aDir.Path( aCase.commandGroup + wxS( "_same." ) + aFormat );
270 const wxString diffOut = aDir.Path( aCase.commandGroup + wxS( "_diff." ) + aFormat );
271
272 std::vector<wxString> sameArgs = { aCase.commandGroup, aCase.commandName, aCase.refPath, aCase.refPath,
273 wxS( "--format" ), aFormat, wxS( "--output" ), sameOut };
274 std::vector<wxString> diffArgs = { aCase.commandGroup, aCase.commandName, aCase.refPath, aCase.changedPath,
275 wxS( "--format" ), aFormat, wxS( "--output" ), diffOut };
276
277 expectCleanExit( caseName + wxS( " identical" ), runCli( sameArgs ), 0 );
278 expectCleanExit( caseName + wxS( " changed" ), runCli( diffArgs ), 5 );
279
280 if( aFormat == wxS( "svg" ) )
281 {
282 expectSvg( caseName + wxS( " identical" ), sameOut );
283 expectSvg( caseName + wxS( " changed" ), diffOut );
284 }
285 else
286 {
287 expectPng( caseName + wxS( " identical" ), sameOut );
288 expectPng( caseName + wxS( " changed" ), diffOut );
289 }
290
291 expectFilesDiffer( caseName, sameOut, diffOut );
292}
293
294} // namespace
295
296
297BOOST_AUTO_TEST_SUITE( DiffJobConfig )
298
299
300// The diff jobs once declared their own m_outputPath, shadowing JOB::m_outputPath and
301// registering a duplicate "output" JSON param alongside the base "output_filename" key.
302// Serialization must now expose exactly one output key, routed through the base member.
303BOOST_AUTO_TEST_CASE( DiffJobSerializesSingleOutputKey )
304{
305 JOB_PCB_DIFF job;
306 job.SetConfiguredOutputPath( wxS( "diff-out.json" ) );
307
308 BOOST_CHECK_EQUAL( job.GetConfiguredOutputPath(), wxString( wxS( "diff-out.json" ) ) );
309
310 nlohmann::json j;
311 job.ToJson( j );
312
313 BOOST_CHECK( j.contains( "output_filename" ) );
314 BOOST_CHECK_EQUAL( j["output_filename"].get<wxString>(), wxString( wxS( "diff-out.json" ) ) );
315 BOOST_CHECK( !j.contains( "output" ) );
316}
317
318
320
321
322BOOST_AUTO_TEST_SUITE( CliVisualDiff )
323
324
325BOOST_AUTO_TEST_CASE( RejectsInvalidVisualDiffArguments )
326{
327 TEMP_DIR dir;
328 CLI_FIXTURES fixtures( dir );
329
330 const std::vector<CLI_CASE> cases = {
331 { wxS( "pcb" ), wxS( "diff" ), fixtures.pcbA, fixtures.pcbB },
332 { wxS( "sch" ), wxS( "diff" ), fixtures.schA, fixtures.schB },
333 { wxS( "fp" ), wxS( "diff" ), fixtures.fpA, fixtures.fpB },
334 { wxS( "sym" ), wxS( "diff" ), fixtures.symA, fixtures.symB },
335 };
336
337 for( const CLI_CASE& c : cases )
338 {
339 BOOST_TEST_CONTEXT( c.commandGroup )
340 {
341 expectInvalidExit( wxS( "invalid format" ),
342 runCli( { c.commandGroup, c.commandName, c.refPath, c.refPath, wxS( "--format" ),
343 wxS( "bogus" ) } ),
344 1 );
345
346 expectInvalidExit( wxS( "svg requires output" ),
347 runCli( { c.commandGroup, c.commandName, c.refPath, c.refPath, wxS( "--format" ),
348 wxS( "svg" ) } ),
349 1 );
350
351 expectInvalidExit( wxS( "png requires output" ),
352 runCli( { c.commandGroup, c.commandName, c.refPath, c.refPath, wxS( "--format" ),
353 wxS( "png" ) } ),
354 1 );
355 }
356 }
357}
358
359
360BOOST_AUTO_TEST_CASE( RendersChangedPngAndSvgContentForEveryCommand )
361{
362 TEMP_DIR dir;
363 CLI_FIXTURES fixtures( dir );
364
365 const std::vector<CLI_CASE> cases = {
366 { wxS( "pcb" ), wxS( "diff" ), fixtures.pcbA, fixtures.pcbB },
367 { wxS( "sch" ), wxS( "diff" ), fixtures.schA, fixtures.schB },
368 { wxS( "fp" ), wxS( "diff" ), fixtures.fpA, fixtures.fpB },
369 { wxS( "sym" ), wxS( "diff" ), fixtures.symA, fixtures.symB },
370 };
371
372 for( const CLI_CASE& c : cases )
373 {
374 expectVisualDifference( dir, c, wxS( "svg" ) );
375 expectVisualDifference( dir, c, wxS( "png" ) );
376 }
377}
378
379
#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()
std::string LoadStringData(const wxString &aPath)
Load the contents of a file into a string.
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")