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, see <https://www.gnu.org/licenses/>.
18 */
19
24
26
27// Code under test
29
30#include <chrono>
31#include <cmath>
32#include <regex>
33#include <wx/wxcrt.h>
34
38BOOST_AUTO_TEST_SUITE( TextEvalParser )
39
40
43BOOST_AUTO_TEST_CASE( BasicArithmetic )
44{
45 EXPRESSION_EVALUATOR evaluator;
46
47 struct TestCase {
48 std::string expression;
49 std::string expected;
50 bool shouldError;
51 };
52
53 const std::vector<TestCase> cases = {
54 // Basic operations
55 { "Text @{2 + 3} more text", "Text 5 more text", false },
56 { "@{10 - 4}", "6", false },
57 { "@{7 * 8}", "56", false },
58 { "@{15 / 3}", "5", false },
59 { "@{17 % 5}", "2", false },
60 { "@{2^3}", "8", false },
61
62 // Order of operations
63 { "@{2 + 3 * 4}", "14", false },
64 { "@{(2 + 3) * 4}", "20", false },
65 { "@{2^3^2}", "512", false }, // Right associative
66 { "@{-5}", "-5", false },
67 { "@{+5}", "5", false },
68
69 // Floating point
70 { "@{3.14 + 1.86}", "5", false },
71 { "@{10.5 / 2}", "5.25", false },
72 { "@{3.5 * 2}", "7", false },
73
74 // Edge cases
75 { "@{1 / 0}", "Text @{1 / 0} more text", true }, // Division by zero
76 { "@{1 % 0}", "Text @{1 % 0} more text", true }, // Modulo by zero
77
78 // Multiple calculations in one string
79 { "@{2 + 2} and @{3 * 3}", "4 and 9", false },
80 };
81
82 for( const auto& testCase : cases )
83 {
84 auto result = evaluator.Evaluate( testCase.expression );
85
86 if( testCase.shouldError )
87 {
88 BOOST_CHECK( evaluator.HasErrors() );
89 }
90 else
91 {
92 BOOST_CHECK( !evaluator.HasErrors() );
93 BOOST_CHECK_EQUAL( result, testCase.expected );
94 }
95 }
96}
97
101BOOST_AUTO_TEST_CASE( VariableSubstitution )
102{
103 EXPRESSION_EVALUATOR evaluator;
104
105 // Set up some variables
106 evaluator.SetVariable( "x", 10.0 );
107 evaluator.SetVariable( "y", 5.0 );
108 evaluator.SetVariable( wxString("name"), wxString("KiCad") );
109 evaluator.SetVariable( "version", 8.0 );
110
111 struct TestCase {
112 std::string expression;
113 std::string expected;
114 bool shouldError;
115 };
116
117 const std::vector<TestCase> cases = {
118 // Basic variable substitution
119 { "@{${x}}", "10", false },
120 { "@{${y}}", "5", false },
121 { "Hello ${name}!", "Hello KiCad!", false },
122
123 // Variables in calculations
124 { "@{${x} + ${y}}", "15", false },
125 { "@{${x} * ${y}}", "50", false },
126 { "@{${x} - ${y}}", "5", false },
127 { "@{${x} / ${y}}", "2", false },
128
129 // Mixed text and variable calculations
130 { "Product: @{${x} * ${y}} units", "Product: 50 units", false },
131 { "Version ${version}.0", "Version 8.0", false },
132
133 // Undefined variables
134 { "@{${undefined}}", "@{${undefined}}", true },
135
136 // String variables
137 { "Welcome to ${name}", "Welcome to KiCad", false },
138 };
139
140 for( const auto& testCase : cases )
141 {
142 auto result = evaluator.Evaluate( testCase.expression );
143
144 if( testCase.shouldError )
145 {
146 BOOST_CHECK( evaluator.HasErrors() );
147 }
148 else
149 {
150 BOOST_CHECK( !evaluator.HasErrors() );
151 BOOST_CHECK_EQUAL( result, testCase.expected );
152 }
153 }
154}
155
159BOOST_AUTO_TEST_CASE( StringOperations )
160{
161 EXPRESSION_EVALUATOR evaluator;
162 evaluator.SetVariable( wxString("prefix"), wxString("Hello") );
163 evaluator.SetVariable( wxString("suffix"), wxString("World") );
164
165 struct TestCase {
166 std::string expression;
167 std::string expected;
168 bool shouldError;
169 };
170
171 const std::vector<TestCase> cases = {
172 // String concatenation with +
173 { "@{\"Hello\" + \" \" + \"World\"}", "Hello World", false },
174 { "@{${prefix} + \" \" + ${suffix}}", "Hello World", false },
175
176 // Mixed string and number concatenation
177 { "@{\"Count: \" + 42}", "Count: 42", false },
178 { "@{42 + \" items\"}", "42 items", false },
179
180 // String literals
181 { "@{\"Simple string\"}", "Simple string", false },
182 { "Prefix @{\"middle\"} suffix", "Prefix middle suffix", false },
183 };
184
185 for( const auto& testCase : cases )
186 {
187 auto result = evaluator.Evaluate( testCase.expression );
188
189 BOOST_CHECK( !evaluator.HasErrors() );
190 BOOST_CHECK_EQUAL( result, testCase.expected );
191 }
192}
193
197BOOST_AUTO_TEST_CASE( MathematicalFunctions )
198{
199 EXPRESSION_EVALUATOR evaluator;
200
201 struct TestCase {
202 std::string expression;
203 std::string expected;
204 bool shouldError;
205 double tolerance;
206 };
207
208 const std::vector<TestCase> cases = {
209 // Basic math functions
210 { "@{abs(-5)}", "5", false, 0.001 },
211 { "@{abs(3.14)}", "3.14", false, 0.001 },
212 { "@{sqrt(16)}", "4", false, 0.001 },
213 { "@{sqrt(2)}", "1.414", false, 0.01 },
214 { "@{pow(2, 3)}", "8", false, 0.001 },
215 { "@{pow(3, 2)}", "9", false, 0.001 },
216
217 // Rounding functions
218 { "@{floor(3.7)}", "3", false, 0.001 },
219 { "@{ceil(3.2)}", "4", false, 0.001 },
220 { "@{round(3.7)}", "4", false, 0.001 },
221 { "@{round(3.2)}", "3", false, 0.001 },
222 { "@{round(3.14159, 2)}", "3.14", false, 0.001 },
223
224 // Min/Max functions
225 { "@{min(5, 3, 8, 1)}", "1", false, 0.001 },
226 { "@{max(5, 3, 8, 1)}", "8", false, 0.001 },
227 { "@{min(3.5, 3.1)}", "3.1", false, 0.001 },
228
229 // Sum and average
230 { "@{sum(1, 2, 3, 4)}", "10", false, 0.001 },
231 { "@{avg(2, 4, 6)}", "4", false, 0.001 },
232
233 // Error cases
234 { "@{sqrt(-1)}", "Text @{sqrt(-1)} more text", true, 0 },
235 };
236
237 for( const auto& testCase : cases )
238 {
239 auto result = evaluator.Evaluate( testCase.expression );
240
241 if( testCase.shouldError )
242 {
243 BOOST_CHECK( evaluator.HasErrors() );
244 }
245 else
246 {
247 BOOST_CHECK( !evaluator.HasErrors() );
248
249 if( testCase.tolerance > 0 )
250 {
251 // For floating point comparisons
252 double actualValue = wxStrtod( result, nullptr );
253 double expectedValue = wxStrtod( testCase.expected, nullptr );
254 BOOST_CHECK_CLOSE( actualValue, expectedValue, testCase.tolerance * 100 );
255 }
256 else
257 {
258 BOOST_CHECK_EQUAL( result, testCase.expected );
259 }
260 }
261 }
262}
263
267BOOST_AUTO_TEST_CASE( StringFunctions )
268{
269 EXPRESSION_EVALUATOR evaluator;
270 evaluator.SetVariable( wxString("text"), wxString("Hello World") );
271
272 struct TestCase {
273 std::string expression;
274 std::string expected;
275 bool shouldError;
276 };
277
278 const std::vector<TestCase> cases = {
279 // Case conversion
280 { "@{upper(\"hello world\")}", "HELLO WORLD", false },
281 { "@{lower(\"HELLO WORLD\")}", "hello world", false },
282 { "@{upper(${text})}", "HELLO WORLD", false },
283
284 // String concatenation function
285 { "@{concat(\"Hello\", \" \", \"World\")}", "Hello World", false },
286 { "@{concat(\"Count: \", 42, \" items\")}", "Count: 42 items", false },
287
288 { "@{beforefirst(\"hello.world.txt\", \".\")}", "hello", false },
289 { "@{beforelast(\"hello.world.txt\", \".\")}", "hello.world", false },
290 { "@{afterfirst(\"hello.world.txt\", \".\")}", "world.txt", false },
291 { "@{afterlast(\"hello.world.txt\", \".\")}", "txt", false },
292 { "@{beforefirst(${text}, \" \")}", "Hello", false },
293
294 { "@{replace(\"Hello World\", \"l\", \"L\")}", "HeLLo WorLd", false },
295 { "@{replace(\"Hello World\", \"llo\", \"y there\")}", "Hey there World", false },
296 { "@{replace(${text}, \"l\", \"L\")}", "HeLLo WorLd", 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( GitFunctions )
404{
405 EXPRESSION_EVALUATOR evaluator;
406
407 // Note: These tests work whether in a git repo or not.
408 // Git functions return empty strings if not in a repo.
409
410 struct TestCase
411 {
412 std::string expression;
413 bool shouldError;
414 };
415
416 const std::vector<TestCase> cases = {
417 // Basic VCS functions
418 { "@{vcsidentifier()}", false },
419 { "@{vcsidentifier(7)}", false },
420 { "@{vcsbranch()}", false },
421 { "@{vcsauthor()}", false },
422 { "@{vcsauthoremail()}", false },
423 { "@{vcscommitter()}", false },
424 { "@{vcscommitteremail()}", false },
425 { "@{vcsnearestlabel()}", false },
426 { "@{vcslabeldistance()}", false },
427 { "@{vcsdirty()}", false },
428 { "@{vcsdirtysuffix()}", false },
429 { "@{vcscommitdate()}", false },
430
431 // VCS file functions with current directory
432 { "@{vcsfileidentifier(\".\")}", false },
433 };
434
435 for( const auto& testCase : cases )
436 {
437 auto result = evaluator.Evaluate( testCase.expression );
438
439 if( testCase.shouldError )
440 {
441 BOOST_CHECK( evaluator.HasErrors() );
442 }
443 else
444 {
445 BOOST_CHECK( !evaluator.HasErrors() );
446 // Result can be empty if not in a git repo - that's valid
447 }
448 }
449}
450
454BOOST_AUTO_TEST_CASE( ConditionalFunctions )
455{
456 EXPRESSION_EVALUATOR evaluator;
457 evaluator.SetVariable( "x", 10.0 );
458 evaluator.SetVariable( "y", 5.0 );
459
460 struct TestCase {
461 std::string expression;
462 std::string expected;
463 bool shouldError;
464 };
465
466 const std::vector<TestCase> cases = {
467 // Basic if function
468 { "@{if(1, \"true\", \"false\")}", "true", false },
469 { "@{if(0, \"true\", \"false\")}", "false", false },
470 { "@{if(${x} > ${y}, \"greater\", \"not greater\")}", "greater", false },
471 { "@{if(${x} < ${y}, \"less\", \"not less\")}", "not less", false },
472
473 // Numeric if results
474 { "@{if(1, 42, 24)}", "42", false },
475 { "@{if(0, 42, 24)}", "24", false },
476 };
477
478 for( const auto& testCase : cases )
479 {
480 auto result = evaluator.Evaluate( testCase.expression );
481
482 BOOST_CHECK( !evaluator.HasErrors() );
483 BOOST_CHECK_EQUAL( result, testCase.expected );
484 }
485}
486
490BOOST_AUTO_TEST_CASE( RandomFunctions )
491{
492 EXPRESSION_EVALUATOR evaluator;
493
494 // Test that random function returns a value between 0 and 1
495 auto result = evaluator.Evaluate( "@{random()}" );
496 BOOST_CHECK( !evaluator.HasErrors() );
497
498 double randomValue = wxStrtod( result, nullptr );
499 BOOST_CHECK_GE( randomValue, 0.0 );
500 BOOST_CHECK_LT( randomValue, 1.0 );
501
502 // Test that consecutive calls return different values (with high probability)
503 auto result2 = evaluator.Evaluate( "@{random()}" );
504 double randomValue2 = wxStrtod( result2, nullptr );
505
506 // It's theoretically possible these could be equal, but extremely unlikely
507 BOOST_CHECK_NE( randomValue, randomValue2 );
508}
509
513BOOST_AUTO_TEST_CASE( ErrorHandling )
514{
515 EXPRESSION_EVALUATOR evaluator;
516
517 struct TestCase {
518 std::string expression;
519 bool shouldError;
520 std::string description;
521 };
522
523 const std::vector<TestCase> cases = {
524 // Syntax errors
525 { "@{2 +}", true, "incomplete expression" },
526 { "@{(2 + 3", true, "unmatched parenthesis" },
527 { "@{2 + 3)}", true, "extra closing parenthesis" },
528 { "@{}", true, "empty calculation" },
529
530 // Unknown functions
531 { "@{unknownfunc(1, 2)}", true, "unknown function" },
532
533 // Wrong number of arguments
534 { "@{abs()}", true, "abs with no arguments" },
535 { "@{abs(1, 2)}", true, "abs with too many arguments" },
536 { "@{sqrt()}", true, "sqrt with no arguments" },
537
538 // Runtime errors
539 { "@{1 / 0}", true, "division by zero" },
540 { "@{sqrt(-1)}", true, "square root of negative" },
541
542 // Valid expressions that should not error
543 { "Plain text", false, "plain text should not error" },
544 { "@{2 + 2}", false, "simple calculation should work" },
545 };
546
547 for( const auto& testCase : cases )
548 {
549 auto result = evaluator.Evaluate( testCase.expression );
550
551 if( testCase.shouldError )
552 {
553 BOOST_CHECK_MESSAGE( evaluator.HasErrors(),
554 "Expected error for: " + testCase.description );
555 }
556 else
557 {
558 BOOST_CHECK_MESSAGE( !evaluator.HasErrors(),
559 "Unexpected error for: " + testCase.description );
560 }
561 }
562}
563
567BOOST_AUTO_TEST_CASE( ComplexExpressions )
568{
569 EXPRESSION_EVALUATOR evaluator;
570 evaluator.SetVariable( "pi", 3.14159 );
571 evaluator.SetVariable( "radius", 5.0 );
572
573 struct TestCase {
574 std::string expression;
575 std::string expected;
576 double tolerance;
577 };
578
579 const std::vector<TestCase> cases = {
580 // Complex mathematical expressions
581 { "@{2 * ${pi} * ${radius}}", "31.42", 0.01 }, // Circumference
582 { "@{${pi} * pow(${radius}, 2)}", "78.54", 0.01 }, // Area
583 { "@{sqrt(pow(3, 2) + pow(4, 2))}", "5", 0.001 }, // Pythagorean theorem
584
585 // Nested function calls
586 { "@{max(abs(-5), sqrt(16), floor(3.7))}", "5", 0.001 },
587 { "@{round(avg(1.1, 2.2, 3.3), 1)}", "2.2", 0.001 },
588
589 // Mixed string and math
590 { "Circle with radius @{${radius}} has area @{format(${pi} * pow(${radius}, 2), 1)}",
591 "Circle with radius 5 has area 78.5", 0 },
592 };
593
594 for( const auto& testCase : cases )
595 {
596 auto result = evaluator.Evaluate( testCase.expression );
597 std::string resultStr = result.ToStdString();
598 BOOST_CHECK( !evaluator.HasErrors() );
599
600 if( testCase.tolerance > 0 )
601 {
602 // Extract numeric part for comparison
603 std::regex numberRegex( R"([\d.]+)" );
604 std::smatch match;
605
606 if( std::regex_search( resultStr, match, numberRegex ) )
607 {
608 double actualValue = std::stod( match[0].str() );
609 double expectedValue = std::stod( testCase.expected );
610 BOOST_CHECK_CLOSE( actualValue, expectedValue, testCase.tolerance * 100 );
611 }
612 }
613 else
614 {
615 BOOST_CHECK_EQUAL( result, testCase.expected );
616 }
617 }
618}
619
624{
625 EXPRESSION_EVALUATOR evaluator;
626
627 // Build a large expression with many calculations
628 std::string largeExpression = "Result: ";
629 for( int i = 0; i < 50; ++i )
630 {
631 largeExpression += "@{" + std::to_string(i) + " * 2} ";
632 }
633
634 auto start = std::chrono::high_resolution_clock::now();
635 auto result = evaluator.Evaluate( largeExpression );
636 auto end = std::chrono::high_resolution_clock::now();
637
638 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>( end - start );
639
640 BOOST_CHECK( !evaluator.HasErrors() );
641 BOOST_CHECK( !result.empty() );
642
643 // Should complete in reasonable time (less than 1 second)
644 BOOST_CHECK_LT( duration.count(), 1000 );
645}
646
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)
BOOST_CHECK_MESSAGE(totalMismatches==0, std::to_string(totalMismatches)+" board(s) with strategy disagreements")
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.