Skip to content

Commit 94f1cad

Browse files
committed
test: verify that hobject_t formatting is stable
using 'Squid' as baseline. Signed-off-by: Ronen Friedman <[email protected]>
1 parent fe26408 commit 94f1cad

File tree

2 files changed

+308
-0
lines changed

2 files changed

+308
-0
lines changed

src/common/hobject.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ struct hobject_t {
342342
return operator<=>(rhs) == 0;
343343
}
344344
friend struct ghobject_t;
345+
friend struct test_hobject_fmt_t;
345346
};
346347
WRITE_CLASS_ENCODER(hobject_t)
347348

src/test/common/test_hobject.cc

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1+
#include <string>
2+
#include <fmt/format.h>
3+
#include <fmt/ranges.h>
4+
15
#include "common/hobject.h"
26
#include "gtest/gtest.h"
37

8+
using namespace std::string_literals;
9+
using std::string;
10+
411
TEST(HObject, cmp)
512
{
613
hobject_t c{object_t{"fooc"}, "food", CEPH_NOSNAP, 42, 0, "nspace"};
@@ -9,3 +16,303 @@ TEST(HObject, cmp)
916
ASSERT_EQ(-1, cmp(c, d));
1017
ASSERT_EQ(-1, cmp(d, e));
1118
}
19+
20+
// ---- test methods that 'stringify' the object while escaping special characters ----
21+
22+
23+
/*
24+
* Two methods are used here: first - using a preset list of objects & hobjects,
25+
* comparing the output to the expected string; and a second method: comparing the
26+
* output for a "random"(*) object to the results when using the code from 'Squid'.
27+
*
28+
* (*) the object is not random, but it's not part of the preset list.
29+
*/
30+
31+
32+
struct obj_n_expected_t {
33+
hobject_t obj;
34+
std::string expected_to_str;
35+
std::string expected_fmt;
36+
};
37+
38+
static std::vector<obj_n_expected_t> known_examples = {
39+
40+
// the first entry will be modified (by setting the max flag)
41+
{hobject_t{}, "MAX", "MAX"},
42+
43+
{hobject_t{object_t("o%:/name2"), "aaaa"s, CEPH_NOSNAP, 67, 0, "n1"s},
44+
"0000000000000000.34000000.head.o%p:/name2.aaaa.n1",
45+
"0:c2000000:n1:aaaa:o%25%3a%2fname2:head"},
46+
47+
{hobject_t{object_t("okey"), "okey"s, CEPH_NOSNAP, 1, 0, "n12"s},
48+
"0000000000000000.10000000.head.okey..n12", "0:80000000:n12::okey:head"},
49+
50+
{hobject_t{}, "8000000000000000.00000000.0...", "MIN"},
51+
52+
/// \todo not sure whether the '-1' or the 'FFF..' is correct:
53+
#if 0
54+
{hobject_t{object_t("oname"), std::string{}, 1, 234, -1, ""s},
55+
"FFFFFFFFFFFFFFFF.AE000000.1.oxxname..",
56+
"18446744073709551615:57000000:::oname:1"},
57+
#endif
58+
{hobject_t{object_t{"oname3"}, "oname3"s, CEPH_SNAPDIR, 910, 1, "n2"s},
59+
"0000000000000001.E8300000.snapdir.oname3..n2",
60+
"1:71c00000:n2::oname3:snapdir"},
61+
62+
{hobject_t{
63+
object_t("nonprint\030%_%.%"), "c"s, 0x12345678, 0xe0e0f0f0, 0x2727,
64+
"n5"s},
65+
"0000000000002727.0F0F0E0E.12345678.nonprint\x18%p%u%p%e%p.c.n5",
66+
"10023:0f0f0707:n5:c:nonprint%18%25_%25.%25:12345678"},
67+
68+
{hobject_t{object_t("o//////"), string("ZZ"), 0xaaaa, 65, 1, "zzzzz"},
69+
"0000000000000001.14000000.aaaa.o//////.ZZ.zzzzz",
70+
"1:82000000:zzzzz:ZZ:o%2f%2f%2f%2f%2f%2f:aaaa"}};
71+
72+
// original Ceph code as it was in version Squid
73+
74+
struct test_hobject_fmt_t : public hobject_t {
75+
76+
template <typename... ARGS>
77+
test_hobject_fmt_t(ARGS&&... args) : hobject_t{std::forward<ARGS>(args)...}
78+
{}
79+
80+
test_hobject_fmt_t(const test_hobject_fmt_t& rhs) = default;
81+
test_hobject_fmt_t(test_hobject_fmt_t&& rhs) = default;
82+
test_hobject_fmt_t& operator=(const test_hobject_fmt_t& rhs) = default;
83+
test_hobject_fmt_t& operator=(test_hobject_fmt_t&& rhs) = default;
84+
test_hobject_fmt_t(hobject_t_max&& singleton) : test_hobject_fmt_t()
85+
{
86+
max = true;
87+
}
88+
test_hobject_fmt_t& operator=(hobject_t_max&& singleton)
89+
{
90+
*this = hobject_t();
91+
max = true;
92+
return *this;
93+
}
94+
bool is_max() const { return max; }
95+
bool is_min() const
96+
{
97+
// this needs to match how it's constructed
98+
return snap == 0 && hash == 0 && !max && pool == INT64_MIN;
99+
}
100+
101+
constexpr auto operator<=>(const test_hobject_fmt_t& rhs) const noexcept
102+
{
103+
auto cmp = is_max() <=> rhs.is_max();
104+
if (cmp != 0)
105+
return cmp;
106+
cmp = pool <=> rhs.pool;
107+
if (cmp != 0)
108+
return cmp;
109+
cmp = get_bitwise_key() <=> rhs.get_bitwise_key();
110+
if (cmp != 0)
111+
return cmp;
112+
cmp = nspace <=> rhs.nspace;
113+
if (cmp != 0)
114+
return cmp;
115+
if (!(get_key().empty() && rhs.get_key().empty())) {
116+
cmp = get_effective_key() <=> rhs.get_effective_key();
117+
if (cmp != 0)
118+
return cmp;
119+
}
120+
cmp = oid <=> rhs.oid;
121+
if (cmp != 0)
122+
return cmp;
123+
return snap <=> rhs.snap;
124+
}
125+
constexpr bool operator==(const hobject_t& rhs) const noexcept
126+
{
127+
return operator<=>(rhs) == 0;
128+
}
129+
};
130+
131+
static inline void append_out_escaped(const std::string& in, std::string* out)
132+
{
133+
for (auto i = in.cbegin(); i != in.cend(); ++i) {
134+
if (*i == '%' || *i == ':' || *i == '/' || *i < 32 || *i >= 127) {
135+
char buf[4];
136+
snprintf(buf, sizeof(buf), "%%%02x", (int)(unsigned char)*i);
137+
out->append(buf);
138+
} else {
139+
out->push_back(*i);
140+
}
141+
}
142+
}
143+
144+
// why don't we escape non-printable characters?
145+
static void append_escaped(const string& in, string* out)
146+
{
147+
for (string::const_iterator i = in.begin(); i != in.end(); ++i) {
148+
if (*i == '%') {
149+
out->push_back('%');
150+
out->push_back('p');
151+
} else if (*i == '.') {
152+
out->push_back('%');
153+
out->push_back('e');
154+
} else if (*i == '_') {
155+
out->push_back('%');
156+
out->push_back('u');
157+
} else {
158+
out->push_back(*i);
159+
}
160+
}
161+
}
162+
163+
// original Ceph code as it was in version Squid
164+
string hobject_t::to_str() const
165+
{
166+
string out;
167+
168+
char snap_with_hash[1000];
169+
char* t = snap_with_hash;
170+
const char* end = t + sizeof(snap_with_hash);
171+
172+
uint64_t poolid(pool);
173+
t += snprintf(t, end - t, "%.*llX", 16, (long long unsigned)poolid);
174+
175+
uint32_t revhash(get_nibblewise_key_u32());
176+
t += snprintf(t, end - t, ".%.*X", 8, revhash);
177+
178+
if (snap == CEPH_NOSNAP)
179+
t += snprintf(t, end - t, ".head");
180+
else if (snap == CEPH_SNAPDIR)
181+
t += snprintf(t, end - t, ".snapdir");
182+
else
183+
t += snprintf(t, end - t, ".%llx", (long long unsigned)snap);
184+
185+
out.append(snap_with_hash, t);
186+
187+
out.push_back('.');
188+
append_escaped(oid.name, &out);
189+
out.push_back('.');
190+
append_escaped(get_key(), &out);
191+
out.push_back('.');
192+
append_escaped(nspace, &out);
193+
194+
return out;
195+
}
196+
197+
198+
namespace fmt {
199+
// original Ceph code as it was in version Squid
200+
// (modified to use test_hobject_fmt_t)
201+
template <>
202+
struct formatter<test_hobject_fmt_t> {
203+
204+
constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); }
205+
206+
template <typename FormatContext>
207+
auto format(const test_hobject_fmt_t& ho, FormatContext& ctx)
208+
{
209+
if (ho == hobject_t{}) {
210+
return fmt::format_to(ctx.out(), "MIN");
211+
}
212+
213+
if (ho.is_max()) {
214+
return fmt::format_to(ctx.out(), "MAX");
215+
}
216+
217+
std::string v;
218+
append_out_escaped(ho.nspace, &v);
219+
v.push_back(':');
220+
append_out_escaped(ho.get_key(), &v);
221+
v.push_back(':');
222+
append_out_escaped(ho.oid.name, &v);
223+
224+
return fmt::format_to(
225+
ctx.out(), "{}:{:08x}:{}:{}", static_cast<uint64_t>(ho.pool),
226+
ho.get_bitwise_key_u32(), v, ho.snap);
227+
}
228+
};
229+
} // namespace fmt
230+
231+
232+
TEST(HObject, to_str)
233+
{
234+
const auto dbg = false; // turns on debug output
235+
known_examples[0].obj = hobject_t::get_max();
236+
237+
for (const auto& [obj, expected_to_str, expected_fmt] : known_examples) {
238+
if (obj.is_max()) {
239+
// no 'max' for to_str()
240+
continue;
241+
}
242+
test_hobject_fmt_t legacy_obj{obj};
243+
if (dbg) {
244+
std::cout << "to_str(): legacy: " << legacy_obj.to_str()
245+
<< " . Now: " << obj.to_str() << std::endl;
246+
}
247+
EXPECT_EQ(legacy_obj.to_str(), obj.to_str());
248+
EXPECT_EQ(expected_to_str, obj.to_str());
249+
}
250+
}
251+
252+
// test the fmt::formatter for hobject_t vs legacy & the stream operator
253+
TEST(HObject, fmt)
254+
{
255+
const auto dbg = false; // turns on debug output
256+
known_examples[0].obj = hobject_t::get_max();
257+
258+
for (const auto& [obj, expected_to_str, expected_fmt] : known_examples) {
259+
260+
test_hobject_fmt_t legacy_obj{obj};
261+
if (dbg) {
262+
std::cout << fmt::format("fmt: legacy: {} now: {}", legacy_obj, obj)
263+
<< std::endl;
264+
}
265+
EXPECT_EQ(fmt::format("{}", legacy_obj), fmt::format("{}", obj));
266+
EXPECT_EQ(expected_fmt, fmt::format("{}", obj));
267+
268+
if (dbg) {
269+
std::cout << "ostream: legacy: " << legacy_obj << " . Now: " << obj
270+
<< std::endl;
271+
}
272+
std::ostringstream oss;
273+
oss << obj;
274+
std::ostringstream oss_legacy;
275+
oss_legacy << legacy_obj;
276+
EXPECT_EQ(oss_legacy.str(), oss.str());
277+
EXPECT_EQ(oss.str(), fmt::format("{}", obj));
278+
}
279+
}
280+
281+
TEST(HObject, fmt_random)
282+
{
283+
const auto dbg = false; // turns on debug output
284+
for (uint32_t i = 0; i < 10; i++) {
285+
286+
auto name_length = (i * 17) % 51;
287+
std::string name;
288+
for (int j = 0; j < name_length; j++) {
289+
name.push_back((i * name_length + j) % 256);
290+
}
291+
292+
std::string key =
293+
(i % 3) ? fmt::format("key_{}::", static_cast<unsigned char>(i)) : name;
294+
295+
snapid_t snap = (i % 7) ? i : ((i % 2) ? CEPH_SNAPDIR : CEPH_NOSNAP);
296+
297+
hobject_t obj{object_t{name}, key, snap, i, i % 10, "n:_%.space"s};
298+
299+
test_hobject_fmt_t legacy_obj{obj};
300+
301+
if (dbg) {
302+
std::cout << fmt::format("fmt: legacy: {} now: {}", legacy_obj, obj)
303+
<< std::endl;
304+
}
305+
EXPECT_EQ(fmt::format("{}", legacy_obj), fmt::format("{}", obj));
306+
307+
if (dbg) {
308+
std::cout << "ostream: legacy: " << legacy_obj << " . Now: " << obj
309+
<< std::endl;
310+
}
311+
std::ostringstream oss;
312+
oss << obj;
313+
std::ostringstream oss_legacy;
314+
oss_legacy << legacy_obj;
315+
EXPECT_EQ(oss_legacy.str(), oss.str());
316+
EXPECT_EQ(oss.str(), fmt::format("{}", obj));
317+
}
318+
}

0 commit comments

Comments
 (0)