|
|
|
#include "absl/strings/internal/str_format/parser.h"
|
|
|
|
|
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
#include "gmock/gmock.h"
|
|
|
|
#include "gtest/gtest.h"
|
|
|
|
#include "absl/base/macros.h"
|
|
|
|
|
|
|
|
namespace absl {
|
|
|
|
ABSL_NAMESPACE_BEGIN
|
|
|
|
namespace str_format_internal {
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
using testing::Pair;
|
|
|
|
|
|
|
|
TEST(LengthModTest, Names) {
|
|
|
|
struct Expectation {
|
|
|
|
int line;
|
|
|
|
LengthMod mod;
|
|
|
|
const char *name;
|
|
|
|
};
|
|
|
|
const Expectation kExpect[] = {
|
|
|
|
{__LINE__, LengthMod::none, "" },
|
|
|
|
{__LINE__, LengthMod::h, "h" },
|
|
|
|
{__LINE__, LengthMod::hh, "hh"},
|
|
|
|
{__LINE__, LengthMod::l, "l" },
|
|
|
|
{__LINE__, LengthMod::ll, "ll"},
|
|
|
|
{__LINE__, LengthMod::L, "L" },
|
|
|
|
{__LINE__, LengthMod::j, "j" },
|
|
|
|
{__LINE__, LengthMod::z, "z" },
|
|
|
|
{__LINE__, LengthMod::t, "t" },
|
|
|
|
{__LINE__, LengthMod::q, "q" },
|
|
|
|
};
|
|
|
|
EXPECT_EQ(ABSL_ARRAYSIZE(kExpect), 10);
|
|
|
|
for (auto e : kExpect) {
|
|
|
|
SCOPED_TRACE(e.line);
|
|
|
|
EXPECT_EQ(e.name, LengthModToString(e.mod));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST(ConversionCharTest, Names) {
|
|
|
|
struct Expectation {
|
|
|
|
FormatConversionChar id;
|
|
|
|
char name;
|
|
|
|
};
|
|
|
|
// clang-format off
|
|
|
|
const Expectation kExpect[] = {
|
|
|
|
#define X(c) {FormatConversionCharInternal::c, #c[0]}
|
|
|
|
X(c), X(s), // text
|
|
|
|
X(d), X(i), X(o), X(u), X(x), X(X), // int
|
|
|
|
X(f), X(F), X(e), X(E), X(g), X(G), X(a), X(A), // float
|
|
|
|
X(n), X(p), // misc
|
|
|
|
#undef X
|
|
|
|
{FormatConversionCharInternal::kNone, '\0'},
|
|
|
|
};
|
|
|
|
// clang-format on
|
|
|
|
for (auto e : kExpect) {
|
|
|
|
SCOPED_TRACE(e.name);
|
|
|
|
FormatConversionChar v = e.id;
|
|
|
|
EXPECT_EQ(e.name, FormatConversionCharToChar(v));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class ConsumeUnboundConversionTest : public ::testing::Test {
|
|
|
|
public:
|
|
|
|
std::pair<string_view, string_view> Consume(string_view src) {
|
|
|
|
int next = 0;
|
|
|
|
o = UnboundConversion(); // refresh
|
|
|
|
const char* p = ConsumeUnboundConversion(
|
|
|
|
src.data(), src.data() + src.size(), &o, &next);
|
|
|
|
if (!p) return {{}, src};
|
|
|
|
return {string_view(src.data(), p - src.data()),
|
|
|
|
string_view(p, src.data() + src.size() - p)};
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Run(const char *fmt, bool force_positional = false) {
|
|
|
|
int next = force_positional ? -1 : 0;
|
|
|
|
o = UnboundConversion(); // refresh
|
|
|
|
return ConsumeUnboundConversion(fmt, fmt + strlen(fmt), &o, &next) ==
|
|
|
|
fmt + strlen(fmt);
|
|
|
|
}
|
|
|
|
UnboundConversion o;
|
|
|
|
};
|
|
|
|
|
|
|
|
TEST_F(ConsumeUnboundConversionTest, ConsumeSpecification) {
|
|
|
|
struct Expectation {
|
|
|
|
int line;
|
|
|
|
string_view src;
|
|
|
|
string_view out;
|
|
|
|
string_view src_post;
|
|
|
|
};
|
|
|
|
const Expectation kExpect[] = {
|
|
|
|
{__LINE__, "", "", "" },
|
|
|
|
{__LINE__, "b", "", "b" }, // 'b' is invalid
|
|
|
|
{__LINE__, "ba", "", "ba"}, // 'b' is invalid
|
|
|
|
{__LINE__, "l", "", "l" }, // just length mod isn't okay
|
|
|
|
{__LINE__, "d", "d", "" }, // basic
|
|
|
|
{__LINE__, "d ", "d", " " }, // leave suffix
|
|
|
|
{__LINE__, "dd", "d", "d" }, // don't be greedy
|
|
|
|
{__LINE__, "d9", "d", "9" }, // leave non-space suffix
|
|
|
|
{__LINE__, "dzz", "d", "zz"}, // length mod as suffix
|
|
|
|
{__LINE__, "1$*2$d", "1$*2$d", "" }, // arg indexing and * allowed.
|
|
|
|
{__LINE__, "0-14.3hhd", "0-14.3hhd", ""}, // precision, width
|
|
|
|
{__LINE__, " 0-+#14.3hhd", " 0-+#14.3hhd", ""}, // flags
|
|
|
|
};
|
|
|
|
for (const auto& e : kExpect) {
|
|
|
|
SCOPED_TRACE(e.line);
|
|
|
|
EXPECT_THAT(Consume(e.src), Pair(e.out, e.src_post));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(ConsumeUnboundConversionTest, BasicConversion) {
|
|
|
|
EXPECT_FALSE(Run(""));
|
|
|
|
EXPECT_FALSE(Run("z"));
|
|
|
|
|
|
|
|
EXPECT_FALSE(Run("dd")); // no excess allowed
|
|
|
|
|
|
|
|
EXPECT_TRUE(Run("d"));
|
|
|
|
EXPECT_EQ('d', FormatConversionCharToChar(o.conv));
|
|
|
|
EXPECT_FALSE(o.width.is_from_arg());
|
|
|
|
EXPECT_LT(o.width.value(), 0);
|
|
|
|
EXPECT_FALSE(o.precision.is_from_arg());
|
|
|
|
EXPECT_LT(o.precision.value(), 0);
|
|
|
|
EXPECT_EQ(1, o.arg_position);
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(ConsumeUnboundConversionTest, ArgPosition) {
|
|
|
|
EXPECT_TRUE(Run("d"));
|
|
|
|
EXPECT_EQ(1, o.arg_position);
|
|
|
|
EXPECT_TRUE(Run("3$d"));
|
|
|
|
EXPECT_EQ(3, o.arg_position);
|
|
|
|
EXPECT_TRUE(Run("1$d"));
|
|
|
|
EXPECT_EQ(1, o.arg_position);
|
|
|
|
EXPECT_TRUE(Run("1$d", true));
|
|
|
|
EXPECT_EQ(1, o.arg_position);
|
|
|
|
EXPECT_TRUE(Run("123$d"));
|
|
|
|
EXPECT_EQ(123, o.arg_position);
|
|
|
|
EXPECT_TRUE(Run("123$d", true));
|
|
|
|
EXPECT_EQ(123, o.arg_position);
|
|
|
|
EXPECT_TRUE(Run("10$d"));
|
|
|
|
EXPECT_EQ(10, o.arg_position);
|
|
|
|
EXPECT_TRUE(Run("10$d", true));
|
|
|
|
EXPECT_EQ(10, o.arg_position);
|
|
|
|
|
|
|
|
// Position can't be zero.
|
|
|
|
EXPECT_FALSE(Run("0$d"));
|
|
|
|
EXPECT_FALSE(Run("0$d", true));
|
|
|
|
EXPECT_FALSE(Run("1$*0$d"));
|
|
|
|
EXPECT_FALSE(Run("1$.*0$d"));
|
|
|
|
|
|
|
|
// Position can't start with a zero digit at all. That is not a 'decimal'.
|
|
|
|
EXPECT_FALSE(Run("01$p"));
|
|
|
|
EXPECT_FALSE(Run("01$p", true));
|
|
|
|
EXPECT_FALSE(Run("1$*01$p"));
|
|
|
|
EXPECT_FALSE(Run("1$.*01$p"));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(ConsumeUnboundConversionTest, WidthAndPrecision) {
|
|
|
|
EXPECT_TRUE(Run("14d"));
|
|
|
|
EXPECT_EQ('d', FormatConversionCharToChar(o.conv));
|
|
|
|
EXPECT_FALSE(o.width.is_from_arg());
|
|
|
|
EXPECT_EQ(14, o.width.value());
|
|
|
|
EXPECT_FALSE(o.precision.is_from_arg());
|
|
|
|
EXPECT_LT(o.precision.value(), 0);
|
|
|
|
|
|
|
|
EXPECT_TRUE(Run("14.d"));
|
|
|
|
EXPECT_FALSE(o.width.is_from_arg());
|
|
|
|
EXPECT_FALSE(o.precision.is_from_arg());
|
|
|
|
EXPECT_EQ(14, o.width.value());
|
|
|
|
EXPECT_EQ(0, o.precision.value());
|
|
|
|
|
|
|
|
EXPECT_TRUE(Run(".d"));
|
|
|
|
EXPECT_FALSE(o.width.is_from_arg());
|
|
|
|
EXPECT_LT(o.width.value(), 0);
|
|
|
|
EXPECT_FALSE(o.precision.is_from_arg());
|
|
|
|
EXPECT_EQ(0, o.precision.value());
|
|
|
|
|
|
|
|
EXPECT_TRUE(Run(".5d"));
|
|
|
|
EXPECT_FALSE(o.width.is_from_arg());
|
|
|
|
EXPECT_LT(o.width.value(), 0);
|
|
|
|
EXPECT_FALSE(o.precision.is_from_arg());
|
|
|
|
EXPECT_EQ(5, o.precision.value());
|
|
|
|
|
|
|
|
EXPECT_TRUE(Run(".0d"));
|
|
|
|
EXPECT_FALSE(o.width.is_from_arg());
|
|
|
|
EXPECT_LT(o.width.value(), 0);
|
|
|
|
EXPECT_FALSE(o.precision.is_from_arg());
|
|
|
|
EXPECT_EQ(0, o.precision.value());
|
|
|
|
|
|
|
|
EXPECT_TRUE(Run("14.5d"));
|
|
|
|
EXPECT_FALSE(o.width.is_from_arg());
|
|
|
|
EXPECT_FALSE(o.precision.is_from_arg());
|
|
|
|
EXPECT_EQ(14, o.width.value());
|
|
|
|
EXPECT_EQ(5, o.precision.value());
|
|
|
|
|
|
|
|
EXPECT_TRUE(Run("*.*d"));
|
|
|
|
EXPECT_TRUE(o.width.is_from_arg());
|
|
|
|
EXPECT_EQ(1, o.width.get_from_arg());
|
|
|
|
EXPECT_TRUE(o.precision.is_from_arg());
|
|
|
|
EXPECT_EQ(2, o.precision.get_from_arg());
|
|
|
|
EXPECT_EQ(3, o.arg_position);
|
|
|
|
|
|
|
|
EXPECT_TRUE(Run("*d"));
|
|
|
|
EXPECT_TRUE(o.width.is_from_arg());
|
|
|
|
EXPECT_EQ(1, o.width.get_from_arg());
|
|
|
|
EXPECT_FALSE(o.precision.is_from_arg());
|
|
|
|
EXPECT_LT(o.precision.value(), 0);
|
|
|
|
EXPECT_EQ(2, o.arg_position);
|
|
|
|
|
|
|
|
EXPECT_TRUE(Run(".*d"));
|
|
|
|
EXPECT_FALSE(o.width.is_from_arg());
|
|
|
|
EXPECT_LT(o.width.value(), 0);
|
|
|
|
EXPECT_TRUE(o.precision.is_from_arg());
|
|
|
|
EXPECT_EQ(1, o.precision.get_from_arg());
|
|
|
|
EXPECT_EQ(2, o.arg_position);
|
|
|
|
|
|
|
|
// mixed implicit and explicit: didn't specify arg position.
|
|
|
|
EXPECT_FALSE(Run("*23$.*34$d"));
|
|
|
|
|
|
|
|
EXPECT_TRUE(Run("12$*23$.*34$d"));
|
|
|
|
EXPECT_EQ(12, o.arg_position);
|
|
|
|
EXPECT_TRUE(o.width.is_from_arg());
|
|
|
|
EXPECT_EQ(23, o.width.get_from_arg());
|
|
|
|
EXPECT_TRUE(o.precision.is_from_arg());
|
|
|
|
EXPECT_EQ(34, o.precision.get_from_arg());
|
|
|
|
|
|
|
|
EXPECT_TRUE(Run("2$*5$.*9$d"));
|
|
|
|
EXPECT_EQ(2, o.arg_position);
|
|
|
|
EXPECT_TRUE(o.width.is_from_arg());
|
|
|
|
EXPECT_EQ(5, o.width.get_from_arg());
|
|
|
|
EXPECT_TRUE(o.precision.is_from_arg());
|
|
|
|
EXPECT_EQ(9, o.precision.get_from_arg());
|
|
|
|
|
|
|
|
EXPECT_FALSE(Run(".*0$d")) << "no arg 0";
|
|
|
|
|
|
|
|
// Large values
|
|
|
|
EXPECT_TRUE(Run("999999999.999999999d"));
|
|
|
|
EXPECT_FALSE(o.width.is_from_arg());
|
|
|
|
EXPECT_EQ(999999999, o.width.value());
|
|
|
|
EXPECT_FALSE(o.precision.is_from_arg());
|
|
|
|
EXPECT_EQ(999999999, o.precision.value());
|
|
|
|
|
|
|
|
EXPECT_FALSE(Run("1000000000.999999999d"));
|
|
|
|
EXPECT_FALSE(Run("999999999.1000000000d"));
|
|
|
|
EXPECT_FALSE(Run("9999999999d"));
|
|
|
|
EXPECT_FALSE(Run(".9999999999d"));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(ConsumeUnboundConversionTest, Flags) {
|
|
|
|
static const char kAllFlags[] = "-+ #0";
|
|
|
|
static const int kNumFlags = ABSL_ARRAYSIZE(kAllFlags) - 1;
|
|
|
|
for (int rev = 0; rev < 2; ++rev) {
|
|
|
|
for (int i = 0; i < 1 << kNumFlags; ++i) {
|
|
|
|
std::string fmt;
|
|
|
|
for (int k = 0; k < kNumFlags; ++k)
|
|
|
|
if ((i >> k) & 1) fmt += kAllFlags[k];
|
|
|
|
// flag order shouldn't matter
|
|
|
|
if (rev == 1) { std::reverse(fmt.begin(), fmt.end()); }
|
|
|
|
fmt += 'd';
|
|
|
|
SCOPED_TRACE(fmt);
|
|
|
|
EXPECT_TRUE(Run(fmt.c_str()));
|
|
|
|
EXPECT_EQ(fmt.find('-') == std::string::npos, !o.flags.left);
|
|
|
|
EXPECT_EQ(fmt.find('+') == std::string::npos, !o.flags.show_pos);
|
|
|
|
EXPECT_EQ(fmt.find(' ') == std::string::npos, !o.flags.sign_col);
|
|
|
|
EXPECT_EQ(fmt.find('#') == std::string::npos, !o.flags.alt);
|
|
|
|
EXPECT_EQ(fmt.find('0') == std::string::npos, !o.flags.zero);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(ConsumeUnboundConversionTest, BasicFlag) {
|
|
|
|
// Flag is on
|
|
|
|
for (const char* fmt : {"d", "llx", "G", "1$X"}) {
|
|
|
|
SCOPED_TRACE(fmt);
|
|
|
|
EXPECT_TRUE(Run(fmt));
|
|
|
|
EXPECT_TRUE(o.flags.basic);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Flag is off
|
|
|
|
for (const char* fmt : {"3d", ".llx", "-G", "1$#X"}) {
|
|
|
|
SCOPED_TRACE(fmt);
|
|
|
|
EXPECT_TRUE(Run(fmt));
|
|
|
|
EXPECT_FALSE(o.flags.basic);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(ConsumeUnboundConversionTest, LengthMod) {
|
|
|
|
EXPECT_TRUE(Run("d"));
|
|
|
|
EXPECT_EQ(LengthMod::none, o.length_mod);
|
|
|
|
EXPECT_TRUE(Run("hd"));
|
|
|
|
EXPECT_EQ(LengthMod::h, o.length_mod);
|
|
|
|
EXPECT_TRUE(Run("hhd"));
|
|
|
|
EXPECT_EQ(LengthMod::hh, o.length_mod);
|
|
|
|
EXPECT_TRUE(Run("ld"));
|
|
|
|
EXPECT_EQ(LengthMod::l, o.length_mod);
|
|
|
|
EXPECT_TRUE(Run("lld"));
|
|
|
|
EXPECT_EQ(LengthMod::ll, o.length_mod);
|
|
|
|
EXPECT_TRUE(Run("Lf"));
|
|
|
|
EXPECT_EQ(LengthMod::L, o.length_mod);
|
|
|
|
EXPECT_TRUE(Run("qf"));
|
|
|
|
EXPECT_EQ(LengthMod::q, o.length_mod);
|
|
|
|
EXPECT_TRUE(Run("jd"));
|
|
|
|
EXPECT_EQ(LengthMod::j, o.length_mod);
|
|
|
|
EXPECT_TRUE(Run("zd"));
|
|
|
|
EXPECT_EQ(LengthMod::z, o.length_mod);
|
|
|
|
EXPECT_TRUE(Run("td"));
|
|
|
|
EXPECT_EQ(LengthMod::t, o.length_mod);
|
|
|
|
}
|
|
|
|
|
|
|
|
struct SummarizeConsumer {
|
|
|
|
std::string* out;
|
|
|
|
explicit SummarizeConsumer(std::string* out) : out(out) {}
|
|
|
|
|
|
|
|
bool Append(string_view s) {
|
|
|
|
*out += "[" + std::string(s) + "]";
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ConvertOne(const UnboundConversion& conv, string_view s) {
|
|
|
|
*out += "{";
|
|
|
|
*out += std::string(s);
|
|
|
|
*out += ":";
|
|
|
|
*out += std::to_string(conv.arg_position) + "$";
|
|
|
|
if (conv.width.is_from_arg()) {
|
|
|
|
*out += std::to_string(conv.width.get_from_arg()) + "$*";
|
|
|
|
}
|
|
|
|
if (conv.precision.is_from_arg()) {
|
|
|
|
*out += "." + std::to_string(conv.precision.get_from_arg()) + "$*";
|
|
|
|
}
|
|
|
|
*out += FormatConversionCharToChar(conv.conv);
|
|
|
|
*out += "}";
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
std::string SummarizeParsedFormat(const ParsedFormatBase& pc) {
|
|
|
|
std::string out;
|
|
|
|
if (!pc.ProcessFormat(SummarizeConsumer(&out))) out += "!";
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
|
|
|
|
class ParsedFormatTest : public testing::Test {};
|
|
|
|
|
|
|
|
TEST_F(ParsedFormatTest, ValueSemantics) {
|
|
|
|
ParsedFormatBase p1({}, true, {}); // empty format
|
|
|
|
EXPECT_EQ("", SummarizeParsedFormat(p1));
|
|
|
|
|
|
|
|
ParsedFormatBase p2 = p1; // copy construct (empty)
|
|
|
|
EXPECT_EQ(SummarizeParsedFormat(p1), SummarizeParsedFormat(p2));
|
|
|
|
|
|
|
|
p1 = ParsedFormatBase("hello%s", true,
|
|
|
|
{FormatConversionCharSetInternal::s}); // move assign
|
|
|
|
EXPECT_EQ("[hello]{s:1$s}", SummarizeParsedFormat(p1));
|
|
|
|
|
|
|
|
ParsedFormatBase p3 = p1; // copy construct (nonempty)
|
|
|
|
EXPECT_EQ(SummarizeParsedFormat(p1), SummarizeParsedFormat(p3));
|
|
|
|
|
|
|
|
using std::swap;
|
|
|
|
swap(p1, p2);
|
|
|
|
EXPECT_EQ("", SummarizeParsedFormat(p1));
|
|
|
|
EXPECT_EQ("[hello]{s:1$s}", SummarizeParsedFormat(p2));
|
|
|
|
swap(p1, p2); // undo
|
|
|
|
|
|
|
|
p2 = p1; // copy assign
|
|
|
|
EXPECT_EQ(SummarizeParsedFormat(p1), SummarizeParsedFormat(p2));
|
|
|
|
}
|
|
|
|
|
|
|
|
struct ExpectParse {
|
|
|
|
const char* in;
|
|
|
|
std::initializer_list<FormatConversionCharSet> conv_set;
|
|
|
|
const char* out;
|
|
|
|
};
|
|
|
|
|
|
|
|
TEST_F(ParsedFormatTest, Parsing) {
|
|
|
|
// Parse should be equivalent to that obtained by ConversionParseIterator.
|
|
|
|
// No need to retest the parsing edge cases here.
|
|
|
|
const ExpectParse kExpect[] = {
|
|
|
|
{"", {}, ""},
|
|
|
|
{"ab", {}, "[ab]"},
|
|
|
|
{"a%d", {FormatConversionCharSetInternal::d}, "[a]{d:1$d}"},
|
|
|
|
{"a%+d", {FormatConversionCharSetInternal::d}, "[a]{+d:1$d}"},
|
|
|
|
{"a% d", {FormatConversionCharSetInternal::d}, "[a]{ d:1$d}"},
|
|
|
|
{"a%b %d", {}, "[a]!"}, // stop after error
|
|
|
|
};
|
|
|
|
for (const auto& e : kExpect) {
|
|
|
|
SCOPED_TRACE(e.in);
|
|
|
|
EXPECT_EQ(e.out,
|
|
|
|
SummarizeParsedFormat(ParsedFormatBase(e.in, false, e.conv_set)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(ParsedFormatTest, ParsingFlagOrder) {
|
|
|
|
const ExpectParse kExpect[] = {
|
|
|
|
{"a%+ 0d", {FormatConversionCharSetInternal::d}, "[a]{+ 0d:1$d}"},
|
|
|
|
{"a%+0 d", {FormatConversionCharSetInternal::d}, "[a]{+0 d:1$d}"},
|
|
|
|
{"a%0+ d", {FormatConversionCharSetInternal::d}, "[a]{0+ d:1$d}"},
|
|
|
|
{"a% +0d", {FormatConversionCharSetInternal::d}, "[a]{ +0d:1$d}"},
|
|
|
|
{"a%0 +d", {FormatConversionCharSetInternal::d}, "[a]{0 +d:1$d}"},
|
|
|
|
{"a% 0+d", {FormatConversionCharSetInternal::d}, "[a]{ 0+d:1$d}"},
|
|
|
|
{"a%+ 0+d", {FormatConversionCharSetInternal::d}, "[a]{+ 0+d:1$d}"},
|
|
|
|
};
|
|
|
|
for (const auto& e : kExpect) {
|
|
|
|
SCOPED_TRACE(e.in);
|
|
|
|
EXPECT_EQ(e.out,
|
|
|
|
SummarizeParsedFormat(ParsedFormatBase(e.in, false, e.conv_set)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
} // namespace str_format_internal
|
|
|
|
ABSL_NAMESPACE_END
|
|
|
|
} // namespace absl
|