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
27// Code under test
29
30#include <chrono>
31#include <regex>
32
36BOOST_AUTO_TEST_SUITE( TextEvalParserIntegration )
37
38
41BOOST_AUTO_TEST_CASE( RealWorldScenarios )
42{
43 EXPRESSION_EVALUATOR evaluator;
44
45 // Set up variables that might be used in actual KiCad projects
46 evaluator.SetVariable( "board_width", 100.0 );
47 evaluator.SetVariable( "board_height", 80.0 );
48 evaluator.SetVariable( "trace_width", 0.2 );
49 evaluator.SetVariable( "component_count", 45.0 );
50 evaluator.SetVariable( "revision", 3.0 );
51 evaluator.SetVariable( std::string("project_name"), std::string("My PCB Project") );
52 evaluator.SetVariable( std::string("designer"), std::string("John Doe") );
53
54 struct TestCase {
55 std::string expression;
56 std::string expectedPattern; // Can be exact match or regex pattern
57 bool isRegex;
58 bool shouldError;
59 std::string description;
60 };
61
62 const std::vector<TestCase> cases = {
63 // Board dimension calculations
64 {
65 "Board area: @{${board_width} * ${board_height}} mm²",
66 "Board area: 8000 mm²",
67 false, false,
68 "Board area calculation"
69 },
70 {
71 "Perimeter: @{2 * (${board_width} + ${board_height})} mm",
72 "Perimeter: 360 mm",
73 false, false,
74 "Board perimeter calculation"
75 },
76 {
77 "Diagonal: @{format(sqrt(pow(${board_width}, 2) + pow(${board_height}, 2)), 1)} mm",
78 "Diagonal: 128.1 mm",
79 false, false,
80 "Board diagonal calculation"
81 },
82
83 // Text formatting scenarios
84 {
85 "Project: ${project_name} | Designer: ${designer} | Rev: @{${revision}}",
86 "Project: My PCB Project | Designer: John Doe | Rev: 3",
87 false, false,
88 "Title block information"
89 },
90 {
91 "Components: @{${component_count}} | Density: @{format(${component_count} / (${board_width} * ${board_height} / 10000), 2)} per cm²",
92 "Components: 45 | Density: 56.25 per cm²",
93 false, false,
94 "Component density calculation"
95 },
96
97 // Date-based revision tracking
98 {
99 "Created: @{dateformat(today())} | Build: @{today()} days since epoch",
100 R"(Created: \d{4}-\d{2}-\d{2} \| Build: \d+ days since epoch)",
101 true, false,
102 "Date-based tracking"
103 },
104
105 // Conditional formatting
106 {
107 "Status: @{if(${component_count} > 50, \"Complex\", \"Simple\")} design",
108 "Status: Simple design",
109 false, false,
110 "Conditional design complexity"
111 },
112 {
113 "Status: @{if(${trace_width} >= 0.2, \"Standard\", \"Fine pitch\")} (@{${trace_width}}mm)",
114 "Status: Standard (0.2mm)",
115 false, false,
116 "Conditional trace width description"
117 },
118
119 // Multi-line documentation
120 {
121 "PCB Summary:\n- Size: @{${board_width}}×@{${board_height}}mm\n- Area: @{${board_width} * ${board_height}}mm²\n- Components: @{${component_count}}",
122 "PCB Summary:\n- Size: 100×80mm\n- Area: 8000mm²\n- Components: 45",
123 false, false,
124 "Multi-line documentation"
125 },
126
127 // Error scenarios - undefined variables error and return unchanged
128 {
129 "Invalid: @{${undefined_var}} test",
130 "Invalid: @{${undefined_var}} test",
131 false, true,
132 "Undefined variable behavior"
133 },
134 };
135
136 for( const auto& testCase : cases )
137 {
138 auto result = evaluator.Evaluate( wxString::FromUTF8( testCase.expression ) );
139
140 if( testCase.shouldError )
141 {
142 BOOST_CHECK_MESSAGE( evaluator.HasErrors(),
143 "Expected error for: " + testCase.description );
144 }
145 else
146 {
147 BOOST_CHECK_MESSAGE( !evaluator.HasErrors(),
148 "Unexpected error for: " + testCase.description +
149 " - " + evaluator.GetErrorSummary().ToStdString() );
150
151 if( testCase.isRegex )
152 {
153 std::regex pattern( testCase.expectedPattern );
154 BOOST_CHECK_MESSAGE( std::regex_match( result.ToStdString( wxConvUTF8 ), pattern ),
155 "Result '" + result.ToStdString( wxConvUTF8 ) + "' doesn't match pattern '" +
156 testCase.expectedPattern + "' for: " + testCase.description );
157 }
158 else
159 {
160 BOOST_CHECK_MESSAGE( result.ToStdString( wxConvUTF8 ) == testCase.expectedPattern,
161 "Expected '" + testCase.expectedPattern + "' but got '" +
162 result.ToStdString( wxConvUTF8 ) + "' for: " + testCase.description );
163 }
164 }
165 }
166}
167
171BOOST_AUTO_TEST_CASE( CallbackVariableResolution )
172{
173 // Create evaluator with custom callback
174 auto variableCallback = []( const std::string& varName ) -> calc_parser::Result<calc_parser::Value> {
175 if( varName == "dynamic_value" )
177 else if( varName == "dynamic_string" )
178 return calc_parser::MakeValue<calc_parser::Value>( std::string("Hello from callback") );
179 else if( varName == "computed_value" )
180 return calc_parser::MakeValue<calc_parser::Value>( std::sin( 3.14159 / 4 ) * 100.0 ); // Should be about 70.7
181 else
182 return calc_parser::MakeError<calc_parser::Value>( "Variable '" + varName + "' not found in callback" );
183 };
184
185 EXPRESSION_EVALUATOR evaluator( variableCallback, false );
186
187 struct TestCase {
188 std::string expression;
189 std::string expected;
190 double tolerance;
191 bool shouldError;
192 };
193
194 const std::vector<TestCase> cases = {
195 { "@{${dynamic_value}}", "42", 0, false },
196 { "Message: ${dynamic_string}", "Message: Hello from callback", 0, false },
197 { "@{format(${computed_value}, 1)}", "70.7", 0.1, false },
198 { "@{${dynamic_value} + ${computed_value}}", "112.7", 0.1, false },
199 { "${nonexistent}", "${nonexistent}", 0, true },
200 };
201
202 for( const auto& testCase : cases )
203 {
204 auto result = evaluator.Evaluate( wxString::FromUTF8( testCase.expression ) );
205
206 if( testCase.shouldError )
207 {
208 BOOST_CHECK( evaluator.HasErrors() );
209 }
210 else
211 {
212 BOOST_CHECK( !evaluator.HasErrors() );
213
214 if( testCase.tolerance > 0 )
215 {
216 // For floating point comparisons, extract the number
217 std::regex numberRegex( R"([\d.]+)" );
218 std::smatch match;
219 std::string resultStr = result.ToStdString( wxConvUTF8 );
220 if( std::regex_search( resultStr, match, numberRegex ) )
221 {
222 double actualValue = std::stod( match[0].str() );
223 double expectedValue = std::stod( testCase.expected );
224 BOOST_CHECK_CLOSE( actualValue, expectedValue, testCase.tolerance * 100 );
225 }
226 }
227 else
228 {
229 BOOST_CHECK_EQUAL( result.ToStdString( wxConvUTF8 ), testCase.expected );
230 }
231 }
232 }
233}
234
238BOOST_AUTO_TEST_CASE( ThreadSafety )
239{
240 // Create multiple evaluators that could be used in different threads
241 std::vector<std::unique_ptr<EXPRESSION_EVALUATOR>> evaluators;
242
243 for( int i = 0; i < 10; ++i )
244 {
245 auto evaluator = std::make_unique<EXPRESSION_EVALUATOR>();
246 evaluator->SetVariable( "thread_id", static_cast<double>( i ) );
247 evaluator->SetVariable( "multiplier", 5.0 );
248 evaluators.push_back( std::move( evaluator ) );
249 }
250
251 // Test that each evaluator maintains its own state
252 for( int i = 0; i < 10; ++i )
253 {
254 auto result = evaluators[i]->Evaluate( "@{${thread_id} * ${multiplier}}" );
255 BOOST_CHECK( !evaluators[i]->HasErrors() );
256
257 double expected = static_cast<double>( i * 5 );
258 double actual = std::stod( result.ToStdString( wxConvUTF8 ) );
259 BOOST_CHECK_CLOSE( actual, expected, 0.001 );
260 }
261}
262
266BOOST_AUTO_TEST_CASE( MemoryManagement )
267{
268 EXPRESSION_EVALUATOR evaluator;
269
270 // Test large nested expressions
271 std::string complexExpression = "@{";
272 for( int i = 0; i < 100; ++i )
273 {
274 if( i > 0 ) complexExpression += " + ";
275 complexExpression += std::to_string( i );
276 }
277 complexExpression += "}";
278
279 auto result = evaluator.Evaluate( wxString::FromUTF8( complexExpression ) );
280 BOOST_CHECK( !evaluator.HasErrors() );
281
282 // Sum of 0..99 is 4950
283 BOOST_CHECK_EQUAL( result.ToStdString( wxConvUTF8 ), std::string( "4950" ) );
284
285 // Test many small expressions
286 for( int i = 0; i < 1000; ++i )
287 {
288 auto expr = "@{" + std::to_string( i ) + " * 2}";
289 auto result = evaluator.Evaluate( wxString::FromUTF8( expr ) );
290 BOOST_CHECK( !evaluator.HasErrors() );
291 BOOST_CHECK_EQUAL( result.ToStdString( wxConvUTF8 ), std::to_string( i * 2 ) );
292 }
293}
294
298BOOST_AUTO_TEST_CASE( ParsingEdgeCases )
299{
300 EXPRESSION_EVALUATOR evaluator;
301
302 struct TestCase {
303 std::string expression;
304 std::string expected;
305 bool shouldError;
306 double precision;
307 std::string description;
308 };
309
310 const std::vector<TestCase> cases = {
311 // Whitespace handling
312 { "@{ 2 + 3 }", "5", false, 0.0, "Spaces in expression" },
313 { "@{\t2\t+\t3\t}", "5", false, 0.0, "Tabs in expression" },
314 { "@{\n2\n+\n3\n}", "5", false, 0.0, "Newlines in expression" },
315
316 // String escaping and special characters
317 { "@{\"Hello\\\"World\\\"\"}", "Hello\"World\"", false, 0.0, "Escaped quotes in string" },
318 { "@{\"Line1\\nLine2\"}", "Line1\nLine2", false, 0.0, "Newline in string" },
319
320 // Multiple calculations in complex text
321 { "A: @{1+1}, B: @{2*2}, C: @{3^2}", "A: 2, B: 4, C: 9", false, 0.0, "Multiple calculations" },
322
323 // Edge cases with parentheses
324 { "@{((((2))))}", "2", false, 0.0, "Multiple nested parentheses" },
325 { "@{(2 + 3) * (4 + 5)}", "45", false, 0.0, "Grouped operations" },
326
327 // Empty and minimal expressions
328 { "No calculations here", "No calculations here", false, 0.0, "Plain text" },
329 { "", "", false, 0.0, "Empty string" },
330 { "@{0}", "0", false, 0.0, "Zero value" },
331 { "@{-0}", "0", false, 0.0, "Negative zero" },
332
333 // Precision and rounding edge cases
334 { "@{0.1 + 0.2}", "0.3", false, 0.01, "Floating point precision" },
335 { "@{1.0 / 3.0}", "0.333333", false, 0.01, "Repeating decimal" },
336
337 // Large numbers
338 { "@{1000000 * 1000000}", "1e+12", false, 0.01, "Large number result" },
339
340 // Error recovery - malformed expressions left unchanged, valid ones evaluated
341 { "Good @{2+2} bad @{2+} good @{3+3}", "Good 4 bad @{2+} good 6", true, 0.0, "Error recovery" },
342 };
343
344 for( const auto& testCase : cases )
345 {
346 auto result = evaluator.Evaluate( wxString::FromUTF8( testCase.expression ) );
347
348 if( testCase.shouldError )
349 {
350 BOOST_CHECK_MESSAGE( evaluator.HasErrors(), "Expected error for: " + testCase.description );
351 }
352 else
353 {
354 if( testCase.precision > 0.0 )
355 {
356 // For floating point comparisons, extract the number
357 std::regex numberRegex( R"([\d.eE+-]+)" );
358 std::smatch match;
359 std::string resultStr = result.ToStdString( wxConvUTF8 );
360 if( std::regex_search( resultStr, match, numberRegex ) )
361 {
362 double actualValue = std::stod( match[0].str() );
363 double expectedValue = std::stod( testCase.expected );
364 BOOST_CHECK_CLOSE( actualValue, expectedValue, testCase.precision * 100 );
365 }
366 }
367 else
368 {
369 BOOST_CHECK_MESSAGE( !evaluator.HasErrors(),
370 "Unexpected error for: " + testCase.description +
371 " - " + evaluator.GetErrorSummary().ToStdString() );
372 BOOST_CHECK_MESSAGE( result.ToStdString( wxConvUTF8 ) == testCase.expected,
373 "Expected '" + testCase.expected + "' but got '" +
374 result.ToStdString( wxConvUTF8 ) + "' for: " + testCase.description );
375 }
376 }
377 }
378}
379
383BOOST_AUTO_TEST_CASE( RealWorldPerformance )
384{
385 EXPRESSION_EVALUATOR evaluator;
386
387 // Set up variables for a typical PCB project
388 evaluator.SetVariable( "board_layers", 4.0 );
389 evaluator.SetVariable( "component_count", 150.0 );
390 evaluator.SetVariable( "net_count", 200.0 );
391 evaluator.SetVariable( "via_count", 300.0 );
392 evaluator.SetVariable( "board_width", 120.0 );
393 evaluator.SetVariable( "board_height", 80.0 );
394
395 // Simulate processing many text objects (like in a real PCB layout)
396 std::vector<std::string> expressions = {
397 "Layer @{${board_layers}}/4",
398 "Components: @{${component_count}}",
399 "Nets: @{${net_count}}",
400 "Vias: @{${via_count}}",
401 "Area: @{${board_width} * ${board_height}} mm²",
402 "Density: @{format(${component_count} / (${board_width} * ${board_height} / 100), 1)} /cm²",
403 "Via density: @{format(${via_count} / (${board_width} * ${board_height} / 100), 1)} /cm²",
404 "Layer utilization: @{format(${net_count} / ${board_layers}, 1)} nets/layer",
405 "Design complexity: @{if(${component_count} > 100, \"High\", \"Low\")}",
406 "Board aspect ratio: @{format(${board_width} / ${board_height}, 2)}:1",
407 };
408
409 auto start = std::chrono::high_resolution_clock::now();
410
411 // Process expressions many times (simulating real usage)
412 for( int iteration = 0; iteration < 100; ++iteration )
413 {
414 for( const auto& expr : expressions )
415 {
416 auto result = evaluator.Evaluate( wxString::FromUTF8( expr ) );
417 BOOST_CHECK( !evaluator.HasErrors() );
418 BOOST_CHECK( !result.empty() );
419 }
420 }
421
422 auto end = std::chrono::high_resolution_clock::now();
423 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>( end - start );
424
425 // Should process 1000 expressions in reasonable time (less than 100 ms)
426 BOOST_CHECK_LT( duration.count(), 100 );
427
428 // Test that results are consistent
429 for( auto& expr : expressions )
430 {
431 auto result1 = evaluator.Evaluate( wxString::FromUTF8( expr ) );
432 auto result2 = evaluator.Evaluate( wxString::FromUTF8( expr ) );
433 BOOST_CHECK_EQUAL( result1, result2 );
434 }
435}
436
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.
auto MakeValue(T aVal) -> Result< T >
auto MakeError(std::string aMsg) -> Result< T >
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_SUITE_END()
VECTOR3I expected(15, 30, 45)
BOOST_CHECK_MESSAGE(totalMismatches==0, std::to_string(totalMismatches)+" board(s) with strategy disagreements")
VECTOR2I end
int actual
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
BOOST_AUTO_TEST_CASE(RealWorldScenarios)
Declare the test suite.