Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
Sebastian Schrader
Kea
Commits
3d405228
Commit
3d405228
authored
Jun 27, 2016
by
Thomas Markwalder
Browse files
[4277] Cleanup and commentary
parent
3466f650
Changes
7
Expand all
Hide whitespace changes
Inline
Side-by-side
src/lib/dhcpsrv/pgsql_connection.cc
View file @
3d405228
...
...
@@ -41,8 +41,7 @@ PgSqlTransaction::PgSqlTransaction(PgSqlConnection& conn)
}
PgSqlTransaction
::~
PgSqlTransaction
()
{
// Rollback if the PgSqlTransaction::commit wasn't explicitly
// called.
// If commit() wasn't explicitly called, rollback.
if
(
!
committed_
)
{
conn_
.
rollback
();
}
...
...
@@ -218,7 +217,7 @@ PgSqlConnection::startTransaction() {
PgSqlResult
r
(
PQexec
(
conn_
,
"START TRANSACTION"
));
if
(
PQresultStatus
(
r
)
!=
PGRES_COMMAND_OK
)
{
const
char
*
error_message
=
PQerrorMessage
(
conn_
);
isc_throw
(
DbOperationError
,
"unable to start transaction"
isc_throw
(
DbOperationError
,
"unable to start transaction"
<<
error_message
);
}
}
...
...
src/lib/dhcpsrv/pgsql_connection.h
View file @
3d405228
...
...
@@ -82,10 +82,17 @@ class PgSqlResult : public boost::noncopyable {
public:
/// @brief Constructor
///
/// Store the pointer to the result set to being fetched.
/// Store the pointer to the result set to being fetched. Set row
/// and column counts for convenience.
///
PgSqlResult
(
PGresult
*
result
)
:
result_
(
result
)
{}
PgSqlResult
(
PGresult
*
result
)
:
result_
(
result
),
rows_
(
0
),
cols_
(
0
)
{
if
(
!
result
)
{
isc_throw
(
BadValue
,
"PgSqlResult result pointer cannot be null"
);
}
rows_
=
PQntuples
(
result
);
cols_
=
PQnfields
(
result
);
}
/// @brief Destructor
///
...
...
@@ -96,6 +103,50 @@ public:
}
}
/// @brief Returns the number of rows in the result set.
int
getRows
()
const
{
return
(
rows_
);
}
/// @brief Returns the number of columns in the result set.
int
getCols
()
const
{
return
(
cols_
);
}
/// @brief Determines if a row index is valid
///
/// @param row index to range check
///
/// @throw throws DbOperationError if the row index is out of range
void
rowCheck
(
int
row
)
const
{
if
(
row
>=
rows_
)
{
isc_throw
(
DbOperationError
,
"row: "
<<
row
<<
", out of range: 0.."
<<
rows_
);
}
}
/// @brief Determines if a column index is valid
///
/// @param col index to range check
///
/// @throw throws DbOperationError if the column index is out of range
void
colCheck
(
int
col
)
const
{
if
(
col
>=
cols_
)
{
isc_throw
(
DbOperationError
,
"col: "
<<
col
<<
", out of range: 0.."
<<
cols_
);
}
}
/// @brief Determines if both a row and column index are valid
///
/// @param row index to range check
/// @param col index to range check
///
/// @throw throws DbOperationError if either the row or column index
/// is out of range
void
rowColCheck
(
int
row
,
int
col
)
const
{
rowCheck
(
row
);
colCheck
(
col
);
}
/// @brief Conversion Operator
///
/// Allows the PgSqlResult object to be passed as the result set argument to
...
...
@@ -113,6 +164,8 @@ public:
private:
PGresult
*
result_
;
///< Result set to be freed
int
rows_
;
///< Number of rows in the result set
int
cols_
;
///< Number of columns in the result set
};
...
...
@@ -181,45 +234,50 @@ private:
/// @brief Forward declaration to @ref PgSqlConnection.
class
PgSqlConnection
;
/// @brief RAII object representing PostgreSQL transaction.
/// @brief RAII object representing
a
PostgreSQL transaction.
///
/// An instance of this class should be created in a scope where multiple
/// INSERT statements should be executed within a single transaction. The
/// transaction is started when the constructor of this class is invoked.
/// The transaction is ended when the @ref PgSqlTransaction::commit is
/// explicitly called or when the instance of this class is destroyed.
/// The @ref PgSqlTransaction::commit commits changes to the database
/// and the changes remain in the database when the instance of the
/// class is destroyed. If the class instance is destroyed before the
/// @ref PgSqlTransaction::commit is called, the transaction is rolled
/// back. The rollback on destruction guarantees that partial data is
/// not stored in the database when there is an error during any
/// of the operations belonging to a transaction.
/// The @ref PgSqlTransaction::commit commits changes to the database.
/// If the class instance is destroyed before @ref PgSqlTransaction::commit
/// has been called, the transaction is rolled back. The rollback on
/// destruction guarantees that partial data is not stored in the database
/// when an error occurs during any of the operations within a transaction.
///
/// The default PostgreSQL backend configuration enables 'autocommit'.
/// Starting a transaction overrides 'autocommit' setting for this
/// particular transaction only. It does not affect the global 'autocommit'
/// setting for the database connection, i.e. all modifications to the
/// database which don't use transactions will still be auto committed.
/// By default PostgreSQL performs a commit following each statement which
/// alters the database (i.e. "autocommit"). Starting a transaction
/// stops autocommit for the connection until the transaction is ended by
/// either commit or rollback. Other connections are unaffected.
class
PgSqlTransaction
:
public
boost
::
noncopyable
{
public:
/// @brief Constructor.
///
/// Starts transaction by
making a
"START TRANSACTION"
query.
/// Starts transaction by
executing the SQL statement:
"START TRANSACTION"
///
/// @param conn PostgreSQL connection to use for the transaction. This
/// connection will be later used to commit or rollback changes.
///
/// @throw DbOperationError if
"START TRANSACTION" query
fails
.
/// @throw DbOperationError if
statement execution
fails
PgSqlTransaction
(
PgSqlConnection
&
conn
);
/// @brief Destructor.
///
/// Rolls back the transaction if changes haven't been committed.
/// If the transaction has not been committed, it is rolled back
/// by executing the SQL statement: "ROLLBACK"
///
/// @throw DbOperationError if statement execution fails
~
PgSqlTransaction
();
/// @brief Commits transaction.
///
/// Commits all changes made during the transaction by executing the
/// SQL statement: "COMMIT">
///
/// @throw DbOperationError if statement execution fails
void
commit
();
private:
...
...
src/lib/dhcpsrv/pgsql_exchange.cc
View file @
3d405228
...
...
@@ -51,16 +51,16 @@ void PsqlBindArray::add(const bool& value) {
void
PsqlBindArray
::
add
(
const
uint8_t
&
byte
)
{
// We static_cast to an unsigned int, otherwise lexcial_cast may to
// treat byte as a character, which yields "" for unprintable values
bind
String
(
boost
::
lexical_cast
<
std
::
string
>
addTemp
String
(
boost
::
lexical_cast
<
std
::
string
>
(
static_cast
<
unsigned
int
>
(
byte
)));
}
void
PsqlBindArray
::
add
(
const
isc
::
asiolink
::
IOAddress
&
addr
)
{
if
(
addr
.
isV4
())
{
bind
String
(
boost
::
lexical_cast
<
std
::
string
>
addTemp
String
(
boost
::
lexical_cast
<
std
::
string
>
(
static_cast
<
uint32_t
>
(
addr
)));
}
else
{
bind
String
(
addr
.
toText
());
addTemp
String
(
addr
.
toText
());
}
}
...
...
@@ -70,9 +70,9 @@ void PsqlBindArray::addNull(const int format) {
formats_
.
push_back
(
format
);
}
//
e
ventually this
sh
ould replace add(std::string
)
void
PsqlBindArray
::
bind
String
(
const
std
::
string
&
str
)
{
bound_strs_
.
push_back
(
StringPtr
(
new
std
::
string
(
str
)));
//
E
ventually this
c
ould replace add(std::string
&) ?
void
PsqlBindArray
::
addTemp
String
(
const
std
::
string
&
str
)
{
bound_strs_
.
push_back
(
Const
StringPtr
(
new
std
::
string
(
str
)));
PsqlBindArray
::
add
((
bound_strs_
.
back
())
->
c_str
());
}
...
...
@@ -94,6 +94,7 @@ std::string PsqlBindArray::toText() const {
<<
static_cast
<
unsigned
int
>
(
data
[
x
]);
}
stream
<<
std
::
endl
;
stream
<<
std
::
setbase
(
10
);
}
}
}
...
...
@@ -146,6 +147,7 @@ PgSqlExchange::convertFromDatabaseTime(const std::string& db_time_val) {
const
char
*
PgSqlExchange
::
getRawColumnValue
(
const
PgSqlResult
&
r
,
const
int
row
,
const
size_t
col
)
{
r
.
rowColCheck
(
row
,
col
);
const
char
*
value
=
PQgetvalue
(
r
,
row
,
col
);
if
(
!
value
)
{
isc_throw
(
DbOperationError
,
"getRawColumnValue no data for :"
...
...
@@ -157,6 +159,7 @@ PgSqlExchange::getRawColumnValue(const PgSqlResult& r, const int row,
bool
PgSqlExchange
::
isColumnNull
(
const
PgSqlResult
&
r
,
const
int
row
,
const
size_t
col
)
{
r
.
rowColCheck
(
row
,
col
);
return
(
PQgetisnull
(
r
,
row
,
col
));
}
...
...
@@ -241,21 +244,9 @@ PgSqlExchange::convertFromBytea(const PgSqlResult& r, const int row,
PQfreemem
(
bytes
);
}
#if 0
std::string
PgSqlExchange::getColumnLabel(const size_t column) const {
if (column > columns_.size()) {
std::ostringstream os;
os << "Unknown column:" << column;
return (os.str());
}
return (columns_[column]);
}
#endif
std
::
string
PgSqlExchange
::
getColumnLabel
(
const
PgSqlResult
&
r
,
const
size_t
column
)
{
r
.
colCheck
(
column
);
const
char
*
label
=
PQfname
(
r
,
column
);
if
(
!
label
)
{
std
::
ostringstream
os
;
...
...
@@ -268,8 +259,9 @@ PgSqlExchange::getColumnLabel(const PgSqlResult& r, const size_t column) {
std
::
string
PgSqlExchange
::
dumpRow
(
const
PgSqlResult
&
r
,
int
row
)
{
r
.
rowCheck
(
row
);
std
::
ostringstream
stream
;
int
columns
=
PQnfield
s
(
r
);
int
columns
=
r
.
getCol
s
();
for
(
int
col
=
0
;
col
<
columns
;
++
col
)
{
const
char
*
val
=
getRawColumnValue
(
r
,
row
,
col
);
std
::
string
name
=
getColumnLabel
(
r
,
col
);
...
...
src/lib/dhcpsrv/pgsql_exchange.h
View file @
3d405228
...
...
@@ -30,10 +30,16 @@ namespace dhcp {
/// be valid for the duration of the PostgreSQL statement execution. In other
/// words populating them with pointers to values that go out of scope before
/// statement is executed is a bad idea.
/// @brief smart pointer to strings used by PsqlBindArray to ensure scope
/// of strings supplying exchange values
typedef
boost
::
shared_ptr
<
std
::
string
>
StringPtr
;
///
/// Other than vectors or buffers of binary data, all other values are currently
/// converted to their string representation prior to sending them to PostgreSQL.
/// All of the add() method variants which accept a non-string value internally
/// create the conversion string which is then retained in the bind array to ensure
/// scope.
///
/// @brief smart pointer to const std::strings used by PsqlBindArray to ensure scope
/// of strings supplying exchange values
typedef
boost
::
shared_ptr
<
const
std
::
string
>
ConstStringPtr
;
struct
PsqlBindArray
{
/// @brief Vector of pointers to the data values.
...
...
@@ -71,8 +77,9 @@ struct PsqlBindArray {
/// @brief Adds a char array to bind array based
///
/// Adds a TEXT_FMT value to the end of the bind array, using the given
/// char* as the data source. Note that value is expected to be NULL
/// terminated.
/// char* as the data source. The value is expected to be NULL
/// terminated. The caller is responsible for ensuring that value
/// remains in scope until the bind array has been discarded.
///
/// @param value char array containing the null-terminated text to add.
void
add
(
const
char
*
value
);
...
...
@@ -80,7 +87,9 @@ struct PsqlBindArray {
/// @brief Adds an string value to the bind array
///
/// Adds a TEXT formatted value to the end of the bind array using the
/// given string as the data source.
/// given string as the data source. The caller is responsible for
/// ensuring that string parameter remains in scope until the bind
/// array has been discarded.
///
/// @param value std::string containing the value to add.
void
add
(
const
std
::
string
&
value
);
...
...
@@ -103,54 +112,59 @@ struct PsqlBindArray {
/// is destroyed.
///
/// @param data buffer of binary data.
/// @param len number of bytes of data in buffer
/// @param len number of bytes of data in buffer
void
add
(
const
uint8_t
*
data
,
const
size_t
len
);
/// @brief Adds a boolean value to the bind array.
///
/// Converts the given boolean value to its corresponding to PostgreSQL
/// string value and adds it as a TEXT_FMT value to the bind array.
/// This creates an internally scoped string.
///
/// @param value bool value to add.
/// @param value
the
bool
ean
value to add.
void
add
(
const
bool
&
value
);
/// @brief Adds a uint8_t value to the bind array.
///
/// Converts the given uint8_t value to its corresponding numeric string
/// literal and adds it as a TEXT_FMT value to the bind array.
/// This creates an internally scoped string.
///
/// @param
value bool
value to add.
/// @param
byte the one byte
value to add.
void
add
(
const
uint8_t
&
byte
);
/// @brief Adds a the given IOAddress value to the bind array.
///
/// Converts the IOAddress, based on its protocol family, to the
/// corresponding string literal and adds it as a TEXT_FMT value to
/// Converts the IOAddress, based on its protocol family, to the
/// corresponding string literal and adds it as a TEXT_FMT value to
/// the bind array.
/// This creates an internally scoped string.
///
/// @param
value bool
value to add.
/// @param
addr IP address
value to add.
void
add
(
const
isc
::
asiolink
::
IOAddress
&
addr
);
/// @brief Adds a the given value to the bind array.
///
/// Converts the given value its corresponding string literal
/// Converts the given value
to
its corresponding string literal
/// boost::lexical_cast and adds it as a TEXT_FMT value to the bind array.
/// This is intended primarily for numeric types.
/// This creates an internally scoped string.
///
/// @param value
bool
value to add.
/// @param value
data
value to add.
template
<
typename
T
>
void
add
(
const
T
&
numeric
)
{
bind
String
(
boost
::
lexical_cast
<
std
::
string
>
(
numeric
));
void
add
(
const
T
&
value
)
{
addTemp
String
(
boost
::
lexical_cast
<
std
::
string
>
(
value
));
}
/// @brief Binds a the given string to the bind array.
///
/// Prior to added the The given string the vector of exchange values,
/// it duplicated as a StringPtr and saved internally. This garauntees
/// it duplicated as a
Const
StringPtr and saved internally. This garauntees
/// the string remains in scope until the PsqlBindArray is destroyed,
/// without the caller maintaining the string values.
/// without the caller maintaining the string values.
///
/// @param
value bool
value to add.
void
bind
String
(
const
std
::
string
&
str
);
/// @param
str string
value to add.
void
addTemp
String
(
const
std
::
string
&
str
);
/// @brief Adds a NULL value to the bind array
///
...
...
@@ -159,7 +173,7 @@ struct PsqlBindArray {
void
addNull
(
const
int
format
=
PsqlBindArray
::
TEXT_FMT
);
//std::vector<const std::string> getBoundStrs() {
std
::
vector
<
StringPtr
>
getBoundStrs
()
{
std
::
vector
<
Const
StringPtr
>
getBoundStrs
()
{
return
(
bound_strs_
);
}
...
...
@@ -169,7 +183,7 @@ struct PsqlBindArray {
private:
/// @brief vector of strings which supplied the values
std
::
vector
<
StringPtr
>
bound_strs_
;
std
::
vector
<
Const
StringPtr
>
bound_strs_
;
};
...
...
@@ -260,7 +274,7 @@ public:
///
/// @throw DbOperationError if the value cannot be fetched or is
/// invalid.
static
void
getColumnValue
(
const
PgSqlResult
&
r
,
const
int
row
,
static
void
getColumnValue
(
const
PgSqlResult
&
r
,
const
int
row
,
const
size_t
col
,
std
::
string
&
value
);
/// @brief Fetches boolean text ('t' or 'f') as a bool.
...
...
@@ -272,7 +286,7 @@ public:
///
/// @throw DbOperationError if the value cannot be fetched or is
/// invalid.
static
void
getColumnValue
(
const
PgSqlResult
&
r
,
const
int
row
,
static
void
getColumnValue
(
const
PgSqlResult
&
r
,
const
int
row
,
const
size_t
col
,
bool
&
value
);
/// @brief Fetches an integer text column as a uint8_t.
...
...
@@ -284,20 +298,34 @@ public:
///
/// @throw DbOperationError if the value cannot be fetched or is
/// invalid.
static
void
getColumnValue
(
const
PgSqlResult
&
r
,
const
int
row
,
static
void
getColumnValue
(
const
PgSqlResult
&
r
,
const
int
row
,
const
size_t
col
,
uint8_t
&
value
);
/// @brief Converts a column in a row in a result set into IPv6 address.
///
/// @param r the result set containing the query results
/// @param row the row number within the result set
/// @param col the column number within the row
///
/// @return isc::asiolink::IOAddress containing the IPv6 address.
/// @throw DbOperationError if the value cannot be fetched or is
/// invalid.
static
isc
::
asiolink
::
IOAddress
getIPv6Value
(
const
PgSqlResult
&
r
,
const
int
row
,
const
size_t
col
);
static
bool
isColumnNull
(
const
PgSqlResult
&
r
,
const
int
row
,
/// @brief Returns true if a column within a row is null
///
/// @param r the result set containing the query results
/// @param row the row number within the result set
/// @param col the column number within the row
static
bool
isColumnNull
(
const
PgSqlResult
&
r
,
const
int
row
,
const
size_t
col
);
/// @brief Fetches a text column as the given value type
///
/// Uses boost::lexicalcast to convert the text column value into
/// a value of type T.
/// a value of type T.
///
/// @param r the result set containing the query results
/// @param row the row number within the result set
...
...
@@ -307,14 +335,14 @@ public:
/// @throw DbOperationError if the value cannot be fetched or is
/// invalid.
template
<
typename
T
>
static
void
getColumnValue
(
const
PgSqlResult
&
r
,
const
int
row
,
static
void
getColumnValue
(
const
PgSqlResult
&
r
,
const
int
row
,
const
size_t
col
,
T
&
value
)
{
const
char
*
data
=
getRawColumnValue
(
r
,
row
,
col
);
try
{
value
=
boost
::
lexical_cast
<
T
>
(
data
);
}
catch
(
const
std
::
exception
&
ex
)
{
isc_throw
(
DbOperationError
,
"Invalid data:["
<<
data
<<
"] for row: "
<<
row
<<
" col: "
<<
col
<<
","
<<
"] for row: "
<<
row
<<
" col: "
<<
col
<<
","
<<
getColumnLabel
(
r
,
col
)
<<
" : "
<<
ex
.
what
());
}
}
...
...
@@ -335,12 +363,15 @@ public:
///
/// @throw DbOperationError if the value cannot be fetched or is
/// invalid.
static
void
convertFromBytea
(
const
PgSqlResult
&
r
,
const
int
row
,
const
size_t
col
,
uint8_t
*
buffer
,
const
size_t
buffer_size
,
static
void
convertFromBytea
(
const
PgSqlResult
&
r
,
const
int
row
,
const
size_t
col
,
uint8_t
*
buffer
,
const
size_t
buffer_size
,
size_t
&
bytes_converted
);
/// @brief Diagnostic tool which dumps the Result row contents as a string
///
/// @param r the result set containing the query results
/// @param row the row number within the result set
static
std
::
string
dumpRow
(
const
PgSqlResult
&
r
,
int
row
);
protected:
...
...
src/lib/dhcpsrv/pgsql_host_data_source.cc
View file @
3d405228
This diff is collapsed.
Click to expand it.
src/lib/dhcpsrv/pgsql_host_data_source.h
View file @
3d405228
...
...
@@ -50,8 +50,6 @@ public:
PgSqlHostDataSource
(
const
DatabaseConnection
::
ParameterMap
&
parameters
);
/// @brief Virtual destructor.
///
/// Releases prepared MySQL statements used by the backend.
virtual
~
PgSqlHostDataSource
();
/// @brief Return all hosts for the specified HW address or DUID.
...
...
@@ -216,7 +214,7 @@ public:
///
/// @return Type of the backend.
virtual
std
::
string
getType
()
const
{
return
(
std
::
string
(
"
my
sql"
));
return
(
std
::
string
(
"
postgre
sql"
));
}
/// @brief Returns backend name.
...
...
src/lib/dhcpsrv/tests/pgsql_exchange_unittest.cc
View file @
3d405228
...
...
@@ -69,45 +69,71 @@ TEST(PgSqlExchangeTest, convertTimeTest) {
EXPECT_EQ
(
ref_time
,
from_time
);
}
TEST
(
PsqlBindArray
,
basicOperation
)
{
/// @brief Verifies the ability to add various data types to
/// the bind array.
TEST
(
PsqlBindArray
,
addDataTest
)
{
PsqlBindArray
b
;
uint8_t
small_int
=
25
;
b
.
add
(
small_int
);
// Declare a vector to add. Vectors are not currently duplicated
// So they will go out of scope, unless caller ensures it.
std
::
vector
<
uint8_t
>
bytes
;
for
(
int
i
=
0
;
i
<
10
;
i
++
)
{
bytes
.
push_back
(
i
+
1
);
}
int
reg_int
=
376
;
b
.
add
(
reg_int
);
// Declare a string
std
::
string
not_temp_str
(
"just a string"
);
uint64_t
big_int
=
86749032
;
b
.
add
(
big_int
);
// Now add all the items within a different scope. Everything should
// still be valid once we exit this scope.
{
// Add a const char*
b
.
add
(
"booya!"
);
b
.
add
((
bool
)(
1
));
b
.
add
(
(
bool
)(
0
)
);
// Add the non temporary string
b
.
add
(
not_temp_str
);
b
.
add
(
isc
::
asiolink
::
IOAddress
(
"192.2.15.34"
));
b
.
add
(
isc
::
asiolink
::
IOAddress
(
"3001::1"
)
);
// Add a temporary string
b
.
add
TempString
(
"walah walah washington"
);
std
::
string
str
(
"just a string"
);
b
.
add
(
str
);
// Add a one byte int
uint8_t
small_int
=
25
;
b
.
add
(
small_int
);
std
::
vector
<
uint8_t
>
bytes
;
for
(
int
i
=
0
;
i
<
10
;
i
++
)
{
bytes
.
push_back
(
i
+
1
);
// Add a four byte int
int
reg_int
=
376
;
b
.
add
(
reg_int
);
// Add a eight byte unsigned int
uint64_t
big_int
=
48786749032
;
b
.
add
(
big_int
);
// Add boolean true and false
b
.
add
((
bool
)(
1
));
b
.
add
((
bool
)(
0
));
// Add IP addresses
b
.
add
(
isc
::
asiolink
::
IOAddress
(
"192.2.15.34"
));
b
.
add
(
isc
::
asiolink
::
IOAddress
(
"3001::1"
));
// Add the vector
b
.
add
(
bytes
);
}
b
.
add
(
bytes
);
std
::
string
expected
=
"0 :
\"
25
\"\n
"
"1 :
\"
376
\"\n
"
"2 :
\"
86749032
\"\n
"
"3 :
\"
TRUE
\"\n
"
"4 :
\"
FALSE
\"\n
"
"5 :
\"
3221360418
\"\n
"
"6 :
\"
3001::1
\"\n
"
"7 :
\"
just a string
\"\n
"
"8 : 0x0102030405060708090a
\n
"
;
// We've left bind scope, everything should be intact.
std
::
string
expected
=
"0 :
\"
booya!
\"\n
"
"1 :
\"
just a string
\"\n
"
"2 :
\"
walah walah washington
\"\n
"
"3 :
\"
25
\"\n
"
"4 :
\"
376
\"\n
"
"5 :
\"
48786749032
\"\n
"
"6 :
\"
TRUE
\"\n
"
"7 :
\"
FALSE
\"\n
"
"8 :
\"
3221360418
\"\n
"
"9 :
\"
3001::1
\"\n
"
"10 : 0x0102030405060708090a
\n
"
;
EXPECT_EQ
(
expected
,
b
.
toText
());
}
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment