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 { "@{beforefirst(\"hello.world.txt\", \".\")}", "hello", false },
293 { "@{beforelast(\"hello.world.txt\", \".\")}", "hello.world", false },
294 { "@{afterfirst(\"hello.world.txt\", \".\")}", "world.txt", false },
295 { "@{afterlast(\"hello.world.txt\", \".\")}", "txt", false },
296 { "@{beforefirst(${text}, \" \")}", "Hello", false },
297 };
298
299 for( const auto& testCase : cases )
300 {
301 auto result = evaluator.Evaluate( testCase.expression );
302
303 BOOST_CHECK( !evaluator.HasErrors() );
304 BOOST_CHECK_EQUAL( result, testCase.expected );
305 }
306}
307
311BOOST_AUTO_TEST_CASE( FormattingFunctions )
312{
313 EXPRESSION_EVALUATOR evaluator;
314
315 struct TestCase {
316 std::string expression;
317 std::string expected;
318 bool shouldError;
319 };
320
321 const std::vector<TestCase> cases = {
322 // Number formatting
323 { "@{format(3.14159)}", "3.14", false },
324 { "@{format(3.14159, 3)}", "3.142", false },
325 { "@{format(1234.5)}", "1234.50", false },
326 { "@{fixed(3.14159, 2)}", "3.14", false },
327
328 // Currency formatting
329 { "@{currency(1234.56)}", "$1234.56", false },
330 { "@{currency(999.99, \"€\")}", "€999.99", false },
331 };
332
333 for( const auto& testCase : cases )
334 {
335 auto result = evaluator.Evaluate( testCase.expression );
336
337 BOOST_CHECK( !evaluator.HasErrors() );
338 BOOST_CHECK_EQUAL( result, testCase.expected );
339 }
340}
341
345BOOST_AUTO_TEST_CASE( DateTimeFunctions )
346{
347 EXPRESSION_EVALUATOR evaluator;
348
349 // Note: These tests will be time-sensitive. We test the functions exist
350 // and return reasonable values rather than exact matches.
351
352 struct TestCase {
353 std::string expression;
354 bool shouldContainNumbers;
355 bool shouldError;
356 };
357
358 const std::vector<TestCase> cases = {
359 // Date functions that return numbers (days since epoch)
360 { "@{today()}", true, false },
361 { "@{now()}", true, false }, // Returns timestamp
362
363 // Date formatting (these return specific dates so we can test exactly)
364 { "@{dateformat(0)}", false, false }, // Should format epoch date
365 { "@{dateformat(0, \"ISO\")}", false, false },
366 { "@{dateformat(0, \"US\")}", false, false },
367 { "@{weekdayname(0)}", false, false }, // Should return weekday name
368 };
369
370 for( const auto& testCase : cases )
371 {
372 auto result = evaluator.Evaluate( testCase.expression );
373
374 if( testCase.shouldError )
375 {
376 BOOST_CHECK( evaluator.HasErrors() );
377 }
378 else
379 {
380 BOOST_CHECK( !evaluator.HasErrors() );
381 BOOST_CHECK( !result.empty() );
382
383 if( testCase.shouldContainNumbers )
384 {
385 // Result should be a number
386 BOOST_CHECK( std::all_of( result.begin(), result.end(),
387 []( char c ) { return std::isdigit( c ) || c == '.' || c == '-'; } ) );
388 }
389 }
390 }
391
392 // Test specific date formatting with known values
393 auto result1 = evaluator.Evaluate( "@{dateformat(0, \"ISO\")}" );
394 BOOST_CHECK_EQUAL( result1, "1970-01-01" ); // Unix epoch
395
396 auto result2 = evaluator.Evaluate( "@{weekdayname(0)}" );
397 BOOST_CHECK_EQUAL( result2, "Thursday" ); // Unix epoch was a Thursday
398}
399
403BOOST_AUTO_TEST_CASE( ConditionalFunctions )
404{
405 EXPRESSION_EVALUATOR evaluator;
406 evaluator.SetVariable( "x", 10.0 );
407 evaluator.SetVariable( "y", 5.0 );
408
409 struct TestCase {
410 std::string expression;
411 std::string expected;
412 bool shouldError;
413 };
414
415 const std::vector<TestCase> cases = {
416 // Basic if function
417 { "@{if(1, \"true\", \"false\")}", "true", false },
418 { "@{if(0, \"true\", \"false\")}", "false", false },
419 { "@{if(${x} > ${y}, \"greater\", \"not greater\")}", "greater", false },
420 { "@{if(${x} < ${y}, \"less\", \"not less\")}", "not less", false },
421
422 // Numeric if results
423 { "@{if(1, 42, 24)}", "42", false },
424 { "@{if(0, 42, 24)}", "24", false },
425 };
426
427 for( const auto& testCase : cases )
428 {
429 auto result = evaluator.Evaluate( testCase.expression );
430
431 BOOST_CHECK( !evaluator.HasErrors() );
432 BOOST_CHECK_EQUAL( result, testCase.expected );
433 }
434}
435
439BOOST_AUTO_TEST_CASE( RandomFunctions )
440{
441 EXPRESSION_EVALUATOR evaluator;
442
443 // Test that random function returns a value between 0 and 1
444 auto result = evaluator.Evaluate( "@{random()}" );
445 BOOST_CHECK( !evaluator.HasErrors() );
446
447 double randomValue = wxStrtod( result, nullptr );
448 BOOST_CHECK_GE( randomValue, 0.0 );
449 BOOST_CHECK_LT( randomValue, 1.0 );
450
451 // Test that consecutive calls return different values (with high probability)
452 auto result2 = evaluator.Evaluate( "@{random()}" );
453 double randomValue2 = wxStrtod( result2, nullptr );
454
455 // It's theoretically possible these could be equal, but extremely unlikely
456 BOOST_CHECK_NE( randomValue, randomValue2 );
457}
458
462BOOST_AUTO_TEST_CASE( ErrorHandling )
463{
464 EXPRESSION_EVALUATOR evaluator;
465
466 struct TestCase {
467 std::string expression;
468 bool shouldError;
469 std::string description;
470 };
471
472 const std::vector<TestCase> cases = {
473 // Syntax errors
474 { "@{2 +}", true, "incomplete expression" },
475 { "@{(2 + 3", true, "unmatched parenthesis" },
476 { "@{2 + 3)}", true, "extra closing parenthesis" },
477 { "@{}", true, "empty calculation" },
478
479 // Unknown functions
480 { "@{unknownfunc(1, 2)}", true, "unknown function" },
481
482 // Wrong number of arguments
483 { "@{abs()}", true, "abs with no arguments" },
484 { "@{abs(1, 2)}", true, "abs with too many arguments" },
485 { "@{sqrt()}", true, "sqrt with no arguments" },
486
487 // Runtime errors
488 { "@{1 / 0}", true, "division by zero" },
489 { "@{sqrt(-1)}", true, "square root of negative" },
490
491 // Valid expressions that should not error
492 { "Plain text", false, "plain text should not error" },
493 { "@{2 + 2}", false, "simple calculation should work" },
494 };
495
496 for( const auto& testCase : cases )
497 {
498 auto result = evaluator.Evaluate( testCase.expression );
499
500 if( testCase.shouldError )
501 {
502 BOOST_CHECK_MESSAGE( evaluator.HasErrors(),
503 "Expected error for: " + testCase.description );
504 }
505 else
506 {
507 BOOST_CHECK_MESSAGE( !evaluator.HasErrors(),
508 "Unexpected error for: " + testCase.description );
509 }
510 }
511}
512
516BOOST_AUTO_TEST_CASE( ComplexExpressions )
517{
518 EXPRESSION_EVALUATOR evaluator;
519 evaluator.SetVariable( "pi", 3.14159 );
520 evaluator.SetVariable( "radius", 5.0 );
521
522 struct TestCase {
523 std::string expression;
524 std::string expected;
525 double tolerance;
526 };
527
528 const std::vector<TestCase> cases = {
529 // Complex mathematical expressions
530 { "@{2 * ${pi} * ${radius}}", "31.42", 0.01 }, // Circumference
531 { "@{${pi} * pow(${radius}, 2)}", "78.54", 0.01 }, // Area
532 { "@{sqrt(pow(3, 2) + pow(4, 2))}", "5", 0.001 }, // Pythagorean theorem
533
534 // Nested function calls
535 { "@{max(abs(-5), sqrt(16), floor(3.7))}", "5", 0.001 },
536 { "@{round(avg(1.1, 2.2, 3.3), 1)}", "2.2", 0.001 },
537
538 // Mixed string and math
539 { "Circle with radius @{${radius}} has area @{format(${pi} * pow(${radius}, 2), 1)}",
540 "Circle with radius 5 has area 78.5", 0 },
541 };
542
543 for( const auto& testCase : cases )
544 {
545 auto result = evaluator.Evaluate( testCase.expression );
546 std::string resultStr = result.ToStdString();
547 BOOST_CHECK( !evaluator.HasErrors() );
548
549 if( testCase.tolerance > 0 )
550 {
551 // Extract numeric part for comparison
552 std::regex numberRegex( R"([\d.]+)" );
553 std::smatch match;
554
555 if( std::regex_search( resultStr, match, numberRegex ) )
556 {
557 double actualValue = std::stod( match[0].str() );
558 double expectedValue = std::stod( testCase.expected );
559 BOOST_CHECK_CLOSE( actualValue, expectedValue, testCase.tolerance * 100 );
560 }
561 }
562 else
563 {
564 BOOST_CHECK_EQUAL( result, testCase.expected );
565 }
566 }
567}
568
573{
574 EXPRESSION_EVALUATOR evaluator;
575
576 // Build a large expression with many calculations
577 std::string largeExpression = "Result: ";
578 for( int i = 0; i < 50; ++i )
579 {
580 largeExpression += "@{" + std::to_string(i) + " * 2} ";
581 }
582
583 auto start = std::chrono::high_resolution_clock::now();
584 auto result = evaluator.Evaluate( largeExpression );
585 auto end = std::chrono::high_resolution_clock::now();
586
587 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>( end - start );
588
589 BOOST_CHECK( !evaluator.HasErrors() );
590 BOOST_CHECK( !result.empty() );
591
592 // Should complete in reasonable time (less than 1 second)
593 BOOST_CHECK_LT( duration.count(), 1000 );
594}
595
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.