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