KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_text_eval_parser_vcs.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
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your 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, see <https://www.gnu.org/licenses/>.
18 */
19
27
31#include <git/git_backend.h>
32#include <git/libgit_backend.h>
33#include <git2.h>
34#include <pgm_base.h>
36
37#include <chrono>
38#include <fstream>
39#include <regex>
40
41#include <wx/dir.h>
42#include <wx/filename.h>
43#include <wx/utils.h>
44
45
46static const char* TEST_AUTHOR_NAME = "Test Author";
48static const char* TEST_COMMIT_MSG = "Initial test commit";
49
50
56{
58 {
60 m_backend->Init();
62
63 m_originalDir = wxGetCwd();
64
65 m_tempDir = wxFileName::GetTempDir() + wxFileName::GetPathSeparator()
66 + wxString::Format( "kicad_vcs_test_%ld", wxGetProcessId() );
67
68 wxFileName::Mkdir( m_tempDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
69
71
72 if( m_repoReady )
73 wxSetWorkingDirectory( m_tempDir );
74 }
75
77 {
78 wxSetWorkingDirectory( m_originalDir );
79
80 if( wxFileName::DirExists( m_tempDir ) )
81 wxFileName::Rmdir( m_tempDir, wxPATH_RMDIR_RECURSIVE );
82
83 SetGitBackend( nullptr );
84 m_backend->Shutdown();
85 delete m_backend;
86 }
87
88 bool repoReady() const { return m_repoReady; }
89 const wxString& tempDir() const { return m_tempDir; }
90 const wxString& originalDir() const { return m_originalDir; }
91
92private:
93 bool initRepo()
94 {
95 git_repository* repo = nullptr;
96
97 if( git_repository_init( &repo, m_tempDir.ToUTF8().data(), 0 ) != 0 )
98 return false;
99
100 // Configure author identity
101 git_config* config = nullptr;
102
103 if( git_repository_config( &config, repo ) == 0 )
104 {
105 git_config_set_string( config, "user.name", TEST_AUTHOR_NAME );
106 git_config_set_string( config, "user.email", TEST_AUTHOR_EMAIL );
107 git_config_free( config );
108 }
109
110 // Write a file into the working tree
111 wxString filePath = m_tempDir + wxFileName::GetPathSeparator() + wxT( "test.txt" );
112
113 {
114 std::ofstream f( filePath.ToStdString() );
115 f << "test content\n";
116 }
117
118 // Stage it
119 git_index* index = nullptr;
120
121 if( git_repository_index( &index, repo ) != 0 )
122 {
123 git_repository_free( repo );
124 return false;
125 }
126
127 git_index_add_bypath( index, "test.txt" );
128 git_index_write( index );
129
130 // Build a tree from the index
131 git_oid treeOid;
132
133 if( git_index_write_tree( &treeOid, index ) != 0 )
134 {
135 git_index_free( index );
136 git_repository_free( repo );
137 return false;
138 }
139
140 git_index_free( index );
141
142 git_tree* tree = nullptr;
143
144 if( git_tree_lookup( &tree, repo, &treeOid ) != 0 )
145 {
146 git_repository_free( repo );
147 return false;
148 }
149
150 // Create the initial commit (no parents)
151 git_signature* sig = nullptr;
152
153 if( git_signature_now( &sig, TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL ) != 0 )
154 {
155 git_tree_free( tree );
156 git_repository_free( repo );
157 return false;
158 }
159
160 git_oid commitOid;
161 int err = git_commit_create_v( &commitOid, repo, "HEAD", sig, sig, nullptr,
162 TEST_COMMIT_MSG, tree, 0 );
163
164 git_signature_free( sig );
165 git_tree_free( tree );
166 git_repository_free( repo );
167 return err == 0;
168 }
169
172 wxString m_tempDir;
174};
175
176
177BOOST_FIXTURE_TEST_SUITE( TextEvalParserVcs, VCS_TEST_FIXTURE )
178
179
182BOOST_AUTO_TEST_CASE( VcsIdentifierFormatting )
183{
184 BOOST_TEST_REQUIRE( repoReady() );
185
186 EXPRESSION_EVALUATOR evaluator;
187
188 struct TestCase
189 {
190 std::string expression;
191 int expectedLength;
192 };
193
194 const std::vector<TestCase> cases = {
195 { "@{vcsidentifier()}", 40 },
196 { "@{vcsidentifier(40)}", 40 },
197 { "@{vcsidentifier(7)}", 7 },
198 { "@{vcsidentifier(8)}", 8 },
199 { "@{vcsidentifier(12)}", 12 },
200 { "@{vcsidentifier(4)}", 4 },
201
202 { "@{vcsfileidentifier(\".\")}", 40 },
203 { "@{vcsfileidentifier(\".\", 8)}", 8 },
204 };
205
206 std::regex hexPattern( "^[0-9a-f]+$" );
207
208 for( const auto& testCase : cases )
209 {
210 auto result = evaluator.Evaluate( wxString::FromUTF8( testCase.expression ) );
211
212 BOOST_CHECK_MESSAGE( !evaluator.HasErrors(),
213 "Error in expression: " + testCase.expression + " Errors: "
214 + evaluator.GetErrorSummary().ToStdString() );
215
216 BOOST_CHECK_EQUAL( result.Length(), testCase.expectedLength );
217 BOOST_CHECK( std::regex_match( result.ToStdString(), hexPattern ) );
218 }
219}
220
224BOOST_AUTO_TEST_CASE( VcsBranchAndAuthorInfo )
225{
226 BOOST_TEST_REQUIRE( repoReady() );
227
228 EXPRESSION_EVALUATOR evaluator;
229
230 auto branch = evaluator.Evaluate( "@{vcsbranch()}" );
231 BOOST_CHECK( !evaluator.HasErrors() );
232 BOOST_CHECK( !branch.IsEmpty() );
233
234 auto authorEmail = evaluator.Evaluate( "@{vcsauthoremail()}" );
235 BOOST_CHECK( !evaluator.HasErrors() );
236 BOOST_CHECK_EQUAL( authorEmail, TEST_AUTHOR_EMAIL );
237
238 auto committerEmail = evaluator.Evaluate( "@{vcscommitteremail()}" );
239 BOOST_CHECK( !evaluator.HasErrors() );
240 BOOST_CHECK_EQUAL( committerEmail, TEST_AUTHOR_EMAIL );
241
242 auto author = evaluator.Evaluate( "@{vcsauthor()}" );
243 BOOST_CHECK( !evaluator.HasErrors() );
245
246 auto committer = evaluator.Evaluate( "@{vcscommitter()}" );
247 BOOST_CHECK( !evaluator.HasErrors() );
249
250 // File variants should return the same values since there's only one commit
251 auto fileAuthorEmail = evaluator.Evaluate( "@{vcsfileauthoremail(\".\")}" );
252 BOOST_CHECK( !evaluator.HasErrors() );
253 BOOST_CHECK_EQUAL( fileAuthorEmail, TEST_AUTHOR_EMAIL );
254
255 auto fileCommitterEmail = evaluator.Evaluate( "@{vcsfilecommitteremail(\".\")}" );
256 BOOST_CHECK( !evaluator.HasErrors() );
257 BOOST_CHECK_EQUAL( fileCommitterEmail, TEST_AUTHOR_EMAIL );
258}
259
263BOOST_AUTO_TEST_CASE( VcsDirtyStatus )
264{
265 BOOST_TEST_REQUIRE( repoReady() );
266
267 EXPRESSION_EVALUATOR evaluator;
268
269 struct TestCase
270 {
271 std::string expression;
272 };
273
274 const std::vector<TestCase> cases = {
275 { "@{vcsdirty()}" },
276 { "@{vcsdirty(0)}" },
277 { "@{vcsdirty(1)}" },
278 };
279
280 for( const auto& testCase : cases )
281 {
282 auto result = evaluator.Evaluate( wxString::FromUTF8( testCase.expression ) );
283
284 BOOST_CHECK( !evaluator.HasErrors() );
285 BOOST_CHECK( result == "0" || result == "1" );
286 }
287}
288
292BOOST_AUTO_TEST_CASE( VcsDirtySuffix )
293{
294 BOOST_TEST_REQUIRE( repoReady() );
295
296 EXPRESSION_EVALUATOR evaluator;
297
298 const std::vector<std::string> cases = {
299 "@{vcsdirtysuffix()}",
300 "@{vcsdirtysuffix(\"-modified\")}",
301 "@{vcsdirtysuffix(\"+\", 1)}",
302 };
303
304 for( const auto& expr : cases )
305 {
306 evaluator.Evaluate( wxString::FromUTF8( expr ) );
307 BOOST_CHECK( !evaluator.HasErrors() );
308 }
309}
310
314BOOST_AUTO_TEST_CASE( VcsLabelsAndDistance )
315{
316 BOOST_TEST_REQUIRE( repoReady() );
317
318 EXPRESSION_EVALUATOR evaluator;
319
320 const std::vector<std::string> cases = {
321 "@{vcsnearestlabel()}",
322 "@{vcsnearestlabel(\"\")}",
323 "@{vcsnearestlabel(\"v*\")}",
324 "@{vcsnearestlabel(\"\", 0)}",
325 "@{vcsnearestlabel(\"\", 1)}",
326
327 "@{vcslabeldistance()}",
328 "@{vcslabeldistance(\"v*\")}",
329 "@{vcslabeldistance(\"\", 1)}",
330 };
331
332 std::regex numberPattern( "^[0-9]+$" );
333
334 for( const auto& expr : cases )
335 {
336 auto result = evaluator.Evaluate( wxString::FromUTF8( expr ) );
337 BOOST_CHECK( !evaluator.HasErrors() );
338
339 if( !result.IsEmpty() && expr.find( "distance" ) != std::string::npos )
340 {
341 BOOST_CHECK( std::regex_match( result.ToStdString(), numberPattern ) );
342 }
343 }
344}
345
349BOOST_AUTO_TEST_CASE( VcsCommitDate )
350{
351 BOOST_TEST_REQUIRE( repoReady() );
352
353 EXPRESSION_EVALUATOR evaluator;
354
355 struct TestCase
356 {
357 std::string expression;
358 std::regex pattern;
359 };
360
361 const std::vector<TestCase> cases = {
362 { "@{vcscommitdate()}", std::regex( "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" ) },
363 { "@{vcscommitdate(\"ISO\")}", std::regex( "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" ) },
364 { "@{vcscommitdate(\"US\")}", std::regex( "^[0-9]{2}/[0-9]{2}/[0-9]{4}$" ) },
365 { "@{vcscommitdate(\"EU\")}", std::regex( "^[0-9]{2}/[0-9]{2}/[0-9]{4}$" ) },
366
367 { "@{vcsfilecommitdate(\".\")}", std::regex( "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" ) },
368 };
369
370 for( const auto& testCase : cases )
371 {
372 auto result = evaluator.Evaluate( wxString::FromUTF8( testCase.expression ) );
373
374 BOOST_CHECK_MESSAGE( !evaluator.HasErrors(),
375 "Error in expression: " + testCase.expression + " Errors: "
376 + evaluator.GetErrorSummary().ToStdString() );
377
378 BOOST_CHECK_MESSAGE( std::regex_match( result.ToStdString(), testCase.pattern ),
379 "Bad date format for " + testCase.expression + ": "
380 + result.ToStdString() );
381 }
382}
383
387BOOST_AUTO_TEST_CASE( VcsPerformance )
388{
389 BOOST_TEST_REQUIRE( repoReady() );
390
391 EXPRESSION_EVALUATOR evaluator;
392
393 auto start = std::chrono::high_resolution_clock::now();
394
395 for( int i = 0; i < 100; ++i )
396 {
397 auto result = evaluator.Evaluate( "@{vcsidentifier(7)}" );
398 BOOST_CHECK( !evaluator.HasErrors() );
399 }
400
401 auto end = std::chrono::high_resolution_clock::now();
402 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>( end - start );
403
404 BOOST_CHECK_LT( duration.count(), 2000 );
405}
406
410BOOST_AUTO_TEST_CASE( VcsMixedExpressions )
411{
412 BOOST_TEST_REQUIRE( repoReady() );
413
414 EXPRESSION_EVALUATOR evaluator;
415 evaluator.SetVariable( wxString( "PROJECT" ), wxString( "MyProject" ) );
416
417 const std::vector<std::string> cases = {
418 "Version: @{vcsbranch()}",
419 "Commit: @{vcsidentifier(7)}",
420 "Author: @{vcsauthor()} <@{vcsauthoremail()}>",
421
422 "${PROJECT} @{vcsbranch()}",
423 "Built from @{vcsnearestlabel()}@{vcsdirtysuffix()}",
424
425 "Distance: @{vcslabeldistance() + 0}",
426 };
427
428 for( const auto& expr : cases )
429 {
430 auto result = evaluator.Evaluate( wxString::FromUTF8( expr ) );
431
432 BOOST_CHECK( !evaluator.HasErrors() );
433 BOOST_CHECK( !result.IsEmpty() );
434 }
435}
436
444BOOST_AUTO_TEST_CASE( VcsContextPathOverride )
445{
446 BOOST_TEST_REQUIRE( repoReady() );
447
448 // Move the process cwd somewhere that is definitely not a git repo, mirroring the
449 // kicad-cli situation where the user runs the binary from an arbitrary directory.
450 wxString scratchDir = wxFileName::GetTempDir() + wxFileName::GetPathSeparator()
451 + wxString::Format( "kicad_vcs_cli_%ld", wxGetProcessId() );
452
453 wxFileName::Mkdir( scratchDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
454 wxSetWorkingDirectory( scratchDir );
455
456 auto cleanupCwd = [&]()
457 {
458 wxSetWorkingDirectory( tempDir() );
459
460 if( wxFileName::DirExists( scratchDir ) )
461 wxFileName::Rmdir( scratchDir, wxPATH_RMDIR_RECURSIVE );
462 };
463
464 try
465 {
466 // Without the context path override, vcsidentifier() falls back to cwd and reports
467 // the "not in a repository" sentinel.
468 EXPRESSION_EVALUATOR evaluator;
469 auto noContext = evaluator.Evaluate( "@{vcsidentifier(7)}" );
470 BOOST_CHECK_EQUAL( noContext.ToStdString(), std::string( "<unknown>" ) );
471
472 // With the override in place, the same expression resolves from the repo anchored
473 // at m_tempDir regardless of cwd.
474 {
475 TEXT_EVAL_VCS::CONTEXT_PATH_SCOPE scope( tempDir() );
476
477 auto hash = evaluator.Evaluate( "@{vcsidentifier(7)}" );
478 BOOST_CHECK( !evaluator.HasErrors() );
479 BOOST_CHECK_EQUAL( hash.Length(), 7 );
480
481 std::regex hexPattern( "^[0-9a-f]+$" );
482 BOOST_CHECK( std::regex_match( hash.ToStdString(), hexPattern ) );
483
484 auto branch = evaluator.Evaluate( "@{vcsbranch()}" );
485 BOOST_CHECK( !evaluator.HasErrors() );
486 BOOST_CHECK( !branch.IsEmpty() );
487 BOOST_CHECK( branch != wxS( "<unknown>" ) );
488
489 auto author = evaluator.Evaluate( "@{vcsauthor()}" );
491 }
492
493 // After the scope ends the override must be cleared and behavior returns to the
494 // cwd-based fallback.
495 auto afterScope = evaluator.Evaluate( "@{vcsidentifier(7)}" );
496 BOOST_CHECK_EQUAL( afterScope.ToStdString(), std::string( "<unknown>" ) );
497 }
498 catch( ... )
499 {
500 cleanupCwd();
501 throw;
502 }
503
504 cleanupCwd();
505}
506
513BOOST_AUTO_TEST_CASE( VcsContextPathSetByLoadProject )
514{
515 BOOST_TEST_REQUIRE( repoReady() );
516
517 wxString scratchDir = wxFileName::GetTempDir() + wxFileName::GetPathSeparator()
518 + wxString::Format( "kicad_vcs_loadproject_%ld", wxGetProcessId() );
519
520 wxFileName::Mkdir( scratchDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
521 wxSetWorkingDirectory( scratchDir );
522
523 // Drop a minimal .kicad_pro inside the git fixture so SETTINGS_MANAGER has something
524 // real to load. The content does not matter for VCS resolution.
525 wxString projectFile = tempDir() + wxFileName::GetPathSeparator()
526 + wxT( "issue23959.kicad_pro" );
527
528 {
529 std::ofstream f( projectFile.ToStdString() );
530 f << "{ \"meta\": { \"filename\": \"issue23959.kicad_pro\", \"version\": 3 } }";
531 }
532
533 auto cleanup = [&]()
534 {
535 wxRemoveFile( projectFile );
536 wxSetWorkingDirectory( tempDir() );
537
538 if( wxFileName::DirExists( scratchDir ) )
539 wxFileName::Rmdir( scratchDir, wxPATH_RMDIR_RECURSIVE );
540 };
541
542 try
543 {
544 // Baseline: with cwd outside the repo and no project loaded, lookups fail.
545 EXPRESSION_EVALUATOR evaluator;
546 auto baseline = evaluator.Evaluate( "@{vcsidentifier(7)}" );
547 BOOST_CHECK_EQUAL( baseline.ToStdString(), std::string( "<unknown>" ) );
548
549 BOOST_REQUIRE( Pgm().GetSettingsManager().LoadProject( projectFile, true ) );
550
551 // LoadProject must anchor VCS lookups to the project dir even though cwd is elsewhere.
552 auto loaded = evaluator.Evaluate( "@{vcsidentifier(7)}" );
553 BOOST_CHECK( !evaluator.HasErrors() );
554 BOOST_CHECK_EQUAL( loaded.Length(), 7 );
555
556 std::regex hexPattern( "^[0-9a-f]+$" );
557 BOOST_CHECK( std::regex_match( loaded.ToStdString(), hexPattern ) );
558
559 Pgm().GetSettingsManager().UnloadProject( &Pgm().GetSettingsManager().Prj(), false );
560
561 // UnloadProject must clear the context so lookups fall back to cwd, which is
562 // outside the repo, reproducing the sentinel state.
563 auto afterUnload = evaluator.Evaluate( "@{vcsidentifier(7)}" );
564 BOOST_CHECK_EQUAL( afterUnload.ToStdString(), std::string( "<unknown>" ) );
565 }
566 catch( ... )
567 {
568 cleanup();
569 throw;
570 }
571
572 cleanup();
573}
574
int index
High-level wrapper for evaluating mathematical and string expressions in wxString format.
wxString Evaluate(const wxString &aInput)
Main evaluation function - processes input string and evaluates all} expressions.
bool HasErrors() const
Check if the last evaluation had errors.
wxString GetErrorSummary() const
Get detailed error information from the last evaluation.
void SetVariable(const wxString &aName, double aValue)
Set a numeric variable for use in expressions.
virtual SETTINGS_MANAGER & GetSettingsManager() const
Definition pgm_base.h:124
bool UnloadProject(PROJECT *aProject, bool aSave=true)
Save, unload and unregister the given PROJECT.
RAII helper that sets the VCS context path on construction and restores the previous value on destruc...
void SetGitBackend(GIT_BACKEND *aBackend)
PROJECT & Prj()
Definition kicad.cpp:730
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
Fixture that creates a temporary git repo with one committed file.
const wxString & tempDir() const
const wxString & originalDir() const
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
static const char * TEST_AUTHOR_NAME
static const char * TEST_AUTHOR_EMAIL
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")
VECTOR2I end
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
static const char * TEST_AUTHOR_NAME
static const char * TEST_AUTHOR_EMAIL
BOOST_AUTO_TEST_CASE(VcsIdentifierFormatting)
Test VCS identifier functions with various lengths.
static const char * TEST_COMMIT_MSG