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
f4982861
Commit
f4982861
authored
Jun 19, 2015
by
Thomas Markwalder
Browse files
[master] Merge branch 'trac3797'
Adds Control Channel support to DHCPv6
parents
ec0d87d2
79df1b2a
Changes
4
Hide whitespace changes
Inline
Side-by-side
src/bin/dhcp6/ctrl_dhcp6_srv.cc
View file @
f4982861
// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC")
// Copyright (C) 2014
-2015
Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
...
...
@@ -14,14 +14,18 @@
#include
<config.h>
#include
<cc/data.h>
#include
<config/command_mgr.h>
#include
<dhcpsrv/cfgmgr.h>
#include
<dhcp6/ctrl_dhcp6_srv.h>
#include
<dhcp6/dhcp6_log.h>
#include
<hooks/hooks_manager.h>
#include
<dhcp6/json_config_parser.h>
#include
<hooks/hooks_manager.h>
#include
<stats/stats_mgr.h>
using
namespace
isc
::
config
;
using
namespace
isc
::
data
;
using
namespace
isc
::
hooks
;
using
namespace
isc
::
stats
;
using
namespace
std
;
namespace
isc
{
...
...
@@ -158,6 +162,32 @@ ControlledDhcpv6Srv::ControlledDhcpv6Srv(uint16_t port)
"There is another Dhcpv6Srv instance already."
);
}
server_
=
this
;
// remember this instance for use in callback
// Register supported commands in CommandMgr
CommandMgr
::
instance
().
registerCommand
(
"shutdown"
,
boost
::
bind
(
&
ControlledDhcpv6Srv
::
commandShutdownHandler
,
this
,
_1
,
_2
));
/// @todo: register config-reload (see CtrlDhcpv4Srv::commandConfigReloadHandler)
/// @todo: register libreload (see CtrlDhcpv4Srv::commandLibReloadHandler)
// Register statistic related commands
CommandMgr
::
instance
().
registerCommand
(
"statistic-get"
,
boost
::
bind
(
&
StatsMgr
::
statisticGetHandler
,
_1
,
_2
));
CommandMgr
::
instance
().
registerCommand
(
"statistic-reset"
,
boost
::
bind
(
&
StatsMgr
::
statisticResetHandler
,
_1
,
_2
));
CommandMgr
::
instance
().
registerCommand
(
"statistic-remove"
,
boost
::
bind
(
&
StatsMgr
::
statisticRemoveHandler
,
_1
,
_2
));
CommandMgr
::
instance
().
registerCommand
(
"statistic-get-all"
,
boost
::
bind
(
&
StatsMgr
::
statisticGetAllHandler
,
_1
,
_2
));
CommandMgr
::
instance
().
registerCommand
(
"statistic-reset-all"
,
boost
::
bind
(
&
StatsMgr
::
statisticResetAllHandler
,
_1
,
_2
));
CommandMgr
::
instance
().
registerCommand
(
"statistic-remove-all"
,
boost
::
bind
(
&
StatsMgr
::
statisticRemoveAllHandler
,
_1
,
_2
));
}
void
ControlledDhcpv6Srv
::
shutdown
()
{
...
...
@@ -168,6 +198,18 @@ void ControlledDhcpv6Srv::shutdown() {
ControlledDhcpv6Srv
::~
ControlledDhcpv6Srv
()
{
cleanup
();
// Close the command socket (if it exists).
CommandMgr
::
instance
().
closeCommandSocket
();
// Deregister any registered commands
CommandMgr
::
instance
().
deregisterCommand
(
"shutdown"
);
CommandMgr
::
instance
().
deregisterCommand
(
"statistic-get"
);
CommandMgr
::
instance
().
deregisterCommand
(
"statistic-reset"
);
CommandMgr
::
instance
().
deregisterCommand
(
"statistic-remove"
);
CommandMgr
::
instance
().
deregisterCommand
(
"statistic-get-all"
);
CommandMgr
::
instance
().
deregisterCommand
(
"statistic-reset-all"
);
CommandMgr
::
instance
().
deregisterCommand
(
"statistic-remove-all"
);
server_
=
NULL
;
// forget this instance. There should be no callback anymore
// at this stage anyway.
}
...
...
src/bin/dhcp6/json_config_parser.cc
View file @
f4982861
...
...
@@ -17,6 +17,7 @@
#include
<asiolink/io_address.h>
#include
<cc/data.h>
#include
<cc/command_interpreter.h>
#include
<config/command_mgr.h>
#include
<dhcp/libdhcp++.h>
#include
<dhcp6/json_config_parser.h>
#include
<dhcp6/dhcp6_log.h>
...
...
@@ -693,6 +694,8 @@ namespace dhcp {
globalContext
());
}
else
if
(
config_id
.
compare
(
"relay-supplied-options"
)
==
0
)
{
parser
=
new
RSOOListConfigParser
(
config_id
);
}
else
if
(
config_id
.
compare
(
"control-socket"
)
==
0
)
{
parser
=
new
ControlSocketParser
(
config_id
);
}
else
{
isc_throw
(
DhcpConfigError
,
"unsupported global configuration parameter: "
...
...
@@ -815,6 +818,26 @@ configureDhcp6Server(Dhcpv6Srv&, isc::data::ConstElementPtr config_set) {
subnet_parser
->
build
(
subnet_config
->
second
);
}
// Get command socket configuration from the config file.
// This code expects the following structure:
// {
// "socket-type": "unix",
// "socket-name": "/tmp/kea6.sock"
// }
ConstElementPtr
sock_cfg
=
CfgMgr
::
instance
().
getStagingCfg
()
->
getControlSocketInfo
();
// Close existing socket (if any).
isc
::
config
::
CommandMgr
::
instance
().
closeCommandSocket
();
if
(
sock_cfg
)
{
// This will create a control socket and will install external socket
// in IfaceMgr. That socket will be monitored when Dhcp4Srv::receivePacket()
// calls IfaceMgr::receive4() and callback in CommandMgr will be called,
// if necessary. If there were previously open command socket, it will
// be closed.
isc
::
config
::
CommandMgr
::
instance
().
openCommandSocket
(
sock_cfg
);
}
// The lease database parser is the last to be run.
std
::
map
<
std
::
string
,
ConstElementPtr
>::
const_iterator
leases_config
=
values_map
.
find
(
"lease-database"
);
...
...
src/bin/dhcp6/tests/Makefile.am
View file @
f4982861
...
...
@@ -21,6 +21,8 @@ AM_CPPFLAGS += -I$(top_builddir)/src/bin # for generated spec_config.h header
AM_CPPFLAGS
+=
-I
$(top_srcdir)
/src/bin
AM_CPPFLAGS
+=
-DTOP_BUILDDIR
=
"
\"
$(top_builddir)
\"
"
AM_CPPFLAGS
+=
$(BOOST_INCLUDES)
AM_CPPFLAGS
+=
-DTEST_DATA_DIR
=
\"
$(abs_top_srcdir)
/src/lib/testutils/testdata
\"
AM_CPPFLAGS
+=
-DTEST_DATA_BUILDDIR
=
\"
$(abs_top_builddir)
/src/bin/dhcp6/tests
\"
AM_CPPFLAGS
+=
-DINSTALL_PROG
=
\"
$(abs_top_srcdir)
/install-sh
\"
CLEANFILES
=
$(builddir)
/logger_lockfile
...
...
src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc
View file @
f4982861
...
...
@@ -15,6 +15,7 @@
#include
<config.h>
#include
<cc/command_interpreter.h>
#include
<config/command_mgr.h>
#include
<dhcpsrv/cfgmgr.h>
#include
<dhcp6/ctrl_dhcp6_srv.h>
#include
<hooks/hooks_manager.h>
...
...
@@ -25,7 +26,11 @@
#include
<boost/scoped_ptr.hpp>
#include
<gtest/gtest.h>
#include
<sys/select.h>
#include
<sys/ioctl.h>
using
namespace
std
;
using
namespace
isc
::
config
;
using
namespace
isc
::
data
;
using
namespace
isc
::
dhcp
;
using
namespace
isc
::
dhcp
::
test
;
...
...
@@ -33,10 +38,155 @@ using namespace isc::hooks;
namespace
{
/// Class that acts as a UnixCommandSocket client
/// It can connect to an open UnixCommandSocket and exchange ControlChannel
/// commands and responses.
class
UnixControlClient
{
public:
UnixControlClient
()
{
socket_fd_
=
-
1
;
}
~
UnixControlClient
()
{
disconnectFromServer
();
}
/// @brief Closes the Control Channel socket
void
disconnectFromServer
()
{
if
(
socket_fd_
>=
0
)
{
static_cast
<
void
>
(
close
(
socket_fd_
));
socket_fd_
=
-
1
;
}
}
/// @brief Connects to a Unix socket at the given path
/// @param socket_path pathname of the socket to open
/// @return true if the connect was successful, false otherwise
bool
connectToServer
(
const
std
::
string
&
socket_path
)
{
// Create UNIX socket
socket_fd_
=
socket
(
AF_UNIX
,
SOCK_STREAM
,
0
);
if
(
socket_fd_
<
0
)
{
const
char
*
errmsg
=
strerror
(
errno
);
ADD_FAILURE
()
<<
"Failed to open unix stream socket: "
<<
errmsg
;
return
(
false
);
}
// Prepare socket address
struct
sockaddr_un
srv_addr
;
memset
(
&
srv_addr
,
0
,
sizeof
(
srv_addr
));
srv_addr
.
sun_family
=
AF_UNIX
;
strncpy
(
srv_addr
.
sun_path
,
socket_path
.
c_str
(),
sizeof
(
srv_addr
.
sun_path
));
socklen_t
len
=
sizeof
(
srv_addr
);
// Connect to the specified UNIX socket
int
status
=
connect
(
socket_fd_
,
(
struct
sockaddr
*
)
&
srv_addr
,
len
);
if
(
status
==
-
1
)
{
const
char
*
errmsg
=
strerror
(
errno
);
ADD_FAILURE
()
<<
"Failed to connect unix socket: fd="
<<
socket_fd_
<<
", path="
<<
socket_path
<<
" : "
<<
errmsg
;
disconnectFromServer
();
return
(
false
);
}
return
(
true
);
}
/// @brief Sends the given command across the open Control Channel
/// @param command the command text to execute in JSON form
/// @return true if the send succeeds, false otherwise
bool
sendCommand
(
const
std
::
string
&
command
)
{
// Send command
int
bytes_sent
=
send
(
socket_fd_
,
command
.
c_str
(),
command
.
length
(),
0
);
if
(
bytes_sent
<
command
.
length
())
{
const
char
*
errmsg
=
strerror
(
errno
);
ADD_FAILURE
()
<<
"Failed to send "
<<
command
.
length
()
<<
" bytes, send() returned "
<<
bytes_sent
<<
" : "
<<
errmsg
;
return
(
false
);
}
return
(
true
);
}
/// @brief Reads the response text from the open Control Channel
/// @param response variable into which the received response should be
/// placed.
/// @return true if data was successfully read from the socket,
/// false otherwise
bool
getResponse
(
std
::
string
&
response
)
{
// Receive response
// @todo implement select check to see if data is waiting
char
buf
[
65536
];
memset
(
buf
,
0
,
sizeof
(
buf
));
switch
(
selectCheck
())
{
case
-
1
:
{
const
char
*
errmsg
=
strerror
(
errno
);
ADD_FAILURE
()
<<
"getResponse - select failed: "
<<
errmsg
;
return
(
false
);
}
case
0
:
ADD_FAILURE
()
<<
"No response data sent"
;
return
(
false
);
default:
break
;
}
int
bytes_rcvd
=
recv
(
socket_fd_
,
buf
,
sizeof
(
buf
),
0
);
if
(
bytes_rcvd
<
0
)
{
const
char
*
errmsg
=
strerror
(
errno
);
ADD_FAILURE
()
<<
"Failed to receive a response. recv() returned "
<<
bytes_rcvd
<<
" : "
<<
errmsg
;
return
(
false
);
}
if
(
bytes_rcvd
>=
sizeof
(
buf
))
{
ADD_FAILURE
()
<<
"Response size too large: "
<<
bytes_rcvd
;
return
(
false
);
}
// Convert the response to a string
response
=
string
(
buf
,
bytes_rcvd
);
return
(
true
);
}
/// @brief Uses select to poll the Control Channel for data waiting
/// @return -1 on error, 0 if no data is available, 1 if data is ready
int
selectCheck
()
{
int
maxfd
=
0
;
fd_set
read_fds
;
FD_ZERO
(
&
read_fds
);
// Add this socket to listening set
FD_SET
(
socket_fd_
,
&
read_fds
);
maxfd
=
socket_fd_
;
struct
timeval
select_timeout
;
select_timeout
.
tv_sec
=
0
;
select_timeout
.
tv_usec
=
0
;
return
(
select
(
maxfd
+
1
,
&
read_fds
,
NULL
,
NULL
,
&
select_timeout
));
}
/// @brief Retains the fd of the open socket
int
socket_fd_
;
};
class
NakedControlledDhcpv6Srv
:
public
ControlledDhcpv6Srv
{
// "Naked" DHCPv6 server, exposes internal fields
public:
NakedControlledDhcpv6Srv
()
:
ControlledDhcpv6Srv
(
DHCP6_SERVER_PORT
+
10000
)
{
}
NakedControlledDhcpv6Srv
()
:
ControlledDhcpv6Srv
(
DHCP6_SERVER_PORT
+
10000
)
{
}
/// @brief Exposes server's receivePacket method
virtual
Pkt6Ptr
receivePacket
(
int
timeout
)
{
return
(
Dhcpv6Srv
::
receivePacket
(
timeout
));
}
};
class
CtrlDhcpv6SrvTest
:
public
::
testing
::
Test
{
...
...
@@ -45,24 +195,130 @@ public:
reset
();
}
~
CtrlDhcpv6SrvTest
()
{
virtual
~
CtrlDhcpv6SrvTest
()
{
reset
();
};
/// @brief Reset hooks data
///
/// Resets the data for the hooks-related portion of the test by ensuring
/// that no libraries are loaded and that any marker files are deleted.
void
reset
()
{
virtual
void
reset
()
{
// Unload any previously-loaded libraries.
HooksManager
::
unloadLibraries
();
// Get rid of any marker files.
static_cast
<
void
>
(
unlink
(
LOAD_MARKER_FILE
));
static_cast
<
void
>
(
unlink
(
UNLOAD_MARKER_FILE
));
IfaceMgr
::
instance
().
deleteAllExternalSockets
();
CfgMgr
::
instance
().
clear
();
}
};
class
CtrlChannelDhcpv6SrvTest
:
public
CtrlDhcpv6SrvTest
{
public:
std
::
string
socket_path_
;
boost
::
shared_ptr
<
NakedControlledDhcpv6Srv
>
server_
;
CtrlChannelDhcpv6SrvTest
()
{
socket_path_
=
string
(
TEST_DATA_BUILDDIR
)
+
"/kea6.sock"
;
reset
();
}
~
CtrlChannelDhcpv6SrvTest
()
{
server_
.
reset
();
reset
();
};
void
createUnixChannelServer
()
{
::
remove
(
socket_path_
.
c_str
());
// Just a simple config. The important part here is the socket
// location information.
std
::
string
header
=
"{"
"
\"
interfaces-config
\"
: {"
"
\"
interfaces
\"
: [
\"
*
\"
]"
" },"
"
\"
rebind-timer
\"
: 2000, "
"
\"
renew-timer
\"
: 1000, "
"
\"
subnet6
\"
: [ ],"
"
\"
valid-lifetime
\"
: 4000,"
"
\"
control-socket
\"
: {"
"
\"
socket-type
\"
:
\"
unix
\"
,"
"
\"
socket-name
\"
:
\"
"
;
std
::
string
footer
=
"
\"
},"
"
\"
lease-database
\"
: {"
"
\"
type
\"
:
\"
memfile
\"
,
\"
persist
\"
: false }"
"}"
;
// Fill in the socket-name value with socket_path_ to
// make the actual configuration text.
std
::
string
config_txt
=
header
+
socket_path_
+
footer
;
ASSERT_NO_THROW
(
server_
.
reset
(
new
NakedControlledDhcpv6Srv
()));
ConstElementPtr
config
=
Element
::
fromJSON
(
config_txt
);
ConstElementPtr
answer
=
server_
->
processConfig
(
config
);
ASSERT_TRUE
(
answer
);
int
status
=
0
;
isc
::
config
::
parseAnswer
(
status
,
answer
);
ASSERT_EQ
(
0
,
status
);
// Now check that the socket was indeed open.
ASSERT_GT
(
isc
::
config
::
CommandMgr
::
instance
().
getControlSocketFD
(),
-
1
);
}
/// @brief Reset
void
reset
()
{
CtrlDhcpv6SrvTest
::
reset
();
::
remove
(
socket_path_
.
c_str
());
}
/// @brief Conducts a command/response exchange via UnixCommandSocket
///
/// This method connects to the given server over the given socket path.
/// If successful, it then sends the given command and retrieves the
/// server's response. Note that it calls the server's receivePacket()
/// method where needed to cause the server to process IO events on
/// control channel the control channel sockets.
///
/// @param command the command text to execute in JSON form
/// @param response variable into which the received response should be
/// placed.
void
sendUnixCommand
(
const
std
::
string
&
command
,
std
::
string
&
response
)
{
response
=
""
;
boost
::
scoped_ptr
<
UnixControlClient
>
client
;
client
.
reset
(
new
UnixControlClient
());
ASSERT_TRUE
(
client
);
// Connect and then call server's receivePacket() so it can
// detect the control socket connect and call the accept handler
ASSERT_TRUE
(
client
->
connectToServer
(
socket_path_
));
ASSERT_NO_THROW
(
server_
->
receivePacket
(
0
));
// Send the command and then call server's receivePacket() so it can
// detect the inbound data and call the read handler
ASSERT_TRUE
(
client
->
sendCommand
(
command
));
ASSERT_NO_THROW
(
server_
->
receivePacket
(
0
));
// Read the response generated by the server. Note that getResponse
// only fails if there an IO error or no response data was present.
// It is not based on the response content.
ASSERT_TRUE
(
client
->
getResponse
(
response
));
// Now disconnect and process the close event
client
->
disconnectFromServer
();
ASSERT_NO_THROW
(
server_
->
receivePacket
(
0
));
}
};
TEST_F
(
CtrlDhcpv6SrvTest
,
commands
)
{
boost
::
scoped_ptr
<
ControlledDhcpv6Srv
>
srv
;
...
...
@@ -202,4 +458,121 @@ TEST_F(CtrlDhcpv6SrvTest, configReload) {
CfgMgr
::
instance
().
clear
();
}
typedef
std
::
map
<
std
::
string
,
isc
::
data
::
ConstElementPtr
>
ElementMap
;
// This test checks which commands are registered by the DHCPv4 server.
TEST_F
(
CtrlDhcpv6SrvTest
,
commandsRegistration
)
{
ConstElementPtr
list_cmds
=
createCommand
(
"list-commands"
);
ConstElementPtr
answer
;
// By default the list should be empty (except the standard list-commands
// supported by the CommandMgr itself)
EXPECT_NO_THROW
(
answer
=
CommandMgr
::
instance
().
processCommand
(
list_cmds
));
ASSERT_TRUE
(
answer
);
ASSERT_TRUE
(
answer
->
get
(
"arguments"
));
EXPECT_EQ
(
"[
\"
list-commands
\"
]"
,
answer
->
get
(
"arguments"
)
->
str
());
// Created server should register several additional commands.
boost
::
scoped_ptr
<
ControlledDhcpv6Srv
>
srv
;
ASSERT_NO_THROW
(
srv
.
reset
(
new
ControlledDhcpv6Srv
(
0
));
);
EXPECT_NO_THROW
(
answer
=
CommandMgr
::
instance
().
processCommand
(
list_cmds
));
ASSERT_TRUE
(
answer
);
ASSERT_TRUE
(
answer
->
get
(
"arguments"
));
std
::
string
command_list
=
answer
->
get
(
"arguments"
)
->
str
();
EXPECT_TRUE
(
command_list
.
find
(
"
\"
list-commands
\"
"
)
!=
string
::
npos
);
EXPECT_TRUE
(
command_list
.
find
(
"
\"
statistic-get
\"
"
)
!=
string
::
npos
);
EXPECT_TRUE
(
command_list
.
find
(
"
\"
statistic-get-all
\"
"
)
!=
string
::
npos
);
EXPECT_TRUE
(
command_list
.
find
(
"
\"
statistic-remove
\"
"
)
!=
string
::
npos
);
EXPECT_TRUE
(
command_list
.
find
(
"
\"
statistic-remove-all
\"
"
)
!=
string
::
npos
);
EXPECT_TRUE
(
command_list
.
find
(
"
\"
statistic-reset
\"
"
)
!=
string
::
npos
);
EXPECT_TRUE
(
command_list
.
find
(
"
\"
statistic-reset-all
\"
"
)
!=
string
::
npos
);
// Ok, and now delete the server. It should deregister its commands.
srv
.
reset
();
// The list should be (almost) empty again.
EXPECT_NO_THROW
(
answer
=
CommandMgr
::
instance
().
processCommand
(
list_cmds
));
ASSERT_TRUE
(
answer
);
ASSERT_TRUE
(
answer
->
get
(
"arguments"
));
EXPECT_EQ
(
"[
\"
list-commands
\"
]"
,
answer
->
get
(
"arguments"
)
->
str
());
}
// Tests that the server properly responds to invalid commands sent
// via ControlChannel
TEST_F
(
CtrlChannelDhcpv6SrvTest
,
controlChannelNegative
)
{
createUnixChannelServer
();
std
::
string
response
;
sendUnixCommand
(
"{
\"
command
\"
:
\"
bogus
\"
}"
,
response
);
EXPECT_EQ
(
"{
\"
result
\"
: 1,"
"
\"
text
\"
:
\"
'bogus' command not supported.
\"
}"
,
response
);
sendUnixCommand
(
"utter nonsense"
,
response
);
EXPECT_EQ
(
"{
\"
result
\"
: 1, "
"
\"
text
\"
:
\"
error: unexpected character u in <string>:1:2
\"
}"
,
response
);
}
// Tests that the server properly responds to shtudown command sent
// via ControlChannel
TEST_F
(
CtrlChannelDhcpv6SrvTest
,
controlChannelShutdown
)
{
createUnixChannelServer
();
std
::
string
response
;
sendUnixCommand
(
"{
\"
command
\"
:
\"
shutdown
\"
}"
,
response
);
EXPECT_EQ
(
"{
\"
result
\"
: 0,
\"
text
\"
:
\"
Shutting down.
\"
}"
,
response
);
}
// Tests that the server properly responds to statistics commands. Note this
// is really only intended to verify that the appropriate Statistics handler
// is called based on the command. It is not intended to be an exhaustive
// test of Dhcpv6 statistics.
TEST_F
(
CtrlChannelDhcpv6SrvTest
,
controlChannelStats
)
{
createUnixChannelServer
();
std
::
string
response
;
// Check statistic-get
sendUnixCommand
(
"{
\"
command
\"
:
\"
statistic-get
\"
, "
"
\"
arguments
\"
: {"
"
\"
name
\"
:
\"
bogus
\"
}}"
,
response
);
EXPECT_EQ
(
"{
\"
arguments
\"
: { },
\"
result
\"
: 0 }"
,
response
);
// Check statistic-get-all
sendUnixCommand
(
"{
\"
command
\"
:
\"
statistic-get-all
\"
, "
"
\"
arguments
\"
: {}}"
,
response
);
EXPECT_EQ
(
"{
\"
arguments
\"
: { },
\"
result
\"
: 0 }"
,
response
);
// Check statistic-reset
sendUnixCommand
(
"{
\"
command
\"
:
\"
statistic-reset
\"
, "
"
\"
arguments
\"
: {"
"
\"
name
\"
:
\"
bogus
\"
}}"
,
response
);
EXPECT_EQ
(
"{
\"
result
\"
: 1,
\"
text
\"
:
\"
No 'bogus' statistic found
\"
}"
,
response
);
// Check statistic-reset-all
sendUnixCommand
(
"{
\"
command
\"
:
\"
statistic-reset-all
\"
, "
"
\"
arguments
\"
: {}}"
,
response
);
EXPECT_EQ
(
"{
\"
result
\"
: 0,
\"
text
\"
: "
"
\"
All statistics reset to neutral values.
\"
}"
,
response
);
// Check statistic-remove
sendUnixCommand
(
"{
\"
command
\"
:
\"
statistic-remove
\"
, "
"
\"
arguments
\"
: {"
"
\"
name
\"
:
\"
bogus
\"
}}"
,
response
);
EXPECT_EQ
(
"{
\"
result
\"
: 1,
\"
text
\"
:
\"
No 'bogus' statistic found
\"
}"
,
response
);
// Check statistic-remove-all
sendUnixCommand
(
"{
\"
command
\"
:
\"
statistic-remove-all
\"
, "
"
\"
arguments
\"
: {}}"
,
response
);
EXPECT_EQ
(
"{
\"
result
\"
: 0,
\"
text
\"
:
\"
All statistics removed.
\"
}"
,
response
);
}
}
// End of anonymous namespace
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