KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_expand_text_vars.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 modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation, either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20
21#define BOOST_TEST_NO_MAIN
22#include <boost/test/unit_test.hpp>
23#include <atomic>
24#include <filesystem>
25#include <optional>
26#include <thread>
27#include <vector>
28#include <common.h>
29#include <env_paths.h>
31#include <pgm_base.h>
33#include <title_block.h>
34#include <wx/filename.h>
35#include <wx/utils.h>
36
41{
42 // Simple resolver that maps VAR->value, X->5, Y->2
43 std::function<bool( wxString* )> resolver = []( wxString* token ) -> bool
44 {
45 if( *token == wxT( "VAR" ) )
46 {
47 *token = wxT( "value" );
48 return true;
49 }
50 else if( *token == wxT( "X" ) )
51 {
52 *token = wxT( "5" );
53 return true;
54 }
55 else if( *token == wxT( "Y" ) )
56 {
57 *token = wxT( "2" );
58 return true;
59 }
60
61 return false;
62 };
63};
64
65BOOST_FIXTURE_TEST_SUITE( ExpandTextVarsTests, ExpandTextVarsFixture )
66
67// Basic variable expansion
68BOOST_AUTO_TEST_CASE( SimpleVariable )
69{
70 wxString result = ExpandTextVars( wxT( "${VAR}" ), &resolver );
71 BOOST_CHECK( result == wxT( "value" ) );
72}
73
74// Multiple variables in one string
75BOOST_AUTO_TEST_CASE( MultipleVariables )
76{
77 wxString result = ExpandTextVars( wxT( "${X}+${Y}" ), &resolver );
78 BOOST_CHECK( result == wxT( "5+2" ) );
79}
80
81// Escaped variable should produce escape marker (not expanded)
82BOOST_AUTO_TEST_CASE( EscapedVariable )
83{
84 wxString result = ExpandTextVars( wxT( "\\${VAR}" ), &resolver );
85 // The escape marker should be in the output
86 BOOST_CHECK( result.Contains( wxT( "<<<ESC_DOLLAR:" ) ) );
87}
88
89// Escaped variable followed by regular variable - both should be processed correctly
90BOOST_AUTO_TEST_CASE( EscapedThenRegularVariable )
91{
92 wxString result = ExpandTextVars( wxT( "\\${literal}${VAR}" ), &resolver );
93 // Should have escape marker for literal, and "value" for VAR
94 BOOST_CHECK( result.Contains( wxT( "<<<ESC_DOLLAR:" ) ) );
95 BOOST_CHECK( result.Contains( wxT( "value" ) ) );
96}
97
98// Regular variable followed by escaped variable
99BOOST_AUTO_TEST_CASE( RegularThenEscapedVariable )
100{
101 wxString result = ExpandTextVars( wxT( "${VAR}\\${literal}" ), &resolver );
102 // Should have "value" for VAR and escape marker for literal
103 BOOST_CHECK( result.StartsWith( wxT( "value" ) ) );
104 BOOST_CHECK( result.Contains( wxT( "<<<ESC_DOLLAR:" ) ) );
105}
106
107// Issue 22497: Escaped variable inside math expression should not prevent other expansions
108// This is the key test case for the bug fix
109BOOST_AUTO_TEST_CASE( EscapedInsideMathExpression )
110{
111 // First pass: @{\${X}+${Y}} should become @{<<<ESC_DOLLAR:X}+2}
112 // Second pass: the marker should be preserved and +2 should NOT be lost
113 wxString result = ExpandTextVars( wxT( "@{\\${X}+${Y}}" ), &resolver );
114
115 // The result should contain the escape marker
116 BOOST_CHECK_MESSAGE( result.Contains( wxT( "<<<ESC_DOLLAR:" ) ),
117 "Expected escape marker in result" );
118
119 // The result should also contain +2 (the expanded Y variable)
120 BOOST_CHECK_MESSAGE( result.Contains( wxT( "+2" ) ),
121 "Expected '+2' (from ${Y} expansion) in result" );
122
123 // The result should be @{<<<ESC_DOLLAR:X}+2}
124 BOOST_CHECK( result == wxT( "@{<<<ESC_DOLLAR:X}+2}" ) );
125}
126
127// Nested escaped variable in regular variable reference
128BOOST_AUTO_TEST_CASE( EscapedInsideVariableReference )
129{
130 // ${prefix\${suffix}} - looking up variable with literal ${suffix} in name
131 // This should try to resolve "prefix\${suffix}" which won't resolve,
132 // but the recursive expansion should convert \${suffix} to the marker
133 wxString result = ExpandTextVars( wxT( "${prefix\\${suffix}}" ), &resolver );
134
135 // The unresolved reference should be preserved with escape marker
136 BOOST_CHECK( result.Contains( wxT( "<<<ESC_DOLLAR:" ) ) );
137}
138
139// Multiple escape markers in a math expression
140BOOST_AUTO_TEST_CASE( MultipleEscapedInMathExpression )
141{
142 wxString result = ExpandTextVars( wxT( "@{\\${A}+\\${B}+${Y}}" ), &resolver );
143
144 // Should have two escape markers and the expanded Y (2)
145 BOOST_CHECK_MESSAGE( result.Contains( wxT( "+2" ) ),
146 "Expected '+2' (from ${Y} expansion) in: " + result );
147
148 // Count escape markers (should be 2)
149 int dollarCount = 0;
150 size_t pos = 0;
151
152 while( ( pos = result.find( wxT( "<<<ESC_DOLLAR:" ), pos ) ) != wxString::npos )
153 {
154 dollarCount++;
155 pos += 14;
156 }
157
158 BOOST_CHECK_EQUAL( dollarCount, 2 );
159}
160
161// Math expression with escaped @ sign
162BOOST_AUTO_TEST_CASE( EscapedAtInExpression )
163{
164 wxString result = ExpandTextVars( wxT( "${VAR}\\@{literal}" ), &resolver );
165
166 // Should have "value" for VAR and escape marker for @{literal}
167 BOOST_CHECK( result.StartsWith( wxT( "value" ) ) );
168 BOOST_CHECK( result.Contains( wxT( "<<<ESC_AT:" ) ) );
169}
170
171// Escaped followed by escaped (both should be preserved)
172BOOST_AUTO_TEST_CASE( ConsecutiveEscaped )
173{
174 wxString result = ExpandTextVars( wxT( "\\${A}\\${B}" ), &resolver );
175
176 // Should have two escape markers
177 int dollarCount = 0;
178 size_t pos = 0;
179
180 while( ( pos = result.find( wxT( "<<<ESC_DOLLAR:" ), pos ) ) != wxString::npos )
181 {
182 dollarCount++;
183 pos += 14;
184 }
185
186 BOOST_CHECK_EQUAL( dollarCount, 2 );
187}
188
189// Issue 23599: backslash path separator before text variable should NOT be treated as an escape.
190// This test documents the ExpandTextVars behavior: \${ IS treated as an escape at this level.
191// The fix is applied at call sites that deal with file paths, which normalize backslashes to
192// forward slashes before calling ExpandTextVars.
193BOOST_AUTO_TEST_CASE( BackslashBeforeVariableIsEscape )
194{
195 wxString result = ExpandTextVars( wxT( "subdir\\${VAR}_file.txt" ), &resolver );
196
197 // ExpandTextVars treats \${ as an escape, so VAR is NOT expanded
198 BOOST_CHECK( result.Contains( wxT( "<<<ESC_DOLLAR:" ) ) );
199}
200
201
202// With forward slashes the variable is expanded normally
203BOOST_AUTO_TEST_CASE( ForwardSlashBeforeVariableExpands )
204{
205 wxString result = ExpandTextVars( wxT( "subdir/${VAR}_file.txt" ), &resolver );
206
207 BOOST_CHECK( result == wxT( "subdir/value_file.txt" ) );
208}
209
211
212
213// Issue 23599: JOB::ResolveOutputPath must expand text variables even when preceded by backslash
214// path separators. This suite tests the fix in ResolveOutputPath that normalizes backslashes
215// before calling ExpandTextVars.
216BOOST_AUTO_TEST_SUITE( JobResolveOutputPath )
217
218BOOST_AUTO_TEST_CASE( BackslashPathSeparatorBeforeTextVar )
219{
221
222 TITLE_BLOCK titleBlock;
223 titleBlock.SetRevision( wxT( "RevA" ) );
224 job.SetTitleBlock( titleBlock );
225
226 // Simulates Windows path with text variable immediately after backslash separator
227 wxString path = wxT( "Board Stats\\${REVISION}_Stats.txt" );
228 wxString result = job.ResolveOutputPath( path, false, nullptr );
229
230 // The variable should be expanded, not escaped
231 BOOST_CHECK_MESSAGE( !result.Contains( wxT( "<<<ESC_DOLLAR:" ) ),
232 "Text variable after backslash path separator should not be escaped. Got: "
233 + result );
234 BOOST_CHECK_MESSAGE( result.Contains( wxT( "RevA" ) ),
235 "Expected resolved REVISION in path. Got: " + result );
236}
237
238
239BOOST_AUTO_TEST_CASE( BackslashPathSeparatorBeforeMultipleTextVars )
240{
242
243 TITLE_BLOCK titleBlock;
244 titleBlock.SetRevision( wxT( "B" ) );
245 titleBlock.SetComment( 0, wxT( "DWG-001" ) );
246 job.SetTitleBlock( titleBlock );
247
248 // The COMMENT1 variable maps to Comment(0) in TITLE_BLOCK
249 wxString path = wxT( "Output\\${COMMENT1}_${REVISION}_file.txt" );
250 wxString result = job.ResolveOutputPath( path, false, nullptr );
251
252 BOOST_CHECK_MESSAGE( result.Contains( wxT( "DWG-001" ) ),
253 "Expected COMMENT1 expanded in path. Got: " + result );
254 BOOST_CHECK_MESSAGE( result.Contains( wxT( "_B_" ) ),
255 "Expected REVISION expanded in path. Got: " + result );
256}
257
258
259BOOST_AUTO_TEST_CASE( TextVarNotFirstInFilename )
260{
262
263 TITLE_BLOCK titleBlock;
264 titleBlock.SetRevision( wxT( "C" ) );
265 job.SetTitleBlock( titleBlock );
266
267 // When there's literal text between the backslash and the variable, it always worked
268 wxString path = wxT( "Output\\Generated_${REVISION}_file.txt" );
269 wxString result = job.ResolveOutputPath( path, false, nullptr );
270
271 BOOST_CHECK_MESSAGE( result.Contains( wxT( "Generated_C_file.txt" ) ),
272 "Expected variable expansion with preceding literal text. Got: " + result );
273}
274
276
277
278
292{
293 wxString rootDir;
294 wxString outerDir;
295 wxString innerDir;
296 wxString targetDir;
297
299 {
300 std::filesystem::path tmp = std::filesystem::temp_directory_path() /
301 "kicad_qa_overlap_env_vars";
302 std::error_code ec;
303 std::filesystem::remove_all( tmp, ec );
304 std::filesystem::create_directories( tmp / "V10" / "symbols", ec );
305
306 rootDir = wxString::FromUTF8( tmp.string() );
308 innerDir = wxString::FromUTF8( ( tmp / "V10" ).string() );
309 targetDir = wxString::FromUTF8( ( tmp / "V10" / "symbols" ).string() );
310
311 wxSetEnv( wxS( "KICAD_QA_3RD_PARTY_OUTER" ), outerDir );
312 wxSetEnv( wxS( "KICAD_QA_USER_LIB_INNER" ), innerDir );
313 }
314
316 {
317 wxUnsetEnv( wxS( "KICAD_QA_3RD_PARTY_OUTER" ) );
318 wxUnsetEnv( wxS( "KICAD_QA_USER_LIB_INNER" ) );
319
320 std::filesystem::path tmp = std::filesystem::temp_directory_path() /
321 "kicad_qa_overlap_env_vars";
322 std::error_code ec;
323 std::filesystem::remove_all( tmp, ec );
324 }
325
327 {
328 ENV_VAR_MAP map;
329 map[wxS( "KICAD_QA_3RD_PARTY_OUTER" )] = ENV_VAR_ITEM( outerDir );
330 map[wxS( "KICAD_QA_USER_LIB_INNER" )] = ENV_VAR_ITEM( innerDir );
331 return map;
332 }
333};
334
335
336BOOST_FIXTURE_TEST_SUITE( OverlappingEnvVarPaths, OverlappingEnvVarsFixture )
337
338
339BOOST_AUTO_TEST_CASE( NormalizePicksLongestPrefix )
340{
341 wxFileName target( targetDir, wxS( "test.kicad_sym" ) );
342 ENV_VAR_MAP envMap = BuildEnvMap();
343
344 wxString normalized = NormalizePath( target, &envMap, wxEmptyString );
345
346 // NormalizePath should pick KICAD_QA_USER_LIB_INNER because it is a deeper match.
348 normalized == wxS( "${KICAD_QA_USER_LIB_INNER}/symbols/test.kicad_sym" ),
349 wxString::Format( wxS( "Expected '%s' but got '%s'" ),
350 wxS( "${KICAD_QA_USER_LIB_INNER}/symbols/test.kicad_sym" ),
351 normalized ) );
352}
353
354
355BOOST_AUTO_TEST_CASE( RoundTripPreservesAbsolutePath )
356{
357 wxFileName target( targetDir, wxS( "test.kicad_sym" ) );
358 ENV_VAR_MAP envMap = BuildEnvMap();
359
360 wxString normalized = NormalizePath( target, &envMap, wxEmptyString );
361 wxString expanded = ExpandEnvVarSubstitutions( normalized, nullptr );
362
363 wxFileName expandedFn( expanded );
364 expandedFn.Normalize( wxPATH_NORM_DOTS | wxPATH_NORM_ABSOLUTE );
365
366 wxFileName originalFn( target );
367 originalFn.Normalize( wxPATH_NORM_DOTS | wxPATH_NORM_ABSOLUTE );
368
370 expandedFn.GetFullPath() == originalFn.GetFullPath(),
371 wxString::Format(
372 wxS( "Round-trip mismatch: normalized='%s' expanded='%s' original='%s'" ),
373 normalized, expandedFn.GetFullPath(), originalFn.GetFullPath() ) );
374}
375
376
378
379
380
392BOOST_AUTO_TEST_SUITE( TextVarExpressionEvaluatorConcurrency )
393
394BOOST_AUTO_TEST_CASE( ParallelResolveTextVarsWithMathExpressions )
395{
396 std::function<bool( wxString* )> resolver = []( wxString* token ) -> bool
397 {
398 if( *token == wxT( "#" ) )
399 {
400 *token = wxT( "3" );
401 return true;
402 }
403
404 if( *token == wxT( "ROW" ) )
405 {
406 *token = wxT( "4" );
407 return true;
408 }
409
410 return false;
411 };
412
413 const std::vector<wxString> inputs = {
414 wxT( "Out@{(${#}-2)*8+0}" ),
415 wxT( "Net_@{${ROW}*2+1}" ),
416 wxT( "@{(2-2)*8+0}" ),
417 wxT( "${ROW}:@{${ROW}*${ROW}}" ),
418 wxT( "plain_label_no_expr" ),
419 wxT( "@{1+1}_@{2+2}_@{3+3}" ),
420 };
421
422 const unsigned int numThreads = std::max( 4u, std::thread::hardware_concurrency() );
423 const int iterations = 2000;
424
425 std::atomic<bool> failed{ false };
426 std::atomic<int> totalRuns{ 0 };
427 std::vector<std::thread> threads;
428 threads.reserve( numThreads );
429
430 for( unsigned int t = 0; t < numThreads; ++t )
431 {
432 threads.emplace_back(
433 [&, t]()
434 {
435 try
436 {
437 for( int i = 0; i < iterations; ++i )
438 {
439 const wxString& src = inputs[( t + i ) % inputs.size()];
440 int depth = 0;
441 wxString result = ResolveTextVars( src, &resolver, depth );
442 (void) result;
443 totalRuns.fetch_add( 1, std::memory_order_relaxed );
444 }
445 }
446 catch( ... )
447 {
448 failed.store( true, std::memory_order_relaxed );
449 }
450 } );
451 }
452
453 for( auto& th : threads )
454 th.join();
455
456 BOOST_CHECK( !failed.load() );
457 BOOST_CHECK_EQUAL( totalRuns.load(), static_cast<int>( numThreads ) * iterations );
458}
459
461
462
463
473{
474 wxString innerPath;
475 std::optional<wxString> oldInner;
476 std::optional<wxString> oldOuter;
477
479 {
480 wxString existing;
481
482 if( wxGetEnv( wxS( "KICAD_QA_INNER" ), &existing ) )
483 oldInner = existing;
484
485 if( wxGetEnv( wxS( "KICAD_QA_OUTER" ), &existing ) )
486 oldOuter = existing;
487
488 innerPath = wxString::FromUTF8(
489 ( std::filesystem::temp_directory_path() / "kicad-qa-24244" ).generic_string() );
490
491 wxSetEnv( wxS( "KICAD_QA_INNER" ), innerPath );
492 wxSetEnv( wxS( "KICAD_QA_OUTER" ), wxS( "${KICAD_QA_INNER}/templates" ) );
493 }
494
496 {
497 if( oldInner )
498 wxSetEnv( wxS( "KICAD_QA_INNER" ), *oldInner );
499 else
500 wxUnsetEnv( wxS( "KICAD_QA_INNER" ) );
501
502 if( oldOuter )
503 wxSetEnv( wxS( "KICAD_QA_OUTER" ), *oldOuter );
504 else
505 wxUnsetEnv( wxS( "KICAD_QA_OUTER" ) );
506 }
507};
508
509BOOST_FIXTURE_TEST_SUITE( EnvVarRecursiveExpansion, EnvVarRecursiveExpansionFixture )
510
511BOOST_AUTO_TEST_CASE( ExpandsNestedReferences )
512{
513 wxString rawValue;
514 BOOST_REQUIRE( wxGetEnv( wxS( "KICAD_QA_OUTER" ), &rawValue ) );
515
516 // The raw value should still contain the unexpanded reference.
517 BOOST_CHECK( rawValue.Contains( wxS( "${KICAD_QA_INNER}" ) ) );
518
519 wxString expanded = ExpandEnvVarSubstitutions( rawValue, nullptr );
520 wxString expected = innerPath + wxS( "/templates" );
521
522 // After expansion the inner reference must be resolved to its concrete path.
523 BOOST_CHECK_MESSAGE( expanded == expected,
524 wxString::Format( wxS( "Expected '%s', got '%s'" ), expected, expanded ) );
525}
526
527
528BOOST_AUTO_TEST_CASE( UndefinedReferenceLeavesLiteralMarker )
529{
530 // If a referenced variable is undefined, ExpandEnvVarSubstitutions preserves the
531 // original token. Callers that then mkdir the result would create a literal
532 // "${MISSING}" directory; production code must detect this and bail out.
533 wxUnsetEnv( wxS( "KICAD_QA_INNER" ) );
534
535 wxString rawValue;
536 BOOST_REQUIRE( wxGetEnv( wxS( "KICAD_QA_OUTER" ), &rawValue ) );
537
538 wxString expanded = ExpandEnvVarSubstitutions( rawValue, nullptr );
539 BOOST_CHECK( expanded.Contains( wxS( "${" ) ) );
540
541 // Restore so the fixture destructor sees a known state.
542 wxSetEnv( wxS( "KICAD_QA_INNER" ), innerPath );
543}
544
KiCad uses environment variables internally for determining the base paths for libraries,...
wxString ResolveOutputPath(const wxString &aPath, bool aPathIsDirectory, PROJECT *aProject) const
Definition job.cpp:100
void SetTitleBlock(const TITLE_BLOCK &aTitleBlock)
Definition job.h:204
Hold the information shown in the lower right corner of a plot, printout, or editing view.
Definition title_block.h:41
void SetRevision(const wxString &aRevision)
Definition title_block.h:81
void SetComment(int aIdx, const wxString &aComment)
const wxString ExpandEnvVarSubstitutions(const wxString &aString, const PROJECT *aProject)
Replace any environment variable & text variable references with their values.
Definition common.cpp:708
wxString ExpandTextVars(const wxString &aSource, const PROJECT *aProject, int aFlags)
Definition common.cpp:63
wxString ResolveTextVars(const wxString &aSource, const std::function< bool(wxString *)> *aResolver, int &aDepth)
Multi-pass text variable expansion and math expression evaluation.
Definition common.cpp:300
The common library.
wxString NormalizePath(const wxFileName &aFilePath, const ENV_VAR_MAP *aEnvVars, const wxString &aProjectPath)
Normalize a file path to an environmental variable, if possible.
Definition env_paths.cpp:73
Helper functions to substitute paths with environmental variables.
static FILENAME_RESOLVER * resolver
std::map< wxString, ENV_VAR_ITEM > ENV_VAR_MAP
see class PGM_BASE
Regression test for KiCad GitLab issue #24244.
Test fixture for ExpandTextVars tests.
std::function< bool(wxString *)> resolver
Regression tests for overlapping-prefix environment variables.
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_CASE(SimpleVariable)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
std::string path
VECTOR3I expected(15, 30, 45)
BOOST_CHECK_MESSAGE(totalMismatches==0, std::to_string(totalMismatches)+" board(s) with strategy disagreements")
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")