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 <thread>
26#include <vector>
27#include <common.h>
28#include <env_paths.h>
29#include <pgm_base.h>
31#include <wx/filename.h>
32#include <wx/utils.h>
33
38{
39 // Simple resolver that maps VAR->value, X->5, Y->2
40 std::function<bool( wxString* )> resolver = []( wxString* token ) -> bool
41 {
42 if( *token == wxT( "VAR" ) )
43 {
44 *token = wxT( "value" );
45 return true;
46 }
47 else if( *token == wxT( "X" ) )
48 {
49 *token = wxT( "5" );
50 return true;
51 }
52 else if( *token == wxT( "Y" ) )
53 {
54 *token = wxT( "2" );
55 return true;
56 }
57
58 return false;
59 };
60};
61
62BOOST_FIXTURE_TEST_SUITE( ExpandTextVarsTests, ExpandTextVarsFixture )
63
64// Basic variable expansion
65BOOST_AUTO_TEST_CASE( SimpleVariable )
66{
67 wxString result = ExpandTextVars( wxT( "${VAR}" ), &resolver );
68 BOOST_CHECK( result == wxT( "value" ) );
69}
70
71// Multiple variables in one string
72BOOST_AUTO_TEST_CASE( MultipleVariables )
73{
74 wxString result = ExpandTextVars( wxT( "${X}+${Y}" ), &resolver );
75 BOOST_CHECK( result == wxT( "5+2" ) );
76}
77
78// Escaped variable should produce escape marker (not expanded)
79BOOST_AUTO_TEST_CASE( EscapedVariable )
80{
81 wxString result = ExpandTextVars( wxT( "\\${VAR}" ), &resolver );
82 // The escape marker should be in the output
83 BOOST_CHECK( result.Contains( wxT( "<<<ESC_DOLLAR:" ) ) );
84}
85
86// Escaped variable followed by regular variable - both should be processed correctly
87BOOST_AUTO_TEST_CASE( EscapedThenRegularVariable )
88{
89 wxString result = ExpandTextVars( wxT( "\\${literal}${VAR}" ), &resolver );
90 // Should have escape marker for literal, and "value" for VAR
91 BOOST_CHECK( result.Contains( wxT( "<<<ESC_DOLLAR:" ) ) );
92 BOOST_CHECK( result.Contains( wxT( "value" ) ) );
93}
94
95// Regular variable followed by escaped variable
96BOOST_AUTO_TEST_CASE( RegularThenEscapedVariable )
97{
98 wxString result = ExpandTextVars( wxT( "${VAR}\\${literal}" ), &resolver );
99 // Should have "value" for VAR and escape marker for literal
100 BOOST_CHECK( result.StartsWith( wxT( "value" ) ) );
101 BOOST_CHECK( result.Contains( wxT( "<<<ESC_DOLLAR:" ) ) );
102}
103
104// Issue 22497: Escaped variable inside math expression should not prevent other expansions
105// This is the key test case for the bug fix
106BOOST_AUTO_TEST_CASE( EscapedInsideMathExpression )
107{
108 // First pass: @{\${X}+${Y}} should become @{<<<ESC_DOLLAR:X}+2}
109 // Second pass: the marker should be preserved and +2 should NOT be lost
110 wxString result = ExpandTextVars( wxT( "@{\\${X}+${Y}}" ), &resolver );
111
112 // The result should contain the escape marker
113 BOOST_CHECK_MESSAGE( result.Contains( wxT( "<<<ESC_DOLLAR:" ) ),
114 "Expected escape marker in result" );
115
116 // The result should also contain +2 (the expanded Y variable)
117 BOOST_CHECK_MESSAGE( result.Contains( wxT( "+2" ) ),
118 "Expected '+2' (from ${Y} expansion) in result" );
119
120 // The result should be @{<<<ESC_DOLLAR:X}+2}
121 BOOST_CHECK( result == wxT( "@{<<<ESC_DOLLAR:X}+2}" ) );
122}
123
124// Nested escaped variable in regular variable reference
125BOOST_AUTO_TEST_CASE( EscapedInsideVariableReference )
126{
127 // ${prefix\${suffix}} - looking up variable with literal ${suffix} in name
128 // This should try to resolve "prefix\${suffix}" which won't resolve,
129 // but the recursive expansion should convert \${suffix} to the marker
130 wxString result = ExpandTextVars( wxT( "${prefix\\${suffix}}" ), &resolver );
131
132 // The unresolved reference should be preserved with escape marker
133 BOOST_CHECK( result.Contains( wxT( "<<<ESC_DOLLAR:" ) ) );
134}
135
136// Multiple escape markers in a math expression
137BOOST_AUTO_TEST_CASE( MultipleEscapedInMathExpression )
138{
139 wxString result = ExpandTextVars( wxT( "@{\\${A}+\\${B}+${Y}}" ), &resolver );
140
141 // Should have two escape markers and the expanded Y (2)
142 BOOST_CHECK_MESSAGE( result.Contains( wxT( "+2" ) ),
143 "Expected '+2' (from ${Y} expansion) in: " + result );
144
145 // Count escape markers (should be 2)
146 int dollarCount = 0;
147 size_t pos = 0;
148
149 while( ( pos = result.find( wxT( "<<<ESC_DOLLAR:" ), pos ) ) != wxString::npos )
150 {
151 dollarCount++;
152 pos += 14;
153 }
154
155 BOOST_CHECK_EQUAL( dollarCount, 2 );
156}
157
158// Math expression with escaped @ sign
159BOOST_AUTO_TEST_CASE( EscapedAtInExpression )
160{
161 wxString result = ExpandTextVars( wxT( "${VAR}\\@{literal}" ), &resolver );
162
163 // Should have "value" for VAR and escape marker for @{literal}
164 BOOST_CHECK( result.StartsWith( wxT( "value" ) ) );
165 BOOST_CHECK( result.Contains( wxT( "<<<ESC_AT:" ) ) );
166}
167
168// Escaped followed by escaped (both should be preserved)
169BOOST_AUTO_TEST_CASE( ConsecutiveEscaped )
170{
171 wxString result = ExpandTextVars( wxT( "\\${A}\\${B}" ), &resolver );
172
173 // Should have two escape markers
174 int dollarCount = 0;
175 size_t pos = 0;
176
177 while( ( pos = result.find( wxT( "<<<ESC_DOLLAR:" ), pos ) ) != wxString::npos )
178 {
179 dollarCount++;
180 pos += 14;
181 }
182
183 BOOST_CHECK_EQUAL( dollarCount, 2 );
184}
185
187
188
189
203{
204 wxString rootDir;
205 wxString outerDir;
206 wxString innerDir;
207 wxString targetDir;
208
210 {
211 std::filesystem::path tmp = std::filesystem::temp_directory_path() /
212 "kicad_qa_overlap_env_vars";
213 std::error_code ec;
214 std::filesystem::remove_all( tmp, ec );
215 std::filesystem::create_directories( tmp / "V10" / "symbols", ec );
216
217 rootDir = wxString::FromUTF8( tmp.string() );
219 innerDir = wxString::FromUTF8( ( tmp / "V10" ).string() );
220 targetDir = wxString::FromUTF8( ( tmp / "V10" / "symbols" ).string() );
221
222 wxSetEnv( wxS( "KICAD_QA_3RD_PARTY_OUTER" ), outerDir );
223 wxSetEnv( wxS( "KICAD_QA_USER_LIB_INNER" ), innerDir );
224 }
225
227 {
228 wxUnsetEnv( wxS( "KICAD_QA_3RD_PARTY_OUTER" ) );
229 wxUnsetEnv( wxS( "KICAD_QA_USER_LIB_INNER" ) );
230
231 std::filesystem::path tmp = std::filesystem::temp_directory_path() /
232 "kicad_qa_overlap_env_vars";
233 std::error_code ec;
234 std::filesystem::remove_all( tmp, ec );
235 }
236
238 {
239 ENV_VAR_MAP map;
240 map[wxS( "KICAD_QA_3RD_PARTY_OUTER" )] = ENV_VAR_ITEM( outerDir );
241 map[wxS( "KICAD_QA_USER_LIB_INNER" )] = ENV_VAR_ITEM( innerDir );
242 return map;
243 }
244};
245
246
247BOOST_FIXTURE_TEST_SUITE( OverlappingEnvVarPaths, OverlappingEnvVarsFixture )
248
249
250BOOST_AUTO_TEST_CASE( NormalizePicksLongestPrefix )
251{
252 wxFileName target( targetDir, wxS( "test.kicad_sym" ) );
253 ENV_VAR_MAP envMap = BuildEnvMap();
254
255 wxString normalized = NormalizePath( target, &envMap, wxEmptyString );
256
257 // NormalizePath should pick KICAD_QA_USER_LIB_INNER because it is a deeper match.
259 normalized == wxS( "${KICAD_QA_USER_LIB_INNER}/symbols/test.kicad_sym" ),
260 wxString::Format( wxS( "Expected '%s' but got '%s'" ),
261 wxS( "${KICAD_QA_USER_LIB_INNER}/symbols/test.kicad_sym" ),
262 normalized ) );
263}
264
265
266BOOST_AUTO_TEST_CASE( RoundTripPreservesAbsolutePath )
267{
268 wxFileName target( targetDir, wxS( "test.kicad_sym" ) );
269 ENV_VAR_MAP envMap = BuildEnvMap();
270
271 wxString normalized = NormalizePath( target, &envMap, wxEmptyString );
272 wxString expanded = ExpandEnvVarSubstitutions( normalized, nullptr );
273
274 wxFileName expandedFn( expanded );
275 expandedFn.Normalize( wxPATH_NORM_DOTS | wxPATH_NORM_ABSOLUTE );
276
277 wxFileName originalFn( target );
278 originalFn.Normalize( wxPATH_NORM_DOTS | wxPATH_NORM_ABSOLUTE );
279
281 expandedFn.GetFullPath() == originalFn.GetFullPath(),
282 wxString::Format(
283 wxS( "Round-trip mismatch: normalized='%s' expanded='%s' original='%s'" ),
284 normalized, expandedFn.GetFullPath(), originalFn.GetFullPath() ) );
285}
286
287
289
290
291
303BOOST_AUTO_TEST_SUITE( TextVarExpressionEvaluatorConcurrency )
304
305BOOST_AUTO_TEST_CASE( ParallelResolveTextVarsWithMathExpressions )
306{
307 std::function<bool( wxString* )> resolver = []( wxString* token ) -> bool
308 {
309 if( *token == wxT( "#" ) )
310 {
311 *token = wxT( "3" );
312 return true;
313 }
314
315 if( *token == wxT( "ROW" ) )
316 {
317 *token = wxT( "4" );
318 return true;
319 }
320
321 return false;
322 };
323
324 const std::vector<wxString> inputs = {
325 wxT( "Out@{(${#}-2)*8+0}" ),
326 wxT( "Net_@{${ROW}*2+1}" ),
327 wxT( "@{(2-2)*8+0}" ),
328 wxT( "${ROW}:@{${ROW}*${ROW}}" ),
329 wxT( "plain_label_no_expr" ),
330 wxT( "@{1+1}_@{2+2}_@{3+3}" ),
331 };
332
333 const unsigned int numThreads = std::max( 4u, std::thread::hardware_concurrency() );
334 const int iterations = 2000;
335
336 std::atomic<bool> failed{ false };
337 std::atomic<int> totalRuns{ 0 };
338 std::vector<std::thread> threads;
339 threads.reserve( numThreads );
340
341 for( unsigned int t = 0; t < numThreads; ++t )
342 {
343 threads.emplace_back(
344 [&, t]()
345 {
346 try
347 {
348 for( int i = 0; i < iterations; ++i )
349 {
350 const wxString& src = inputs[( t + i ) % inputs.size()];
351 int depth = 0;
352 wxString result = ResolveTextVars( src, &resolver, depth );
353 (void) result;
354 totalRuns.fetch_add( 1, std::memory_order_relaxed );
355 }
356 }
357 catch( ... )
358 {
359 failed.store( true, std::memory_order_relaxed );
360 }
361 } );
362 }
363
364 for( auto& th : threads )
365 th.join();
366
367 BOOST_CHECK( !failed.load() );
368 BOOST_CHECK_EQUAL( totalRuns.load(), static_cast<int>( numThreads ) * iterations );
369}
370
KiCad uses environment variables internally for determining the base paths for libraries,...
const wxString ExpandEnvVarSubstitutions(const wxString &aString, const PROJECT *aProject)
Replace any environment variable & text variable references with their values.
Definition common.cpp:707
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
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_AUTO_TEST_SUITE_END()
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")