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
32#include <pgm_base.h>
34
35#include <chrono>
36#include <fstream>
37#include <regex>
38
39#include <wx/dir.h>
40#include <wx/filename.h>
41#include <wx/utils.h>
42
43
44static const char* TEST_AUTHOR_NAME = "Test Author";
46static const char* TEST_COMMIT_MSG = "Initial test commit";
47
48
54{
56 m_originalDir( wxGetCwd() )
57 {
60 KI_TEST::WriteGitFile( TempPath(), "test.txt", "test content\n" );
61 KI_TEST::CommitGitFile( repo.get(), "test.txt", TEST_COMMIT_MSG );
62 BOOST_REQUIRE( wxSetWorkingDirectory( TempPath() ) );
63 }
64
65 ~VCS_TEST_FIXTURE() { wxSetWorkingDirectory( m_originalDir ); }
66
67 const wxString& tempDir() const { return TempPath(); }
68
69private:
70 // These tests exercise cwd-based discovery independently of projects loaded by other tests.
72 wxString m_originalDir;
73};
74
75
76BOOST_FIXTURE_TEST_SUITE( TextEvalParserVcs, VCS_TEST_FIXTURE )
77
78
81BOOST_AUTO_TEST_CASE( VcsIdentifierFormatting )
82{
83 EXPRESSION_EVALUATOR evaluator;
84
85 struct TestCase
86 {
87 std::string expression;
88 int expectedLength;
89 };
90
91 const std::vector<TestCase> cases = {
92 { "@{vcsidentifier()}", 40 },
93 { "@{vcsidentifier(40)}", 40 },
94 { "@{vcsidentifier(7)}", 7 },
95 { "@{vcsidentifier(8)}", 8 },
96 { "@{vcsidentifier(12)}", 12 },
97 { "@{vcsidentifier(4)}", 4 },
98
99 { "@{vcsfileidentifier(\".\")}", 40 },
100 { "@{vcsfileidentifier(\".\", 8)}", 8 },
101 };
102
103 std::regex hexPattern( "^[0-9a-f]+$" );
104
105 for( const auto& testCase : cases )
106 {
107 auto result = evaluator.Evaluate( wxString::FromUTF8( testCase.expression ) );
108
109 BOOST_CHECK_MESSAGE( !evaluator.HasErrors(),
110 "Error in expression: " + testCase.expression + " Errors: "
111 + evaluator.GetErrorSummary().ToStdString() );
112
113 BOOST_CHECK_EQUAL( result.Length(), testCase.expectedLength );
114 BOOST_CHECK( std::regex_match( result.ToStdString(), hexPattern ) );
115 }
116}
117
121BOOST_AUTO_TEST_CASE( VcsBranchAndAuthorInfo )
122{
123 EXPRESSION_EVALUATOR evaluator;
124
125 auto branch = evaluator.Evaluate( "@{vcsbranch()}" );
126 BOOST_CHECK( !evaluator.HasErrors() );
127 BOOST_CHECK( !branch.IsEmpty() );
128
129 auto authorEmail = evaluator.Evaluate( "@{vcsauthoremail()}" );
130 BOOST_CHECK( !evaluator.HasErrors() );
131 BOOST_CHECK_EQUAL( authorEmail, TEST_AUTHOR_EMAIL );
132
133 auto committerEmail = evaluator.Evaluate( "@{vcscommitteremail()}" );
134 BOOST_CHECK( !evaluator.HasErrors() );
135 BOOST_CHECK_EQUAL( committerEmail, TEST_AUTHOR_EMAIL );
136
137 auto author = evaluator.Evaluate( "@{vcsauthor()}" );
138 BOOST_CHECK( !evaluator.HasErrors() );
140
141 auto committer = evaluator.Evaluate( "@{vcscommitter()}" );
142 BOOST_CHECK( !evaluator.HasErrors() );
144
145 // File variants should return the same values since there's only one commit
146 auto fileAuthorEmail = evaluator.Evaluate( "@{vcsfileauthoremail(\".\")}" );
147 BOOST_CHECK( !evaluator.HasErrors() );
148 BOOST_CHECK_EQUAL( fileAuthorEmail, TEST_AUTHOR_EMAIL );
149
150 auto fileCommitterEmail = evaluator.Evaluate( "@{vcsfilecommitteremail(\".\")}" );
151 BOOST_CHECK( !evaluator.HasErrors() );
152 BOOST_CHECK_EQUAL( fileCommitterEmail, TEST_AUTHOR_EMAIL );
153}
154
158BOOST_AUTO_TEST_CASE( VcsDirtyStatus )
159{
160 EXPRESSION_EVALUATOR evaluator;
161
162 struct TestCase
163 {
164 std::string expression;
165 };
166
167 const std::vector<TestCase> cases = {
168 { "@{vcsdirty()}" },
169 { "@{vcsdirty(0)}" },
170 { "@{vcsdirty(1)}" },
171 };
172
173 for( const auto& testCase : cases )
174 {
175 auto result = evaluator.Evaluate( wxString::FromUTF8( testCase.expression ) );
176
177 BOOST_CHECK( !evaluator.HasErrors() );
178 BOOST_CHECK( result == "0" || result == "1" );
179 }
180}
181
185BOOST_AUTO_TEST_CASE( VcsDirtySuffix )
186{
187 EXPRESSION_EVALUATOR evaluator;
188
189 const std::vector<std::string> cases = {
190 "@{vcsdirtysuffix()}",
191 "@{vcsdirtysuffix(\"-modified\")}",
192 "@{vcsdirtysuffix(\"+\", 1)}",
193 };
194
195 for( const auto& expr : cases )
196 {
197 evaluator.Evaluate( wxString::FromUTF8( expr ) );
198 BOOST_CHECK( !evaluator.HasErrors() );
199 }
200}
201
205BOOST_AUTO_TEST_CASE( VcsLabelsAndDistance )
206{
207 EXPRESSION_EVALUATOR evaluator;
208
209 const std::vector<std::string> cases = {
210 "@{vcsnearestlabel()}",
211 "@{vcsnearestlabel(\"\")}",
212 "@{vcsnearestlabel(\"v*\")}",
213 "@{vcsnearestlabel(\"\", 0)}",
214 "@{vcsnearestlabel(\"\", 1)}",
215
216 "@{vcslabeldistance()}",
217 "@{vcslabeldistance(\"v*\")}",
218 "@{vcslabeldistance(\"\", 1)}",
219 };
220
221 std::regex numberPattern( "^[0-9]+$" );
222
223 for( const auto& expr : cases )
224 {
225 auto result = evaluator.Evaluate( wxString::FromUTF8( expr ) );
226 BOOST_CHECK( !evaluator.HasErrors() );
227
228 if( !result.IsEmpty() && expr.find( "distance" ) != std::string::npos )
229 {
230 BOOST_CHECK( std::regex_match( result.ToStdString(), numberPattern ) );
231 }
232 }
233}
234
238BOOST_AUTO_TEST_CASE( VcsCommitDate )
239{
240 EXPRESSION_EVALUATOR evaluator;
241
242 struct TestCase
243 {
244 std::string expression;
245 std::regex pattern;
246 };
247
248 const std::vector<TestCase> cases = {
249 { "@{vcscommitdate()}", std::regex( "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" ) },
250 { "@{vcscommitdate(\"ISO\")}", std::regex( "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" ) },
251 { "@{vcscommitdate(\"US\")}", std::regex( "^[0-9]{2}/[0-9]{2}/[0-9]{4}$" ) },
252 { "@{vcscommitdate(\"EU\")}", std::regex( "^[0-9]{2}/[0-9]{2}/[0-9]{4}$" ) },
253
254 { "@{vcsfilecommitdate(\".\")}", std::regex( "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" ) },
255 };
256
257 for( const auto& testCase : cases )
258 {
259 auto result = evaluator.Evaluate( wxString::FromUTF8( testCase.expression ) );
260
261 BOOST_CHECK_MESSAGE( !evaluator.HasErrors(),
262 "Error in expression: " + testCase.expression + " Errors: "
263 + evaluator.GetErrorSummary().ToStdString() );
264
265 BOOST_CHECK_MESSAGE( std::regex_match( result.ToStdString(), testCase.pattern ),
266 "Bad date format for " + testCase.expression + ": "
267 + result.ToStdString() );
268 }
269}
270
274BOOST_AUTO_TEST_CASE( VcsPerformance )
275{
276 EXPRESSION_EVALUATOR evaluator;
277
278 auto start = std::chrono::high_resolution_clock::now();
279
280 for( int i = 0; i < 100; ++i )
281 {
282 auto result = evaluator.Evaluate( "@{vcsidentifier(7)}" );
283 BOOST_CHECK( !evaluator.HasErrors() );
284 }
285
286 auto end = std::chrono::high_resolution_clock::now();
287 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>( end - start );
288
289 // Sanitizer instrumentation changes execution cost, not the evaluation contract
290#if defined( KICAD_SANITIZE_THREADS ) || defined( KICAD_SANITIZE_ADDRESS )
291 BOOST_TEST_MESSAGE( "Instrumented VCS evaluation: " << duration.count() << " ms" );
292#else
293 BOOST_CHECK_LT( duration.count(), 2000 );
294#endif
295}
296
300BOOST_AUTO_TEST_CASE( VcsMixedExpressions )
301{
302 EXPRESSION_EVALUATOR evaluator;
303 evaluator.SetVariable( wxString( "PROJECT" ), wxString( "MyProject" ) );
304
305 const std::vector<std::string> cases = {
306 "Version: @{vcsbranch()}",
307 "Commit: @{vcsidentifier(7)}",
308 "Author: @{vcsauthor()} <@{vcsauthoremail()}>",
309
310 "${PROJECT} @{vcsbranch()}",
311 "Built from @{vcsnearestlabel()}@{vcsdirtysuffix()}",
312
313 "Distance: @{vcslabeldistance() + 0}",
314 };
315
316 for( const auto& expr : cases )
317 {
318 auto result = evaluator.Evaluate( wxString::FromUTF8( expr ) );
319
320 BOOST_CHECK( !evaluator.HasErrors() );
321 BOOST_CHECK( !result.IsEmpty() );
322 }
323}
324
332BOOST_AUTO_TEST_CASE( VcsContextPathOverride )
333{
334 // Move the process cwd somewhere that is definitely not a git repo, mirroring the
335 // kicad-cli situation where the user runs the binary from an arbitrary directory.
336 wxString scratchDir = wxFileName::GetTempDir() + wxFileName::GetPathSeparator()
337 + wxString::Format( "kicad_vcs_cli_%ld", wxGetProcessId() );
338
339 wxFileName::Mkdir( scratchDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
340 wxSetWorkingDirectory( scratchDir );
341
342 auto cleanupCwd = [&]()
343 {
344 wxSetWorkingDirectory( tempDir() );
345
346 if( wxFileName::DirExists( scratchDir ) )
347 wxFileName::Rmdir( scratchDir, wxPATH_RMDIR_RECURSIVE );
348 };
349
350 try
351 {
352 // Without the context path override, vcsidentifier() falls back to cwd and reports
353 // the "not in a repository" sentinel.
354 EXPRESSION_EVALUATOR evaluator;
355 auto noContext = evaluator.Evaluate( "@{vcsidentifier(7)}" );
356 BOOST_CHECK_EQUAL( noContext.ToStdString(), std::string( "<unknown>" ) );
357
358 // With the override in place, the same expression resolves from the repo anchored
359 // at the temporary repository regardless of cwd.
360 {
361 TEXT_EVAL_VCS::CONTEXT_PATH_SCOPE scope( tempDir() );
362
363 auto hash = evaluator.Evaluate( "@{vcsidentifier(7)}" );
364 BOOST_CHECK( !evaluator.HasErrors() );
365 BOOST_CHECK_EQUAL( hash.Length(), 7 );
366
367 std::regex hexPattern( "^[0-9a-f]+$" );
368 BOOST_CHECK( std::regex_match( hash.ToStdString(), hexPattern ) );
369
370 auto branch = evaluator.Evaluate( "@{vcsbranch()}" );
371 BOOST_CHECK( !evaluator.HasErrors() );
372 BOOST_CHECK( !branch.IsEmpty() );
373 BOOST_CHECK( branch != wxS( "<unknown>" ) );
374
375 auto author = evaluator.Evaluate( "@{vcsauthor()}" );
377 }
378
379 // After the scope ends the override must be cleared and behavior returns to the
380 // cwd-based fallback.
381 auto afterScope = evaluator.Evaluate( "@{vcsidentifier(7)}" );
382 BOOST_CHECK_EQUAL( afterScope.ToStdString(), std::string( "<unknown>" ) );
383 }
384 catch( ... )
385 {
386 cleanupCwd();
387 throw;
388 }
389
390 cleanupCwd();
391}
392
399BOOST_AUTO_TEST_CASE( VcsContextPathSetByLoadProject )
400{
401 wxString scratchDir = wxFileName::GetTempDir() + wxFileName::GetPathSeparator()
402 + wxString::Format( "kicad_vcs_loadproject_%ld", wxGetProcessId() );
403
404 wxFileName::Mkdir( scratchDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
405 wxSetWorkingDirectory( scratchDir );
406
407 // Drop a minimal .kicad_pro inside the git fixture so SETTINGS_MANAGER has something
408 // real to load. The content does not matter for VCS resolution.
409 wxString projectFile = tempDir() + wxFileName::GetPathSeparator()
410 + wxT( "issue23959.kicad_pro" );
411
412 {
413 std::ofstream f( projectFile.ToStdString() );
414 f << "{ \"meta\": { \"filename\": \"issue23959.kicad_pro\", \"version\": 3 } }";
415 }
416
417 auto cleanup = [&]()
418 {
419 wxRemoveFile( projectFile );
420 wxSetWorkingDirectory( tempDir() );
421
422 if( wxFileName::DirExists( scratchDir ) )
423 wxFileName::Rmdir( scratchDir, wxPATH_RMDIR_RECURSIVE );
424 };
425
426 try
427 {
428 // Baseline: with cwd outside the repo and no project loaded, lookups fail.
429 EXPRESSION_EVALUATOR evaluator;
430 auto baseline = evaluator.Evaluate( "@{vcsidentifier(7)}" );
431 BOOST_CHECK_EQUAL( baseline.ToStdString(), std::string( "<unknown>" ) );
432
433 BOOST_REQUIRE( Pgm().GetSettingsManager().LoadProject( projectFile, true ) );
434
435 // LoadProject must anchor VCS lookups to the project dir even though cwd is elsewhere.
436 auto loaded = evaluator.Evaluate( "@{vcsidentifier(7)}" );
437 BOOST_CHECK( !evaluator.HasErrors() );
438 BOOST_CHECK_EQUAL( loaded.Length(), 7 );
439
440 std::regex hexPattern( "^[0-9a-f]+$" );
441 BOOST_CHECK( std::regex_match( loaded.ToStdString(), hexPattern ) );
442
443 Pgm().GetSettingsManager().UnloadProject( &Pgm().GetSettingsManager().Prj(), false );
444 BOOST_REQUIRE( wxSetWorkingDirectory( scratchDir ) );
445
446 // UnloadProject must clear the context so lookups fall back to cwd, which is
447 // outside the repo, reproducing the sentinel state.
448 auto afterUnload = evaluator.Evaluate( "@{vcsidentifier(7)}" );
449 BOOST_CHECK_EQUAL( afterUnload.ToStdString(), std::string( "<unknown>" ) );
450 }
451 catch( ... )
452 {
453 cleanup();
454 throw;
455 }
456
457 cleanup();
458}
459
460
461// The context is process-wide, so a manager that dies with a project loaded leaves it stale.
462BOOST_AUTO_TEST_CASE( VcsContextClearedWhenSettingsManagerDies )
463{
464 wxString projectPath = tempDir() + wxFileName::GetPathSeparator() + wxT( "ctx.kicad_pro" );
465
466 {
467 std::ofstream f( projectPath.ToStdString() );
468 f << "{\"meta\": {\"filename\": \"ctx.kicad_pro\", \"version\": 3}}";
469 }
470
471 {
473 BOOST_TEST_REQUIRE( mgr.LoadProject( projectPath ) );
474 BOOST_CHECK( TEXT_EVAL_VCS::GetContextPath() != wxT( "." ) );
475 }
476
478}
479
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.
Give each test a backend and a temporary directory.
const wxString & TempPath() const
virtual SETTINGS_MANAGER & GetSettingsManager() const
Definition pgm_base.h:123
bool LoadProject(const wxString &aFullPath, bool aSetActive=true)
Load a project or sets up a new project with a specified path.
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...
static std::string ToStdString(const wxString &aStr)
PROJECT & Prj()
Definition kicad.cpp:737
std::unique_ptr< git_repository, decltype([](git_repository *aRepo) { git_repository_free(aRepo); })> GitRepositoryPtr
A unique pointer for git_repository objects with automatic cleanup.
KIGIT::GitRepositoryPtr InitGitRepository(const wxString &aPath, const char *aBranch="main", const char *aAuthorName="QA Test", const char *aAuthorEmail="[email protected]")
Pin the branch and author so repository setup does not depend on the user's Git config.
void WriteGitFile(const wxString &aRepoPath, const wxString &aRelativePath, const std::string &aContents)
Write test data without staging it. Create parent directories when needed.
git_oid CommitGitFile(git_repository *aRepo, const wxString &aPath, const wxString &aMessage)
Stage one file and commit the index. Other staged changes are kept.
wxString GetContextPath()
Return the current context path for repo-scoped VCS queries.
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
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