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
