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