39#include <wx/filename.h>
41#include <fmt/ranges.h>
68 const wxString projectFile = owner.
PathStr() +
"/multinetclasses.kicad_pro";
70 +
"/netlists/multinetclasses/multinetclasses.kicad_pro";
74 git_repository* rawRepo =
nullptr;
75 BOOST_REQUIRE_EQUAL( git_repository_init( &rawRepo, owner.
PathStr().ToUTF8().data(), 0 ), 0 );
77 BOOST_REQUIRE_EQUAL( git_repository_set_head( repo.get(),
"refs/heads/owner" ), 0 );
78 git_index* rawIndex =
nullptr;
79 BOOST_REQUIRE_EQUAL( git_repository_index( &rawIndex, repo.get() ), 0 );
81 BOOST_REQUIRE_EQUAL( git_index_add_bypath(
index.get(),
"multinetclasses.kicad_pro" ), 0 );
82 BOOST_REQUIRE_EQUAL( git_index_add_bypath(
index.get(),
"sub/multinetclasses.kicad_pro" ), 0 );
84 BOOST_REQUIRE_EQUAL( git_index_write_tree( &treeId,
index.get() ), 0 );
85 BOOST_REQUIRE_EQUAL( git_index_write(
index.get() ), 0 );
86 git_tree* rawTree =
nullptr;
87 BOOST_REQUIRE_EQUAL( git_tree_lookup( &rawTree, repo.get(), &treeId ), 0 );
89 git_signature* rawSignature =
nullptr;
90 BOOST_REQUIRE_EQUAL( git_signature_now( &rawSignature,
"Connectivity QA",
"[email protected]" ), 0 );
93 BOOST_REQUIRE_EQUAL( git_commit_create_v( &commitId, repo.get(),
"HEAD", signature.get(), signature.get(),
94 nullptr,
"Native project", tree.get(), 0 ), 0 );
96 const auto evaluateVcs = [](
const wxString& expression )
101 return result.ToStdString( wxConvUTF8 );
103 const auto commitHash = [&](
const std::string&
path =
"." )
105 wxString quoted = wxString::FromUTF8(
path );
106 quoted.Replace(
"\\",
"\\\\" );
107 quoted.Replace(
"\"",
"\\\"" );
108 return evaluateVcs( wxString::Format(
"@{vcsfileidentifier(\"%s\")}", quoted ) );
113 const auto hash = commitHash();
114 BOOST_REQUIRE_EQUAL( hash.size(), 40u );
140 wxString projectHash;
148 git_commit* rawCommit =
nullptr;
149 BOOST_REQUIRE_EQUAL( git_commit_lookup( &rawCommit, repo.get(), &commitId ), 0 );
152 BOOST_REQUIRE_EQUAL( git_commit_create_v( &changedId, repo.get(),
"HEAD", signature.get(), signature.get(),
153 nullptr,
"Next native project", tree.get(), 1, original.get() ), 0 );
154 git_reference* rawBranch =
nullptr;
155 BOOST_REQUIRE_EQUAL( git_reference_create( &rawBranch, repo.get(),
"refs/heads/changed",
156 &changedId, 0,
nullptr ), 0 );
157 git_reference_free( rawBranch );
158 BOOST_REQUIRE_EQUAL( git_repository_set_head( repo.get(),
"refs/heads/changed" ), 0 );
163 for(
const auto& [key, value] : sources.
vcsValues )
180 const wxDateTime time( 3, wxDateTime::Jan, 2001, 4, 5, 6 );
193 wxString::Format(
"%lld",
static_cast<long long>( time.GetTicks() ) ) );
195 wxString::Format(
"%lld",
static_cast<long long>( time.GetTicks() ) / ( 24 * 3600 ) ) );
204 evaluator.
Evaluate(
"@{random()}" );
207 BOOST_CHECK( *sources.
time == time );
209 const wxString envName = wxS(
"KICAD_QA_TEXT_EVAL_SOURCE" );
210 wxSetEnv( envName, wxS(
"captured" ) );
212 wxString( wxS(
"captured" ) ) );
213 wxUnsetEnv( envName );
218 const wxString liveBefore = wxDateTime::Now().FormatISODate();
220 BOOST_CHECK( outside == liveBefore || outside == wxDateTime::Now().FormatISODate() );
237 evaluator.
SetVariable( std::string(
"project_name"), std::string(
"My PCB Project") );
238 evaluator.
SetVariable( std::string(
"designer"), std::string(
"John Doe") );
241 std::string expression;
242 std::string expectedPattern;
245 std::string description;
248 const std::vector<TestCase> cases = {
251 "Board area: @{${board_width} * ${board_height}} mm²",
252 "Board area: 8000 mm²",
254 "Board area calculation"
257 "Perimeter: @{2 * (${board_width} + ${board_height})} mm",
260 "Board perimeter calculation"
263 "Diagonal: @{format(sqrt(pow(${board_width}, 2) + pow(${board_height}, 2)), 1)} mm",
264 "Diagonal: 128.1 mm",
266 "Board diagonal calculation"
271 "Project: ${project_name} | Designer: ${designer} | Rev: @{${revision}}",
272 "Project: My PCB Project | Designer: John Doe | Rev: 3",
274 "Title block information"
277 "Components: @{${component_count}} | Density: @{format(${component_count} / (${board_width} * ${board_height} / 10000), 2)} per cm²",
278 "Components: 45 | Density: 56.25 per cm²",
280 "Component density calculation"
285 "Created: @{dateformat(today())} | Build: @{today()} days since epoch",
286 R
"(Created: \d{4}-\d{2}-\d{2} \| Build: \d+ days since epoch)",
288 "Date-based tracking"
293 "Status: @{if(${component_count} > 50, \"Complex\", \"Simple\")} design",
294 "Status: Simple design",
296 "Conditional design complexity"
299 "Status: @{if(${trace_width} >= 0.2, \"Standard\", \"Fine pitch\")} (@{${trace_width}}mm)",
300 "Status: Standard (0.2mm)",
302 "Conditional trace width description"
307 "PCB Summary:\n- Size: @{${board_width}}×@{${board_height}}mm\n- Area: @{${board_width} * ${board_height}}mm²\n- Components: @{${component_count}}",
308 "PCB Summary:\n- Size: 100×80mm\n- Area: 8000mm²\n- Components: 45",
310 "Multi-line documentation"
315 "Invalid: @{${undefined_var}} test",
316 "Invalid: @{${undefined_var}} test",
318 "Undefined variable behavior"
322 for(
const auto& testCase : cases )
324 auto result = evaluator.
Evaluate( wxString::FromUTF8( testCase.expression ) );
326 if( testCase.shouldError )
328 BOOST_CHECK_MESSAGE( evaluator.
HasErrors(),
329 "Expected error for: " + testCase.description );
333 BOOST_CHECK_MESSAGE( !evaluator.
HasErrors(),
334 "Unexpected error for: " + testCase.description +
337 if( testCase.isRegex )
339 std::regex pattern( testCase.expectedPattern );
340 BOOST_CHECK_MESSAGE( std::regex_match(
result.ToStdString( wxConvUTF8 ), pattern ),
341 "Result '" +
result.ToStdString( wxConvUTF8 ) +
"' doesn't match pattern '" +
342 testCase.expectedPattern +
"' for: " + testCase.description );
346 BOOST_CHECK_MESSAGE(
result.ToStdString( wxConvUTF8 ) == testCase.expectedPattern,
347 "Expected '" + testCase.expectedPattern +
"' but got '" +
348 result.ToStdString( wxConvUTF8 ) +
"' for: " + testCase.description );
361 if( varName ==
"dynamic_value" )
363 else if( varName ==
"dynamic_string" )
365 else if( varName ==
"computed_value" )
374 std::string expression;
380 const std::vector<TestCase> cases = {
381 {
"@{${dynamic_value}}",
"42", 0,
false },
382 {
"Message: ${dynamic_string}",
"Message: Hello from callback", 0,
false },
383 {
"@{format(${computed_value}, 1)}",
"70.7", 0.1,
false },
384 {
"@{${dynamic_value} + ${computed_value}}",
"112.7", 0.1,
false },
385 {
"${nonexistent}",
"${nonexistent}", 0,
true },
388 for(
const auto& testCase : cases )
390 auto result = evaluator.
Evaluate( wxString::FromUTF8( testCase.expression ) );
392 if( testCase.shouldError )
400 if( testCase.tolerance > 0 )
403 std::regex numberRegex( R
"([\d.]+)" );
405 std::string resultStr = result.ToStdString( wxConvUTF8 );
406 if( std::regex_search( resultStr, match, numberRegex ) )
408 double actualValue = std::stod( match[0].str() );
409 double expectedValue = std::stod( testCase.expected );
410 BOOST_CHECK_CLOSE( actualValue, expectedValue, testCase.tolerance * 100 );
427 std::vector<std::unique_ptr<EXPRESSION_EVALUATOR>> evaluators;
429 for(
int i = 0; i < 10; ++i )
431 auto evaluator = std::make_unique<EXPRESSION_EVALUATOR>();
432 evaluator->SetVariable(
"thread_id",
static_cast<double>( i ) );
433 evaluator->SetVariable(
"multiplier", 5.0 );
434 evaluators.push_back( std::move( evaluator ) );
438 for(
int i = 0; i < 10; ++i )
440 auto result = evaluators[i]->Evaluate(
"@{${thread_id} * ${multiplier}}" );
441 BOOST_CHECK( !evaluators[i]->HasErrors() );
443 double expected =
static_cast<double>( i * 5 );
444 double actual = std::stod(
result.ToStdString( wxConvUTF8 ) );
457 std::string complexExpression =
"@{";
458 for(
int i = 0; i < 100; ++i )
460 if( i > 0 ) complexExpression +=
" + ";
461 complexExpression += std::to_string( i );
463 complexExpression +=
"}";
465 auto result = evaluator.
Evaluate( wxString::FromUTF8( complexExpression ) );
472 for(
int i = 0; i < 1000; ++i )
474 auto expr =
"@{" + std::to_string( i ) +
" * 2}";
489 std::string expression;
493 std::string description;
496 const std::vector<TestCase> cases = {
498 {
"@{ 2 + 3 }",
"5",
false, 0.0,
"Spaces in expression" },
499 {
"@{\t2\t+\t3\t}",
"5",
false, 0.0,
"Tabs in expression" },
500 {
"@{\n2\n+\n3\n}",
"5",
false, 0.0,
"Newlines in expression" },
503 {
"@{\"Hello\\\"World\\\"\"}",
"Hello\"World\"",
false, 0.0,
"Escaped quotes in string" },
504 {
"@{\"Line1\\nLine2\"}",
"Line1\nLine2",
false, 0.0,
"Newline in string" },
507 {
"A: @{1+1}, B: @{2*2}, C: @{3^2}",
"A: 2, B: 4, C: 9",
false, 0.0,
"Multiple calculations" },
510 {
"@{((((2))))}",
"2",
false, 0.0,
"Multiple nested parentheses" },
511 {
"@{(2 + 3) * (4 + 5)}",
"45",
false, 0.0,
"Grouped operations" },
514 {
"No calculations here",
"No calculations here",
false, 0.0,
"Plain text" },
515 {
"",
"",
false, 0.0,
"Empty string" },
516 {
"@{0}",
"0",
false, 0.0,
"Zero value" },
517 {
"@{-0}",
"0",
false, 0.0,
"Negative zero" },
520 {
"@{0.1 + 0.2}",
"0.3",
false, 0.01,
"Floating point precision" },
521 {
"@{1.0 / 3.0}",
"0.333333",
false, 0.01,
"Repeating decimal" },
524 {
"@{1000000 * 1000000}",
"1e+12",
false, 0.01,
"Large number result" },
527 {
"Good @{2+2} bad @{2+} good @{3+3}",
"Good 4 bad @{2+} good 6",
true, 0.0,
"Error recovery" },
530 for(
const auto& testCase : cases )
532 auto result = evaluator.
Evaluate( wxString::FromUTF8( testCase.expression ) );
534 if( testCase.shouldError )
536 BOOST_CHECK_MESSAGE( evaluator.
HasErrors(),
"Expected error for: " + testCase.description );
540 if( testCase.precision > 0.0 )
543 std::regex numberRegex( R
"([\d.eE+-]+)" );
545 std::string resultStr = result.ToStdString( wxConvUTF8 );
546 if( std::regex_search( resultStr, match, numberRegex ) )
548 double actualValue = std::stod( match[0].str() );
549 double expectedValue = std::stod( testCase.expected );
550 BOOST_CHECK_CLOSE( actualValue, expectedValue, testCase.precision * 100 );
555 BOOST_CHECK_MESSAGE( !evaluator.
HasErrors(),
556 "Unexpected error for: " + testCase.description +
558 BOOST_CHECK_MESSAGE(
result.ToStdString( wxConvUTF8 ) == testCase.expected,
559 "Expected '" + testCase.expected +
"' but got '" +
560 result.ToStdString( wxConvUTF8 ) +
"' for: " + testCase.description );
582 std::vector<std::string> expressions = {
583 "Layer @{${board_layers}}/4",
584 "Components: @{${component_count}}",
585 "Nets: @{${net_count}}",
586 "Vias: @{${via_count}}",
587 "Area: @{${board_width} * ${board_height}} mm²",
588 "Density: @{format(${component_count} / (${board_width} * ${board_height} / 100), 1)} /cm²",
589 "Via density: @{format(${via_count} / (${board_width} * ${board_height} / 100), 1)} /cm²",
590 "Layer utilization: @{format(${net_count} / ${board_layers}, 1)} nets/layer",
591 "Design complexity: @{if(${component_count} > 100, \"High\", \"Low\")}",
592 "Board aspect ratio: @{format(${board_width} / ${board_height}, 2)}:1",
595 auto start = std::chrono::high_resolution_clock::now();
597 std::set<std::string> errors;
600 for(
int iteration = 0; iteration < 100; ++iteration )
602 for(
const auto& expr : expressions )
607 errors.insert( expr );
611 BOOST_REQUIRE_MESSAGE( errors.empty(), fmt::format(
"Evaluation of expressions had errors: {}", fmt::join( errors,
", " ) ) );
613 auto end = std::chrono::high_resolution_clock::now();
614 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
end - start );
617#if defined( KICAD_SANITIZE_THREADS ) || defined( KICAD_SANITIZE_ADDRESS )
618 BOOST_TEST_MESSAGE(
"Instrumented expression evaluation: " << duration.count() <<
" ms" );
620 BOOST_CHECK_LT( duration.count(), 100 );
624 for(
auto& expr : expressions )
626 auto result1 = evaluator.
Evaluate( wxString::FromUTF8( expr ) );
627 auto result2 = evaluator.
Evaluate( wxString::FromUTF8( expr ) );
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.
static wxString GetCurrentHash(const wxString &aProjectFile, bool aShort)
Return the current HEAD commit hash for the repository containing aProjectFile.
wxString PathStr() const
Get the path to the temporary directory as a wxString.
Make an environment current on this thread for the lifetime of the scope.
A text evaluation frame with one frozen clock and memoized external queries.
static ENVIRONMENT * Current()
Record every source read through aEnvironment into aValues, including memo hits.
RAII helper that sets the VCS context path on construction and restores the previous value on destruc...
static wxString GetCurrentTimeLocale()
static wxString GetCurrentTimeHHMMSS()
static wxString GetCurrentDate()
const wxString ExpandEnvVarSubstitutions(const wxString &aString, const PROJECT *aProject)
Replace any environment variable & text variable references with their values.
void SetGitBackend(GIT_BACKEND *aBackend)
GIT_BACKEND * GetGitBackend()
std::unique_ptr< git_tree, decltype([](git_tree *aTree) { git_tree_free(aTree); })> GitTreePtr
A unique pointer for git_tree objects with automatic cleanup.
std::unique_ptr< git_repository, decltype([](git_repository *aRepo) { git_repository_free(aRepo); })> GitRepositoryPtr
A unique pointer for git_repository objects with automatic cleanup.
std::unique_ptr< git_commit, decltype([](git_commit *aCommit) { git_commit_free(aCommit); })> GitCommitPtr
A unique pointer for git_commit objects with automatic cleanup.
std::unique_ptr< git_signature, decltype([](git_signature *aSignature) { git_signature_free(aSignature); })> GitSignaturePtr
A unique pointer for git_signature objects with automatic cleanup.
std::unique_ptr< git_index, decltype([](git_index *aIndex) { git_index_free(aIndex); })> GitIndexPtr
A unique pointer for git_index objects with automatic cleanup.
std::string GetEeschemaTestDataDir()
Get the configured location of Eeschema test data.
wxString GetContextPath()
Return the current context path for repo-scoped VCS queries.
bool GetContextIsFile()
File/directory classification captured when the current context was activated.
TEXT_EVAL::ENVIRONMENT::VCS_VALUE ReadSource(const TEXT_EVAL::ENVIRONMENT::VCS_KEY &aKey)
Re-read an owned query descriptor using any active frame memo.
auto MakeValue(T aVal) -> Result< T >
auto MakeError(std::string aMsg) -> Result< T >
std::map< VCS_KEY, VCS_VALUE > vcsValues
std::map< wxString, std::optional< wxString > > environmentVariables
std::optional< wxDateTime > time
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
VECTOR3I expected(15, 30, 45)
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
BOOST_AUTO_TEST_CASE(VcsNativeProjectContextPaths)
Declare the test suite.