Blender V4.5
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_integer("hello"));
72 EXPECT_EQ(std::nullopt, map.get_float("hello"));
73
74 /* Populate the map. */
75 EXPECT_TRUE(map.add_string("hello", "What a wonderful world."));
76 EXPECT_TRUE(map.add_integer("bye", 42));
77 EXPECT_TRUE(map.add_float("what", 3.14159));
78
79 /* Attempting to add variables with those names again should fail, since they
80 * already exist now. */
81 EXPECT_FALSE(map.add_string("hello", "Sup."));
82 EXPECT_FALSE(map.add_string("bye", "Sup."));
83 EXPECT_FALSE(map.add_string("what", "Sup."));
84 EXPECT_FALSE(map.add_integer("hello", 2));
85 EXPECT_FALSE(map.add_integer("bye", 2));
86 EXPECT_FALSE(map.add_integer("what", 2));
87 EXPECT_FALSE(map.add_float("hello", 2.71828));
88 EXPECT_FALSE(map.add_float("bye", 2.71828));
89 EXPECT_FALSE(map.add_float("what", 2.71828));
90
91 /* Confirm that the right variables exist. */
92 EXPECT_TRUE(map.contains("hello"));
93 EXPECT_TRUE(map.contains("bye"));
94 EXPECT_TRUE(map.contains("what"));
95 EXPECT_FALSE(map.contains("not here"));
96
97 /* Fetch the variables we added. */
98 EXPECT_EQ("What a wonderful world.", map.get_string("hello"));
99 EXPECT_EQ(42, map.get_integer("bye"));
100 EXPECT_EQ(3.14159, map.get_float("what"));
101
102 /* The same variables shouldn't exist for the other types, despite our attempt
103 * to add them earlier. */
104 EXPECT_EQ(std::nullopt, map.get_integer("hello"));
105 EXPECT_EQ(std::nullopt, map.get_float("hello"));
106 EXPECT_EQ(std::nullopt, map.get_string("bye"));
107 EXPECT_EQ(std::nullopt, map.get_float("bye"));
108 EXPECT_EQ(std::nullopt, map.get_string("what"));
109 EXPECT_EQ(std::nullopt, map.get_integer("what"));
110
111 /* Remove the variables. */
112 EXPECT_TRUE(map.remove("hello"));
113 EXPECT_TRUE(map.remove("bye"));
114 EXPECT_TRUE(map.remove("what"));
115
116 /* The variables shouldn't exist anymore. */
117 EXPECT_FALSE(map.contains("hello"));
118 EXPECT_FALSE(map.contains("bye"));
119 EXPECT_FALSE(map.contains("what"));
120 EXPECT_EQ(std::nullopt, map.get_string("hello"));
121 EXPECT_EQ(std::nullopt, map.get_integer("bye"));
122 EXPECT_EQ(std::nullopt, map.get_float("what"));
123 EXPECT_FALSE(map.remove("hello"));
124 EXPECT_FALSE(map.remove("bye"));
125 EXPECT_FALSE(map.remove("what"));
126}
127
133
134TEST(path_templates, validate_and_apply_template)
135{
136 VariableMap variables;
137 {
138 variables.add_string("hi", "hello");
139 variables.add_string("bye", "goodbye");
140 variables.add_string("long", "This string is exactly 32 bytes.");
141 variables.add_integer("the_answer", 42);
142 variables.add_integer("prime", 7);
143 variables.add_integer("i_negative", -7);
144 variables.add_float("pi", 3.14159265358979323846);
145 variables.add_float("e", 2.71828182845904523536);
146 variables.add_float("ntsc", 30.0 / 1.001);
147 variables.add_float("two", 2.0);
148 variables.add_float("f_negative", -3.14159265358979323846);
149 variables.add_float("huge", 200000000000000000000000000000000.0);
150 variables.add_float("tiny", 0.000000000000000000000000000000002);
151 }
152
153 const Vector<PathTemplateTestCase> test_cases = {
154 /* Simple case, testing all variables. */
155 {
156 "{hi}_{bye}_{the_answer}_{prime}_{i_negative}_{pi}_{e}_{ntsc}_{two}_{f_negative}_{huge}_"
157 "{tiny}",
158 "hello_goodbye_42_7_-7_3.141592653589793_2.718281828459045_29.970029970029973_2.0_-3."
159 "141592653589793_2e+32_2e-33",
160 {},
161 },
162
163 /* Integer formatting. */
164 {
165 "{the_answer:#}_{the_answer:##}_{the_answer:####}_{i_negative:####}",
166 "42_42_0042_-007",
167 {},
168 },
169
170 /* Integer formatting as float. */
171 {
172 "{the_answer:.###}_{the_answer:#.##}_{the_answer:###.##}_{i_negative:###.####}",
173 "42.000_42.00_042.00_-07.0000",
174 {},
175 },
176
177 /* Float formatting: specify fractional digits only. */
178 {
179 "{pi:.####}_{e:.###}_{ntsc:.########}_{two:.##}_{f_negative:.##}_{huge:.##}_{tiny:.##}",
180 "3.1416_2.718_29.97002997_2.00_-3.14_200000000000000010732324408786944.00_0.00",
181 {},
182 },
183
184 /* Float formatting: specify both integer and fractional digits. */
185 {
186 "{pi:##.####}_{e:####.###}_{ntsc:#.########}_{two:###.##}_{f_negative:###.##}_{huge:###."
187 "##}_{tiny:###.##}",
188 "03.1416_0002.718_29.97002997_002.00_-03.14_200000000000000010732324408786944.00_000.00",
189 {},
190 },
191
192 /* Float formatting: format as integer. */
193 {
194 "{pi:##}_{e:####}_{ntsc:#}_{two:###}",
195 "03_0003_30_002",
196 {},
197 },
198
199 /* Escaping. "{{" and "}}" are the escape codes for literal "{" and "}". */
200 {
201 "{hi}_{{hi}}_{{{bye}}}_{bye}",
202 "hello_{hi}_{goodbye}_goodbye",
203 {},
204 },
205
206 /* Error: string variables do not support format specifiers. */
207 {
208 "{hi:##}_{bye:#}",
209 "{hi:##}_{bye:#}",
210 {
213 },
214 },
215
216 /* Error: float formatting: specifying integer digits only (but still wanting
217 * it printed as a float) is currently not supported. */
218 {
219 "{pi:##.}_{e:####.}_{ntsc:#.}_{two:###.}_{f_negative:###.}_{huge:###.}_{tiny:###.}",
220 "{pi:##.}_{e:####.}_{ntsc:#.}_{two:###.}_{f_negative:###.}_{huge:###.}_{tiny:###.}",
221 {
229 },
230 },
231
232 /* Error: missing variable. */
233 {
234 "{hi}_{missing}_{bye}",
235 "{hi}_{missing}_{bye}",
236 {
238 },
239 },
240
241 /* Error: incomplete variable expression. */
242 {
243 "foo{hi",
244 "foo{hi",
245 {
247 },
248 },
249
250 /* Error: incomplete variable expression after complete one. */
251 {
252 "foo{bye}{hi",
253 "foo{bye}{hi",
254 {
256 },
257 },
258
259 /* Error: invalid format specifiers. */
260 {
261 "{prime:}_{prime:.}_{prime:#.#.#}_{prime:sup}_{prime::sup}_{prime}",
262 "{prime:}_{prime:.}_{prime:#.#.#}_{prime:sup}_{prime::sup}_{prime}",
263 {
269 },
270 },
271
272 /* Error: unclosed variable. */
273 {
274 "{hi_{hi}_{bye}",
275 "{hi_{hi}_{bye}",
276 {
278 },
279 },
280
281 /* Error: escaped braces inside variable. */
282 {
283 "{hi_{{hi}}_{bye}",
284 "{hi_{{hi}}_{bye}",
285 {
287 },
288 },
289
290 /* Test what happens when the path would expand to a string that's longer than
291 * `FILE_MAX`.
292 *
293 * We don't care so much about any kind of "correctness" here, we just want to
294 * ensure that it still results in a valid null-terminated string that fits in
295 * `FILE_MAX` bytes.
296 *
297 * NOTE: this test will have to be updated if `FILE_MAX` is ever changed. */
298 {
299 "___{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}"
300 "{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
301 "long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
302 "long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
303 "long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
304 "long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
305 "long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
306 "long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
307 "long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
308 "long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
309 "long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
310 "long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
311 "long}{long}",
312
313 "___This string is exactly 32 bytes.This string is exactly 32 bytes.This string is "
314 "exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This "
315 "string is exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 "
316 "bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This string is "
317 "exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This "
318 "string is exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 "
319 "bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This string is "
320 "exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This "
321 "string is exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 "
322 "bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This string is "
323 "exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This "
324 "string is exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 "
325 "by",
326
327 {},
328 },
329 };
330
331 for (const PathTemplateTestCase &test_case : test_cases) {
332 char path[FILE_MAX];
333 BLI_strncpy(path, test_case.path_in, FILE_MAX);
334
335 /* Do validation first, which shouldn't modify the path. */
336 const Vector<Error> validation_errors = BKE_path_validate_template(path, variables);
337 EXPECT_EQ(validation_errors, test_case.expected_errors)
338 << " Template errors: " << errors_to_string(validation_errors) << std::endl
339 << " Expected errors: " << errors_to_string(test_case.expected_errors) << std::endl
340 << " Note: test_case.path_in = " << test_case.path_in << std::endl;
341 EXPECT_EQ(blender::StringRef(path), test_case.path_in)
342 << " Note: test_case.path_in = " << test_case.path_in << std::endl;
343
344 /* Then do application, which should modify the path. */
345 const Vector<Error> application_errors = BKE_path_apply_template(path, FILE_MAX, variables);
346 EXPECT_EQ(application_errors, test_case.expected_errors)
347 << " Template errors: " << errors_to_string(application_errors) << std::endl
348 << " Expected errors: " << errors_to_string(test_case.expected_errors) << std::endl
349 << " Note: test_case.path_in = " << test_case.path_in << std::endl;
350 EXPECT_EQ(blender::StringRef(path), test_case.path_result)
351 << " Note: test_case.path_in = " << test_case.path_in << std::endl;
352 }
353}
354
355} // namespace blender::bke::tests
Functions and classes for applying templates with variable expressions to filepaths.
blender::Vector< blender::bke::path_templates::Error > BKE_path_validate_template(blender::StringRef path, 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 * BLI_strncpy(char *__restrict dst, const char *__restrict src, size_t dst_maxncpy) ATTR_NONNULL(1
std::optional< blender::StringRefNull > get_string(blender::StringRef name) const
std::optional< int64_t > get_integer(blender::StringRef name) const
bool add_string(blender::StringRef name, blender::StringRef value)
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)
blender::Vector< Error > BKE_path_apply_template(char *path, int path_max_length, const VariableMap &template_variables)