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
170 // These tests exercise cwd-based discovery independently of projects loaded by other tests.
174 wxString m_tempDir;
176};
177
178
179BOOST_FIXTURE_TEST_SUITE( TextEvalParserVcs, VCS_TEST_FIXTURE )
180
181
184BOOST_AUTO_TEST_CASE( VcsIdentifierFormatting )
185{
186 BOOST_TEST_REQUIRE( repoReady() );
187
188 EXPRESSION_EVALUATOR evaluator;
189
190 struct TestCase
191 {
192 std::string expression;
193 int expectedLength;
194 };
195
196 const std::vector<TestCase> cases = {
197 { "@{vcsidentifier()}", 40 },
198 { "@{vcsidentifier(40)}", 40 },
199 { "@{vcsidentifier(7)}", 7 },
200 { "@{vcsidentifier(8)}", 8 },
201 { "@{vcsidentifier(12)}", 12 },
202 { "@{vcsidentifier(4)}", 4 },
203
204 { "@{vcsfileidentifier(\".\")}", 40 },
205 { "@{vcsfileidentifier(\".\", 8)}", 8 },
206 };
207
208 std::regex hexPattern( "^[0-9a-f]+$" );
209
210 for( const auto& testCase : cases )
211 {
212 auto result = evaluator.Evaluate( wxString::FromUTF8( testCase.expression ) );
213
214 BOOST_CHECK_MESSAGE( !evaluator.HasErrors(),
215 "Error in expression: " + testCase.expression + " Errors: "
216 + evaluator.GetErrorSummary().ToStdString() );
217
218 BOOST_CHECK_EQUAL( result.Length(), testCase.expectedLength );
219 BOOST_CHECK( std::regex_match( result.ToStdString(), hexPattern ) );
220 }
221}
222
226BOOST_AUTO_TEST_CASE( VcsBranchAndAuthorInfo )
227{
228 BOOST_TEST_REQUIRE( repoReady() );
229
230 EXPRESSION_EVALUATOR evaluator;
231
232 auto branch = evaluator.Evaluate( "@{vcsbranch()}" );
233 BOOST_CHECK( !evaluator.HasErrors() );
234 BOOST_CHECK( !branch.IsEmpty() );
235
236 auto authorEmail = evaluator.Evaluate( "@{vcsauthoremail()}" );
237 BOOST_CHECK( !evaluator.HasErrors() );
238 BOOST_CHECK_EQUAL( authorEmail, TEST_AUTHOR_EMAIL );
239
240 auto committerEmail = evaluator.Evaluate( "@{vcscommitteremail()}" );
241 BOOST_CHECK( !evaluator.HasErrors() );
242 BOOST_CHECK_EQUAL( committerEmail, TEST_AUTHOR_EMAIL );
243
244 auto author = evaluator.Evaluate( "@{vcsauthor()}" );
245 BOOST_CHECK( !evaluator.HasErrors() );
247
248 auto committer = evaluator.Evaluate( "@{vcscommitter()}" );
249 BOOST_CHECK( !evaluator.HasErrors() );
251
252 // File variants should return the same values since there's only one commit
253 auto fileAuthorEmail = evaluator.Evaluate( "@{vcsfileauthoremail(\".\")}" );
254 BOOST_CHECK( !evaluator.HasErrors() );
255 BOOST_CHECK_EQUAL( fileAuthorEmail, TEST_AUTHOR_EMAIL );
256
257 auto fileCommitterEmail = evaluator.Evaluate( "@{vcsfilecommitteremail(\".\")}" );
258 BOOST_CHECK( !evaluator.HasErrors() );
259 BOOST_CHECK_EQUAL( fileCommitterEmail, TEST_AUTHOR_EMAIL );
260}
261
265BOOST_AUTO_TEST_CASE( VcsDirtyStatus )
266{
267 BOOST_TEST_REQUIRE( repoReady() );
268
269 EXPRESSION_EVALUATOR evaluator;
270
271 struct TestCase
272 {
273 std::string expression;
274 };
275
276 const std::vector<TestCase> cases = {
277 { "@{vcsdirty()}" },
278 { "@{vcsdirty(0)}" },
279 { "@{vcsdirty(1)}" },
280 };
281
282 for( const auto& testCase : cases )
283 {
284 auto result = evaluator.Evaluate( wxString::FromUTF8( testCase.expression ) );
285
286 BOOST_CHECK( !evaluator.HasErrors() );
287 BOOST_CHECK( result == "0" || result == "1" );
288 }
289}
290
294BOOST_AUTO_TEST_CASE( VcsDirtySuffix )
295{
296 BOOST_TEST_REQUIRE( repoReady() );
297
298 EXPRESSION_EVALUATOR evaluator;
299
300 const std::vector<std::string> cases = {
301 "@{vcsdirtysuffix()}",
302 "@{vcsdirtysuffix(\"-modified\")}",
303 "@{vcsdirtysuffix(\"+\", 1)}",
304 };
305
306 for( const auto& expr : cases )
307 {
308 evaluator.Evaluate( wxString::FromUTF8( expr ) );
309 BOOST_CHECK( !evaluator.HasErrors() );
310 }
311}
312
316BOOST_AUTO_TEST_CASE( VcsLabelsAndDistance )
317{
318 BOOST_TEST_REQUIRE( repoReady() );
319
320 EXPRESSION_EVALUATOR evaluator;
321
322 const std::vector<std::string> cases = {
323 "@{vcsnearestlabel()}",
324 "@{vcsnearestlabel(\"\")}",
325 "@{vcsnearestlabel(\"v*\")}",
326 "@{vcsnearestlabel(\"\", 0)}",
327 "@{vcsnearestlabel(\"\", 1)}",
328
329 "@{vcslabeldistance()}",
330 "@{vcslabeldistance(\"v*\")}",
331 "@{vcslabeldistance(\"\", 1)}",
332 };
333
334 std::regex numberPattern( "^[0-9]+$" );
335
336 for( const auto& expr : cases )
337 {
338 auto result = evaluator.Evaluate( wxString::FromUTF8( expr ) );
339 BOOST_CHECK( !evaluator.HasErrors() );
340
341 if( !result.IsEmpty() && expr.find( "distance" ) != std::string::npos )
342 {
343 BOOST_CHECK( std::regex_match( result.ToStdString(), numberPattern ) );
344 }
345 }
346}
347
351BOOST_AUTO_TEST_CASE( VcsCommitDate )
352{
353 BOOST_TEST_REQUIRE( repoReady() );
354
355 EXPRESSION_EVALUATOR evaluator;
356
357 struct TestCase
358 {
359 std::string expression;
360 std::regex pattern;
361 };
362
363 const std::vector<TestCase> cases = {
364 { "@{vcscommitdate()}", std::regex( "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" ) },
365 { "@{vcscommitdate(\"ISO\")}", std::regex( "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" ) },
366 { "@{vcscommitdate(\"US\")}", std::regex( "^[0-9]{2}/[0-9]{2}/[0-9]{4}$" ) },
367 { "@{vcscommitdate(\"EU\")}", std::regex( "^[0-9]{2}/[0-9]{2}/[0-9]{4}$" ) },
368
369 { "@{vcsfilecommitdate(\".\")}", std::regex( "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" ) },
370 };
371
372 for( const auto& testCase : cases )
373 {
374 auto result = evaluator.Evaluate( wxString::FromUTF8( testCase.expression ) );
375
376 BOOST_CHECK_MESSAGE( !evaluator.HasErrors(),
377 "Error in expression: " + testCase.expression + " Errors: "
378 + evaluator.GetErrorSummary().ToStdString() );
379
380 BOOST_CHECK_MESSAGE( std::regex_match( result.ToStdString(), testCase.pattern ),
381 "Bad date format for " + testCase.expression + ": "
382 + result.ToStdString() );
383 }
384}
385
389BOOST_AUTO_TEST_CASE( VcsPerformance )
390{
391 BOOST_TEST_REQUIRE( repoReady() );
392
393 EXPRESSION_EVALUATOR evaluator;
394
395 auto start = std::chrono::high_resolution_clock::now();
396
397 for( int i = 0; i < 100; ++i )
398 {
399 auto result = evaluator.Evaluate( "@{vcsidentifier(7)}" );
400 BOOST_CHECK( !evaluator.HasErrors() );
401 }
402
403 auto end = std::chrono::high_resolution_clock::now();
404 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>( end - start );
405
406 // Sanitizer instrumentation changes execution cost, not the evaluation contract
407#if defined( KICAD_SANITIZE_THREADS ) || defined( KICAD_SANITIZE_ADDRESS )
408 BOOST_TEST_MESSAGE( "Instrumented VCS evaluation: " << duration.count() << " ms" );
409#else
410 BOOST_CHECK_LT( duration.count(), 2000 );
411#endif
412}
413
417BOOST_AUTO_TEST_CASE( VcsMixedExpressions )
418{
419 BOOST_TEST_REQUIRE( repoReady() );
420
421 EXPRESSION_EVALUATOR evaluator;
422 evaluator.SetVariable( wxString( "PROJECT" ), wxString( "MyProject" ) );
423
424 const std::vector<std::string> cases = {
425 "Version: @{vcsbranch()}",
426 "Commit: @{vcsidentifier(7)}",
427 "Author: @{vcsauthor()} <@{vcsauthoremail()}>",
428
429 "${PROJECT} @{vcsbranch()}",
430 "Built from @{vcsnearestlabel()}@{vcsdirtysuffix()}",
431
432 "Distance: @{vcslabeldistance() + 0}",
433 };
434
435 for( const auto& expr : cases )
436 {
437 auto result = evaluator.Evaluate( wxString::FromUTF8( expr ) );
438
439 BOOST_CHECK( !evaluator.HasErrors() );
440 BOOST_CHECK( !result.IsEmpty() );
441 }
442}
443
451BOOST_AUTO_TEST_CASE( VcsContextPathOverride )
452{
453 BOOST_TEST_REQUIRE( repoReady() );
454
455 // Move the process cwd somewhere that is definitely not a git repo, mirroring the
456 // kicad-cli situation where the user runs the binary from an arbitrary directory.
457 wxString scratchDir = wxFileName::GetTempDir() + wxFileName::GetPathSeparator()
458 + wxString::Format( "kicad_vcs_cli_%ld", wxGetProcessId() );
459
460 wxFileName::Mkdir( scratchDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
461 wxSetWorkingDirectory( scratchDir );
462
463 auto cleanupCwd = [&]()
464 {
465 wxSetWorkingDirectory( tempDir() );
466
467 if( wxFileName::DirExists( scratchDir ) )
468 wxFileName::Rmdir( scratchDir, wxPATH_RMDIR_RECURSIVE );
469 };
470
471 try
472 {
473 // Without the context path override, vcsidentifier() falls back to cwd and reports
474 // the "not in a repository" sentinel.
475 EXPRESSION_EVALUATOR evaluator;
476 auto noContext = evaluator.Evaluate( "@{vcsidentifier(7)}" );
477 BOOST_CHECK_EQUAL( noContext.ToStdString(), std::string( "<unknown>" ) );
478
479 // With the override in place, the same expression resolves from the repo anchored
480 // at m_tempDir regardless of cwd.
481 {
482 TEXT_EVAL_VCS::CONTEXT_PATH_SCOPE scope( tempDir() );
483
484 auto hash = evaluator.Evaluate( "@{vcsidentifier(7)}" );
485 BOOST_CHECK( !evaluator.HasErrors() );
486 BOOST_CHECK_EQUAL( hash.Length(), 7 );
487
488 std::regex hexPattern( "^[0-9a-f]+$" );
489 BOOST_CHECK( std::regex_match( hash.ToStdString(), hexPattern ) );
490
491 auto branch = evaluator.Evaluate( "@{vcsbranch()}" );
492 BOOST_CHECK( !evaluator.HasErrors() );
493 BOOST_CHECK( !branch.IsEmpty() );
494 BOOST_CHECK( branch != wxS( "<unknown>" ) );
495
496 auto author = evaluator.Evaluate( "@{vcsauthor()}" );
498 }
499
500 // After the scope ends the override must be cleared and behavior returns to the
501 // cwd-based fallback.
502 auto afterScope = evaluator.Evaluate( "@{vcsidentifier(7)}" );
503 BOOST_CHECK_EQUAL( afterScope.ToStdString(), std::string( "<unknown>" ) );
504 }
505 catch( ... )
506 {
507 cleanupCwd();
508 throw;
509 }
510
511 cleanupCwd();
512}
513
520BOOST_AUTO_TEST_CASE( VcsContextPathSetByLoadProject )
521{
522 BOOST_TEST_REQUIRE( repoReady() );
523
524 wxString scratchDir = wxFileName::GetTempDir() + wxFileName::GetPathSeparator()
525 + wxString::Format( "kicad_vcs_loadproject_%ld", wxGetProcessId() );
526
527 wxFileName::Mkdir( scratchDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
528 wxSetWorkingDirectory( scratchDir );
529
530 // Drop a minimal .kicad_pro inside the git fixture so SETTINGS_MANAGER has something
531 // real to load. The content does not matter for VCS resolution.
532 wxString projectFile = tempDir() + wxFileName::GetPathSeparator()
533 + wxT( "issue23959.kicad_pro" );
534
535 {
536 std::ofstream f( projectFile.ToStdString() );
537 f << "{ \"meta\": { \"filename\": \"issue23959.kicad_pro\", \"version\": 3 } }";
538 }
539
540 auto cleanup = [&]()
541 {
542 wxRemoveFile( projectFile );
543 wxSetWorkingDirectory( tempDir() );
544
545 if( wxFileName::DirExists( scratchDir ) )
546 wxFileName::Rmdir( scratchDir, wxPATH_RMDIR_RECURSIVE );
547 };
548
549 try
550 {
551 // Baseline: with cwd outside the repo and no project loaded, lookups fail.
552 EXPRESSION_EVALUATOR evaluator;
553 auto baseline = evaluator.Evaluate( "@{vcsidentifier(7)}" );
554 BOOST_CHECK_EQUAL( baseline.ToStdString(), std::string( "<unknown>" ) );
555
556 BOOST_REQUIRE( Pgm().GetSettingsManager().LoadProject( projectFile, true ) );
557
558 // LoadProject must anchor VCS lookups to the project dir even though cwd is elsewhere.
559 auto loaded = evaluator.Evaluate( "@{vcsidentifier(7)}" );
560 BOOST_CHECK( !evaluator.HasErrors() );
561 BOOST_CHECK_EQUAL( loaded.Length(), 7 );
562
563 std::regex hexPattern( "^[0-9a-f]+$" );
564 BOOST_CHECK( std::regex_match( loaded.ToStdString(), hexPattern ) );
565
566 Pgm().GetSettingsManager().UnloadProject( &Pgm().GetSettingsManager().Prj(), false );
567
568 // UnloadProject must clear the context so lookups fall back to cwd, which is
569 // outside the repo, reproducing the sentinel state.
570 auto afterUnload = evaluator.Evaluate( "@{vcsidentifier(7)}" );
571 BOOST_CHECK_EQUAL( afterUnload.ToStdString(), std::string( "<unknown>" ) );
572 }
573 catch( ... )
574 {
575 cleanup();
576 throw;
577 }
578
579 cleanup();
580}
581
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:123
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:727
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
Fixture that creates a temporary git repo with one committed file.
TEXT_EVAL_VCS::CONTEXT_PATH_SCOPE m_context
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()
VECTOR2I end
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
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