[toranj] test-case to check child mode change and parent reset recovery (#3129)
diff --git a/tests/toranj/start.sh b/tests/toranj/start.sh
index 3318bfb..aa15587 100755
--- a/tests/toranj/start.sh
+++ b/tests/toranj/start.sh
@@ -191,6 +191,7 @@
 run test-024-partition-merge.py
 run test-025-network-data-timeout.py
 run test-026-slaac-address.py
+run test-027-child-mode-change.py
 run test-100-mcu-power-state.py
 run test-600-channel-manager-properties.py
 run test-601-channel-manager-channel-change.py
diff --git a/tests/toranj/test-027-child-mode-change.py b/tests/toranj/test-027-child-mode-change.py
new file mode 100644
index 0000000..a203f51
--- /dev/null
+++ b/tests/toranj/test-027-child-mode-change.py
@@ -0,0 +1,143 @@
+#!/usr/bin/env python
+#
+#  Copyright (c) 2018, The OpenThread Authors.
+#  All rights reserved.
+#
+#  Redistribution and use in source and binary forms, with or without
+#  modification, are permitted provided that the following conditions are met:
+#  1. Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+#  2. Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+#  3. Neither the name of the copyright holder nor the
+#     names of its contributors may be used to endorse or promote products
+#     derived from this software without specific prior written permission.
+#
+#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+#  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+#  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+#  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+#  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+#  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+#  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+#  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+#  POSSIBILITY OF SUCH DAMAGE.
+
+import time
+import wpan
+from wpan import verify
+
+#-----------------------------------------------------------------------------------------------------------------------
+# Test description: Verify Thread mode change on children and recovery after parent reset
+#
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print '-' * 120
+print 'Starting \'{}\''.format(test_name)
+
+#-----------------------------------------------------------------------------------------------------------------------
+# Utility functions
+
+def verify_child_table(parent, children):
+    """
+    This function verifies that child table on `parent` node contains all the entries in `children` list and the child
+    table entry's mode value matches the children Thread mode.
+    """
+    child_table = wpan.parse_child_table_result(parent.get(wpan.WPAN_THREAD_CHILD_TABLE))
+    verify(len(child_table) == len(children))
+    for child in children:
+        ext_addr = child.get(wpan.WPAN_EXT_ADDRESS)[1:-1]
+        for entry in child_table:
+            if entry.ext_address == ext_addr:
+                break
+        else:
+            raise wpan.VerifyError('Failed to find a child entry for extended address {} in table'.format(ext_addr))
+            exit(1)
+
+        verify(int(entry.rloc16, 16) == int(child.get(wpan.WPAN_THREAD_RLOC16), 16))
+        mode = int(child.get(wpan.WPAN_THREAD_DEVICE_MODE), 0)
+        verify(entry.is_rx_on_when_idle() == (mode & wpan.THREAD_MODE_FLAG_RX_ON_WHEN_IDLE != 0))
+        verify(entry.is_ftd() == (mode & wpan.THREAD_MODE_FLAG_FULL_THREAD_DEV != 0))
+        verify(entry.is_full_net_data() == (mode & wpan.THREAD_MODE_FLAG_FULL_NETWORK_DATA != 0))
+        verify(entry.is_sec_data_req() == (mode & wpan.THREAD_MODE_FLAG_SECURE_DATA_REQUEST != 0))
+
+#-----------------------------------------------------------------------------------------------------------------------
+# Creating `wpan.Nodes` instances
+
+speedup = 4
+wpan.Node.set_time_speedup_factor(speedup)
+
+parent = wpan.Node()
+child1 = wpan.Node()
+child2 = wpan.Node()
+
+#-----------------------------------------------------------------------------------------------------------------------
+# Init all nodes
+
+wpan.Node.init_all_nodes()
+
+#-----------------------------------------------------------------------------------------------------------------------
+# Build network topology
+#
+
+parent.form("ModeChangeReset")
+
+child1.join_node(parent, wpan.JOIN_TYPE_END_DEVICE)
+child2.join_node(parent, wpan.JOIN_TYPE_SLEEPY_END_DEVICE)
+
+child1.set(wpan.WPAN_POLL_INTERVAL, '8000')
+child2.set(wpan.WPAN_POLL_INTERVAL, '8000')
+
+children = [child1, child2]
+
+#-----------------------------------------------------------------------------------------------------------------------
+# Test implementation
+
+WAIT_INTERVAL = 6
+
+# Thread Mode for end-device and sleepy end-device
+DEVICE_MODE_SLEEPY_END_DEVICE  = wpan.THREAD_MODE_FLAG_FULL_NETWORK_DATA | wpan.THREAD_MODE_FLAG_SECURE_DATA_REQUEST
+DEVICE_MODE_END_DEVICE  = wpan.THREAD_MODE_FLAG_FULL_NETWORK_DATA | wpan.THREAD_MODE_FLAG_FULL_THREAD_DEV \
+     | wpan.THREAD_MODE_FLAG_SECURE_DATA_REQUEST | wpan.THREAD_MODE_FLAG_RX_ON_WHEN_IDLE
+
+# Disable child supervision on all devices
+parent.set(wpan.WPAN_CHILD_SUPERVISION_INTERVAL, '0')
+child1.set(wpan.WPAN_CHILD_SUPERVISION_CHECK_TIMEOUT, '0')
+child2.set(wpan.WPAN_CHILD_SUPERVISION_CHECK_TIMEOUT, '0')
+
+# Verify Thread Device Mode on both children
+verify(int(child1.get(wpan.WPAN_THREAD_DEVICE_MODE),0) == DEVICE_MODE_END_DEVICE)
+verify(int(child2.get(wpan.WPAN_THREAD_DEVICE_MODE),0) == DEVICE_MODE_SLEEPY_END_DEVICE)
+
+def check_child_table():
+    verify_child_table(parent, children)
+
+wpan.verify_within(check_child_table, WAIT_INTERVAL)
+
+# Reset parent and verify all children are recovered
+parent.reset()
+wpan.verify_within(check_child_table, WAIT_INTERVAL)
+
+# Change mode on both children (make child1 sleepy, and child2 non-sleepy)
+child1.set(wpan.WPAN_THREAD_DEVICE_MODE, str(DEVICE_MODE_SLEEPY_END_DEVICE))
+verify(int(child1.get(wpan.WPAN_THREAD_DEVICE_MODE),0) == DEVICE_MODE_SLEEPY_END_DEVICE)
+
+child2.set(wpan.WPAN_THREAD_DEVICE_MODE, str(DEVICE_MODE_END_DEVICE))
+verify(int(child2.get(wpan.WPAN_THREAD_DEVICE_MODE),0) == DEVICE_MODE_END_DEVICE)
+
+# Verify that the child table on parent is also updated
+wpan.verify_within(check_child_table, WAIT_INTERVAL)
+
+# Reset parent and verify all children are recovered
+parent.reset()
+wpan.verify_within(check_child_table, WAIT_INTERVAL)
+
+#-----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+wpan.Node.finalize_all_nodes()
+
+print '\'{}\' passed.'.format(test_name)
diff --git a/tests/toranj/wpan.py b/tests/toranj/wpan.py
index 0c28e27..b791283 100644
--- a/tests/toranj/wpan.py
+++ b/tests/toranj/wpan.py
@@ -183,7 +183,6 @@
 NODE_TYPE_NEST_LURKER                          = '"nl-lurker"'
 
 #-----------------------------------------------------------------------------------------------------------------------
-
 # Node types used by `Node.join()`
 
 JOIN_TYPE_ROUTER                               = 'r'
@@ -191,6 +190,14 @@
 JOIN_TYPE_SLEEPY_END_DEVICE                    = 's'
 
 #-----------------------------------------------------------------------------------------------------------------------
+# Bit Flags for Thread Device Mode `WPAN_THREAD_DEVICE_MODE`
+
+THREAD_MODE_FLAG_FULL_NETWORK_DATA   = (1 << 0)
+THREAD_MODE_FLAG_FULL_THREAD_DEV     = (1 << 1)
+THREAD_MODE_FLAG_SECURE_DATA_REQUEST = (1 << 2)
+THREAD_MODE_FLAG_RX_ON_WHEN_IDLE     = (1 << 3)
+
+#-----------------------------------------------------------------------------------------------------------------------
 
 def _log(text, new_line=True, flush=True):
     sys.stdout.write(text)
@@ -1126,10 +1133,12 @@
         # Convert the rest into a dictionary by splitting using ':' as separator
         dict = {item.split(':')[0] : item.split(':')[1] for item in items[1:]}
 
-        self._rloc16     = dict['RLOC16']
-        self._timeout    = dict['Timeout']
-        self._rx_on_idle = (dict['RxOnIdle'] == 'yes')
-        self._ftd        = (dict['FTD'] == 'yes')
+        self._rloc16        = dict['RLOC16']
+        self._timeout       = dict['Timeout']
+        self._rx_on_idle    = (dict['RxOnIdle'] == 'yes')
+        self._ftd           = (dict['FTD'] == 'yes')
+        self._sec_data_req  = (dict['SecDataReq'] == 'yes')
+        self._full_net_data = (dict['FullNetData'] == 'yes')
 
     @property
     def ext_address(self):
@@ -1149,6 +1158,12 @@
     def is_ftd(self):
         return self._ftd
 
+    def is_sec_data_req(self):
+        return self._sec_data_req
+
+    def is_full_net_data(self):
+        return self._full_net_data
+
     def __repr__(self):
         return 'ChildEntry({})'.format(self.__dict__)