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