40#include <wx/filename.h>
42#include <fmt/ranges.h>
69 const wxString projectFile = owner.
PathStr() +
"/multinetclasses.kicad_pro";
71 +
"/netlists/multinetclasses/multinetclasses.kicad_pro";
75 git_repository* rawRepo =
nullptr;
76 BOOST_REQUIRE_EQUAL( git_repository_init( &rawRepo, owner.
PathStr().ToUTF8().data(), 0 ), 0 );
78 BOOST_REQUIRE_EQUAL( git_repository_set_head( repo.get(),
"refs/heads/owner" ), 0 );
79 git_index* rawIndex =
nullptr;
80 BOOST_REQUIRE_EQUAL( git_repository_index( &rawIndex, repo.get() ), 0 );
82 BOOST_REQUIRE_EQUAL( git_index_add_bypath(
index.get(),
"multinetclasses.kicad_pro" ), 0 );
83 BOOST_REQUIRE_EQUAL( git_index_add_bypath(
index.get(),
"sub/multinetclasses.kicad_pro" ), 0 );
85 BOOST_REQUIRE_EQUAL( git_index_write_tree( &treeId,
index.get() ), 0 );
86 BOOST_REQUIRE_EQUAL( git_index_write(
index.get() ), 0 );
87 git_tree* rawTree =
nullptr;
88 BOOST_REQUIRE_EQUAL( git_tree_lookup( &rawTree, repo.get(), &treeId ), 0 );
90 git_signature* rawSignature =
nullptr;
91 BOOST_REQUIRE_EQUAL( git_signature_now( &rawSignature,
"Connectivity QA",
"[email protected]" ), 0 );
94 BOOST_REQUIRE_EQUAL( git_commit_create_v( &commitId, repo.get(),
"HEAD", signature.get(), signature.get(),
95 nullptr,
"Native project", tree.get(), 0 ), 0 );
97 const auto evaluateVcs = [](
const wxString& expression )
102 return result.ToStdString( wxConvUTF8 );
104 const auto commitHash = [&](
const std::string&
path =
"." )
106 wxString quoted = wxString::FromUTF8(
path );
107 quoted.Replace(
"\\",
"\\\\" );
108 quoted.Replace(
"\"",
"\\\"" );
109 return evaluateVcs( wxString::Format(
"@{vcsfileidentifier(\"%s\")}", quoted ) );
114 const auto hash = commitHash();
115 BOOST_REQUIRE_EQUAL( hash.size(), 40u );
141 wxString projectHash;
149 git_commit* rawCommit =
nullptr;
150 BOOST_REQUIRE_EQUAL( git_commit_lookup( &rawCommit, repo.get(), &commitId ), 0 );
153 BOOST_REQUIRE_EQUAL( git_commit_create_v( &changedId, repo.get(),
"HEAD", signature.get(), signature.get(),
154 nullptr,
"Next native project", tree.get(), 1, original.get() ), 0 );
155 git_reference* rawBranch =
nullptr;
156 BOOST_REQUIRE_EQUAL( git_reference_create( &rawBranch, repo.get(),
"refs/heads/changed",
157 &changedId, 0,
nullptr ), 0 );
158 git_reference_free( rawBranch );
159 BOOST_REQUIRE_EQUAL( git_repository_set_head( repo.get(),
"refs/heads/changed" ), 0 );
164 for(
const auto& [key, value] : sources.
vcsValues )
181 const wxDateTime time( 3, wxDateTime::Jan, 2001, 4, 5, 6 );
194 wxString::Format(
"%lld",
static_cast<long long>( time.GetTicks() ) ) );
196 wxString::Format(
"%lld",
static_cast<long long>( time.GetTicks() ) / ( 24 * 3600 ) ) );
205 evaluator.
Evaluate(
"@{random()}" );
208 BOOST_CHECK( *sources.
time == time );
210 const wxString envName = wxS(
"KICAD_QA_TEXT_EVAL_SOURCE" );
224 const wxString liveBefore = wxDateTime::Now().FormatISODate();
226 BOOST_CHECK( outside == liveBefore || outside == wxDateTime::Now().FormatISODate() );
243 evaluator.
SetVariable( std::string(
"project_name"), std::string(
"My PCB Project") );
244 evaluator.
SetVariable( std::string(
"designer"), std::string(
"John Doe") );
247 std::string expression;
248 std::string expectedPattern;
251 std::string description;
254 const std::vector<TestCase> cases = {
257 "Board area: @{${board_width} * ${board_height}} mm²",
258 "Board area: 8000 mm²",
260 "Board area calculation"
263 "Perimeter: @{2 * (${board_width} + ${board_height})} mm",
266 "Board perimeter calculation"
269 "Diagonal: @{format(sqrt(pow(${board_width}, 2) + pow(${board_height}, 2)), 1)} mm",
270 "Diagonal: 128.1 mm",
272 "Board diagonal calculation"
277 "Project: ${project_name} | Designer: ${designer} | Rev: @{${revision}}",
278 "Project: My PCB Project | Designer: John Doe | Rev: 3",
280 "Title block information"
283 "Components: @{${component_count}} | Density: @{format(${component_count} / (${board_width} * ${board_height} / 10000), 2)} per cm²",
284 "Components: 45 | Density: 56.25 per cm²",
286 "Component density calculation"
291 "Created: @{dateformat(today())} | Build: @{today()} days since epoch",
292 R
"(Created: \d{4}-\d{2}-\d{2} \| Build: \d+ days since epoch)",
294 "Date-based tracking"
299 "Status: @{if(${component_count} > 50, \"Complex\", \"Simple\")} design",
300 "Status: Simple design",
302 "Conditional design complexity"
305 "Status: @{if(${trace_width} >= 0.2, \"Standard\", \"Fine pitch\")} (@{${trace_width}}mm)",
306 "Status: Standard (0.2mm)",
308 "Conditional trace width description"
313 "PCB Summary:\n- Size: @{${board_width}}×@{${board_height}}mm\n- Area: @{${board_width} * ${board_height}}mm²\n- Components: @{${component_count}}",
314 "PCB Summary:\n- Size: 100×80mm\n- Area: 8000mm²\n- Components: 45",
316 "Multi-line documentation"
321 "Invalid: @{${undefined_var}} test",
322 "Invalid: @{${undefined_var}} test",
324 "Undefined variable behavior"
328 for(
const auto& testCase : cases )
330 auto result = evaluator.
Evaluate( wxString::FromUTF8( testCase.expression ) );
332 if( testCase.shouldError )
334 BOOST_CHECK_MESSAGE( evaluator.
HasErrors(),
335 "Expected error for: " + testCase.description );
339 BOOST_CHECK_MESSAGE( !evaluator.
HasErrors(),
340 "Unexpected error for: " + testCase.description +
343 if( testCase.isRegex )
345 std::regex pattern( testCase.expectedPattern );
346 BOOST_CHECK_MESSAGE( std::regex_match(
result.ToStdString( wxConvUTF8 ), pattern ),
347 "Result '" +
result.ToStdString( wxConvUTF8 ) +
"' doesn't match pattern '" +
348 testCase.expectedPattern +
"' for: " + testCase.description );
352 BOOST_CHECK_MESSAGE(
result.ToStdString( wxConvUTF8 ) == testCase.expectedPattern,
353 "Expected '" + testCase.expectedPattern +
"' but got '" +
354 result.ToStdString( wxConvUTF8 ) +
"' for: " + testCase.description );
366 auto variableCallback =
369 if( varName ==
"dynamic_value" )
371 else if( varName ==
"dynamic_string" )
373 else if( varName ==
"computed_value" )
382 std::string expression;
388 const std::vector<TestCase> cases = {
389 {
"@{${dynamic_value}}",
"42", 0,
false },
390 {
"Message: ${dynamic_string}",
"Message: Hello from callback", 0,
false },
391 {
"@{format(${computed_value}, 1)}",
"70.7", 0.1,
false },
392 {
"@{${dynamic_value} + ${computed_value}}",
"112.7", 0.1,
false },
393 {
"${nonexistent}",
"${nonexistent}", 0,
true },
396 for(
const auto& testCase : cases )
398 auto result = evaluator.
Evaluate( wxString::FromUTF8( testCase.expression ) );
400 if( testCase.shouldError )
408 if( testCase.tolerance > 0 )
411 std::regex numberRegex( R
"([\d.]+)" );
413 std::string resultStr = result.ToStdString( wxConvUTF8 );
414 if( std::regex_search( resultStr, match, numberRegex ) )
416 double actualValue = std::stod( match[0].str() );
417 double expectedValue = std::stod( testCase.expected );
418 BOOST_CHECK_CLOSE( actualValue, expectedValue, testCase.tolerance * 100 );
435 std::vector<std::unique_ptr<EXPRESSION_EVALUATOR>> evaluators;
437 for(
int i = 0; i < 10; ++i )
439 auto evaluator = std::make_unique<EXPRESSION_EVALUATOR>();
440 evaluator->SetVariable(
"thread_id",
static_cast<double>( i ) );
441 evaluator->SetVariable(
"multiplier", 5.0 );
442 evaluators.push_back( std::move( evaluator ) );
446 for(
int i = 0; i < 10; ++i )
448 auto result = evaluators[i]->Evaluate(
"@{${thread_id} * ${multiplier}}" );
449 BOOST_CHECK( !evaluators[i]->HasErrors() );
451 double expected =
static_cast<double>( i * 5 );
452 double actual = std::stod(
result.ToStdString( wxConvUTF8 ) );
465 std::string complexExpression =
"@{";
466 for(
int i = 0; i < 100; ++i )
468 if( i > 0 ) complexExpression +=
" + ";
469 complexExpression += std::to_string( i );
471 complexExpression +=
"}";
473 auto result = evaluator.
Evaluate( wxString::FromUTF8( complexExpression ) );
480 for(
int i = 0; i < 1000; ++i )
482 auto expr =
"@{" + std::to_string( i ) +
" * 2}";
497 std::string expression;
501 std::string description;
504 const std::vector<TestCase> cases = {
506 {
"@{ 2 + 3 }",
"5",
false, 0.0,
"Spaces in expression" },
507 {
"@{\t2\t+\t3\t}",
"5",
false, 0.0,
"Tabs in expression" },
508 {
"@{\n2\n+\n3\n}",
"5",
false, 0.0,
"Newlines in expression" },
511 {
"@{\"Hello\\\"World\\\"\"}",
"Hello\"World\"",
false, 0.0,
"Escaped quotes in string" },
512 {
"@{\"Line1\\nLine2\"}",
"Line1\nLine2",
false, 0.0,
"Newline in string" },
515 {
"A: @{1+1}, B: @{2*2}, C: @{3^2}",
"A: 2, B: 4, C: 9",
false, 0.0,
"Multiple calculations" },
518 {
"@{((((2))))}",
"2",
false, 0.0,
"Multiple nested parentheses" },
519 {
"@{(2 + 3) * (4 + 5)}",
"45",
false, 0.0,
"Grouped operations" },
522 {
"No calculations here",
"No calculations here",
false, 0.0,
"Plain text" },
523 {
"",
"",
false, 0.0,
"Empty string" },
524 {
"@{0}",
"0",
false, 0.0,
"Zero value" },
525 {
"@{-0}",
"0",
false, 0.0,
"Negative zero" },
528 {
"@{0.1 + 0.2}",
"0.3",
false, 0.01,
"Floating point precision" },
529 {
"@{1.0 / 3.0}",
"0.333333",
false, 0.01,
"Repeating decimal" },
532 {
"@{1000000 * 1000000}",
"1e+12",
false, 0.01,
"Large number result" },
535 {
"Good @{2+2} bad @{2+} good @{3+3}",
"Good 4 bad @{2+} good 6",
true, 0.0,
"Error recovery" },
538 for(
const auto& testCase : cases )
540 auto result = evaluator.
Evaluate( wxString::FromUTF8( testCase.expression ) );
542 if( testCase.shouldError )
544 BOOST_CHECK_MESSAGE( evaluator.
HasErrors(),
"Expected error for: " + testCase.description );
548 if( testCase.precision > 0.0 )
551 std::regex numberRegex( R
"([\d.eE+-]+)" );
553 std::string resultStr = result.ToStdString( wxConvUTF8 );
554 if( std::regex_search( resultStr, match, numberRegex ) )
556 double actualValue = std::stod( match[0].str() );
557 double expectedValue = std::stod( testCase.expected );
558 BOOST_CHECK_CLOSE( actualValue, expectedValue, testCase.precision * 100 );
563 BOOST_CHECK_MESSAGE( !evaluator.
HasErrors(),
564 "Unexpected error for: " + testCase.description +
566 BOOST_CHECK_MESSAGE(
result.ToStdString( wxConvUTF8 ) == testCase.expected,
567 "Expected '" + testCase.expected +
"' but got '" +
568 result.ToStdString( wxConvUTF8 ) +
"' for: " + testCase.description );
590 std::vector<std::string> expressions = {
591 "Layer @{${board_layers}}/4",
592 "Components: @{${component_count}}",
593 "Nets: @{${net_count}}",
594 "Vias: @{${via_count}}",
595 "Area: @{${board_width} * ${board_height}} mm²",
596 "Density: @{format(${component_count} / (${board_width} * ${board_height} / 100), 1)} /cm²",
597 "Via density: @{format(${via_count} / (${board_width} * ${board_height} / 100), 1)} /cm²",
598 "Layer utilization: @{format(${net_count} / ${board_layers}, 1)} nets/layer",
599 "Design complexity: @{if(${component_count} > 100, \"High\", \"Low\")}",
600 "Board aspect ratio: @{format(${board_width} / ${board_height}, 2)}:1",
603 auto start = std::chrono::high_resolution_clock::now();
605 std::set<std::string> errors;
608 for(
int iteration = 0; iteration < 100; ++iteration )
610 for(
const auto& expr : expressions )
615 errors.insert( expr );
619 BOOST_REQUIRE_MESSAGE( errors.empty(), fmt::format(
"Evaluation of expressions had errors: {}", fmt::join( errors,
", " ) ) );
621 auto end = std::chrono::high_resolution_clock::now();
622 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
end - start );
625#if defined( KICAD_SANITIZE_THREADS ) || defined( KICAD_SANITIZE_ADDRESS )
626 BOOST_TEST_MESSAGE(
"Instrumented expression evaluation: " << duration.count() <<
" ms" );
628 BOOST_CHECK_LT( duration.count(), 100 );
632 for(
auto& expr : expressions )
634 auto result1 = evaluator.
Evaluate( wxString::FromUTF8( expr ) );
635 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.
RAII helper that sets a process-level environment variable for the lifetime of the object.
void ClearValue()
Clear the environment variable for the lifetime of this object.
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.
RESULT< T > MakeError(std::string aMsg)
RESULT< T > MakeValue(T aVal)
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.