mn_ctl.py: mininet controller
auto_topo.py: auto topology create/delete script
gui_client: auto test for GUI (use selenium).

Signed-off-by: YAMADA Hideki <[email protected]>
---
 ryu/tests/topology/auto_topo.py               |  204 +++++++++
 ryu/tests/topology/gui_client/gui_elements.py |  323 ++++++++++++++
 ryu/tests/topology/gui_client/gui_test.py     |  571 +++++++++++++++++++++++++
 ryu/tests/topology/gui_client/test_chrome.py  |   35 ++
 ryu/tests/topology/gui_client/test_firefox.py |   31 ++
 ryu/tests/topology/mn_ctl.py                  |   96 +++++
 6 files changed, 1260 insertions(+), 0 deletions(-)
 create mode 100644 ryu/tests/topology/auto_topo.py
 create mode 100644 ryu/tests/topology/gui_client/gui_elements.py
 create mode 100644 ryu/tests/topology/gui_client/gui_test.py
 create mode 100755 ryu/tests/topology/gui_client/test_chrome.py
 create mode 100755 ryu/tests/topology/gui_client/test_firefox.py
 create mode 100755 ryu/tests/topology/mn_ctl.py

diff --git a/ryu/tests/topology/auto_topo.py b/ryu/tests/topology/auto_topo.py
new file mode 100644
index 0000000..067c143
--- /dev/null
+++ b/ryu/tests/topology/auto_topo.py
@@ -0,0 +1,204 @@
+# Copyright (C) 2013 Nippon Telegraph and Telephone Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import time
+import json
+import httplib
+from argparse import ArgumentParser
+
+from mn_ctl import MNCtl
+from ryu.ofproto.ether import ETH_TYPE_ARP, ETH_TYPE_IP
+from ryu.ofproto.inet import IPPROTO_TCP
+
+
+parser = ArgumentParser(
+    description='Topology auto creation '
+                'and modification for test.')
+parser.add_argument('--ryu_host', dest='ryu_host',
+                    default='127.0.0.1', help='ryu ofp listen host')
+parser.add_argument('--ryu_port', dest='ryu_port',
+                    type=int, default=6633, help='ryu ofp listen port')
+parser.add_argument('--rest_port', dest='rest_port',
+                    type=int, default=8080, help='rest api listen port')
+args = parser.parse_args()
+
+
+_FLOW_PATH_BASE = '/stats/flowentry/%(cmd)s'
+MN = MNCtl()
+
+
+def main():
+    MN.add_controller(args.ryu_host, args.ryu_port)
+
+    ### Initializeing
+    print """
+Initializeing...
+  addSwitch s1, s2, s3, s4, s5
+  addLink   (s1, s2), (s2, s3)... (s5, s1)
+"""
+    # add switches
+    for i in range(5):
+        sw = 's%d' % (i + 1)
+        MN.add_switch(sw)
+
+    # add links
+    for i in range(5):
+        sw1 = 's%d' % (i + 1)
+        sw2 = 's%d' % (i + 2)
+        if sw1 == 's5':
+            sw2 = 's1'
+        MN.add_link(sw1, sw2)
+    _wait(15)
+
+    ### Added some switch
+    print """
+Added some switch
+  addSwitch s6, s7, s8, s9, s10
+"""
+    for i in range(5, 10):
+        sw = 's%d' % (i + 1)
+        MN.add_switch(sw)
+    _wait()
+
+    ### Added some link
+    print """
+Added some link
+  addLink   (s5, s6), (s6, s7) ...(s10, s1)
+"""
+    for i in range(4, 10):
+        sw1 = 's%d' % (i + 1)
+        sw2 = 's%d' % (i + 2)
+        if sw1 == 's10':
+            sw2 = 's1'
+        MN.add_link(sw1, sw2)
+    _wait()
+
+    ### Added some link
+    print """
+Delete some links
+  delLink  (s8, s9), (s9, s10), (s10, s1)
+"""
+    MN.del_link('s8', 's9')
+    MN.del_link('s9', 's10')
+    MN.del_link('s10', 's1')
+    _wait()
+
+    ### Delete some switch
+    print """
+Delete some switch
+  delSwitch  s6, s7, s8, s9, s10
+"""
+    MN.del_switch('s6')
+    MN.del_switch('s7')
+    MN.del_switch('s8')
+    MN.del_switch('s9')
+    MN.del_switch('s10')
+    _wait(10)
+
+    ### Added some flow
+    print """
+Added some flow
+    dpid   : 1
+    rules  : dl_type=0x0800(ip), ip_proto=6(tcp), tp_src=100-104
+    actions: OUTPUT: 2
+"""
+    path = _FLOW_PATH_BASE % {'cmd': 'add'}
+    for tp_src in range(100, 105):
+        body = {}
+        body['dpid'] = 1
+        body['match'] = {'dl_type': ETH_TYPE_IP,
+                         'nw_proto': IPPROTO_TCP,
+                         'tp_src': tp_src}
+        body['actions'] = [{'type': "OUTPUT", "port": 2}]
+        _do_request(path, 'POST', json.dumps(body))
+    _wait(10)
+
+    ### Modify some flow
+    print """
+Modify some flow
+    dpid   : 1
+    rules  : dl_type=0x0800(ip), ip_proto=6(tcp), tp_src=100-102
+    actions: OUTPUT: 2->1
+"""
+    path = _FLOW_PATH_BASE % {'cmd': 'modify'}
+    for tp_src in range(100, 103):
+        body = {}
+        body['dpid'] = 1
+        body['match'] = {'dl_type': ETH_TYPE_IP,
+                         'nw_proto': IPPROTO_TCP,
+                         'tp_src': tp_src}
+        body['actions'] = [{'type': "OUTPUT", "port": 1}]
+        _do_request(path, 'POST', json.dumps(body))
+    _wait(10)
+
+    ### Delete some flow
+    print """
+Delete some flow
+    dpid   : 1
+    rules  : dl_type=0x0800(ip), ip_proto=6(tcp), tp_src=100-102
+"""
+    path = _FLOW_PATH_BASE % {'cmd': 'delete'}
+    for tp_src in range(100, 103):
+        body = {}
+        body['dpid'] = 1
+        body['match'] = {'dl_type': ETH_TYPE_IP,
+                         'nw_proto': IPPROTO_TCP,
+                         'tp_src': tp_src}
+        _do_request(path, 'POST', json.dumps(body))
+    _wait(10)
+
+    ### Delete all flows
+    print """
+Delete all flows
+    dpid   : 1
+"""
+    path = _FLOW_PATH_BASE % {'cmd': 'clear'}
+    path += '/1'
+    _do_request(path, 'DELETE')
+    _wait()
+
+    ### Delete all switches
+    print "Delete all switches"
+    MN.stop()
+    print "Finished"
+
+
+def _wait(wait=5):
+    print "  ...waiting %s" % wait
+    time.sleep(wait)
+
+
+def _do_request(path, method="GET", body=None):
+    address = '%s:%s' % (args.ryu_host, args.rest_port)
+    conn = httplib.HTTPConnection(address)
+    conn.request(method, path, body)
+    res = conn.getresponse()
+    if res.status in (httplib.OK,
+                      httplib.CREATED,
+                      httplib.ACCEPTED,
+                      httplib.NO_CONTENT):
+        return res
+
+    raise httplib.HTTPException(
+        res, 'code %d reason %s' % (res.status, res.reason),
+        res.getheaders(), res.read())
+
+
+if __name__ == "__main__":
+    try:
+        main()
+    except:
+        MN.stop()
+        raise
diff --git a/ryu/tests/topology/gui_client/gui_elements.py 
b/ryu/tests/topology/gui_client/gui_elements.py
new file mode 100644
index 0000000..49273ee
--- /dev/null
+++ b/ryu/tests/topology/gui_client/gui_elements.py
@@ -0,0 +1,323 @@
+# Copyright (C) 2013 Nippon Telegraph and Telephone Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import time
+import re
+from math import sqrt
+from selenium.webdriver.common.by import By
+from selenium.common.exceptions import NoSuchElementException
+from selenium.common.exceptions import StaleElementReferenceException
+
+
+class DriverUtil(object):
+    def __init__(self):
+        self.fail = AssertionError
+
+    @staticmethod
+    def wait_for_true(timeout, fnc, *args, **kwargs):
+        res = None
+        for i in range(timeout):
+            res = fnc(*args, **kwargs)
+            if res:
+                break
+            time.sleep(1)
+        assert res, 'Timeout(%d) %s %s %s' % (timeout, fnc, args, kwargs)
+
+        return res
+
+    def is_displayed(self, el):
+        if el and el.is_displayed():
+            return True
+        return False
+
+    def wait_for_displayed(self, el, timeout=30):
+        return DriverUtil.wait_for_true(timeout, self.is_displayed, el)
+
+    def is_hidden(self, el):
+        return not self.is_displayed(el)
+
+    def wait_for_hidden(self, el, timeout=30):
+        return DriverUtil.wait_for_true(timeout, self.is_hidden, el)
+
+    def has_text(self, el, text):
+        if el and re.search(r'%s' % text, el.text):
+            return True
+        return False
+
+    def wait_for_text(self, el, text, timeout=30):
+        return DriverUtil.wait_for_true(timeout, self.has_text, el, text)
+
+    def has_not_text(self, el, text):
+        return not self.has_text(el, text)
+
+    def wait_for_text_deleted(self, el, text, timeout=30):
+        return DriverUtil.wait_for_true(timeout, self.has_not_text, el, text)
+
+    def get_element_center(self, el):
+        x = (el.location['x'] + el.size['width']) / 2
+        y = (el.location['y'] + el.size['height']) / 2
+        return {'x': x, 'y': y}
+
+    def get_distance(self, el1, el2):
+        c1 = self.get_element_center(el1)
+        c2 = self.get_element_center(el2)
+        dx = c1['x'] - c2['x']
+        dy = c1['y'] - c2['y']
+        return sqrt(pow(dx, 2) + pow(dy, 2))
+
+
+class ElementBase(object):
+    def __init__(self, driver):
+        self._driver = driver
+        self.fail = AssertionError
+        self.name = self.__class__.__name__
+
+    def _get_el(self, by, value):
+        try:
+            element = self._driver.find_element(by=by, value=value)
+        except NoSuchElementException, e:
+            return False
+        return element
+
+    def _get_els(self, by, value):
+        try:
+            elements = self._driver.find_elements(by=by, value=value)
+        except NoSuchElementException, e:
+            return False
+        return elements
+
+
+class Menu(ElementBase):
+    @property
+    def body(self):
+        return self._get_el(By.ID, "menu")
+
+    @property
+    def titlebar(self):
+        return self._get_el(By.CSS_SELECTOR,
+                            "#menu > div.content-title")
+
+    @property
+    def dialog(self):
+        return self._get_el(By.ID, "jquery-ui-dialog-opener")
+
+    @property
+    def redesign(self):
+        return self._get_el(By.ID, "menu-redesign")
+
+    @property
+    def link_list(self):
+        return self._get_el(By.ID, "menu-link-status")
+
+    @property
+    def flow_list(self):
+        return self._get_el(By.ID, "menu-flow-entries")
+
+    @property
+    def resize(self):
+        return self._get_el(By.XPATH, "//div[@id='menu']/div[6]")
+
+
+class Dialog(ElementBase):
+    @property
+    def body(self):
+        return self._get_el(By.ID, "jquery-ui-dialog")
+
+    @property
+    def host(self):
+        return self._get_el(By.ID, "jquery-ui-dialog-form-host")
+
+    @property
+    def port(self):
+        return self._get_el(By.ID, "jquery-ui-dialog-form-port")
+
+    @property
+    def cancel(self):
+        return self._get_el(By.XPATH, "(//button[@type='button'])[2]")
+
+    @property
+    def launch(self):
+        return self._get_el(By.XPATH, "//button[@type='button']")
+
+    @property
+    def close(self):
+        return self._get_el(By.CSS_SELECTOR, "span.ui-icon.ui-icon-closethick")
+
+    @property
+    def resize(self):
+        return self._get_el(By.XPATH, "//div[7]")
+
+
+class Topology(ElementBase):
+    def __init__(self, driver):
+        super(Topology, self).__init__(driver)
+        self._not_selected_switch_width = None
+
+    @property
+    def body(self):
+        return self._get_el(By.ID, "topology")
+
+    @property
+    def titlebar(self):
+        return self._get_el(By.CSS_SELECTOR, "#topology > div.content-title")
+
+    @property
+    def resize(self):
+        return self._get_el(By.XPATH, "//div[@id='topology']/div[5]")
+
+    @property
+    def switches(self):
+        return self._get_els(By.CSS_SELECTOR,
+                             "#topology > div.content-body > div.switch")
+
+    def get_switch(self, dpid):
+        id_ = "node-switch-%d" % int(dpid)
+        el = self._get_el(By.ID, id_)
+        if not el:
+            self.fail('element not found. dpid=%d' % int(dpid))
+            return
+        elif self._not_selected_switch_width is None:
+            # set element default width for is_selected()
+            self._not_selected_switch_width = el.size["width"]
+        return el
+
+    def is_selected(self, el):
+        # chromedriver could not use "get_value_of_css('border-color')".
+        # check to wider than default switch element.
+        return el.size['width'] > self._not_selected_switch_width
+
+    def get_dpid(self, el):
+        return int(el.get_attribute("id")[len('node-switch-'):])
+
+    def get_text_dpid(self, dpid):
+        return 'dpid: 0x%x' % (dpid)
+
+class LinkList(ElementBase):
+    @property
+    def body(self):
+        return self._get_el(By.ID, "link-list")
+
+    @property
+    def close(self):
+        return self._get_el(By.XPATH, "//div[@id='link-list']/div/div[2]")
+
+    @property
+    def titlebar(self):
+        return self._get_el(By.CSS_SELECTOR, "#link-list > div.content-title")
+
+    @property
+    def resize(self):
+        return self._get_el(By.XPATH, "//div[@id='link-list']/div[6]")
+
+    @property
+    def scrollbar_x(self):
+        return self._get_el(By.CSS_SELECTOR, "#link-list-body > "
+            "div.ps-scrollbar-x")
+
+    @property
+    def scrollbar_y(self):
+        return self._get_el(By.CSS_SELECTOR, "#link-list-body > "
+            "div.ps-scrollbar-y")
+
+    @property
+    def rows(self):
+        links = []
+        css = '#%s > td.%s'
+        # loop rows
+        for row in self._get_els(By.CSS_SELECTOR, "#link-list > "
+            "div.content-body > table > tbody > tr.content-table-item"):
+            id_ = row.get_attribute('id')
+
+            # set inner elements
+            no = self._get_el(By.CSS_SELECTOR, css % (id_, 'port-no'))
+            name = self._get_el(By.CSS_SELECTOR, css % (id_, 'port-name'))
+            peer = self._get_el(By.CSS_SELECTOR, css % (id_, 'port-peer'))
+            setattr(row, 'no', no)
+            setattr(row, 'name', name)
+            setattr(row, 'peer', peer)
+            links.append(row)
+        return links
+
+
+class FlowList(ElementBase):
+    @property
+    def body(self):
+        return self._get_el(By.ID, "flow-list")
+
+    @property
+    def close(self):
+        return self._get_el(By.XPATH, "//div[@id='flow-list']/div/div[2]")
+
+    @property
+    def titlebar(self):
+        return self._get_el(By.CSS_SELECTOR, "#flow-list > div.content-title")
+
+    @property
+    def resize(self):
+        return self._get_el(By.XPATH, "//div[@id='flow-list']/div[6]")
+
+    @property
+    def scrollbar_x(self):
+        return self._get_el(By.CSS_SELECTOR, "#flow-list-body > "
+            "div.ps-scrollbar-x")
+
+    @property
+    def scrollbar_y(self):
+        return self._get_el(By.CSS_SELECTOR, "#flow-list-body > "
+            "div.ps-scrollbar-y")
+
+    @property
+    def rows(self):
+        return self._get_els(By.CSS_SELECTOR, "#flow-list > "
+            "div.content-body > table > tbody > tr.content-table-item")
+
+    def _get_row_text(self, row_no):
+        css = '#%s > td > div > span.flow-item-value'
+        texts = {'stats': None, 'rules': None, 'actions': None}
+        try:
+            # get inner elements
+            id_ = self.rows[row_no].get_attribute('id')
+            inner = self._get_els(By.CSS_SELECTOR, css % (id_))
+            if not len(inner) == 3:
+                raise StaleElementReferenceException
+            texts['stats'] = inner[0].text
+            texts['rules'] = inner[1].text
+            texts['actions'] = inner[2].text
+        except StaleElementReferenceException:
+            # flow-list refreashed.
+            return False
+        return texts
+
+    def get_row_text(self, row_no):
+        return DriverUtil.wait_for_true(2, self._get_row_text, row_no)
+
+    def wait_for_refreshed(self, timeout=10):
+        old = None
+        rows = self.rows
+        if rows:
+            old = rows[0].id
+
+        now = None
+        for i in range(timeout):
+            rows = self.rows
+            if rows:
+                now = rows[0].id
+            if (old and not now) or (now and old != now):
+                return True
+            time.sleep(1)
+        if (old == None and now == None):
+            # no data
+            return False
+        self.fail('flow-list does not refreshed.')
diff --git a/ryu/tests/topology/gui_client/gui_test.py 
b/ryu/tests/topology/gui_client/gui_test.py
new file mode 100644
index 0000000..daffa3e
--- /dev/null
+++ b/ryu/tests/topology/gui_client/gui_test.py
@@ -0,0 +1,571 @@
+# Copyright (C) 2013 Nippon Telegraph and Telephone Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import re
+import time
+import unittest
+import xmlrpclib
+import json
+import httplib
+from nose.tools import ok_, eq_
+from selenium.webdriver.common.action_chains import ActionChains
+
+import gui_elements
+from ryu.ofproto.ether import ETH_TYPE_IP
+from ryu.ofproto.inet import IPPROTO_TCP
+from ryu.ofproto import ofproto_v1_0
+
+
+# GUI app address
+GUI_HOST = '127.0.0.1'
+GUI_PORT = '8000'
+BASE_URL = 'http://%s:%s' % (GUI_HOST, GUI_PORT)
+
+# REST app address
+REST_HOST = 'localhost'
+REST_PORT = '8080'
+
+# ryu controller address
+RYU_HOST = '127.0.0.1'
+RYU_PORT = '6633'
+
+# mininet controller address
+MN_HOST = '127.0.0.1'
+MN_PORT = '18000'
+MN_CTL_URL = 'http://%s:%s' % (MN_HOST, MN_PORT)
+
+
+OFP_DEFAULT_PRIORITY = ofproto_v1_0.OFP_DEFAULT_PRIORITY
+
+
+# flow-list sort key
+def _flows_sort_key(a, b):
+    # ascending table_id
+    if a.get('table_id', 0) > b.get('table_id', 0):
+        return 1
+    elif a.get('table_id', 0) < b.get('table_id', 0):
+        return -1
+    # descending priority
+    elif a.get('priority', OFP_DEFAULT_PRIORITY) < \
+        b.get('priority', OFP_DEFAULT_PRIORITY):
+        return 1
+    elif a.get('priority', OFP_DEFAULT_PRIORITY) > \
+        b.get('priority', OFP_DEFAULT_PRIORITY):
+        return -1
+    # ascending duration
+    elif a.get('duration_sec', 0) < b.get('duration_sec', 0):
+        return 1
+    elif a.get('duration_sec', 0) > b.get('duration_sec', 0):
+        return -1
+    elif a.get('duration_nsec', 0) < b.get('duration_nsec', 0):
+        return 1
+    elif a.get('duration_nsec', 0) > b.get('duration_nsec', 0):
+        return -1
+    else:
+        return 0
+
+
+def _rest_request(path, method="GET", body=None):
+    address = '%s:%s' % (REST_HOST, REST_PORT)
+    conn = httplib.HTTPConnection(address)
+    conn.request(method, path, body)
+    res = conn.getresponse()
+    if res.status in (httplib.OK,
+                      httplib.CREATED,
+                      httplib.ACCEPTED,
+                      httplib.NO_CONTENT):
+        return res
+    raise httplib.HTTPException(
+        res, 'code %d reason %s' % (res.status, res.reason),
+        res.getheaders(), res.read())
+
+
+def _is_rest_link_deleted():
+    try:
+        links = json.load(_rest_request('/v1.0/topology/links'))
+    except (IOError):
+        # REST API is not avaliable.
+        return True
+    return not links
+
+
+class TestGUI(unittest.TestCase):
+    # called before the TestCase run.
+    @classmethod
+    def setUpClass(cls):
+        cls._mn = None
+        cls._set_driver()
+        ok_(cls.driver, 'driver dose not setting.')
+
+        # elements
+        cls.util = gui_elements.DriverUtil()
+        cls.menu = gui_elements.Menu(cls.driver)
+        cls.dialog = gui_elements.Dialog(cls.driver)
+        cls.topology = gui_elements.Topology(cls.driver)
+        cls.link_list = gui_elements.LinkList(cls.driver)
+        cls.flow_list = gui_elements.FlowList(cls.driver)
+
+    # called after the TestCase run.
+    @classmethod
+    def tearDownClass(cls):
+        cls.driver.quit()
+
+    # called before an individual test_* run.
+    def setUp(self):
+        self.driver.get(BASE_URL + "/")
+        self.util.wait_for_displayed(self.dialog.body)
+
+    # called after an individual test_* run.
+    def tearDown(self):
+        if self._mn is not None:
+            self._mn.stop()
+            self.util.wait_for_true(10, _is_rest_link_deleted)
+
+    # called in to setUpClass().
+    @classmethod
+    def _set_driver(cls):
+        # set the driver of the test browser.
+        # self.driver = selenium.webdriver.Firefox()
+        cls.driver = None
+
+    def _get_mininet_controller(self):
+        if self._mn is None:
+            self._mn = xmlrpclib.ServerProxy(MN_CTL_URL, allow_none=True)
+            self._mn.add_controller(RYU_HOST, int(RYU_PORT))
+        return self._mn
+
+    def mouse(self):
+        return ActionChains(self.driver)
+
+    def _rest_connect(self):
+        if not self.dialog.body.is_displayed():
+            # dialog open
+            self.menu.dialog.click()
+            self.util.wait_for_displayed(self.dialog.body)
+
+        # input address
+        self.dialog.host.clear()
+        self.dialog.host.send_keys(REST_HOST)
+        self.dialog.port.clear()
+        self.dialog.port.send_keys(REST_PORT)
+
+        # click "launch"
+        self.dialog.launch.click()
+        self.util.wait_for_text(self.topology.body, "Connected")
+
+    def test_default(self):
+        ## input-dialog
+        # is_displayed, host=GUI_HOST, port=8080
+        dialog = self.dialog
+        ok_(dialog.body.is_displayed())
+        eq_(GUI_HOST, dialog.host.get_attribute("value"))
+        eq_('8080', dialog.port.get_attribute("value"))
+
+        # click "cancel"
+        dialog.cancel.click()
+
+        ## topology
+        # "Disconnected", no switches
+        topology = self.topology
+        ok_(re.search(r"Disconnected", topology.body.text))
+        ok_(not topology.switches)
+
+        ## link-list
+        # is_displayed, no data
+        link = self.link_list
+        ok_(link.body.is_displayed())
+        ok_(not link.rows)
+
+        ## flow-list
+        # is_displayed, no data
+        flow = self.flow_list
+        ok_(flow.body.is_displayed())
+        ok_(not flow.rows)
+
+    def _test_contents_close_open(self, target, opener):
+        self.util.wait_for_displayed(target.body)
+
+        # close
+        target.close.click()
+        ok_(not target.body.is_displayed(),
+            '%s does not close content.' % target.name)
+
+        # open
+        opener.click()
+        ok_(self.util.wait_for_displayed(target.body),
+            '%s does not open content.' % target.name)
+
+    def test_contents_close_open(self):
+        menu = self.menu
+        ## input-dialog
+        self._test_contents_close_open(self.dialog, menu.dialog)
+        self.dialog.close.click()
+
+        ## link-list
+        self._test_contents_close_open(self.link_list, menu.link_list)
+
+        ## flow-list
+        self._test_contents_close_open(self.flow_list, menu.flow_list)
+
+    def _test_contents_draggable(self, target):
+        move = 50
+        titlebar = target.titlebar
+        xoffset = titlebar.location['x'] + move
+        yoffset = titlebar.location['y'] + move
+
+        # move
+        mouse = self.mouse()
+        mouse.click(titlebar)
+        mouse.drag_and_drop_by_offset(titlebar, move, move)
+        mouse.perform()
+
+        err = '%s draggable error' % (target.name)
+        eq_(titlebar.location['x'], xoffset, err)
+        eq_(titlebar.location['y'], yoffset, err)
+
+        # move back
+        # content can not drag if overlaps with other contents.
+        mouse = self.mouse()
+        mouse.click(titlebar)
+        mouse.drag_and_drop_by_offset(titlebar, -move, -move)
+        mouse.perform()
+
+    def test_contents_draggable(self):
+        self.dialog.close.click()
+
+        ## menu
+        self._test_contents_draggable(self.menu)
+
+        ## topology
+        self._test_contents_draggable(self.topology)
+
+        ## link-list
+        self._test_contents_draggable(self.link_list)
+
+        ## flow-list
+        self._test_contents_draggable(self.flow_list)
+
+    def _test_contents_resize(self, target):
+        self.util.wait_for_displayed(target.body)
+
+        size = target.body.size
+
+        # resize
+        resize = 20
+        mouse = self.mouse()
+        mouse.move_to_element(target.body)
+        mouse.drag_and_drop_by_offset(target.resize, resize, resize)
+        mouse.perform()
+
+        # check
+        err = '%s resize error' % (target.name)
+        eq_(target.body.size['width'], size['width'] + resize, err)
+        eq_(target.body.size['height'], size['height'] + resize, err)
+
+        # resize back
+        mouse = self.mouse()
+        mouse.move_to_element(target.body)
+        mouse.drag_and_drop_by_offset(target.resize, -resize, -resize)
+        mouse.perform()
+
+    def test_contents_resize(self):
+        ## input-dialog
+        self._test_contents_resize(self.dialog)
+        self.dialog.cancel.click()
+
+        ## menu
+        self._test_contents_resize(self.menu)
+
+        ## topology
+        self._test_contents_resize(self.topology)
+
+        ## link-list
+        self._test_contents_resize(self.link_list)
+
+        ## flow-list
+        self._test_contents_resize(self.flow_list)
+
+    def test_connected(self):
+        # input host
+        host = self.dialog.host
+        host.clear()
+        host.send_keys(REST_HOST)
+
+        # input port
+        port = self.dialog.port
+        port.clear()
+        port.send_keys(REST_PORT)
+
+        # click "Launch"
+        self.dialog.launch.click()
+        ok_(self.util.wait_for_text(self.topology.body, "Connected"))
+
+    def test_topology_discovery(self):
+        util = self.util
+        topo = self.topology
+        mn = self._get_mininet_controller()
+
+        self._rest_connect()
+
+        ## add switch (dpid=1)
+        mn.add_switch('s1')
+        ok_(util.wait_for_text(topo.body, topo.get_text_dpid(1)))
+
+        ## add some switches (dpid=2-8)
+        mn.add_switch('s2')
+        mn.add_switch('s3')
+        mn.add_switch('s4')
+
+        # check drawed
+        ok_(util.wait_for_text(topo.body, topo.get_text_dpid(2)))
+        ok_(util.wait_for_text(topo.body, topo.get_text_dpid(3)))
+        ok_(util.wait_for_text(topo.body, topo.get_text_dpid(4)))
+        time.sleep(1)  # wait for switch move animation
+
+        # check positions (diamond shape)
+        d_1_2 = util.get_distance(topo.get_switch(1), topo.get_switch(2))
+        d_2_3 = util.get_distance(topo.get_switch(2), topo.get_switch(3))
+        d_3_4 = util.get_distance(topo.get_switch(3), topo.get_switch(4))
+        d_4_1 = util.get_distance(topo.get_switch(4), topo.get_switch(1))
+        ok_(d_1_2 == d_2_3 == d_3_4 == d_4_1)
+
+        ## selected
+        for sw in topo.switches:
+            sw.click()
+            ok_(topo.is_selected(sw))
+
+        ## draggable
+        default_locations = {}
+        move = 10
+        for sw in topo.switches:
+            dpid = topo.get_dpid(sw)
+            default_locations[dpid] = sw.location
+            xoffset = sw.location['x'] + move
+            yoffset = sw.location['y'] + move
+
+            # move
+            mouse = self.mouse()
+            mouse.drag_and_drop_by_offset(sw, move, move)
+            mouse.perform()
+
+            err = 'dpid=%d draggable error' % (dpid)
+            eq_(sw.location['x'], xoffset, err)
+            eq_(sw.location['y'], yoffset, err)
+
+        ## refresh
+        self.menu.redesign.click()
+        time.sleep(1)  # wait for switch move animation
+        for sw in topo.switches:
+            dpid = topo.get_dpid(sw)
+            default_location = default_locations[dpid]
+            eq_(sw.location, default_location)
+
+        ## del switch (dpid=4)
+        mn.del_switch('s4')
+        ok_(util.wait_for_text_deleted(topo.body, topo.get_text_dpid(4)))
+
+        time.sleep(1)  # wait for switch move animation
+
+        # check position (isosceles triangle)
+        d_1_2 = util.get_distance(topo.get_switch(1), topo.get_switch(2))
+        d_1_3 = util.get_distance(topo.get_switch(1), topo.get_switch(3))
+        eq_(d_1_2, d_1_3)
+
+        ## del all switches
+        mn.stop()
+        ok_(util.wait_for_text_deleted(topo.body, topo.get_text_dpid(1)))
+        ok_(util.wait_for_text_deleted(topo.body, topo.get_text_dpid(2)))
+        ok_(util.wait_for_text_deleted(topo.body, topo.get_text_dpid(3)))
+
+    def _test_link_discovery(self, links):
+        link_list = self.link_list
+        util = self.util
+
+        # check Row count
+        if links:
+            eq_(len(links), len(link_list.rows))
+        else:
+            ok_(not link_list.rows)
+
+        # check text
+        for link in link_list.rows:
+            ok_(link.name.text in links)
+            eq_(str(links[link.name.text]['port_no']), link.no.text)
+            eq_(links[link.name.text]['peer'], link.peer.text)
+
+        # TODO: check connections on Topology
+
+    def test_link_discovery(self):
+        util = self.util
+        link_list = self.link_list
+        links = {}
+        mn = self._get_mininet_controller()
+
+        self._rest_connect()
+
+        # add some switches (dpid=1-4)
+        mn.add_switch('s1')
+        mn.add_switch('s2')
+        mn.add_switch('s3')
+        mn.add_switch('s4')
+        # s1 selected
+        util.wait_for_text(self.topology.body,
+                           self.topology.get_text_dpid(1))
+        self.topology.get_switch(dpid=1).click()
+
+        ## add links (s1 to s2, s3 and s4)
+        mn.add_link('s1', 's2')
+        mn.add_link('s1', 's3')
+        mn.add_link('s1', 's4')
+
+        links = {}
+        links['s1-eth1'] = {'port_no': 1, 'peer': 's2-eth1'}
+        links['s1-eth2'] = {'port_no': 2, 'peer': 's3-eth1'}
+        links['s1-eth3'] = {'port_no': 3, 'peer': 's4-eth1'}
+        util.wait_for_text(link_list.body, 's4-eth1')
+
+        # check
+        self._test_link_discovery(links)
+
+        ## del link (s1 to s4)
+        mn.del_link('s1', 's4')
+        del links['s1-eth3']
+        util.wait_for_text_deleted(link_list.body, 's4-eth1')
+
+        # check
+        self._test_link_discovery(links)
+
+    def _test_flow_discovery(self, flows):
+        flow_list = self.flow_list
+        body = flow_list.body
+        scrollbar = flow_list.scrollbar_y
+        flows.sort(cmp=_flows_sort_key)
+
+        # wait list refreshed
+        flow_list.wait_for_refreshed()
+
+        # check Row count
+        if flows:
+            eq_(len(flow_list.rows), len(flows))
+        else:
+            ok_(not flow_list.rows)
+
+        for i, flow in enumerate(flows):
+            row = flow_list.get_row_text(i)
+
+            # Row is be out of content area?
+            if not row['actions']:
+                # hold scrollbar
+                mouse = self.mouse()
+                mouse.click_and_hold(scrollbar).perform()
+
+                do_scroll = self.mouse().move_by_offset(0, 3)
+                end = body.location['y'] + body.size['height']
+                end -= scrollbar.size['height']
+                while scrollbar.location['y'] < end:
+                    # do scroll
+                    do_scroll.perform()
+                    row = flow_list.get_row_text(i)
+                    if row['actions']:
+                        # loock up
+                        break
+                # scroll to top of content
+                mouse = self.mouse()
+                mouse.move_by_offset(0, -body.size['height'])
+                mouse.release(scrollbar).perform()
+
+            # check text
+            stats = row['stats']
+            rules = row['rules']
+            actions = row['actions']
+
+            # TODO: other attributes
+            priority = flow['priority']
+            tp_src = flow['match']['tp_src']
+            output = flow['actions'][0]['port']
+
+            ok_(re.search(r'priority=%d' % (priority), stats),
+                'i=%d, priority=%d, display=%s' % (i, priority, stats))
+            ok_(re.search(r'tp_src=%d' % (tp_src), rules),
+                'i=%d, tp_src=%d, display=%s' % (i, tp_src, rules))
+            ok_(re.search(r'OUTPUT:%d' % (output), actions),
+                'i=%d, OUTPUT=%d, display=%s' % (i, output, actions))
+
+    def test_flow_discovery(self):
+        mn = self._get_mininet_controller()
+        path = '/stats/flowentry/%s'
+        flows = []
+
+        self._rest_connect()
+
+        # add switche (dpid=1) and select
+        mn.add_switch('s1')
+        self.util.wait_for_text(self.topology.body,
+                                self.topology.get_text_dpid(1))
+        self.topology.get_switch(dpid=1).click()
+
+        ## add flow
+        # stats  : priority=100
+        # rules  : tp_src=99
+        # actions: OUTPUT: 1
+        body = {}
+        body['dpid'] = 1
+        body['priority'] = 100
+        body['match'] = {'dl_type': ETH_TYPE_IP,
+                         'nw_proto': IPPROTO_TCP,
+                         'tp_src': 99}
+        body['actions'] = [{'type': "OUTPUT", "port": 1}]
+        _rest_request(path % ('add'), 'POST', json.dumps(body))
+
+        flows.append(body)
+        self._test_flow_discovery(flows)
+
+        ## add more flow
+        # stats  : priority=100-104
+        # rules  : tp_src=101-105 (=priority + 1)
+        # actions: OUTPUT: 2
+        for priority in [100, 101, 102, 103, 104]:
+            tp_src = priority + 1
+            body = {}
+            body['dpid'] = 1
+            body['priority'] = priority
+            body['match'] = {'dl_type': ETH_TYPE_IP,
+                             'nw_proto': IPPROTO_TCP,
+                             'tp_src': tp_src}
+            body['actions'] = [{'type': "OUTPUT", "port": 2}]
+            _rest_request(path % ('add'), 'POST', json.dumps(body))
+            flows.append(body)
+        self._test_flow_discovery(flows)
+
+        ## mod flow
+        # rules  : tp_src=103, 104 (=priority + 1)
+        # actions: OUTPUT: 2 -> 3
+        for flow in flows:
+            if flow['match']['tp_src'] in [103, 104]:
+                flow['actions'][0]['port'] = 3
+                _rest_request(path % ('modify'), 'POST', json.dumps(flow))
+        self._test_flow_discovery(flows)
+
+        ## del some flow
+        # rules  : tp_src=103, 104 (=priority + 1)
+        for i, flow in enumerate(flows):
+            if flow['match']['tp_src'] in [103, 104]:
+                body = flows.pop(i)
+                _rest_request(path % ('delete'), 'POST', json.dumps(body))
+        self._test_flow_discovery(flows)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/ryu/tests/topology/gui_client/test_chrome.py 
b/ryu/tests/topology/gui_client/test_chrome.py
new file mode 100755
index 0000000..a8c4a7e
--- /dev/null
+++ b/ryu/tests/topology/gui_client/test_chrome.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2013 Nippon Telegraph and Telephone Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+
+import gui_test
+from selenium import webdriver
+
+
+class TestChrome(gui_test.TestGUI):
+    @classmethod
+    def _set_driver(cls):
+        # ChromeDriver executable needs to be available in the path.
+        # Please download from
+        #   https://code.google.com/p/chromedriver/downloads/list
+        driver = 'chromedriver'
+        cls.driver = webdriver.Chrome(driver)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/ryu/tests/topology/gui_client/test_firefox.py 
b/ryu/tests/topology/gui_client/test_firefox.py
new file mode 100755
index 0000000..dfdde8b
--- /dev/null
+++ b/ryu/tests/topology/gui_client/test_firefox.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2013 Nippon Telegraph and Telephone Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+
+import gui_test
+from selenium import webdriver
+
+
+class TestFirefox(gui_test.TestGUI):
+    @classmethod
+    def _set_driver(cls):
+        cls.driver = webdriver.Firefox()
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/ryu/tests/topology/mn_ctl.py b/ryu/tests/topology/mn_ctl.py
new file mode 100755
index 0000000..e943e12
--- /dev/null
+++ b/ryu/tests/topology/mn_ctl.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python
+# Copyright (C) 2013 Nippon Telegraph and Telephone Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from argparse import ArgumentParser
+from SimpleXMLRPCServer import SimpleXMLRPCServer
+
+from mininet.net import Mininet
+from mininet.node import RemoteController, OVSKernelSwitch
+
+
+parser = ArgumentParser()
+parser.add_argument('--listen-host', dest='host', default='127.0.0.1')
+parser.add_argument('--listen-port', dest='port', type=int, default=18000)
+args = parser.parse_args()
+
+
+class MNCtl(object):
+    def __init__(self):
+        self.mn = Mininet()
+        self._links = {}
+
+    def add_controller(self, ip, port):
+        for controller in self.mn.controllers:
+            if controller.ip == ip and controller.port == port:
+                return
+        controller = self.mn.addController(
+            controller=RemoteController, ip=ip, port=port)
+        controller.start()
+
+    def add_switch(self, name):
+        s = self.mn.addSwitch(name, cls=OVSKernelSwitch)
+        s.start(self.mn.controllers)
+
+    def del_switch(self, name):
+        s = self.mn.get(name)
+        s.stop()
+
+    def add_link(self, node1, node2):
+        [n1, n2] = self.mn.get(node1, node2)
+        link = self.mn.addLink(n1, n2)
+        self._links[(node1, node2)] = link
+        n1.attach(link.intf1)
+        n2.attach(link.intf2)
+
+    def del_link(self, node1, node2):
+        [n1, n2] = self.mn.get(node1, node2)
+        if self._links.get((node1, node2)):
+            link = self._links.pop((node1, node2))
+            n1.detach(link.intf1)
+            n2.detach(link.intf2)
+        else:
+            link = self._links.pop((node2, node1))
+            n2.detach(link.intf1)
+            n1.detach(link.intf2)
+        link.delete()
+
+    def stop(self):
+        self.mn.stop()
+
+
+class MNCtlServer(MNCtl):
+    def __init__(self):
+        super(MNCtlServer, self).__init__()
+        self.server = SimpleXMLRPCServer((args.host, args.port),
+                                         allow_none=True)
+
+        self._register_function(self.add_controller)
+        self._register_function(self.add_switch)
+        self._register_function(self.del_switch)
+        self._register_function(self.add_link)
+        self._register_function(self.del_link)
+        self._register_function(self.stop)
+
+        print "Running on %s:%d" % (args.host, args.port)
+        self.server.serve_forever()
+
+    def _register_function(self, fnc):
+        self.server.register_function(fnc)
+        print "register %s" % (fnc.__name__)
+
+
+if __name__ == "__main__":
+    MNCtlServer()
-- 
1.7.1



------------------------------------------------------------------------------
Try New Relic Now & We'll Send You this Cool Shirt
New Relic is the only SaaS-based application performance monitoring service 
that delivers powerful full stack analytics. Optimize and monitor your
browser, app, & servers with just a few lines of code. Try New Relic
and get this awesome Nerd Life shirt! http://p.sf.net/sfu/newrelic_d2d_apr
_______________________________________________
Ryu-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/ryu-devel

Reply via email to