Blender V5.0
path_templates_test.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2025 Blender Authors
2 *
3 * SPDX-License-Identifier: GPL-2.0-or-later */
4
5#include <fmt/format.h>
6
8
9#include "testing/testing.h"
10
11namespace blender::bke::tests {
12
13using namespace blender::bke::path_templates;
14
15static std::string error_to_string(const Error &error)
16{
17 const char *type;
18 switch (error.type) {
20 type = "UNESCAPED_CURLY_BRACE";
21 break;
23 type = "VARIABLE_SYNTAX";
24 break;
26 type = "FORMAT_SPECIFIER";
27 break;
29 type = "UNKNOWN_VARIABLE";
30 break;
31 }
32
33 std::string s;
34 fmt::format_to(std::back_inserter(s),
35 "({}, ({}, {}))",
36 type,
37 error.byte_range.start(),
38 error.byte_range.size());
39
40 return s;
41}
42
43static std::string errors_to_string(Span<Error> errors)
44{
45 std::string s;
46
47 fmt::format_to(std::back_inserter(s), "[");
48 bool is_first = true;
49 for (const Error &error : errors) {
50 if (is_first) {
51 is_first = false;
52 }
53 else {
54 fmt::format_to(std::back_inserter(s), ", ");
55 }
56 fmt::format_to(std::back_inserter(s), "{}", error_to_string(error));
57 }
58 fmt::format_to(std::back_inserter(s), "]");
59
60 return s;
61}
62
64{
65 VariableMap map;
66
67 /* With in empty variable map, these should all return false / fail. */
68 EXPECT_FALSE(map.contains("hello"));
69 EXPECT_FALSE(map.remove("hello"));
70 EXPECT_EQ(std::nullopt, map.get_string("hello"));
71 EXPECT_EQ(std::nullopt, map.get_filepath("hello"));
72 EXPECT_EQ(std::nullopt, map.get_integer("hello"));
73 EXPECT_EQ(std::nullopt, map.get_float("hello"));
74
75 /* Populate the map. */
76 EXPECT_TRUE(map.add_string("hello", "What a wonderful world."));
77 EXPECT_TRUE(map.add_filepath("where", "/my/path"));
78 EXPECT_TRUE(map.add_integer("bye", 42));
79 EXPECT_TRUE(map.add_float("what", 3.14159));
80
81 /* Attempting to add variables with those names again should fail, since they
82 * already exist now. */
83 EXPECT_FALSE(map.add_string("hello", "Sup."));
84 EXPECT_FALSE(map.add_string("where", "Sup."));
85 EXPECT_FALSE(map.add_string("bye", "Sup."));
86 EXPECT_FALSE(map.add_string("what", "Sup."));
87 EXPECT_FALSE(map.add_filepath("hello", "/place"));
88 EXPECT_FALSE(map.add_filepath("where", "/place"));
89 EXPECT_FALSE(map.add_filepath("bye", "/place"));
90 EXPECT_FALSE(map.add_filepath("what", "/place"));
91 EXPECT_FALSE(map.add_integer("hello", 2));
92 EXPECT_FALSE(map.add_integer("where", 2));
93 EXPECT_FALSE(map.add_integer("bye", 2));
94 EXPECT_FALSE(map.add_integer("what", 2));
95 EXPECT_FALSE(map.add_float("hello", 2.71828));
96 EXPECT_FALSE(map.add_float("where", 2.71828));
97 EXPECT_FALSE(map.add_float("bye", 2.71828));
98 EXPECT_FALSE(map.add_float("what", 2.71828));
99
100 /* Confirm that the right variables exist. */
101 EXPECT_TRUE(map.contains("hello"));
102 EXPECT_TRUE(map.contains("where"));
103 EXPECT_TRUE(map.contains("bye"));
104 EXPECT_TRUE(map.contains("what"));
105 EXPECT_FALSE(map.contains("not here"));
106
107 /* Fetch the variables we added. */
108 EXPECT_EQ("What a wonderful world.", map.get_string("hello"));
109 EXPECT_EQ("/my/path", map.get_filepath("where"));
110 EXPECT_EQ(42, map.get_integer("bye"));
111 EXPECT_EQ(3.14159, map.get_float("what"));
112
113 /* The same variables shouldn't exist for the other types, despite our attempt
114 * to add them earlier. */
115 EXPECT_EQ(std::nullopt, map.get_filepath("hello"));
116 EXPECT_EQ(std::nullopt, map.get_integer("hello"));
117 EXPECT_EQ(std::nullopt, map.get_float("hello"));
118 EXPECT_EQ(std::nullopt, map.get_string("where"));
119 EXPECT_EQ(std::nullopt, map.get_integer("where"));
120 EXPECT_EQ(std::nullopt, map.get_float("where"));
121 EXPECT_EQ(std::nullopt, map.get_string("bye"));
122 EXPECT_EQ(std::nullopt, map.get_filepath("bye"));
123 EXPECT_EQ(std::nullopt, map.get_float("bye"));
124 EXPECT_EQ(std::nullopt, map.get_string("what"));
125 EXPECT_EQ(std::nullopt, map.get_filepath("what"));
126 EXPECT_EQ(std::nullopt, map.get_integer("what"));
127
128 /* Remove the variables. */
129 EXPECT_TRUE(map.remove("hello"));
130 EXPECT_TRUE(map.remove("where"));
131 EXPECT_TRUE(map.remove("bye"));
132 EXPECT_TRUE(map.remove("what"));
133
134 /* The variables shouldn't exist anymore. */
135 EXPECT_FALSE(map.contains("hello"));
136 EXPECT_FALSE(map.contains("where"));
137 EXPECT_FALSE(map.contains("bye"));
138 EXPECT_FALSE(map.contains("what"));
139 EXPECT_EQ(std::nullopt, map.get_string("hello"));
140 EXPECT_EQ(std::nullopt, map.get_filepath("where"));
141 EXPECT_EQ(std::nullopt, map.get_integer("bye"));
142 EXPECT_EQ(std::nullopt, map.get_float("what"));
143 EXPECT_FALSE(map.remove("hello"));
144 EXPECT_FALSE(map.remove("where"));
145 EXPECT_FALSE(map.remove("bye"));
146 EXPECT_FALSE(map.remove("what"));
147}
148
149TEST(path_templates, VariableMap_add_filename_only)
150{
151 VariableMap map;
152
153 EXPECT_TRUE(map.add_filename_only("a", "/home/bob/project_joe/scene_3.blend", "fallback"));
154 EXPECT_EQ("scene_3", map.get_filepath("a"));
155
156 EXPECT_TRUE(map.add_filename_only("b", "/home/bob/project_joe/scene_3", "fallback"));
157 EXPECT_EQ("scene_3", map.get_filepath("b"));
158
159 EXPECT_TRUE(map.add_filename_only("c", "/home/bob/project_joe/scene.03.blend", "fallback"));
160 EXPECT_EQ("scene.03", map.get_filepath("c"));
161
162 EXPECT_TRUE(map.add_filename_only("d", "/home/bob/project_joe/.scene_3.blend", "fallback"));
163 EXPECT_EQ(".scene_3", map.get_filepath("d"));
164
165 EXPECT_TRUE(map.add_filename_only("e", "/home/bob/project_joe/.scene_3", "fallback"));
166 EXPECT_EQ(".scene_3", map.get_filepath("e"));
167
168 EXPECT_TRUE(map.add_filename_only("f", "scene_3.blend", "fallback"));
169 EXPECT_EQ("scene_3", map.get_filepath("f"));
170
171 EXPECT_TRUE(map.add_filename_only("g", "scene_3", "fallback"));
172 EXPECT_EQ("scene_3", map.get_filepath("g"));
173
174 /* No filename in path (ending slash means it's a directory). */
175 EXPECT_TRUE(map.add_filename_only("h", "/home/bob/project_joe/", "fallback"));
176 EXPECT_EQ("fallback", map.get_filepath("h"));
177
178 /* Empty path. */
179 EXPECT_TRUE(map.add_filename_only("i", "", "fallback"));
180 EXPECT_EQ("fallback", map.get_filepath("i"));
181
182 /* Attempt to add already-added variable. */
183 EXPECT_FALSE(map.add_filename_only("i", "", "fallback"));
184}
185
186TEST(path_templates, VariableMap_add_path_up_to_file)
187{
188 VariableMap map;
189
190 EXPECT_TRUE(map.add_path_up_to_file("a", "/home/bob/project_joe/scene_3.blend", "fallback"));
191 EXPECT_EQ("/home/bob/project_joe/", map.get_filepath("a"));
192
193 EXPECT_TRUE(map.add_path_up_to_file("b", "project_joe/scene_3.blend", "fallback"));
194 EXPECT_EQ("project_joe/", map.get_filepath("b"));
195
196 EXPECT_TRUE(map.add_path_up_to_file("c", "/scene_3.blend", "fallback"));
197 EXPECT_EQ("/", map.get_filepath("c"));
198
199 /* No filename in path (ending slash means it's a directory). */
200 EXPECT_TRUE(map.add_path_up_to_file("e", "/home/bob/project_joe/", "fallback"));
201 EXPECT_EQ("/home/bob/project_joe/", map.get_filepath("e"));
202
203 EXPECT_TRUE(map.add_path_up_to_file("f", "/", "fallback"));
204 EXPECT_EQ("/", map.get_filepath("f"));
205
206 /* No leading path. */
207 EXPECT_TRUE(map.add_path_up_to_file("d", "scene_3.blend", "fallback"));
208 EXPECT_EQ("fallback", map.get_filepath("d"));
209
210 /* Empty path. */
211 EXPECT_TRUE(map.add_path_up_to_file("g", "", "fallback"));
212 EXPECT_EQ("fallback", map.get_filepath("g"));
213
214 /* Attempt to add already-added variable. */
215 EXPECT_FALSE(map.add_filename_only("g", "", "fallback"));
216}
217
223
224TEST(path_templates, validate_and_apply_template)
225{
226 VariableMap variables;
227 {
228 variables.add_string("hi", "hello");
229 variables.add_string("bye", "goodbye");
230 variables.add_string("empty", "");
231 variables.add_string("sanitize", "./\\?*:|\"<>");
232 variables.add_string("long", "This string is exactly 32 bytes_");
233 variables.add_filepath("path", "C:\\and/or/../nor/");
234 variables.add_integer("the_answer", 42);
235 variables.add_integer("prime", 7);
236 variables.add_integer("i_negative", -7);
237 variables.add_float("pi", 3.14159265358979323846);
238 variables.add_float("e", 2.71828182845904523536);
239 variables.add_float("ntsc", 30.0 / 1.001);
240 variables.add_float("two", 2.0);
241 variables.add_float("f_negative", -3.14159265358979323846);
242 variables.add_float("huge", 200000000000000000000000000000000.0);
243 variables.add_float("tiny", 0.000000000000000000000000000000002);
244 }
245
246 const Vector<PathTemplateTestCase> test_cases = {
247 /* Simple case, testing all variables. */
248 {
249 "{hi}_{bye}_{empty}_{sanitize}_{path}"
250 "_{the_answer}_{prime}_{i_negative}_{pi}_{e}_{ntsc}_{two}"
251 "_{f_negative}_{huge}_{tiny}",
252 "hello_goodbye__.__________C:\\and/or/../nor/"
253 "_42_7_-7_3.141592653589793_2.718281828459045_29.970029970029973_2.0"
254 "_-3.141592653589793_2e+32_2e-33",
255 {},
256 },
257
258 /* Integer formatting. */
259 {
260 "{the_answer:#}_{the_answer:##}_{the_answer:####}_{i_negative:####}",
261 "42_42_0042_-007",
262 {},
263 },
264
265 /* Integer formatting as float. */
266 {
267 "{the_answer:.###}_{the_answer:#.##}_{the_answer:###.##}_{i_negative:###.####}",
268 "42.000_42.00_042.00_-07.0000",
269 {},
270 },
271
272 /* Float formatting: specify fractional digits only. */
273 {
274 "{pi:.####}_{e:.###}_{ntsc:.########}_{two:.##}_{f_negative:.##}_{huge:.##}_{tiny:.##}",
275 "3.1416_2.718_29.97002997_2.00_-3.14_200000000000000010732324408786944.00_0.00",
276 {},
277 },
278
279 /* Float formatting: specify both integer and fractional digits. */
280 {
281 "{pi:##.####}_{e:####.###}_{ntsc:#.########}_{two:###.##}_{f_negative:###.##}_{huge:###."
282 "##}_{tiny:###.##}",
283 "03.1416_0002.718_29.97002997_002.00_-03.14_200000000000000010732324408786944.00_000.00",
284 {},
285 },
286
287 /* Float formatting: format as integer. */
288 {
289 "{pi:##}_{e:####}_{ntsc:#}_{two:###}",
290 "03_0003_30_002",
291 {},
292 },
293
294 /* Escaping. "{{" and "}}" are the escape codes for literal "{" and "}". */
295 {
296 "{hi}_{{hi}}_{{{bye}}}_{bye}",
297 "hello_{hi}_{goodbye}_goodbye",
298 {},
299 },
300
301 /* Error: string variables do not support format specifiers. */
302 {
303 "{hi:##}_{bye:#}",
304 "{hi:##}_{bye:#}",
305 {
308 },
309 },
310
311 /* Error: float formatting: specifying integer digits only (but still wanting
312 * it printed as a float) is currently not supported. */
313 {
314 "{pi:##.}_{e:####.}_{ntsc:#.}_{two:###.}_{f_negative:###.}_{huge:###.}_{tiny:###.}",
315 "{pi:##.}_{e:####.}_{ntsc:#.}_{two:###.}_{f_negative:###.}_{huge:###.}_{tiny:###.}",
316 {
324 },
325 },
326
327 /* Error: missing variable. */
328 {
329 "{hi}_{missing}_{bye}",
330 "{hi}_{missing}_{bye}",
331 {
333 },
334 },
335
336 /* Error: incomplete variable expression. */
337 {
338 "foo{hi",
339 "foo{hi",
340 {
342 },
343 },
344
345 /* Error: incomplete variable expression after complete one. */
346 {
347 "foo{bye}{hi",
348 "foo{bye}{hi",
349 {
351 },
352 },
353
354 /* Error: invalid format specifiers. */
355 {
356 "{prime:}_{prime:.}_{prime:#.#.#}_{prime:sup}_{prime::sup}_{prime}",
357 "{prime:}_{prime:.}_{prime:#.#.#}_{prime:sup}_{prime::sup}_{prime}",
358 {
364 },
365 },
366
367 /* Error: unclosed variable. */
368 {
369 "{hi_{hi}_{bye}",
370 "{hi_{hi}_{bye}",
371 {
373 },
374 },
375
376 /* Error: escaped braces inside variable. */
377 {
378 "{hi_{{hi}}_{bye}",
379 "{hi_{{hi}}_{bye}",
380 {
382 },
383 },
384
385 /* Test what happens when the path would expand to a string that's longer than
386 * `FILE_MAX`.
387 *
388 * We don't care so much about any kind of "correctness" here, we just want to
389 * ensure that it still results in a valid null-terminated string that fits in
390 * `FILE_MAX` bytes.
391 *
392 * NOTE: this test will have to be updated if `FILE_MAX` is ever changed. */
393 {
394 "___{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}"
395 "{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
396 "long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
397 "long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
398 "long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
399 "long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
400 "long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
401 "long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
402 "long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
403 "long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
404 "long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
405 "long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
406 "long}{long}",
407
408 "___This string is exactly 32 bytes_This string is exactly 32 bytes_This string is "
409 "exactly 32 bytes_This string is exactly 32 bytes_This string is exactly 32 bytes_This "
410 "string is exactly 32 bytes_This string is exactly 32 bytes_This string is exactly 32 "
411 "bytes_This string is exactly 32 bytes_This string is exactly 32 bytes_This string is "
412 "exactly 32 bytes_This string is exactly 32 bytes_This string is exactly 32 bytes_This "
413 "string is exactly 32 bytes_This string is exactly 32 bytes_This string is exactly 32 "
414 "bytes_This string is exactly 32 bytes_This string is exactly 32 bytes_This string is "
415 "exactly 32 bytes_This string is exactly 32 bytes_This string is exactly 32 bytes_This "
416 "string is exactly 32 bytes_This string is exactly 32 bytes_This string is exactly 32 "
417 "bytes_This string is exactly 32 bytes_This string is exactly 32 bytes_This string is "
418 "exactly 32 bytes_This string is exactly 32 bytes_This string is exactly 32 bytes_This "
419 "string is exactly 32 bytes_This string is exactly 32 bytes_This string is exactly 32 "
420 "by",
421
422 {},
423 },
424 };
425
426 for (const PathTemplateTestCase &test_case : test_cases) {
427 char path[FILE_MAX];
428 STRNCPY(path, test_case.path_in);
429
430 /* Do validation first, which shouldn't modify the path. */
431 const Vector<Error> validation_errors = BKE_path_validate_template(path, variables);
432 EXPECT_EQ(validation_errors, test_case.expected_errors)
433 << " Template errors: " << errors_to_string(validation_errors) << std::endl
434 << " Expected errors: " << errors_to_string(test_case.expected_errors) << std::endl
435 << " Note: test_case.path_in = " << test_case.path_in << std::endl;
436 EXPECT_EQ(blender::StringRef(path), test_case.path_in)
437 << " Note: test_case.path_in = " << test_case.path_in << std::endl;
438
439 /* Then do application, which should modify the path. */
440 const Vector<Error> application_errors = BKE_path_apply_template(path, FILE_MAX, variables);
441 EXPECT_EQ(application_errors, test_case.expected_errors)
442 << " Template errors: " << errors_to_string(application_errors) << std::endl
443 << " Expected errors: " << errors_to_string(test_case.expected_errors) << std::endl
444 << " Note: test_case.path_in = " << test_case.path_in << std::endl;
445 EXPECT_EQ(blender::StringRef(path), test_case.path_result)
446 << " Note: test_case.path_in = " << test_case.path_in << std::endl;
447 }
448}
449
450} // namespace blender::bke::tests
Functions and classes for evaluating template expressions in filepaths.
blender::Vector< blender::bke::path_templates::Error > BKE_path_validate_template(blender::StringRef path, const blender::bke::path_templates::VariableMap &template_variables)
blender::Vector< blender::bke::path_templates::Error > BKE_path_apply_template(char *path, int path_maxncpy, const blender::bke::path_templates::VariableMap &template_variables)
EXPECT_EQ(BLI_expr_pylike_eval(expr, nullptr, 0, &result), EXPR_PYLIKE_INVALID)
#define FILE_MAX
char * STRNCPY(char(&dst)[N], const char *src)
Definition BLI_string.h:693
bool add_path_up_to_file(blender::StringRef var_name, blender::StringRefNull full_path, blender::StringRef fallback)
std::optional< blender::StringRefNull > get_string(blender::StringRef name) const
bool add_filepath(blender::StringRef name, blender::StringRef value)
bool add_filename_only(blender::StringRef var_name, blender::StringRefNull full_path, blender::StringRef fallback)
std::optional< int64_t > get_integer(blender::StringRef name) const
bool add_string(blender::StringRef name, blender::StringRef value)
std::optional< blender::StringRefNull > get_filepath(blender::StringRef name) const
bool contains(blender::StringRef name) const
bool add_float(blender::StringRef name, double value)
bool remove(blender::StringRef name)
bool add_integer(blender::StringRef name, int64_t value)
std::optional< double > get_float(blender::StringRef name) const
static void error(const char *str)
static std::string error_to_string(const Error &error)
TEST(action_groups, ReconstructGroupsWithReordering)
static std::string errors_to_string(Span< Error > errors)