KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_text_eval_parser.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 <cmath>
36#include <regex>
37#include <wx/wxcrt.h>
38
42BOOST_AUTO_TEST_SUITE( TextEvalParser )
43
44
47BOOST_AUTO_TEST_CASE( BasicArithmetic )
48{
49 EXPRESSION_EVALUATOR evaluator;
50
51 struct TestCase {
52 std::string expression;
53 std::string expected;
54 bool shouldError;
55 };
56
57 const std::vector<TestCase> cases = {
58 // Basic operations
59 { "Text @{2 + 3} more text", "Text 5 more text", false },
60 { "@{10 - 4}", "6", false },
61 { "@{7 * 8}", "56", false },
62 { "@{15 / 3}", "5", false },
63 { "@{17 % 5}", "2", false },
64 { "@{2^3}", "8", false },
65
66 // Order of operations
67 { "@{2 + 3 * 4}", "14", false },
68 { "@{(2 + 3) * 4}", "20", false },
69 { "@{2^3^2}", "512", false }, // Right associative
70 { "@{-5}", "-5", false },
71 { "@{+5}", "5", false },
72
73 // Floating point
74 { "@{3.14 + 1.86}", "5", false },
75 { "@{10.5 / 2}", "5.25", false },
76 { "@{3.5 * 2}", "7", false },
77
78 // Edge cases
79 { "@{1 / 0}", "Text @{1 / 0} more text", true }, // Division by zero
80 { "@{1 % 0}", "Text @{1 % 0} more text", true }, // Modulo by zero
81
82 // Multiple calculations in one string
83 { "@{2 + 2} and @{3 * 3}", "4 and 9", false },
84 };
85
86 for( const auto& testCase : cases )
87 {
88 auto result = evaluator.Evaluate( testCase.expression );
89
90 if( testCase.shouldError )
91 {
92 BOOST_CHECK( evaluator.HasErrors() );
93 }
94 else
95 {
96 BOOST_CHECK( !evaluator.HasErrors() );
97 BOOST_CHECK_EQUAL( result, testCase.expected );
98 }
99 }
100}
101
105BOOST_AUTO_TEST_CASE( VariableSubstitution )
106{
107 EXPRESSION_EVALUATOR evaluator;
108
109 // Set up some variables
110 evaluator.SetVariable( "x", 10.0 );
111 evaluator.SetVariable( "y", 5.0 );
112 evaluator.SetVariable( wxString("name"), wxString("KiCad") );
113 evaluator.SetVariable( "version", 8.0 );
114
115 struct TestCase {
116 std::string expression;
117 std::string expected;
118 bool shouldError;
119 };
120
121 const std::vector<TestCase> cases = {
122 // Basic variable substitution
123 { "@{${x}}", "10", false },
124 { "@{${y}}", "5", false },
125 { "Hello ${name}!", "Hello KiCad!", false },
126
127 // Variables in calculations
128 { "@{${x} + ${y}}", "15", false },
129 { "@{${x} * ${y}}", "50", false },
130 { "@{${x} - ${y}}", "5", false },
131 { "@{${x} / ${y}}", "2", false },
132
133 // Mixed text and variable calculations
134 { "Product: @{${x} * ${y}} units", "Product: 50 units", false },
135 { "Version ${version}.0", "Version 8.0", false },
136
137 // Undefined variables
138 { "@{${undefined}}", "@{${undefined}}", true },
139
140 // String variables
141 { "Welcome to ${name}", "Welcome to KiCad", false },
142 };
143
144 for( const auto& testCase : cases )
145 {
146 auto result = evaluator.Evaluate( testCase.expression );
147
148 if( testCase.shouldError )
149 {
150 BOOST_CHECK( evaluator.HasErrors() );
151 }
152 else
153 {
154 BOOST_CHECK( !evaluator.HasErrors() );
155 BOOST_CHECK_EQUAL( result, testCase.expected );
156 }
157 }
158}
159
163BOOST_AUTO_TEST_CASE( StringOperations )
164{
165 EXPRESSION_EVALUATOR evaluator;
166 evaluator.SetVariable( wxString("prefix"), wxString("Hello") );
167 evaluator.SetVariable( wxString("suffix"), wxString("World") );
168
169 struct TestCase {
170 std::string expression;
171 std::string expected;
172 bool shouldError;
173 };
174
175 const std::vector<TestCase> cases = {
176 // String concatenation with +
177 { "@{\"Hello\" + \" \" + \"World\"}", "Hello World", false },
178 { "@{${prefix} + \" \" + ${suffix}}", "Hello World", false },
179
180 // Mixed string and number concatenation
181 { "@{\"Count: \" + 42}", "Count: 42", false },
182 { "@{42 + \" items\"}", "42 items", false },
183
184 // String literals
185 { "@{\"Simple string\"}", "Simple string", false },
186 { "Prefix @{\"middle\"} suffix", "Prefix middle suffix", false },
187 };
188
189 for( const auto& testCase : cases )
190 {
191 auto result = evaluator.Evaluate( testCase.expression );
192
193 BOOST_CHECK( !evaluator.HasErrors() );
194 BOOST_CHECK_EQUAL( result, testCase.expected );
195 }
196}
197
201BOOST_AUTO_TEST_CASE( MathematicalFunctions )
202{
203 EXPRESSION_EVALUATOR evaluator;
204
205 struct TestCase {
206 std::string expression;
207 std::string expected;
208 bool shouldError;
209 double tolerance;
210 };
211
212 const std::vector<TestCase> cases = {
213 // Basic math functions
214 { "@{abs(-5)}", "5", false, 0.001 },
215 { "@{abs(3.14)}", "3.14", false, 0.001 },
216 { "@{sqrt(16)}", "4", false, 0.001 },
217 { "@{sqrt(2)}", "1.414", false, 0.01 },
218 { "@{pow(2, 3)}", "8", false, 0.001 },
219 { "@{pow(3, 2)}", "9", false, 0.001 },
220
221 // Rounding functions
222 { "@{floor(3.7)}", "3", false, 0.001 },
223 { "@{ceil(3.2)}", "4", false, 0.001 },
224 { "@{round(3.7)}", "4", false, 0.001 },
225 { "@{round(3.2)}", "3", false, 0.001 },
226 { "@{round(3.14159, 2)}", "3.14", false, 0.001 },
227
228 // Min/Max functions
229 { "@{min(5, 3, 8, 1)}", "1", false, 0.001 },
230 { "@{max(5, 3, 8, 1)}", "8", false, 0.001 },
231 { "@{min(3.5, 3.1)}", "3.1", false, 0.001 },
232
233 // Sum and average
234 { "@{sum(1, 2, 3, 4)}", "10", false, 0.001 },
235 { "@{avg(2, 4, 6)}", "4", false, 0.001 },
236
237 // Error cases
238 { "@{sqrt(-1)}", "Text @{sqrt(-1)} more text", true, 0 },
239 };
240
241 for( const auto& testCase : cases )
242 {
243 auto result = evaluator.Evaluate( testCase.expression );
244
245 if( testCase.shouldError )
246 {
247 BOOST_CHECK( evaluator.HasErrors() );
248 }
249 else
250 {
251 BOOST_CHECK( !evaluator.HasErrors() );
252
253 if( testCase.tolerance > 0 )
254 {
255 // For floating point comparisons
256 double actualValue = wxStrtod( result, nullptr );
257 double expectedValue = wxStrtod( testCase.expected, nullptr );
258 BOOST_CHECK_CLOSE( actualValue, expectedValue, testCase.tolerance * 100 );
259 }
260 else
261 {
262 BOOST_CHECK_EQUAL( result, testCase.expected );
263 }
264 }
265 }
266}
267
271BOOST_AUTO_TEST_CASE( StringFunctions )
272{
273 EXPRESSION_EVALUATOR evaluator;
274 evaluator.SetVariable( wxString("text"), wxString("Hello World") );
275
276 struct TestCase {
277 std::string expression;
278 std::string expected;
279 bool shouldError;
280 };
281
282 const std::vector<TestCase> cases = {
283 // Case conversion
284 { "@{upper(\"hello world\")}", "HELLO WORLD", false },
285 { "@{lower(\"HELLO WORLD\")}", "hello world", false },
286 { "@{upper(${text})}", "HELLO WORLD", false },
287
288 // String concatenation function
289 { "@{concat(\"Hello\", \" \", \"World\")}", "Hello World", false },
290 { "@{concat(\"Count: \", 42, \" items\")}", "Count: 42 items", false },
291 };
292
293 for( const auto& testCase : cases )
294 {
295 auto result = evaluator.Evaluate( testCase.expression );
296
297 BOOST_CHECK( !evaluator.HasErrors() );
298 BOOST_CHECK_EQUAL( result, testCase.expected );
299 }
300}
301
305BOOST_AUTO_TEST_CASE( FormattingFunctions )
306{
307 EXPRESSION_EVALUATOR evaluator;
308
309 struct TestCase {
310 std::string expression;
311 std::string expected;
312 bool shouldError;
313 };
314
315 const std::vector<TestCase> cases = {
316 // Number formatting
317 { "@{format(3.14159)}", "3.14", false },
318 { "@{format(3.14159, 3)}", "3.142", false },
319 { "@{format(1234.5)}", "1234.50", false },
320 { "@{fixed(3.14159, 2)}", "3.14", false },
321
322 // Currency formatting
323 { "@{currency(1234.56)}", "$1234.56", false },
324 { "@{currency(999.99, \"€\")}", "€999.99", false },
325 };
326
327 for( const auto& testCase : cases )
328 {
329 auto result = evaluator.Evaluate( testCase.expression );
330
331 BOOST_CHECK( !evaluator.HasErrors() );
332 BOOST_CHECK_EQUAL( result, testCase.expected );
333 }
334}
335
339BOOST_AUTO_TEST_CASE( DateTimeFunctions )
340{
341 EXPRESSION_EVALUATOR evaluator;
342
343 // Note: These tests will be time-sensitive. We test the functions exist
344 // and return reasonable values rather than exact matches.
345
346 struct TestCase {
347 std::string expression;
348 bool shouldContainNumbers;
349 bool shouldError;
350 };
351
352 const std::vector<TestCase> cases = {
353 // Date functions that return numbers (days since epoch)
354 { "@{today()}", true, false },
355 { "@{now()}", true, false }, // Returns timestamp
356
357 // Date formatting (these return specific dates so we can test exactly)
358 { "@{dateformat(0)}", false, false }, // Should format epoch date
359 { "@{dateformat(0, \"ISO\")}", false, false },
360 { "@{dateformat(0, \"US\")}", false, false },
361 { "@{weekdayname(0)}", false, false }, // Should return weekday name
362 };
363
364 for( const auto& testCase : cases )
365 {
366 auto result = evaluator.Evaluate( testCase.expression );
367
368 if( testCase.shouldError )
369 {
370 BOOST_CHECK( evaluator.HasErrors() );
371 }
372 else
373 {
374 BOOST_CHECK( !evaluator.HasErrors() );
375 BOOST_CHECK( !result.empty() );
376
377 if( testCase.shouldContainNumbers )
378 {
379 // Result should be a number
380 BOOST_CHECK( std::all_of( result.begin(), result.end(),
381 []( char c ) { return std::isdigit( c ) || c == '.' || c == '-'; } ) );
382 }
383 }
384 }
385
386 // Test specific date formatting with known values
387 auto result1 = evaluator.Evaluate( "@{dateformat(0, \"ISO\")}" );
388 BOOST_CHECK_EQUAL( result1, "1970-01-01" ); // Unix epoch
389
390 auto result2 = evaluator.Evaluate( "@{weekdayname(0)}" );
391 BOOST_CHECK_EQUAL( result2, "Thursday" ); // Unix epoch was a Thursday
392}
393
397BOOST_AUTO_TEST_CASE( ConditionalFunctions )
398{
399 EXPRESSION_EVALUATOR evaluator;
400 evaluator.SetVariable( "x", 10.0 );
401 evaluator.SetVariable( "y", 5.0 );
402
403 struct TestCase {
404 std::string expression;
405 std::string expected;
406 bool shouldError;
407 };
408
409 const std::vector<TestCase> cases = {
410 // Basic if function
411 { "@{if(1, \"true\", \"false\")}", "true", false },
412 { "@{if(0, \"true\", \"false\")}", "false", false },
413 { "@{if(${x} > ${y}, \"greater\", \"not greater\")}", "greater", false },
414 { "@{if(${x} < ${y}, \"less\", \"not less\")}", "not less", false },
415
416 // Numeric if results
417 { "@{if(1, 42, 24)}", "42", false },
418 { "@{if(0, 42, 24)}", "24", false },
419 };
420
421 for( const auto& testCase : cases )
422 {
423 auto result = evaluator.Evaluate( testCase.expression );
424
425 BOOST_CHECK( !evaluator.HasErrors() );
426 BOOST_CHECK_EQUAL( result, testCase.expected );
427 }
428}
429
433BOOST_AUTO_TEST_CASE( RandomFunctions )
434{
435 EXPRESSION_EVALUATOR evaluator;
436
437 // Test that random function returns a value between 0 and 1
438 auto result = evaluator.Evaluate( "@{random()}" );
439 BOOST_CHECK( !evaluator.HasErrors() );
440
441 double randomValue = wxStrtod( result, nullptr );
442 BOOST_CHECK_GE( randomValue, 0.0 );
443 BOOST_CHECK_LT( randomValue, 1.0 );
444
445 // Test that consecutive calls return different values (with high probability)
446 auto result2 = evaluator.Evaluate( "@{random()}" );
447 double randomValue2 = wxStrtod( result2, nullptr );
448
449 // It's theoretically possible these could be equal, but extremely unlikely
450 BOOST_CHECK_NE( randomValue, randomValue2 );
451}
452
456BOOST_AUTO_TEST_CASE( ErrorHandling )
457{
458 EXPRESSION_EVALUATOR evaluator;
459
460 struct TestCase {
461 std::string expression;
462 bool shouldError;
463 std::string description;
464 };
465
466 const std::vector<TestCase> cases = {
467 // Syntax errors
468 { "@{2 +}", true, "incomplete expression" },
469 { "@{(2 + 3", true, "unmatched parenthesis" },
470 { "@{2 + 3)}", true, "extra closing parenthesis" },
471 { "@{}", true, "empty calculation" },
472
473 // Unknown functions
474 { "@{unknownfunc(1, 2)}", true, "unknown function" },
475
476 // Wrong number of arguments
477 { "@{abs()}", true, "abs with no arguments" },
478 { "@{abs(1, 2)}", true, "abs with too many arguments" },
479 { "@{sqrt()}", true, "sqrt with no arguments" },
480
481 // Runtime errors
482 { "@{1 / 0}", true, "division by zero" },
483 { "@{sqrt(-1)}", true, "square root of negative" },
484
485 // Valid expressions that should not error
486 { "Plain text", false, "plain text should not error" },
487 { "@{2 + 2}", false, "simple calculation should work" },
488 };
489
490 for( const auto& testCase : cases )
491 {
492 auto result = evaluator.Evaluate( testCase.expression );
493
494 if( testCase.shouldError )
495 {
496 BOOST_CHECK_MESSAGE( evaluator.HasErrors(),
497 "Expected error for: " + testCase.description );
498 }
499 else
500 {
501 BOOST_CHECK_MESSAGE( !evaluator.HasErrors(),
502 "Unexpected error for: " + testCase.description );
503 }
504 }
505}
506
510BOOST_AUTO_TEST_CASE( ComplexExpressions )
511{
512 EXPRESSION_EVALUATOR evaluator;
513 evaluator.SetVariable( "pi", 3.14159 );
514 evaluator.SetVariable( "radius", 5.0 );
515
516 struct TestCase {
517 std::string expression;
518 std::string expected;
519 double tolerance;
520 };
521
522 const std::vector<TestCase> cases = {
523 // Complex mathematical expressions
524 { "@{2 * ${pi} * ${radius}}", "31.42", 0.01 }, // Circumference
525 { "@{${pi} * pow(${radius}, 2)}", "78.54", 0.01 }, // Area
526 { "@{sqrt(pow(3, 2) + pow(4, 2))}", "5", 0.001 }, // Pythagorean theorem
527
528 // Nested function calls
529 { "@{max(abs(-5), sqrt(16), floor(3.7))}", "5", 0.001 },
530 { "@{round(avg(1.1, 2.2, 3.3), 1)}", "2.2", 0.001 },
531
532 // Mixed string and math
533 { "Circle with radius @{${radius}} has area @{format(${pi} * pow(${radius}, 2), 1)}",
534 "Circle with radius 5 has area 78.5", 0 },
535 };
536
537 for( const auto& testCase : cases )
538 {
539 auto result = evaluator.Evaluate( testCase.expression );
540 std::string resultStr = result.ToStdString();
541 BOOST_CHECK( !evaluator.HasErrors() );
542
543 if( testCase.tolerance > 0 )
544 {
545 // Extract numeric part for comparison
546 std::regex numberRegex( R"([\d.]+)" );
547 std::smatch match;
548
549 if( std::regex_search( resultStr, match, numberRegex ) )
550 {
551 double actualValue = std::stod( match[0].str() );
552 double expectedValue = std::stod( testCase.expected );
553 BOOST_CHECK_CLOSE( actualValue, expectedValue, testCase.tolerance * 100 );
554 }
555 }
556 else
557 {
558 BOOST_CHECK_EQUAL( result, testCase.expected );
559 }
560 }
561}
562
567{
568 EXPRESSION_EVALUATOR evaluator;
569
570 // Build a large expression with many calculations
571 std::string largeExpression = "Result: ";
572 for( int i = 0; i < 50; ++i )
573 {
574 largeExpression += "@{" + std::to_string(i) + " * 2} ";
575 }
576
577 auto start = std::chrono::high_resolution_clock::now();
578 auto result = evaluator.Evaluate( largeExpression );
579 auto end = std::chrono::high_resolution_clock::now();
580
581 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>( end - start );
582
583 BOOST_CHECK( !evaluator.HasErrors() );
584 BOOST_CHECK( !result.empty() );
585
586 // Should complete in reasonable time (less than 1 second)
587 BOOST_CHECK_LT( duration.count(), 1000 );
588}
589
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.
void SetVariable(const wxString &aName, double aValue)
Set a numeric variable for use in expressions.
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_SUITE_END()
VECTOR3I expected(15, 30, 45)
VECTOR2I end
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
BOOST_AUTO_TEST_CASE(BasicArithmetic)
Declare the test suite.