KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_text_eval_parser_integration.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
24
26#include <qa_utils/file_utils.h>
28
29// Code under test
30#include <common.h>
31#include <git/git_backend.h>
33#include <git/libgit_backend.h>
38#include <title_block.h>
39#include <wx/filefn.h>
40#include <wx/filename.h>
41
42#include <fmt/ranges.h>
43#include <chrono>
44#include <regex>
45
49BOOST_AUTO_TEST_SUITE( TextEvalParserIntegration )
50
51BOOST_AUTO_TEST_CASE( VcsNativeProjectContextPaths )
52{
53 struct BACKEND_SCOPE
54 {
55 GIT_BACKEND* previous = GetGitBackend();
56 LIBGIT_BACKEND backend;
57 BACKEND_SCOPE()
58 {
59 backend.Init();
60 SetGitBackend( &backend );
61 }
62 ~BACKEND_SCOPE()
63 {
64 SetGitBackend( previous );
65 backend.Shutdown();
66 }
67 } backend;
68 KI_TEST::SCOPED_TEMP_DIR owner( "text-vcs-owner" );
69 const wxString projectFile = owner.PathStr() + "/multinetclasses.kicad_pro";
70 const wxString source = wxString::FromUTF8( KI_TEST::GetEeschemaTestDataDir() )
71 + "/netlists/multinetclasses/multinetclasses.kicad_pro";
72 BOOST_REQUIRE( wxCopyFile( source, projectFile ) );
73 BOOST_REQUIRE( wxMkdir( owner.PathStr() + "/sub" ) );
74 BOOST_REQUIRE( wxCopyFile( source, owner.PathStr() + "/sub/multinetclasses.kicad_pro" ) );
75 git_repository* rawRepo = nullptr;
76 BOOST_REQUIRE_EQUAL( git_repository_init( &rawRepo, owner.PathStr().ToUTF8().data(), 0 ), 0 );
77 KIGIT::GitRepositoryPtr repo( rawRepo );
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 );
81 KIGIT::GitIndexPtr index( rawIndex );
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 );
84 git_oid treeId;
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 );
89 KIGIT::GitTreePtr tree( rawTree );
90 git_signature* rawSignature = nullptr;
91 BOOST_REQUIRE_EQUAL( git_signature_now( &rawSignature, "Connectivity QA", "[email protected]" ), 0 );
92 KIGIT::GitSignaturePtr signature( rawSignature );
93 git_oid commitId;
94 BOOST_REQUIRE_EQUAL( git_commit_create_v( &commitId, repo.get(), "HEAD", signature.get(), signature.get(),
95 nullptr, "Native project", tree.get(), 0 ), 0 );
96 const wxString previous = TEXT_EVAL_VCS::GetContextPath();
97 const auto evaluateVcs = []( const wxString& expression )
98 {
99 EXPRESSION_EVALUATOR evaluator;
100 const wxString result = evaluator.Evaluate( expression );
101 BOOST_REQUIRE_MESSAGE( !evaluator.HasErrors(), evaluator.GetErrorSummary() );
102 return result.ToStdString( wxConvUTF8 );
103 };
104 const auto commitHash = [&]( const std::string& path = "." )
105 {
106 wxString quoted = wxString::FromUTF8( path );
107 quoted.Replace( "\\", "\\\\" );
108 quoted.Replace( "\"", "\\\"" );
109 return evaluateVcs( wxString::Format( "@{vcsfileidentifier(\"%s\")}", quoted ) );
110 };
111
112 {
114 const auto hash = commitHash();
115 BOOST_REQUIRE_EQUAL( hash.size(), 40u );
116 BOOST_CHECK_EQUAL( evaluateVcs( "@{vcsbranch()}" ), "owner" );
117 BOOST_CHECK_EQUAL( commitHash( "multinetclasses.kicad_pro" ), hash );
118 BOOST_CHECK_EQUAL( commitHash( projectFile.ToStdString( wxConvUTF8 ) ), hash );
119 BOOST_CHECK_EQUAL( commitHash( "untracked.kicad_pro" ), "<unknown>" );
120
121 {
122 TEXT_EVAL_VCS::CONTEXT_PATH_SCOPE fileContext( owner.PathStr() + "/sub/multinetclasses.kicad_pro" );
123 BOOST_CHECK( TEXT_EVAL_VCS::GetContextIsFile() );
124 BOOST_CHECK_EQUAL( commitHash( "multinetclasses.kicad_pro" ), hash );
125 BOOST_CHECK_EQUAL( commitHash( "../multinetclasses.kicad_pro" ), hash );
126 BOOST_REQUIRE( wxRemoveFile( owner.PathStr() + "/sub/multinetclasses.kicad_pro" ) );
127 BOOST_CHECK_EQUAL( commitHash( "multinetclasses.kicad_pro" ), hash );
128 }
129
131 BOOST_CHECK( !TEXT_EVAL_VCS::GetContextIsFile() );
132 }
133
135
136 {
138 TEXT_EVAL::ENVIRONMENT environment;
139 TEXT_EVAL::ENVIRONMENT_SCOPE frame( environment );
141 wxString projectHash;
142
143 {
144 TEXT_EVAL::SOURCE_SCOPE collect( environment, sources );
145 projectHash = KIGIT::PROJECT_GIT_UTILS::GetCurrentHash( projectFile, false );
146 BOOST_CHECK_EQUAL( evaluateVcs( "@{vcsbranch()}" ), "owner" );
147 }
148
149 git_commit* rawCommit = nullptr;
150 BOOST_REQUIRE_EQUAL( git_commit_lookup( &rawCommit, repo.get(), &commitId ), 0 );
151 KIGIT::GitCommitPtr original( rawCommit );
152 git_oid changedId;
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 );
160 BOOST_CHECK_EQUAL( KIGIT::PROJECT_GIT_UTILS::GetCurrentHash( projectFile, false ), projectHash );
161 BOOST_CHECK_EQUAL( commitHash(), projectHash.ToStdString( wxConvUTF8 ) );
162 BOOST_CHECK_EQUAL( evaluateVcs( "@{vcsbranch()}" ), "owner" );
163
164 for( const auto& [key, value] : sources.vcsValues )
165 BOOST_CHECK( TEXT_EVAL_VCS::ReadSource( key ) == value );
166
167 {
168 TEXT_EVAL::ENVIRONMENT refreshed;
169 TEXT_EVAL::ENVIRONMENT_SCOPE refreshedFrame( refreshed );
170 BOOST_CHECK_EQUAL( evaluateVcs( "@{vcsbranch()}" ), "changed" );
171 BOOST_CHECK( KIGIT::PROJECT_GIT_UTILS::GetCurrentHash( projectFile, false ) != projectHash );
172 }
173
174 BOOST_CHECK_EQUAL( evaluateVcs( "@{vcsbranch()}" ), "owner" );
175 }
176}
177
178
179BOOST_AUTO_TEST_CASE( EnvironmentFrameCapturesDynamicSources )
180{
181 const wxDateTime time( 3, wxDateTime::Jan, 2001, 4, 5, 6 );
182 TEXT_EVAL::ENVIRONMENT environment( time );
184 BOOST_CHECK( TEXT_EVAL::ENVIRONMENT::Current() == nullptr );
185
186 {
187 TEXT_EVAL::ENVIRONMENT_SCOPE frame( environment );
188 TEXT_EVAL::SOURCE_SCOPE collect( environment, sources );
189 BOOST_CHECK_EQUAL( TITLE_BLOCK::GetCurrentDate(), time.FormatISODate() );
190 BOOST_CHECK_EQUAL( TITLE_BLOCK::GetCurrentTimeHHMMSS(), time.Format( "%Hh%Mm%Ss" ) );
192 EXPRESSION_EVALUATOR evaluator;
193 BOOST_CHECK_EQUAL( evaluator.Evaluate( "@{format(now(), 0)}" ),
194 wxString::Format( "%lld", static_cast<long long>( time.GetTicks() ) ) );
195 BOOST_CHECK_EQUAL( evaluator.Evaluate( "@{format(today(), 0)}" ),
196 wxString::Format( "%lld", static_cast<long long>( time.GetTicks() ) / ( 24 * 3600 ) ) );
197 TEXT_EVAL::ENVIRONMENT other( wxDateTime( 4, wxDateTime::Feb, 2002 ) );
198
199 {
200 TEXT_EVAL::ENVIRONMENT_SCOPE nested( other );
201 BOOST_CHECK_EQUAL( TITLE_BLOCK::GetCurrentDate(), wxString( "2002-02-04" ) );
202 }
203
204 BOOST_CHECK_EQUAL( TITLE_BLOCK::GetCurrentDate(), time.FormatISODate() );
205 evaluator.Evaluate( "@{random()}" );
206 BOOST_CHECK( sources.randomUsed );
207 BOOST_REQUIRE( sources.time );
208 BOOST_CHECK( *sources.time == time );
209
210 const wxString envName = wxS( "KICAD_QA_TEXT_EVAL_SOURCE" );
211
212 {
213 KI_TEST::SCOPED_PROCESS_ENV_VAR scoped( envName, wxS( "captured" ) );
214 const wxString expanded = ExpandEnvVarSubstitutions( wxS( "${KICAD_QA_TEXT_EVAL_SOURCE}" ), nullptr );
215
216 BOOST_CHECK_EQUAL( expanded, wxS( "captured" ) );
217
218 scoped.ClearValue();
219 BOOST_CHECK( sources.environmentVariables[envName] == wxString( wxS( "captured" ) ) );
220 }
221 }
222
223 BOOST_CHECK( TEXT_EVAL::ENVIRONMENT::Current() == nullptr );
224 const wxString liveBefore = wxDateTime::Now().FormatISODate();
225 const wxString outside = TITLE_BLOCK::GetCurrentDate();
226 BOOST_CHECK( outside == liveBefore || outside == wxDateTime::Now().FormatISODate() );
227}
228
229
233BOOST_AUTO_TEST_CASE( RealWorldScenarios )
234{
235 EXPRESSION_EVALUATOR evaluator;
236
237 // Set up variables that might be used in actual KiCad projects
238 evaluator.SetVariable( "board_width", 100.0 );
239 evaluator.SetVariable( "board_height", 80.0 );
240 evaluator.SetVariable( "trace_width", 0.2 );
241 evaluator.SetVariable( "component_count", 45.0 );
242 evaluator.SetVariable( "revision", 3.0 );
243 evaluator.SetVariable( std::string("project_name"), std::string("My PCB Project") );
244 evaluator.SetVariable( std::string("designer"), std::string("John Doe") );
245
246 struct TestCase {
247 std::string expression;
248 std::string expectedPattern; // Can be exact match or regex pattern
249 bool isRegex;
250 bool shouldError;
251 std::string description;
252 };
253
254 const std::vector<TestCase> cases = {
255 // Board dimension calculations
256 {
257 "Board area: @{${board_width} * ${board_height}} mm²",
258 "Board area: 8000 mm²",
259 false, false,
260 "Board area calculation"
261 },
262 {
263 "Perimeter: @{2 * (${board_width} + ${board_height})} mm",
264 "Perimeter: 360 mm",
265 false, false,
266 "Board perimeter calculation"
267 },
268 {
269 "Diagonal: @{format(sqrt(pow(${board_width}, 2) + pow(${board_height}, 2)), 1)} mm",
270 "Diagonal: 128.1 mm",
271 false, false,
272 "Board diagonal calculation"
273 },
274
275 // Text formatting scenarios
276 {
277 "Project: ${project_name} | Designer: ${designer} | Rev: @{${revision}}",
278 "Project: My PCB Project | Designer: John Doe | Rev: 3",
279 false, false,
280 "Title block information"
281 },
282 {
283 "Components: @{${component_count}} | Density: @{format(${component_count} / (${board_width} * ${board_height} / 10000), 2)} per cm²",
284 "Components: 45 | Density: 56.25 per cm²",
285 false, false,
286 "Component density calculation"
287 },
288
289 // Date-based revision tracking
290 {
291 "Created: @{dateformat(today())} | Build: @{today()} days since epoch",
292 R"(Created: \d{4}-\d{2}-\d{2} \| Build: \d+ days since epoch)",
293 true, false,
294 "Date-based tracking"
295 },
296
297 // Conditional formatting
298 {
299 "Status: @{if(${component_count} > 50, \"Complex\", \"Simple\")} design",
300 "Status: Simple design",
301 false, false,
302 "Conditional design complexity"
303 },
304 {
305 "Status: @{if(${trace_width} >= 0.2, \"Standard\", \"Fine pitch\")} (@{${trace_width}}mm)",
306 "Status: Standard (0.2mm)",
307 false, false,
308 "Conditional trace width description"
309 },
310
311 // Multi-line documentation
312 {
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",
315 false, false,
316 "Multi-line documentation"
317 },
318
319 // Error scenarios - undefined variables error and return unchanged
320 {
321 "Invalid: @{${undefined_var}} test",
322 "Invalid: @{${undefined_var}} test",
323 false, true,
324 "Undefined variable behavior"
325 },
326 };
327
328 for( const auto& testCase : cases )
329 {
330 auto result = evaluator.Evaluate( wxString::FromUTF8( testCase.expression ) );
331
332 if( testCase.shouldError )
333 {
334 BOOST_CHECK_MESSAGE( evaluator.HasErrors(),
335 "Expected error for: " + testCase.description );
336 }
337 else
338 {
339 BOOST_CHECK_MESSAGE( !evaluator.HasErrors(),
340 "Unexpected error for: " + testCase.description +
341 " - " + evaluator.GetErrorSummary().ToStdString() );
342
343 if( testCase.isRegex )
344 {
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 );
349 }
350 else
351 {
352 BOOST_CHECK_MESSAGE( result.ToStdString( wxConvUTF8 ) == testCase.expectedPattern,
353 "Expected '" + testCase.expectedPattern + "' but got '" +
354 result.ToStdString( wxConvUTF8 ) + "' for: " + testCase.description );
355 }
356 }
357 }
358}
359
363BOOST_AUTO_TEST_CASE( CallbackVariableResolution )
364{
365 // Create evaluator with custom callback
366 auto variableCallback =
367 []( const std::string& varName ) -> calc_parser::RESULT<calc_parser::VALUE>
368 {
369 if( varName == "dynamic_value" )
371 else if( varName == "dynamic_string" )
372 return calc_parser::MakeValue<calc_parser::VALUE>( std::string( "Hello from callback" ) );
373 else if( varName == "computed_value" )
374 return calc_parser::MakeValue<calc_parser::VALUE>( std::sin( 3.14159 / 4 ) * 100.0 ); // Should be about 70.7
375 else
376 return calc_parser::MakeError<calc_parser::VALUE>( "Variable '" + varName + "' not found in callback" );
377 };
378
379 EXPRESSION_EVALUATOR evaluator( variableCallback, false );
380
381 struct TestCase {
382 std::string expression;
383 std::string expected;
384 double tolerance;
385 bool shouldError;
386 };
387
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 },
394 };
395
396 for( const auto& testCase : cases )
397 {
398 auto result = evaluator.Evaluate( wxString::FromUTF8( testCase.expression ) );
399
400 if( testCase.shouldError )
401 {
402 BOOST_CHECK( evaluator.HasErrors() );
403 }
404 else
405 {
406 BOOST_CHECK( !evaluator.HasErrors() );
407
408 if( testCase.tolerance > 0 )
409 {
410 // For floating point comparisons, extract the number
411 std::regex numberRegex( R"([\d.]+)" );
412 std::smatch match;
413 std::string resultStr = result.ToStdString( wxConvUTF8 );
414 if( std::regex_search( resultStr, match, numberRegex ) )
415 {
416 double actualValue = std::stod( match[0].str() );
417 double expectedValue = std::stod( testCase.expected );
418 BOOST_CHECK_CLOSE( actualValue, expectedValue, testCase.tolerance * 100 );
419 }
420 }
421 else
422 {
423 BOOST_CHECK_EQUAL( result.ToStdString( wxConvUTF8 ), testCase.expected );
424 }
425 }
426 }
427}
428
432BOOST_AUTO_TEST_CASE( ThreadSafety )
433{
434 // Create multiple evaluators that could be used in different threads
435 std::vector<std::unique_ptr<EXPRESSION_EVALUATOR>> evaluators;
436
437 for( int i = 0; i < 10; ++i )
438 {
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 ) );
443 }
444
445 // Test that each evaluator maintains its own state
446 for( int i = 0; i < 10; ++i )
447 {
448 auto result = evaluators[i]->Evaluate( "@{${thread_id} * ${multiplier}}" );
449 BOOST_CHECK( !evaluators[i]->HasErrors() );
450
451 double expected = static_cast<double>( i * 5 );
452 double actual = std::stod( result.ToStdString( wxConvUTF8 ) );
453 BOOST_CHECK_CLOSE( actual, expected, 0.001 );
454 }
455}
456
460BOOST_AUTO_TEST_CASE( MemoryManagement )
461{
462 EXPRESSION_EVALUATOR evaluator;
463
464 // Test large nested expressions
465 std::string complexExpression = "@{";
466 for( int i = 0; i < 100; ++i )
467 {
468 if( i > 0 ) complexExpression += " + ";
469 complexExpression += std::to_string( i );
470 }
471 complexExpression += "}";
472
473 auto result = evaluator.Evaluate( wxString::FromUTF8( complexExpression ) );
474 BOOST_CHECK( !evaluator.HasErrors() );
475
476 // Sum of 0..99 is 4950
477 BOOST_CHECK_EQUAL( result.ToStdString( wxConvUTF8 ), std::string( "4950" ) );
478
479 // Test many small expressions
480 for( int i = 0; i < 1000; ++i )
481 {
482 auto expr = "@{" + std::to_string( i ) + " * 2}";
483 auto result = evaluator.Evaluate( wxString::FromUTF8( expr ) );
484 BOOST_CHECK( !evaluator.HasErrors() );
485 BOOST_CHECK_EQUAL( result.ToStdString( wxConvUTF8 ), std::to_string( i * 2 ) );
486 }
487}
488
492BOOST_AUTO_TEST_CASE( ParsingEdgeCases )
493{
494 EXPRESSION_EVALUATOR evaluator;
495
496 struct TestCase {
497 std::string expression;
498 std::string expected;
499 bool shouldError;
500 double precision;
501 std::string description;
502 };
503
504 const std::vector<TestCase> cases = {
505 // Whitespace handling
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" },
509
510 // String escaping and special characters
511 { "@{\"Hello\\\"World\\\"\"}", "Hello\"World\"", false, 0.0, "Escaped quotes in string" },
512 { "@{\"Line1\\nLine2\"}", "Line1\nLine2", false, 0.0, "Newline in string" },
513
514 // Multiple calculations in complex text
515 { "A: @{1+1}, B: @{2*2}, C: @{3^2}", "A: 2, B: 4, C: 9", false, 0.0, "Multiple calculations" },
516
517 // Edge cases with parentheses
518 { "@{((((2))))}", "2", false, 0.0, "Multiple nested parentheses" },
519 { "@{(2 + 3) * (4 + 5)}", "45", false, 0.0, "Grouped operations" },
520
521 // Empty and minimal expressions
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" },
526
527 // Precision and rounding edge cases
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" },
530
531 // Large numbers
532 { "@{1000000 * 1000000}", "1e+12", false, 0.01, "Large number result" },
533
534 // Error recovery - malformed expressions left unchanged, valid ones evaluated
535 { "Good @{2+2} bad @{2+} good @{3+3}", "Good 4 bad @{2+} good 6", true, 0.0, "Error recovery" },
536 };
537
538 for( const auto& testCase : cases )
539 {
540 auto result = evaluator.Evaluate( wxString::FromUTF8( testCase.expression ) );
541
542 if( testCase.shouldError )
543 {
544 BOOST_CHECK_MESSAGE( evaluator.HasErrors(), "Expected error for: " + testCase.description );
545 }
546 else
547 {
548 if( testCase.precision > 0.0 )
549 {
550 // For floating point comparisons, extract the number
551 std::regex numberRegex( R"([\d.eE+-]+)" );
552 std::smatch match;
553 std::string resultStr = result.ToStdString( wxConvUTF8 );
554 if( std::regex_search( resultStr, match, numberRegex ) )
555 {
556 double actualValue = std::stod( match[0].str() );
557 double expectedValue = std::stod( testCase.expected );
558 BOOST_CHECK_CLOSE( actualValue, expectedValue, testCase.precision * 100 );
559 }
560 }
561 else
562 {
563 BOOST_CHECK_MESSAGE( !evaluator.HasErrors(),
564 "Unexpected error for: " + testCase.description +
565 " - " + evaluator.GetErrorSummary().ToStdString() );
566 BOOST_CHECK_MESSAGE( result.ToStdString( wxConvUTF8 ) == testCase.expected,
567 "Expected '" + testCase.expected + "' but got '" +
568 result.ToStdString( wxConvUTF8 ) + "' for: " + testCase.description );
569 }
570 }
571 }
572}
573
577BOOST_AUTO_TEST_CASE( RealWorldPerformance )
578{
579 EXPRESSION_EVALUATOR evaluator;
580
581 // Set up variables for a typical PCB project
582 evaluator.SetVariable( "board_layers", 4.0 );
583 evaluator.SetVariable( "component_count", 150.0 );
584 evaluator.SetVariable( "net_count", 200.0 );
585 evaluator.SetVariable( "via_count", 300.0 );
586 evaluator.SetVariable( "board_width", 120.0 );
587 evaluator.SetVariable( "board_height", 80.0 );
588
589 // Simulate processing many text objects (like in a real PCB layout)
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",
601 };
602
603 auto start = std::chrono::high_resolution_clock::now();
604
605 std::set<std::string> errors;
606
607 // Process expressions many times (simulating real usage)
608 for( int iteration = 0; iteration < 100; ++iteration )
609 {
610 for( const auto& expr : expressions )
611 {
612 auto result = evaluator.Evaluate( wxString::FromUTF8( expr ) );
613
614 if( evaluator.HasErrors() || result.empty() )
615 errors.insert( expr );
616 }
617 }
618
619 BOOST_REQUIRE_MESSAGE( errors.empty(), fmt::format( "Evaluation of expressions had errors: {}", fmt::join( errors, ", " ) ) );
620
621 auto end = std::chrono::high_resolution_clock::now();
622 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>( end - start );
623
624 // Sanitizer instrumentation changes execution cost, not the evaluation contract.
625#if defined( KICAD_SANITIZE_THREADS ) || defined( KICAD_SANITIZE_ADDRESS )
626 BOOST_TEST_MESSAGE( "Instrumented expression evaluation: " << duration.count() << " ms" );
627#else
628 BOOST_CHECK_LT( duration.count(), 100 );
629#endif
630
631 // Test that results are consistent
632 for( auto& expr : expressions )
633 {
634 auto result1 = evaluator.Evaluate( wxString::FromUTF8( expr ) );
635 auto result2 = evaluator.Evaluate( wxString::FromUTF8( expr ) );
636 BOOST_CHECK_EQUAL( result1, result2 );
637 }
638}
639
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.
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.
Definition file_utils.h:62
void Init() override
void Shutdown() override
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.
Definition common.cpp:776
void SetGitBackend(GIT_BACKEND *aBackend)
GIT_BACKEND * GetGitBackend()
std::string source
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
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
std::string path
VECTOR3I expected(15, 30, 45)
VECTOR2I end
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
int actual
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.