40#include <wx/filename.h>
42#include <fmt/ranges.h>
55 const wxString projectFile = owner.
PathStr() +
"/multinetclasses.kicad_pro";
57 +
"/netlists/multinetclasses/multinetclasses.kicad_pro";
65 git_index* rawIndex =
nullptr;
66 BOOST_REQUIRE_EQUAL( git_repository_index( &rawIndex, repo.get() ), GIT_OK );
68 BOOST_REQUIRE_EQUAL( git_index_add_bypath(
index.get(),
"multinetclasses.kicad_pro" ), GIT_OK );
69 BOOST_REQUIRE_EQUAL( git_index_add_bypath(
index.get(),
"sub/multinetclasses.kicad_pro" ), GIT_OK );
70 BOOST_REQUIRE_EQUAL( git_index_write(
index.get() ), GIT_OK );
73 const auto evaluateVcs = [](
const wxString& expression )
78 return result.ToStdString( wxConvUTF8 );
80 const auto commitHash = [&](
const std::string&
path =
"." )
82 wxString quoted = wxString::FromUTF8(
path );
83 quoted.Replace(
"\\",
"\\\\" );
84 quoted.Replace(
"\"",
"\\\"" );
85 return evaluateVcs( wxString::Format(
"@{vcsfileidentifier(\"%s\")}", quoted ) );
90 const auto hash = commitHash();
91 BOOST_REQUIRE_EQUAL( hash.size(), 40u );
117 wxString projectHash;
126 git_reference* rawBranch =
nullptr;
127 BOOST_REQUIRE_EQUAL( git_reference_create( &rawBranch, repo.get(),
"refs/heads/changed",
128 &changedId, 0,
nullptr ), 0 );
129 git_reference_free( rawBranch );
130 BOOST_REQUIRE_EQUAL( git_repository_set_head( repo.get(),
"refs/heads/changed" ), 0 );
135 for(
const auto& [key, value] : sources.
vcsValues )
152 const wxDateTime time( 3, wxDateTime::Jan, 2001, 4, 5, 6 );
165 wxString::Format(
"%lld",
static_cast<long long>( time.GetTicks() ) ) );
167 wxString::Format(
"%lld",
static_cast<long long>( time.GetTicks() ) / ( 24 * 3600 ) ) );
176 evaluator.
Evaluate(
"@{random()}" );
179 BOOST_CHECK( *sources.
time == time );
181 const wxString envName = wxS(
"KICAD_QA_TEXT_EVAL_SOURCE" );
195 const wxString liveBefore = wxDateTime::Now().FormatISODate();
197 BOOST_CHECK( outside == liveBefore || outside == wxDateTime::Now().FormatISODate() );
214 evaluator.
SetVariable( std::string(
"project_name"), std::string(
"My PCB Project") );
215 evaluator.
SetVariable( std::string(
"designer"), std::string(
"John Doe") );
218 std::string expression;
219 std::string expectedPattern;
222 std::string description;
225 const std::vector<TestCase> cases = {
228 "Board area: @{${board_width} * ${board_height}} mm²",
229 "Board area: 8000 mm²",
231 "Board area calculation"
234 "Perimeter: @{2 * (${board_width} + ${board_height})} mm",
237 "Board perimeter calculation"
240 "Diagonal: @{format(sqrt(pow(${board_width}, 2) + pow(${board_height}, 2)), 1)} mm",
241 "Diagonal: 128.1 mm",
243 "Board diagonal calculation"
248 "Project: ${project_name} | Designer: ${designer} | Rev: @{${revision}}",
249 "Project: My PCB Project | Designer: John Doe | Rev: 3",
251 "Title block information"
254 "Components: @{${component_count}} | Density: @{format(${component_count} / (${board_width} * ${board_height} / 10000), 2)} per cm²",
255 "Components: 45 | Density: 56.25 per cm²",
257 "Component density calculation"
262 "Created: @{dateformat(today())} | Build: @{today()} days since epoch",
263 R
"(Created: \d{4}-\d{2}-\d{2} \| Build: \d+ days since epoch)",
265 "Date-based tracking"
270 "Status: @{if(${component_count} > 50, \"Complex\", \"Simple\")} design",
271 "Status: Simple design",
273 "Conditional design complexity"
276 "Status: @{if(${trace_width} >= 0.2, \"Standard\", \"Fine pitch\")} (@{${trace_width}}mm)",
277 "Status: Standard (0.2mm)",
279 "Conditional trace width description"
284 "PCB Summary:\n- Size: @{${board_width}}×@{${board_height}}mm\n- Area: @{${board_width} * ${board_height}}mm²\n- Components: @{${component_count}}",
285 "PCB Summary:\n- Size: 100×80mm\n- Area: 8000mm²\n- Components: 45",
287 "Multi-line documentation"
292 "Invalid: @{${undefined_var}} test",
293 "Invalid: @{${undefined_var}} test",
295 "Undefined variable behavior"
299 for(
const auto& testCase : cases )
301 auto result = evaluator.
Evaluate( wxString::FromUTF8( testCase.expression ) );
303 if( testCase.shouldError )
305 BOOST_CHECK_MESSAGE( evaluator.
HasErrors(),
306 "Expected error for: " + testCase.description );
310 BOOST_CHECK_MESSAGE( !evaluator.
HasErrors(),
311 "Unexpected error for: " + testCase.description +
314 if( testCase.isRegex )
316 std::regex pattern( testCase.expectedPattern );
317 BOOST_CHECK_MESSAGE( std::regex_match(
result.ToStdString( wxConvUTF8 ), pattern ),
318 "Result '" +
result.ToStdString( wxConvUTF8 ) +
"' doesn't match pattern '" +
319 testCase.expectedPattern +
"' for: " + testCase.description );
323 BOOST_CHECK_MESSAGE(
result.ToStdString( wxConvUTF8 ) == testCase.expectedPattern,
324 "Expected '" + testCase.expectedPattern +
"' but got '" +
325 result.ToStdString( wxConvUTF8 ) +
"' for: " + testCase.description );
337 auto variableCallback =
340 if( varName ==
"dynamic_value" )
342 else if( varName ==
"dynamic_string" )
344 else if( varName ==
"computed_value" )
353 std::string expression;
359 const std::vector<TestCase> cases = {
360 {
"@{${dynamic_value}}",
"42", 0,
false },
361 {
"Message: ${dynamic_string}",
"Message: Hello from callback", 0,
false },
362 {
"@{format(${computed_value}, 1)}",
"70.7", 0.1,
false },
363 {
"@{${dynamic_value} + ${computed_value}}",
"112.7", 0.1,
false },
364 {
"${nonexistent}",
"${nonexistent}", 0,
true },
367 for(
const auto& testCase : cases )
369 auto result = evaluator.
Evaluate( wxString::FromUTF8( testCase.expression ) );
371 if( testCase.shouldError )
379 if( testCase.tolerance > 0 )
382 std::regex numberRegex( R
"([\d.]+)" );
384 std::string resultStr = result.ToStdString( wxConvUTF8 );
385 if( std::regex_search( resultStr, match, numberRegex ) )
387 double actualValue = std::stod( match[0].str() );
388 double expectedValue = std::stod( testCase.expected );
389 BOOST_CHECK_CLOSE( actualValue, expectedValue, testCase.tolerance * 100 );
406 std::vector<std::unique_ptr<EXPRESSION_EVALUATOR>> evaluators;
408 for(
int i = 0; i < 10; ++i )
410 auto evaluator = std::make_unique<EXPRESSION_EVALUATOR>();
411 evaluator->SetVariable(
"thread_id",
static_cast<double>( i ) );
412 evaluator->SetVariable(
"multiplier", 5.0 );
413 evaluators.push_back( std::move( evaluator ) );
417 for(
int i = 0; i < 10; ++i )
419 auto result = evaluators[i]->Evaluate(
"@{${thread_id} * ${multiplier}}" );
420 BOOST_CHECK( !evaluators[i]->HasErrors() );
422 double expected =
static_cast<double>( i * 5 );
423 double actual = std::stod(
result.ToStdString( wxConvUTF8 ) );
436 std::string complexExpression =
"@{";
437 for(
int i = 0; i < 100; ++i )
439 if( i > 0 ) complexExpression +=
" + ";
440 complexExpression += std::to_string( i );
442 complexExpression +=
"}";
444 auto result = evaluator.
Evaluate( wxString::FromUTF8( complexExpression ) );
451 for(
int i = 0; i < 1000; ++i )
453 auto expr =
"@{" + std::to_string( i ) +
" * 2}";
468 std::string expression;
472 std::string description;
475 const std::vector<TestCase> cases = {
477 {
"@{ 2 + 3 }",
"5",
false, 0.0,
"Spaces in expression" },
478 {
"@{\t2\t+\t3\t}",
"5",
false, 0.0,
"Tabs in expression" },
479 {
"@{\n2\n+\n3\n}",
"5",
false, 0.0,
"Newlines in expression" },
482 {
"@{\"Hello\\\"World\\\"\"}",
"Hello\"World\"",
false, 0.0,
"Escaped quotes in string" },
483 {
"@{\"Line1\\nLine2\"}",
"Line1\nLine2",
false, 0.0,
"Newline in string" },
486 {
"A: @{1+1}, B: @{2*2}, C: @{3^2}",
"A: 2, B: 4, C: 9",
false, 0.0,
"Multiple calculations" },
489 {
"@{((((2))))}",
"2",
false, 0.0,
"Multiple nested parentheses" },
490 {
"@{(2 + 3) * (4 + 5)}",
"45",
false, 0.0,
"Grouped operations" },
493 {
"No calculations here",
"No calculations here",
false, 0.0,
"Plain text" },
494 {
"",
"",
false, 0.0,
"Empty string" },
495 {
"@{0}",
"0",
false, 0.0,
"Zero value" },
496 {
"@{-0}",
"0",
false, 0.0,
"Negative zero" },
499 {
"@{0.1 + 0.2}",
"0.3",
false, 0.01,
"Floating point precision" },
500 {
"@{1.0 / 3.0}",
"0.333333",
false, 0.01,
"Repeating decimal" },
503 {
"@{1000000 * 1000000}",
"1e+12",
false, 0.01,
"Large number result" },
506 {
"Good @{2+2} bad @{2+} good @{3+3}",
"Good 4 bad @{2+} good 6",
true, 0.0,
"Error recovery" },
509 for(
const auto& testCase : cases )
511 auto result = evaluator.
Evaluate( wxString::FromUTF8( testCase.expression ) );
513 if( testCase.shouldError )
515 BOOST_CHECK_MESSAGE( evaluator.
HasErrors(),
"Expected error for: " + testCase.description );
519 if( testCase.precision > 0.0 )
522 std::regex numberRegex( R
"([\d.eE+-]+)" );
524 std::string resultStr = result.ToStdString( wxConvUTF8 );
525 if( std::regex_search( resultStr, match, numberRegex ) )
527 double actualValue = std::stod( match[0].str() );
528 double expectedValue = std::stod( testCase.expected );
529 BOOST_CHECK_CLOSE( actualValue, expectedValue, testCase.precision * 100 );
534 BOOST_CHECK_MESSAGE( !evaluator.
HasErrors(),
535 "Unexpected error for: " + testCase.description +
537 BOOST_CHECK_MESSAGE(
result.ToStdString( wxConvUTF8 ) == testCase.expected,
538 "Expected '" + testCase.expected +
"' but got '" +
539 result.ToStdString( wxConvUTF8 ) +
"' for: " + testCase.description );
561 std::vector<std::string> expressions = {
562 "Layer @{${board_layers}}/4",
563 "Components: @{${component_count}}",
564 "Nets: @{${net_count}}",
565 "Vias: @{${via_count}}",
566 "Area: @{${board_width} * ${board_height}} mm²",
567 "Density: @{format(${component_count} / (${board_width} * ${board_height} / 100), 1)} /cm²",
568 "Via density: @{format(${via_count} / (${board_width} * ${board_height} / 100), 1)} /cm²",
569 "Layer utilization: @{format(${net_count} / ${board_layers}, 1)} nets/layer",
570 "Design complexity: @{if(${component_count} > 100, \"High\", \"Low\")}",
571 "Board aspect ratio: @{format(${board_width} / ${board_height}, 2)}:1",
574 auto start = std::chrono::high_resolution_clock::now();
576 std::set<std::string> errors;
579 for(
int iteration = 0; iteration < 100; ++iteration )
581 for(
const auto& expr : expressions )
586 errors.insert( expr );
590 BOOST_REQUIRE_MESSAGE( errors.empty(), fmt::format(
"Evaluation of expressions had errors: {}", fmt::join( errors,
", " ) ) );
592 auto end = std::chrono::high_resolution_clock::now();
593 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
end - start );
596#if defined( KICAD_SANITIZE_THREADS ) || defined( KICAD_SANITIZE_ADDRESS )
597 BOOST_TEST_MESSAGE(
"Instrumented expression evaluation: " << duration.count() <<
" ms" );
599 BOOST_CHECK_LT( duration.count(), 100 );
603 for(
auto& expr : expressions )
605 auto result1 = evaluator.
Evaluate( wxString::FromUTF8( expr ) );
606 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.
Install a real backend for a test and restore the previous backend afterwards.
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, bool aAllowEscapes)
Replace any environment variable & text variable references with their values.
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_index, decltype([](git_index *aIndex) { git_index_free(aIndex); })> GitIndexPtr
A unique pointer for git_index objects with automatic cleanup.
git_oid CommitGitIndex(git_repository *aRepo, const wxString &aMessage)
Commit the current index, using HEAD as the parent when it exists.
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.
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.