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