From bf7f4ee1e1ab2bd27a1ae21f87d26a02390494f3 Mon Sep 17 00:00:00 2001 From: Ben Thomasson Date: Mon, 8 Jan 2018 11:34:23 -0500 Subject: [PATCH] Adds network UI test framework This adds a test framework to drive UI tests from the client instead of injecting events from the websocket. Tests consist of a pair of snapshots (before and after the test) and a list of UI events to process. Tests are run using a FSM in the client that controls the resetting of state to the snapshot, injecting the events into the UI, recording test coverage, and reporting tests to the server. * Adds design for event trace table * Adds design for a coverage tracking table * Adds models for EventTrace and Coverage * Adds trace_id to recording messages * Adds design for TopologySnapshot table * Adds order to TopologySnapshot table * Adds TopologySnapshot table * Adds Snapshot message when recordings are started and stoppped * Adds models for tracking test cases and test results * Adds designs for a test runner FSM * Updates test management commands with new schema * Adds download recording button * Adds models to track tests * Adds ui test runner * Adds id and client to TestResult design * Adds id and client to TestResult * Update message types * Stores test results and code coverage from the test runner * Adds tool to generate a test coverage report * Adds APIs for tests and code coverage * Adds per-test-case coverage reports * Breaks out coverage for loading the modules from the tests * Re-raises server-side errors * Captures errors during tests * Adds defaults for host name and host type * Disables test FSM trace storage * Adds support for sending server error message to the client * Resets the UI flags, history, and toolbox contents between tests * Adds istanbul instrumentation to network-ui --- awx/network_ui/admin.py | 70 ++++++ awx/network_ui/consumers.py | 91 ++++++-- awx/network_ui/designs/models.yml | 128 ++++++++++- .../create_2_tier_clos_test_topology.py | 33 +-- .../create_worst_case_test_topology.py | 37 +++- awx/network_ui/management/commands/util.py | 5 + awx/network_ui/migrations/0027_eventtrace.py | 24 ++ awx/network_ui/migrations/0028_coverage.py | 23 ++ .../migrations/0029_topologysnapshot.py | 27 +++ .../migrations/0030_auto_20180110_1751.py | 66 ++++++ .../migrations/0031_auto_20180110_1752.py | 32 +++ .../migrations/0032_auto_20180112_2135.py | 27 +++ .../migrations/0033_auto_20180112_2202.py | 82 +++++++ .../migrations/0034_auto_20180113_1725.py | 24 ++ awx/network_ui/models.py | 60 +++++ .../static/network_ui/designs/messages.yml | 24 +- .../static/network_ui/designs/test.yml | 49 +++++ awx/network_ui/static/network_ui/test.js | 112 ++++++++++ .../templates/network_ui/index.html | 11 - .../templates/network_ui/upload_test.html | 5 + awx/network_ui/tools/Makefile | 13 ++ awx/network_ui/tools/coverage_report.py | 61 ++++++ awx/network_ui/tools/requirements.txt | 2 + awx/network_ui/urls.py | 4 + awx/network_ui/views.py | 84 ++++++- awx/ui/build/webpack.base.js | 11 + awx/ui/client/src/network-ui/messages.js | 75 ++++++- awx/ui/client/src/network-ui/models.js | 31 ++- .../client/src/network-ui/network.ui.app.js | 2 + .../src/network-ui/network.ui.controller.js | 206 ++++++++++++++++-- .../src/network-ui/network_ui.partial.svg | 1 + awx/ui/client/src/network-ui/style.less | 5 + awx/ui/client/src/network-ui/test.fsm.js | 160 ++++++++++++++ .../src/network-ui/test_results.directive.js | 8 + .../src/network-ui/test_results.partial.svg | 14 ++ awx/ui/client/src/network-ui/time.js | 21 ++ awx/ui/client/src/network-ui/tower.app.js | 2 +- awx/ui/package.json | 1 + 38 files changed, 1534 insertions(+), 97 deletions(-) create mode 100644 awx/network_ui/management/commands/util.py create mode 100644 awx/network_ui/migrations/0027_eventtrace.py create mode 100644 awx/network_ui/migrations/0028_coverage.py create mode 100644 awx/network_ui/migrations/0029_topologysnapshot.py create mode 100644 awx/network_ui/migrations/0030_auto_20180110_1751.py create mode 100644 awx/network_ui/migrations/0031_auto_20180110_1752.py create mode 100644 awx/network_ui/migrations/0032_auto_20180112_2135.py create mode 100644 awx/network_ui/migrations/0033_auto_20180112_2202.py create mode 100644 awx/network_ui/migrations/0034_auto_20180113_1725.py create mode 100644 awx/network_ui/static/network_ui/designs/test.yml create mode 100644 awx/network_ui/static/network_ui/test.js delete mode 100644 awx/network_ui/templates/network_ui/index.html create mode 100644 awx/network_ui/templates/network_ui/upload_test.html create mode 100644 awx/network_ui/tools/Makefile create mode 100755 awx/network_ui/tools/coverage_report.py create mode 100644 awx/network_ui/tools/requirements.txt create mode 100644 awx/ui/client/src/network-ui/test.fsm.js create mode 100644 awx/ui/client/src/network-ui/test_results.directive.js create mode 100644 awx/ui/client/src/network-ui/test_results.partial.svg diff --git a/awx/network_ui/admin.py b/awx/network_ui/admin.py index 3154699382..a1bb367306 100644 --- a/awx/network_ui/admin.py +++ b/awx/network_ui/admin.py @@ -36,6 +36,20 @@ from awx.network_ui.models import FSMTrace from awx.network_ui.models import TopologyInventory +from awx.network_ui.models import EventTrace + +from awx.network_ui.models import Coverage + +from awx.network_ui.models import TopologySnapshot + +from awx.network_ui.models import TestCase + +from awx.network_ui.models import Result + +from awx.network_ui.models import CodeUnderTest + +from awx.network_ui.models import TestResult + class DeviceAdmin(admin.ModelAdmin): fields = ('topology', 'name', 'x', 'y', 'id', 'type', 'interface_id_seq', 'process_id_seq', 'host_id',) @@ -179,3 +193,59 @@ class TopologyInventoryAdmin(admin.ModelAdmin): admin.site.register(TopologyInventory, TopologyInventoryAdmin) + + +class EventTraceAdmin(admin.ModelAdmin): + fields = ('client', 'trace_session_id', 'event_data', 'message_id',) + raw_id_fields = ('client',) + + +admin.site.register(EventTrace, EventTraceAdmin) + + +class CoverageAdmin(admin.ModelAdmin): + fields = ('coverage_data', 'test_result',) + raw_id_fields = ('test_result',) + + +admin.site.register(Coverage, CoverageAdmin) + + +class TopologySnapshotAdmin(admin.ModelAdmin): + fields = ('client', 'topology_id', 'trace_session_id', 'snapshot_data', 'order',) + raw_id_fields = ('client', 'snapshot_data',) + + +admin.site.register(TopologySnapshot, TopologySnapshotAdmin) + + +class TestCaseAdmin(admin.ModelAdmin): + fields = ('name', 'test_case_data',) + raw_id_fields = ('name',) + + +admin.site.register(TestCase, TestCaseAdmin) + + +class ResultAdmin(admin.ModelAdmin): + fields = ('name',) + raw_id_fields = () + + +admin.site.register(Result, ResultAdmin) + + +class CodeUnderTestAdmin(admin.ModelAdmin): + fields = ('version_x', 'version_y', 'version_z', 'commits_since', 'commit_hash',) + raw_id_fields = ('code_under_test_id',) + + +admin.site.register(CodeUnderTest, CodeUnderTestAdmin) + + +class TestResultAdmin(admin.ModelAdmin): + fields = ('test_case', 'result', 'code_under_test', 'time', 'id', 'client',) + raw_id_fields = ('test_case', 'result', 'code_under_test', 'client',) + + +admin.site.register(TestResult, TestResultAdmin) diff --git a/awx/network_ui/consumers.py b/awx/network_ui/consumers.py index de5a0c262b..6b51847d2c 100644 --- a/awx/network_ui/consumers.py +++ b/awx/network_ui/consumers.py @@ -8,8 +8,9 @@ from awx.network_ui.models import GroupDevice as GroupDeviceMap from awx.network_ui.models import DataSheet, DataBinding, DataType from awx.network_ui.models import Process, Stream from awx.network_ui.models import Toolbox, ToolboxItem -from awx.network_ui.models import FSMTrace +from awx.network_ui.models import FSMTrace, EventTrace, Coverage, TopologySnapshot from awx.network_ui.models import TopologyInventory +from awx.network_ui.models import TestCase, TestResult, CodeUnderTest, Result from awx.network_ui.messages import MultipleMessage, InterfaceCreate, LinkCreate, to_dict import urlparse from django.core.exceptions import ObjectDoesNotExist @@ -18,14 +19,14 @@ from collections import defaultdict import math import random import logging +from django.utils.dateparse import parse_datetime + from awx.network_ui.utils import transform_dict import dpath.util from pprint import pformat -import os import json -import time # Connected to websocket.connect HISTORY_MESSAGE_IGNORE_TYPES = ['DeviceSelected', @@ -44,6 +45,10 @@ RACK_SPACING = 50 logger = logging.getLogger("awx.network_ui.consumers") +class NetworkUIException(Exception): + + pass + def circular_layout(topology_id): n = Device.objects.filter(topology_id=topology_id).count() @@ -281,7 +286,7 @@ class _Persistence(object): try: message_type_id = MessageType.objects.get(name=message_type).pk except ObjectDoesNotExist: - logger.warning("Unsupported message %s", message_type) + logger.warning("Unsupported message %s: no message type", message_type) return TopologyHistory(topology_id=topology_id, client_id=client_id, @@ -290,9 +295,19 @@ class _Persistence(object): message_data=message['text']).save() handler = self.get_handler(message_type) if handler is not None: - handler(message_value, topology_id, client_id) + try: + handler(message_value, topology_id, client_id) + except NetworkUIException, e: + Group("client-%s" % client_id).send({"text": json.dumps(["Error", str(e)])}) + raise + except Exception, e: + Group("client-%s" % client_id).send({"text": json.dumps(["Error", "Server Error"])}) + raise + except BaseException, e: + Group("client-%s" % client_id).send({"text": json.dumps(["Error", "Server Error"])}) + raise else: - logger.warning("Unsupported message %s", message_type) + logger.warning("Unsupported message %s: no handler", message_type) def get_handler(self, message_type): return getattr(self, "on{0}".format(message_type), None) @@ -454,12 +469,44 @@ class _Persistence(object): # grid_layout(topology_id) tier_layout(topology_id) + def onCoverageRequest(self, coverage, topology_id, client_id): pass + def onTestResult(self, test_result, topology_id, client_id): + xyz, _, rest = test_result['code_under_test'].partition('-') + commits_since, _, commit_hash = rest.partition('-') + commit_hash = commit_hash.strip('g') + + print (xyz) + print (commits_since) + print (commit_hash) + + x, y, z = [int(i) for i in xyz.split('.')] + + print (x, y, z) + + code_under_test, _ = CodeUnderTest.objects.get_or_create(version_x=x, + version_y=y, + version_z=z, + commits_since=int(commits_since), + commit_hash=commit_hash) + + print (code_under_test) + + tr = TestResult(id=test_result['id'], + result_id=Result.objects.get(name=test_result['result']).pk, + test_case_id=TestCase.objects.get(name=test_result['name']).pk, + code_under_test_id=code_under_test.pk, + client_id=client_id, + time=parse_datetime(test_result['date'])) + tr.save() + print (tr.pk) + + def onCoverage(self, coverage, topology_id, client_id): - with open(os.path.abspath("coverage/coverage{0}.json".format(int(time.time()))), "w") as f: - f.write(json.dumps(coverage['coverage'])) + Coverage(test_result_id=TestResult.objects.get(id=coverage['result_id'], client_id=client_id).pk, + coverage_data=json.dumps(coverage['coverage'])).save() def onStartRecording(self, recording, topology_id, client_id): pass @@ -469,9 +516,10 @@ class _Persistence(object): def write_event(self, event, topology_id, client_id): if event.get('save', True): - with open(os.path.abspath("recording/recording_{0}.log".format(topology_id)), "a") as f: - f.write(json.dumps(event)) - f.write("\n") + EventTrace(trace_session_id=event['trace_id'], + event_data=json.dumps(event), + message_id=event['message_id'], + client_id=client_id).save() onViewPort = write_event onMouseEvent = write_event @@ -538,6 +586,13 @@ class _Persistence(object): client_id=client_id, message_type=message_value['recv_message_type'] or "none").save() + def onSnapshot(self, snapshot, topology_id, client_id): + TopologySnapshot(trace_session_id=snapshot['trace_id'], + snapshot_data=json.dumps(snapshot), + order=snapshot['order'], + client_id=client_id, + topology_id=topology_id).save() + persistence = _Persistence() @@ -554,7 +609,7 @@ class _UndoPersistence(object): if handler is not None: handler(message_value, topology_id, client_id) else: - logger.warnding("Unsupported undo message %s", message_type) + logger.warning("Unsupported undo message %s", message_type) def onSnapshot(self, snapshot, topology_id, client_id): pass @@ -648,7 +703,7 @@ class _Discovery(object): if handler is not None: handler(message_value, topology_id) else: - logger.warning("Unsupported message %s", message_type) + logger.warning("Unsupported discovery message %s", message_type) def get_handler(self, message_type): return getattr(self, "on{0}".format(message_type), None) @@ -810,6 +865,7 @@ def ws_connect(message): client = Client() client.save() message.channel_session['client_id'] = client.pk + Group("client-%s" % client.pk).add(message.reply_channel) message.reply_channel.send({"text": json.dumps(["id", client.pk])}) message.reply_channel.send({"text": json.dumps(["topology_id", topology_id])}) topology_data = transform_dict(dict(topology_id='topology_id', @@ -825,6 +881,7 @@ def ws_connect(message): send_snapshot(message.reply_channel, topology_id) send_history(message.reply_channel, topology_id) send_toolboxes(message.reply_channel) + send_tests(message.reply_channel) def send_toolboxes(channel): @@ -834,6 +891,11 @@ def send_toolboxes(channel): channel.send({"text": json.dumps(["ToolboxItem", item])}) +def send_tests(channel): + for name, test_case_data in TestCase.objects.all().values_list('name', 'test_case_data'): + channel.send({"text": json.dumps(["TestCase", [name, json.loads(test_case_data)]])}) + + def send_snapshot(channel, topology_id): interfaces = defaultdict(list) processes = defaultdict(list) @@ -919,7 +981,8 @@ def ws_message(message): @channel_session def ws_disconnect(message): - Group("topology-%s" % message.channel_session['topology_id']).discard(message.reply_channel) + if 'topology_id' in message.channel_session: + Group("topology-%s" % message.channel_session['topology_id']).discard(message.reply_channel) def console_printer(message): diff --git a/awx/network_ui/designs/models.yml b/awx/network_ui/designs/models.yml index d1384fc837..e204679670 100644 --- a/awx/network_ui/designs/models.yml +++ b/awx/network_ui/designs/models.yml @@ -102,8 +102,8 @@ models: pk: true type: AutoField name: Client - x: -510 - y: 141 + x: -518 + y: 138 - fields: - name: topology_history_id pk: true @@ -365,6 +365,130 @@ models: name: TopologyInventory x: -226 y: -19 +- fields: + - name: event_trace_id + pk: true + type: AutoField + - name: client + ref: Client + ref_field: client_id + type: ForeignKey + - default: 0 + name: trace_session_id + type: IntegerField + - name: event_data + type: TextField + - name: message_id + type: IntegerField + name: EventTrace + x: -1087 + y: 202 +- fields: + - name: coverage_id + pk: true + type: AutoField + - name: coverage_data + type: TextField + - name: test_result + ref: TestResult + ref_field: test_result_id + type: ForeignKey + name: Coverage + x: -1068 + y: -4 +- fields: + - name: topology_snapshot_id + pk: true + type: AutoField + - name: client + ref: Client + ref_field: client_id + type: ForeignKey + - name: topology_id + type: IntegerField + - name: trace_session_id + type: IntegerField + - name: snapshot_data + ref: TopologySnapshot + ref_field: snapshot_data + type: TextField + - name: order + type: IntegerField + name: TopologySnapshot + x: -1123 + y: -277 +- fields: + - name: test_case_id + pk: true + type: AutoField + - len: 200 + name: name + ref: TestCase + ref_field: name + type: CharField + - name: test_case_data + type: TextField + name: TestCase + x: -1642 + y: -38 +- fields: + - name: result_id + pk: true + type: AutoField + - len: 20 + name: name + type: CharField + name: Result + x: -1610 + y: 120 +- fields: + - name: code_under_test_id + pk: true + ref: CodeUnderTest + ref_field: code_under_test_id + type: AutoField + - name: version_x + type: IntegerField + - name: version_y + type: IntegerField + - name: version_z + type: IntegerField + - name: commits_since + type: IntegerField + - len: 40 + name: commit_hash + type: CharField + name: CodeUnderTest + x: -1612 + y: 259 +- fields: + - name: test_result_id + pk: true + type: AutoField + - name: test_case + ref: TestCase + ref_field: test_case_id + type: ForeignKey + - name: result + ref: Result + ref_field: result_id + type: ForeignKey + - name: code_under_test + ref: CodeUnderTest + ref_field: code_under_test_id + type: ForeignKey + - name: time + type: DateTimeField + - default: 0 + name: id + type: IntegerField + - name: client + ref: Client + ref_field: client_id + type: ForeignKey + name: TestResult + x: -1336 + y: -49 modules: [] view: panX: 213.729555519212 diff --git a/awx/network_ui/management/commands/create_2_tier_clos_test_topology.py b/awx/network_ui/management/commands/create_2_tier_clos_test_topology.py index b0555086e9..e558b1a2f2 100644 --- a/awx/network_ui/management/commands/create_2_tier_clos_test_topology.py +++ b/awx/network_ui/management/commands/create_2_tier_clos_test_topology.py @@ -3,26 +3,22 @@ from django.core.management.base import BaseCommand from awx.network_ui.models import Topology, Device, Link, Interface from collections import defaultdict - - -def natural_numbers(): - i = 1 - while True: - yield i - i += 1 +from .util import natural_numbers class Command(BaseCommand): - help = '''Creates a 2 tier clos topology with n nodes in the 1st tier and m nodes - in the 2nd tier and h hosts per pair of switches''' + help = '''Adds a 2 tier clos topology with n nodes in the 1st tier and m nodes + in the 2nd tier and h hosts per pair of switches to the topology with id `id`''' def add_arguments(self, parser): + parser.add_argument('id', type=int) parser.add_argument('n', type=int) parser.add_argument('m', type=int) parser.add_argument('h', type=int) def handle(self, *args, **options): + topology_id = options['id'] n = options['n'] m = options['m'] h = options['h'] @@ -30,15 +26,15 @@ class Command(BaseCommand): print "n", n print "m", m - topology = Topology(name="test_{0}".format(n), scale=1.0, panX=0, panY=0) - topology.save() + topology = Topology.objects.get(pk=topology_id) devices = [] hosts_per_leaf = [] leaves = [] spines = [] - id_seq = natural_numbers() + id_seq = natural_numbers(topology.device_id_seq) + link_id_seq = natural_numbers(topology.link_id_seq) tier2 = 100 tier1 = 500 @@ -106,7 +102,8 @@ class Command(BaseCommand): link = Link(from_device=devices[leaf.id], to_device=devices[spine.id], from_interface=from_interface, - to_interface=to_interface) + to_interface=to_interface, + id=next(link_id_seq)) links.append(link) for i, hosts in enumerate(hosts_per_leaf): leaf1 = leaves[2 * i] @@ -125,7 +122,8 @@ class Command(BaseCommand): link = Link(from_device=devices[leaf1.id], to_device=devices[host.id], from_interface=from_interface, - to_interface=to_interface) + to_interface=to_interface, + id=next(link_id_seq)) links.append(link) from_interface = Interface(device=devices[leaf2.id], name="swp" + str(len(interfaces[leaf2.id]) + 1), @@ -140,9 +138,14 @@ class Command(BaseCommand): link = Link(from_device=devices[leaf2.id], to_device=devices[host.id], from_interface=from_interface, - to_interface=to_interface) + to_interface=to_interface, + id=next(link_id_seq)) links.append(link) Link.objects.bulk_create(links) + topology.device_id_seq = next(id_seq) + topology.link_id_seq = next(link_id_seq) + topology.save() + print "Topology: ", topology.pk diff --git a/awx/network_ui/management/commands/create_worst_case_test_topology.py b/awx/network_ui/management/commands/create_worst_case_test_topology.py index 4a2312ed6e..34eb5c9837 100644 --- a/awx/network_ui/management/commands/create_worst_case_test_topology.py +++ b/awx/network_ui/management/commands/create_worst_case_test_topology.py @@ -1,24 +1,29 @@ # Copyright (c) 2017 Red Hat, Inc from django.core.management.base import BaseCommand -from awx.network_ui.models import Topology, Device, Link +from awx.network_ui.models import Topology, Device, Link, Interface import math +from collections import defaultdict +from .util import natural_numbers class Command(BaseCommand): - help = 'Creates a fully connected topology with n nodes' + help = 'Adds a fully connected topology with n nodes to topology pk id' def add_arguments(self, parser): + parser.add_argument('id', type=int) parser.add_argument('n', type=int) def handle(self, *args, **options): + topology_id = options['id'] n = options['n'] - topology = Topology(name="test_{0}".format(n), scale=1.0, panX=0, panY=0) - topology.save() + topology = Topology.objects.get(topology_id=topology_id) + link_id_seq = natural_numbers(topology.link_id_seq) + device_id_seq = natural_numbers(topology.device_id_seq) devices = [] r = 1000 @@ -30,25 +35,39 @@ class Command(BaseCommand): for i in xrange(n): device = Device(name="R{0}".format(i), - x=math.cos(arc_radians*i)*r, - y=math.sin(arc_radians*i)*r, - id=i, + x=math.cos(arc_radians * i) * r, + y=math.sin(arc_radians * i) * r, + id=next(device_id_seq), type="router", topology_id=topology.pk) devices.append(device) Device.objects.bulk_create(devices) - devices = {x.id: x for x in Device.objects.filter(topology_id=topology.pk)} + devices = list(Device.objects.filter(topology_id=topology.pk)) links = [] + interfaces = defaultdict(list) for i in xrange(n): for j in xrange(i): if i == j: continue + from_interface = Interface(device=devices[i], + name="swp" + str(len(interfaces[i]) + 1), + id=(len(interfaces[i]) + 1)) + from_interface.save() + interfaces[i].append(from_interface) + to_interface = Interface(device=devices[j], + name="swp" + str(len(interfaces[j]) + 1), + id=(len(interfaces[j]) + 1)) + to_interface.save() + interfaces[j].append(to_interface) link = Link(from_device=devices[i], - to_device=devices[j]) + to_device=devices[j], + from_interface=from_interface, + to_interface=to_interface, + id=next(link_id_seq)) links.append(link) Link.objects.bulk_create(links) diff --git a/awx/network_ui/management/commands/util.py b/awx/network_ui/management/commands/util.py new file mode 100644 index 0000000000..8e28a44704 --- /dev/null +++ b/awx/network_ui/management/commands/util.py @@ -0,0 +1,5 @@ + +def natural_numbers(i=1): + while True: + yield i + i += 1 diff --git a/awx/network_ui/migrations/0027_eventtrace.py b/awx/network_ui/migrations/0027_eventtrace.py new file mode 100644 index 0000000000..05b65c0972 --- /dev/null +++ b/awx/network_ui/migrations/0027_eventtrace.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('network_ui', '0026_auto_20180105_1403'), + ] + + operations = [ + migrations.CreateModel( + name='EventTrace', + fields=[ + ('event_trace_id', models.AutoField(serialize=False, primary_key=True)), + ('trace_session_id', models.IntegerField(default=0)), + ('event_data', models.TextField()), + ('message_id', models.IntegerField()), + ('client', models.ForeignKey(to='network_ui.Client')), + ], + ), + ] diff --git a/awx/network_ui/migrations/0028_coverage.py b/awx/network_ui/migrations/0028_coverage.py new file mode 100644 index 0000000000..f799f726c5 --- /dev/null +++ b/awx/network_ui/migrations/0028_coverage.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('network_ui', '0027_eventtrace'), + ] + + operations = [ + migrations.CreateModel( + name='Coverage', + fields=[ + ('coverage_id', models.AutoField(serialize=False, primary_key=True)), + ('trace_session_id', models.IntegerField()), + ('coverage_data', models.TextField()), + ('client', models.ForeignKey(to='network_ui.Client')), + ], + ), + ] diff --git a/awx/network_ui/migrations/0029_topologysnapshot.py b/awx/network_ui/migrations/0029_topologysnapshot.py new file mode 100644 index 0000000000..e185927b32 --- /dev/null +++ b/awx/network_ui/migrations/0029_topologysnapshot.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.7 on 2018-01-09 17:12 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('network_ui', '0028_coverage'), + ] + + operations = [ + migrations.CreateModel( + name='TopologySnapshot', + fields=[ + ('topology_snapshot_id', models.AutoField(primary_key=True, serialize=False)), + ('topology_id', models.IntegerField()), + ('trace_session_id', models.IntegerField()), + ('snapshot_data', models.TextField(verbose_name=b'TopologySnapshot')), + ('order', models.IntegerField()), + ('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='network_ui.Client')), + ], + ), + ] diff --git a/awx/network_ui/migrations/0030_auto_20180110_1751.py b/awx/network_ui/migrations/0030_auto_20180110_1751.py new file mode 100644 index 0000000000..d21ca4d456 --- /dev/null +++ b/awx/network_ui/migrations/0030_auto_20180110_1751.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.7 on 2018-01-10 17:51 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('network_ui', '0029_topologysnapshot'), + ] + + operations = [ + migrations.CreateModel( + name='CodeUnderTest', + fields=[ + ('code_under_test_id', models.AutoField(primary_key=True, serialize=False, verbose_name=b'CodeUnderTest')), + ('version_x', models.IntegerField()), + ('version_y', models.IntegerField()), + ('version_z', models.IntegerField()), + ('commits_since', models.IntegerField()), + ('commit_hash', models.CharField(max_length=40)), + ], + ), + migrations.CreateModel( + name='Result', + fields=[ + ('result_id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=20)), + ], + ), + migrations.CreateModel( + name='TestCase', + fields=[ + ('test_case_id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=200, verbose_name=b'TestCase')), + ('test_case_data', models.TextField()), + ], + ), + migrations.CreateModel( + name='TestResult', + fields=[ + ('test_result_id', models.AutoField(primary_key=True, serialize=False)), + ('time', models.DateTimeField()), + ('code_under_test', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='network_ui.CodeUnderTest')), + ('result', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='network_ui.Result')), + ('test_case', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='network_ui.TestCase')), + ], + ), + migrations.RemoveField( + model_name='coverage', + name='client', + ), + migrations.RemoveField( + model_name='coverage', + name='trace_session_id', + ), + migrations.AddField( + model_name='coverage', + name='test_result', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='network_ui.TestResult'), + preserve_default=False, + ), + ] diff --git a/awx/network_ui/migrations/0031_auto_20180110_1752.py b/awx/network_ui/migrations/0031_auto_20180110_1752.py new file mode 100644 index 0000000000..c4474da956 --- /dev/null +++ b/awx/network_ui/migrations/0031_auto_20180110_1752.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.7 on 2018-01-10 17:52 +from __future__ import unicode_literals + +from django.db import migrations + +results = ['passed', + 'failed', + 'errored', + 'skipped', + 'aborted', + 'not run', + 'blocked'] + +def populate_result_types(apps, schema_editor): + + Result = apps.get_model('network_ui', 'Result') + for result in results: + Result.objects.get_or_create(name=result) + + +class Migration(migrations.Migration): + + dependencies = [ + ('network_ui', '0030_auto_20180110_1751'), + ] + + operations = [ + migrations.RunPython( + code=populate_result_types, + ), + ] diff --git a/awx/network_ui/migrations/0032_auto_20180112_2135.py b/awx/network_ui/migrations/0032_auto_20180112_2135.py new file mode 100644 index 0000000000..c43827ef58 --- /dev/null +++ b/awx/network_ui/migrations/0032_auto_20180112_2135.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.7 on 2018-01-12 21:35 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('network_ui', '0031_auto_20180110_1752'), + ] + + operations = [ + migrations.AddField( + model_name='testresult', + name='client', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='network_ui.Client'), + preserve_default=False, + ), + migrations.AddField( + model_name='testresult', + name='id', + field=models.IntegerField(default=0), + ), + ] diff --git a/awx/network_ui/migrations/0033_auto_20180112_2202.py b/awx/network_ui/migrations/0033_auto_20180112_2202.py new file mode 100644 index 0000000000..c88ebe9cae --- /dev/null +++ b/awx/network_ui/migrations/0033_auto_20180112_2202.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.7 on 2018-01-12 22:02 +from __future__ import unicode_literals + +from django.db import migrations + + +import yaml + +messages = yaml.load(''' +messages: + - {msg_type: DeviceMove, fields: [msg_type, sender, id, x, y, previous_x, previous_y]} + - {msg_type: DeviceCreate, fields: [msg_type, sender, id, x, y, name, type, host_id]} + - {msg_type: DeviceDestroy, fields: [msg_type, sender, id, previous_x, previous_y, previous_name, previous_type, previous_host_id]} + - {msg_type: DeviceLabelEdit, fields: [msg_type, sender, id, name, previous_name]} + - {msg_type: DeviceSelected, fields: [msg_type, sender, id]} + - {msg_type: DeviceUnSelected, fields: [msg_type, sender, id]} + - {msg_type: InterfaceCreate, fields: [msg_type, sender, device_id, id, name]} + - {msg_type: InterfaceLabelEdit, fields: [msg_type, sender, id, device_id, name, previous_name]} + - {msg_type: LinkLabelEdit, fields: [msg_type, sender, id, name, previous_name]} + - {msg_type: LinkCreate, fields: [msg_type, id, sender, name, from_device_id, to_device_id, from_interface_id, to_interface_id]} + - {msg_type: LinkDestroy, fields: [msg_type, id, sender, name, from_device_id, to_device_id, from_interface_id, to_interface_id]} + - {msg_type: LinkSelected, fields: [msg_type, sender, id]} + - {msg_type: LinkUnSelected, fields: [msg_type, sender, id]} + - {msg_type: Undo, fields: [msg_type, sender, original_message]} + - {msg_type: Redo, fields: [msg_type, sender, original_message]} + - {msg_type: Deploy, fields: [msg_type, sender]} + - {msg_type: Destroy, fields: [msg_type, sender]} + - {msg_type: Discover, fields: [msg_type, sender]} + - {msg_type: Layout, fields: [msg_type, sender]} + - {msg_type: MultipleMessage, fields: [msg_type, sender, messages]} + - {msg_type: MouseEvent, fields: [msg_type, sender, x, y, type, trace_id]} + - {msg_type: MouseWheelEvent, fields: [msg_type, sender, delta, deltaX, deltaY, type, originalEvent, trace_id]} + - {msg_type: KeyEvent, fields: [msg_type, sender, key, keyCode, type, altKey, shiftKey, ctrlKey, metaKey, trace_id]} + - {msg_type: TouchEvent, fields: [msg_type, sender, type, touches]} + - {msg_type: StartRecording, fields: [msg_type, sender, trace_id]} + - {msg_type: StopRecording, fields: [msg_type, sender, trace_id]} + - {msg_type: ViewPort, fields: [msg_type, sender, scale, panX, panY, trace_id]} + - {msg_type: CopySite, fields: [msg_type, site]} + - {msg_type: GroupMove, fields: [msg_type, sender, id, x1, y1, x2, y2, previous_x1, previous_y1, previous_x2, previous_y2]} + - {msg_type: GroupCreate, fields: [msg_type, sender, id, x1, y1, x2, y2, name, type]} + - {msg_type: GroupDestroy, fields: [msg_type, sender, id, previous_x1, previous_y1, previous_x2, previous_y2, previous_name, previous_type]} + - {msg_type: GroupLabelEdit, fields: [msg_type, sender, id, name, previous_name]} + - {msg_type: GroupSelected, fields: [msg_type, sender, id]} + - {msg_type: GroupUnSelected, fields: [msg_type, sender, id]} + - {msg_type: GroupMembership, fields: [msg_type, sender, id, members]} + - {msg_type: TableCellEdit, fields: [msg_type, sender, sheet, col, row, old_value, new_value]} + - {msg_type: ProcessCreate, fields: [msg_type, id, name, type, device_id, x, y]} + - {msg_type: StreamCreate, fields: [msg_type, sender, id, from_id, to_id, label]} + - {msg_type: StreamDestroy, fields: [msg_type, sender, id, from_id, to_id, label]} + - {msg_type: StreamLabelEdit, fields: [msg_type, sender, id, label, previous_label]} + - {msg_type: StreamSelected, fields: [msg_type, sender, id]} + - {msg_type: StreamUnSelected, fields: [msg_type, sender, id]} + - {msg_type: FSMTrace, fields: [msg_type, order, sender, trace_id, fsm_name, from_state, to_state, recv_message_type]} + - {msg_type: ChannelTrace, fields: [msg_type, sender, trace_id, from_fsm, to_fsm, sent_message_type]} + - {msg_type: Snapshot, fields: [msg_type, sender, devices, links, groups, streams, order, trace_id]} + - {msg_type: EnableTest, fields: [msg_type]} + - {msg_type: DisableTest, fields: [msg_type]} + - {msg_type: StartTest, fields: [msg_type]} + - {msg_type: TestCompleted, fields: [msg_type]} + - {msg_type: TestResult, fields: [msg_type, sender, id, name, result, date, code_under_test]} + - {msg_type: Coverage, fields: [msg_type, sender, coverage, result_id]} +''') + + +def populate_message_types(apps, schema_editor): + + MessageType = apps.get_model('network_ui', 'MessageType') + for message in messages['messages']: + MessageType.objects.get_or_create(name=message['msg_type']) + +class Migration(migrations.Migration): + + dependencies = [ + ('network_ui', '0032_auto_20180112_2135'), + ] + + operations = [ + migrations.RunPython( + code=populate_message_types, + ), + ] diff --git a/awx/network_ui/migrations/0034_auto_20180113_1725.py b/awx/network_ui/migrations/0034_auto_20180113_1725.py new file mode 100644 index 0000000000..8a062d468d --- /dev/null +++ b/awx/network_ui/migrations/0034_auto_20180113_1725.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.7 on 2018-01-13 17:25 +from __future__ import unicode_literals + +from django.db import migrations +import json + +def add_load_test_case(apps, schema_editor): + + TestCase = apps.get_model('network_ui', 'TestCase') + TestCase.objects.get_or_create(name="Load", test_case_data=json.dumps(dict(runnable=False))) + + +class Migration(migrations.Migration): + + dependencies = [ + ('network_ui', '0033_auto_20180112_2202'), + ] + + operations = [ + migrations.RunPython( + code=add_load_test_case, + ), + ] diff --git a/awx/network_ui/models.py b/awx/network_ui/models.py index 7c3cee39dd..c18159cc97 100644 --- a/awx/network_ui/models.py +++ b/awx/network_ui/models.py @@ -175,3 +175,63 @@ class TopologyInventory(models.Model): topology_inventory_id = models.AutoField(primary_key=True,) topology = models.ForeignKey('Topology',) inventory_id = models.IntegerField() + + +class EventTrace(models.Model): + + event_trace_id = models.AutoField(primary_key=True,) + client = models.ForeignKey('Client',) + trace_session_id = models.IntegerField(default=0) + event_data = models.TextField() + message_id = models.IntegerField() + + +class Coverage(models.Model): + + coverage_id = models.AutoField(primary_key=True,) + coverage_data = models.TextField() + test_result = models.ForeignKey('TestResult',) + + +class TopologySnapshot(models.Model): + + topology_snapshot_id = models.AutoField(primary_key=True,) + client = models.ForeignKey('Client',) + topology_id = models.IntegerField() + trace_session_id = models.IntegerField() + snapshot_data = models.TextField('TopologySnapshot',) + order = models.IntegerField() + + +class TestCase(models.Model): + + test_case_id = models.AutoField(primary_key=True,) + name = models.CharField('TestCase', max_length=200,) + test_case_data = models.TextField() + + +class Result(models.Model): + + result_id = models.AutoField(primary_key=True,) + name = models.CharField(max_length=20,) + + +class CodeUnderTest(models.Model): + + code_under_test_id = models.AutoField('CodeUnderTest', primary_key=True,) + version_x = models.IntegerField() + version_y = models.IntegerField() + version_z = models.IntegerField() + commits_since = models.IntegerField() + commit_hash = models.CharField(max_length=40,) + + +class TestResult(models.Model): + + test_result_id = models.AutoField(primary_key=True,) + test_case = models.ForeignKey('TestCase',) + result = models.ForeignKey('Result',) + code_under_test = models.ForeignKey('CodeUnderTest',) + time = models.DateTimeField() + id = models.IntegerField(default=0) + client = models.ForeignKey('Client',) diff --git a/awx/network_ui/static/network_ui/designs/messages.yml b/awx/network_ui/static/network_ui/designs/messages.yml index 92813cb71e..c0d8c0fa7d 100644 --- a/awx/network_ui/static/network_ui/designs/messages.yml +++ b/awx/network_ui/static/network_ui/designs/messages.yml @@ -1,7 +1,7 @@ messages: - {msg_type: DeviceMove, fields: [msg_type, sender, id, x, y, previous_x, previous_y]} - - {msg_type: DeviceCreate, fields: [msg_type, sender, id, x, y, name, type]} - - {msg_type: DeviceDestroy, fields: [msg_type, sender, id, previous_x, previous_y, previous_name, previous_type]} + - {msg_type: DeviceCreate, fields: [msg_type, sender, id, x, y, name, type, host_id]} + - {msg_type: DeviceDestroy, fields: [msg_type, sender, id, previous_x, previous_y, previous_name, previous_type, previous_host_id]} - {msg_type: DeviceLabelEdit, fields: [msg_type, sender, id, name, previous_name]} - {msg_type: DeviceSelected, fields: [msg_type, sender, id]} - {msg_type: DeviceUnSelected, fields: [msg_type, sender, id]} @@ -19,14 +19,13 @@ messages: - {msg_type: Discover, fields: [msg_type, sender]} - {msg_type: Layout, fields: [msg_type, sender]} - {msg_type: MultipleMessage, fields: [msg_type, sender, messages]} - - {msg_type: Coverage, fields: [msg_type, sender, coverage]} - - {msg_type: MouseEvent, fields: [msg_type, sender, x, y, type]} - - {msg_type: MouseWheelEvent, fields: [msg_type, sender, delta, deltaX, deltaY, type, originalEvent]} - - {msg_type: KeyEvent, fields: [msg_type, sender, key, keyCode, type, altKey, shiftKey, ctrlKey, metaKey]} + - {msg_type: MouseEvent, fields: [msg_type, sender, x, y, type, trace_id]} + - {msg_type: MouseWheelEvent, fields: [msg_type, sender, delta, deltaX, deltaY, type, originalEvent, trace_id]} + - {msg_type: KeyEvent, fields: [msg_type, sender, key, keyCode, type, altKey, shiftKey, ctrlKey, metaKey, trace_id]} - {msg_type: TouchEvent, fields: [msg_type, sender, type, touches]} - - {msg_type: StartRecording, fields: [msg_type, sender]} - - {msg_type: StopRecording, fields: [msg_type, sender]} - - {msg_type: ViewPort, fields: [msg_type, sender, scale, panX, panY]} + - {msg_type: StartRecording, fields: [msg_type, sender, trace_id]} + - {msg_type: StopRecording, fields: [msg_type, sender, trace_id]} + - {msg_type: ViewPort, fields: [msg_type, sender, scale, panX, panY, trace_id]} - {msg_type: CopySite, fields: [msg_type, site]} - {msg_type: GroupMove, fields: [msg_type, sender, id, x1, y1, x2, y2, previous_x1, previous_y1, previous_x2, previous_y2]} - {msg_type: GroupCreate, fields: [msg_type, sender, id, x1, y1, x2, y2, name, type]} @@ -44,4 +43,11 @@ messages: - {msg_type: StreamUnSelected, fields: [msg_type, sender, id]} - {msg_type: FSMTrace, fields: [msg_type, order, sender, trace_id, fsm_name, from_state, to_state, recv_message_type]} - {msg_type: ChannelTrace, fields: [msg_type, sender, trace_id, from_fsm, to_fsm, sent_message_type]} + - {msg_type: Snapshot, fields: [msg_type, sender, devices, links, groups, streams, order, trace_id]} + - {msg_type: EnableTest, fields: [msg_type]} + - {msg_type: DisableTest, fields: [msg_type]} + - {msg_type: StartTest, fields: [msg_type]} + - {msg_type: TestCompleted, fields: [msg_type]} + - {msg_type: TestResult, fields: [msg_type, sender, id, name, result, date, code_under_test]} + - {msg_type: Coverage, fields: [msg_type, sender, coverage, result_id]} diff --git a/awx/network_ui/static/network_ui/designs/test.yml b/awx/network_ui/static/network_ui/designs/test.yml new file mode 100644 index 0000000000..a224b16690 --- /dev/null +++ b/awx/network_ui/static/network_ui/designs/test.yml @@ -0,0 +1,49 @@ +diagram_id: 42 +name: diagram +states: +- id: 6 + label: Reporting + x: 926 + y: 721 +- id: 2 + label: Disabled + x: 895 + y: 344 +- id: 5 + label: Running + x: 720 + y: 922 +- id: 1 + label: Ready + x: 722 + y: 509 +- id: 3 + label: Start + x: 702 + y: 186 +- id: 4 + label: Loading + x: 524 + y: 710 +transitions: +- from_state: Running + label: onTestCompleted + to_state: Reporting +- from_state: Ready + label: onDisable + to_state: Disabled +- from_state: Start + label: start + to_state: Disabled +- from_state: Loading + label: onTestLoaded + to_state: Running +- from_state: Disabled + label: onEnable + to_state: Ready +- from_state: Reporting + label: onTestReported + to_state: Ready +- from_state: Ready + label: onStartTest + to_state: Loading diff --git a/awx/network_ui/static/network_ui/test.js b/awx/network_ui/static/network_ui/test.js new file mode 100644 index 0000000000..3f850386d7 --- /dev/null +++ b/awx/network_ui/static/network_ui/test.js @@ -0,0 +1,112 @@ +var inherits = require('inherits'); +var fsm = require('./fsm.js'); + +function _State () { +} +inherits(_State, fsm._State); + + +function _Disabled () { + this.name = 'Disabled'; +} +inherits(_Disabled, _State); +var Disabled = new _Disabled(); +exports.Disabled = Disabled; + +function _Start () { + this.name = 'Start'; +} +inherits(_Start, _State); +var Start = new _Start(); +exports.Start = Start; + +function _Running () { + this.name = 'Running'; +} +inherits(_Running, _State); +var Running = new _Running(); +exports.Running = Running; + +function _Loading () { + this.name = 'Loading'; +} +inherits(_Loading, _State); +var Loading = new _Loading(); +exports.Loading = Loading; + +function _Ready () { + this.name = 'Ready'; +} +inherits(_Ready, _State); +var Ready = new _Ready(); +exports.Ready = Ready; + +function _Reporting () { + this.name = 'Reporting'; +} +inherits(_Reporting, _State); +var Reporting = new _Reporting(); +exports.Reporting = Reporting; + + + + +_Disabled.prototype.onEnable = function (controller) { + + controller.changeState(Ready); + +}; +_Disabled.prototype.onEnable.transitions = ['Ready']; + + + +_Start.prototype.start = function (controller) { + + controller.changeState(Disabled); + +}; +_Start.prototype.start.transitions = ['Disabled']; + + + +_Running.prototype.onTestCompleted = function (controller) { + + controller.changeState(Reporting); + +}; +_Running.prototype.onTestCompleted.transitions = ['Reporting']; + + + +_Loading.prototype.onTestLoaded = function (controller) { + + controller.changeState(Running); + +}; +_Loading.prototype.onTestLoaded.transitions = ['Running']; + + + +_Ready.prototype.onDisable = function (controller) { + + controller.changeState(Disabled); + +}; +_Ready.prototype.onDisable.transitions = ['Disabled']; + +_Ready.prototype.onStartTest = function (controller) { + + controller.changeState(Loading); + +}; +_Ready.prototype.onStartTest.transitions = ['Loading']; + + + +_Reporting.prototype.onTestReported = function (controller) { + + controller.changeState(Ready); + +}; +_Reporting.prototype.onTestReported.transitions = ['Ready']; + diff --git a/awx/network_ui/templates/network_ui/index.html b/awx/network_ui/templates/network_ui/index.html deleted file mode 100644 index fbee2cdc9b..0000000000 --- a/awx/network_ui/templates/network_ui/index.html +++ /dev/null @@ -1,11 +0,0 @@ - diff --git a/awx/network_ui/templates/network_ui/upload_test.html b/awx/network_ui/templates/network_ui/upload_test.html new file mode 100644 index 0000000000..4b23655229 --- /dev/null +++ b/awx/network_ui/templates/network_ui/upload_test.html @@ -0,0 +1,5 @@ +
+ {% csrf_token %} + {{form}} + +
diff --git a/awx/network_ui/tools/Makefile b/awx/network_ui/tools/Makefile new file mode 100644 index 0000000000..c736a388b9 --- /dev/null +++ b/awx/network_ui/tools/Makefile @@ -0,0 +1,13 @@ + +SERVER = "https://meganuke:8043" +PORT = "9000" + +.PHONY: clean coverage + +clean: + git clean -fdX . + git clean -fd . + +coverage: + ./coverage_report.py ${SERVER} + python -m SimpleHTTPServer ${PORT} diff --git a/awx/network_ui/tools/coverage_report.py b/awx/network_ui/tools/coverage_report.py new file mode 100755 index 0000000000..c4ad776b55 --- /dev/null +++ b/awx/network_ui/tools/coverage_report.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Usage: + coverage_report [options] + +Options: + -h, --help Show this page + --debug Show debug logging + --verbose Show verbose logging +""" +from docopt import docopt +import logging +import sys +import os +import requests +import subprocess + +logger = logging.getLogger('coverage_report') + +TESTS_API = '/network_ui/tests' + + + +def main(args=None): + if args is None: + args = sys.argv[1:] + parsed_args = docopt(__doc__, args) + if parsed_args['--debug']: + logging.basicConfig(level=logging.DEBUG) + elif parsed_args['--verbose']: + logging.basicConfig(level=logging.INFO) + else: + logging.basicConfig(level=logging.WARNING) + + + print (parsed_args['']) + server = parsed_args[''] + + tests = requests.get(server + TESTS_API, verify=False).json() + + for test in tests['tests']: + if not os.path.exists(test['name']): + os.mkdir(test['name']) + with open(test['name'] + "/coverage.json", 'w') as f: + f.write(requests.get(server + test['coverage'], verify=False).text) + + + for test in tests['tests']: + subprocess.Popen('istanbul report html', shell=True, cwd=test['name']).wait() + + + subprocess.Popen('istanbul report html', shell=True).wait() + + + return 0 + +if __name__ == '__main__': + import sys + sys.exit(main(sys.argv[1:])) + diff --git a/awx/network_ui/tools/requirements.txt b/awx/network_ui/tools/requirements.txt new file mode 100644 index 0000000000..bfe4ae8612 --- /dev/null +++ b/awx/network_ui/tools/requirements.txt @@ -0,0 +1,2 @@ +requests +docopt diff --git a/awx/network_ui/urls.py b/awx/network_ui/urls.py index 5f4f885cdf..f16ca6590e 100644 --- a/awx/network_ui/urls.py +++ b/awx/network_ui/urls.py @@ -7,7 +7,11 @@ import awx.network_ui.routing app_name = 'network_ui' urlpatterns = [ + url(r'^tests$', views.tests, name='tests'), + url(r'^upload_test$', views.upload_test, name='upload_test'), + url(r'^download_coverage/(?P[0-9]+)$', views.download_coverage, name='download_coverage'), url(r'^download_trace$', views.download_trace, name='download_trace'), + url(r'^download_recording$', views.download_recording, name='download_recording'), url(r'^topology.json$', views.json_topology_data, name='json_topology_data'), url(r'^topology.yaml$', views.yaml_topology_data, name='json_topology_data'), url(r'^$', views.index, name='index'), diff --git a/awx/network_ui/views.py b/awx/network_ui/views.py index 1f52179c52..428296fc7e 100644 --- a/awx/network_ui/views.py +++ b/awx/network_ui/views.py @@ -1,12 +1,16 @@ # Copyright (c) 2017 Red Hat, Inc from django.shortcuts import render from django import forms -from django.http import JsonResponse, HttpResponseBadRequest, HttpResponse +from django.http import JsonResponse, HttpResponseBadRequest, HttpResponse, HttpResponseRedirect +from django.core.exceptions import ObjectDoesNotExist import yaml +import json + # Create your views here. -from .models import Topology, FSMTrace +from .models import Topology, FSMTrace, EventTrace, TopologySnapshot +from .models import TestCase, TestResult, Coverage from .serializers import topology_data @@ -56,3 +60,79 @@ def download_trace(request): return response else: return HttpResponse(form.errors) + + +class RecordingForm(forms.Form): + topology_id = forms.IntegerField() + trace_id = forms.IntegerField() + client_id = forms.IntegerField() + + +def download_recording(request): + form = RecordingForm(request.GET) + if form.is_valid(): + topology_id = form.cleaned_data['topology_id'] + trace_id = form.cleaned_data['trace_id'] + client_id = form.cleaned_data['client_id'] + data = dict() + data['event_trace'] = [json.loads(x) for x in EventTrace + .objects.filter(trace_session_id=trace_id, client_id=client_id) + .order_by('message_id') + .values_list('event_data', flat=True)] + data['fsm_trace'] = list(FSMTrace + .objects + .filter(trace_session_id=trace_id, client_id=client_id) + .order_by('order') + .values()) + data['snapshots'] = [json.loads(x) for x in TopologySnapshot + .objects.filter(trace_session_id=trace_id, client_id=client_id) + .order_by('order') + .values_list('snapshot_data', flat=True)] + response = HttpResponse(json.dumps(data, sort_keys=True, indent=4), + content_type="application/force-download") + response['Content-Disposition'] = 'attachment; filename="trace_{0}_{1}_{2}.yml"'.format(topology_id, client_id, trace_id) + return response + else: + return HttpResponse(form.errors) + + +def tests(request): + tests = list(TestCase.objects.all().values('test_case_id', 'name')) + for x in tests: + x['coverage'] = "/network_ui/download_coverage/{0}".format(x['test_case_id']) + return JsonResponse(dict(tests=tests)) + + +def create_test(name, data): + try: + test_case = TestCase.objects.get(name=name) + test_case.test_case_data=json.dumps(data) + test_case.save() + except ObjectDoesNotExist: + TestCase(name=name, test_case_data=json.dumps(data)).save() + + +class UploadTestForm(forms.Form): + name = forms.CharField() + file = forms.FileField() + + +def upload_test(request): + if request.method == 'POST': + form = UploadTestForm(request.POST, request.FILES) + if form.is_valid(): + name = form.cleaned_data['name'] + data = json.loads(request.FILES['file'].read()) + create_test(name, data) + return HttpResponseRedirect('/network_ui/tests') + else: + form = UploadTestForm() + return render(request, 'network_ui/upload_test.html', {'form': form}) + + +def download_coverage(request, pk): + latest_tr = TestResult.objects.filter(test_case_id=pk).order_by('-time')[0] + coverage = Coverage.objects.get(test_result_id=latest_tr.pk) + response = HttpResponse(coverage.coverage_data, + content_type="application/json") + return response diff --git a/awx/ui/build/webpack.base.js b/awx/ui/build/webpack.base.js index 2a9f7aa105..633987dde0 100644 --- a/awx/ui/build/webpack.base.js +++ b/awx/ui/build/webpack.base.js @@ -58,6 +58,17 @@ const base = { }, module: { rules: [ + { + test: /\.js$/, + use: { + loader: 'istanbul-instrumenter-loader', + options: { esModules: true } + }, + enforce: 'pre', + include: [ + /src\/network-ui\// + ] + }, { test: /\.js$/, loader: 'babel-loader', diff --git a/awx/ui/client/src/network-ui/messages.js b/awx/ui/client/src/network-ui/messages.js index 9ab0f0c11e..e0de43cbc4 100644 --- a/awx/ui/client/src/network-ui/messages.js +++ b/awx/ui/client/src/network-ui/messages.js @@ -176,23 +176,18 @@ function MultipleMessage(sender, messages) { } exports.MultipleMessage = MultipleMessage; -function Coverage(sender, coverage) { - this.msg_type = "Coverage"; - this.sender = sender; - this.coverage = coverage; -} -exports.Coverage = Coverage; -function MouseEvent(sender, x, y, type) { +function MouseEvent(sender, x, y, type, trace_id) { this.msg_type = "MouseEvent"; this.sender = sender; this.x = x; this.y = y; this.type = type; + this.trace_id = trace_id; } exports.MouseEvent = MouseEvent; -function MouseWheelEvent(sender, delta, deltaX, deltaY, type, metaKey) { +function MouseWheelEvent(sender, delta, deltaX, deltaY, type, metaKey, trace_id) { this.msg_type = "MouseWheelEvent"; this.sender = sender; this.delta = delta; @@ -200,10 +195,11 @@ function MouseWheelEvent(sender, delta, deltaX, deltaY, type, metaKey) { this.deltaY = deltaY; this.type = type; this.originalEvent = {metaKey: metaKey}; + this.trace_id = trace_id; } exports.MouseWheelEvent = MouseWheelEvent; -function KeyEvent(sender, key, keyCode, type, altKey, shiftKey, ctrlKey, metaKey) { +function KeyEvent(sender, key, keyCode, type, altKey, shiftKey, ctrlKey, metaKey, trace_id) { this.msg_type = "KeyEvent"; this.sender = sender; this.key = key; @@ -213,6 +209,7 @@ function KeyEvent(sender, key, keyCode, type, altKey, shiftKey, ctrlKey, metaKey this.shiftKey = shiftKey; this.ctrlKey = ctrlKey; this.metaKey = metaKey; + this.trace_id = trace_id; } exports.KeyEvent = KeyEvent; @@ -224,24 +221,27 @@ function TouchEvent(sender, type, touches) { } exports.TouchEvent = TouchEvent; -function StartRecording(sender) { +function StartRecording(sender, trace_id) { this.msg_type = "StartRecording"; this.sender = sender; + this.trace_id = trace_id; } exports.StartRecording = StartRecording; -function StopRecording(sender) { +function StopRecording(sender, trace_id) { this.msg_type = "StopRecording"; this.sender = sender; + this.trace_id = trace_id; } exports.StopRecording = StopRecording; -function ViewPort(sender, scale, panX, panY) { +function ViewPort(sender, scale, panX, panY, trace_id) { this.msg_type = "ViewPort"; this.sender = sender; this.scale = scale; this.panX = panX; this.panY = panY; + this.trace_id = trace_id; } exports.ViewPort = ViewPort; @@ -446,3 +446,54 @@ function ChannelTrace(from_fsm, to_fsm, sent_message_type) { this.sent_message_type = sent_message_type; } exports.ChannelTrace = ChannelTrace; + +function Snapshot(sender, devices, links, groups, streams, order, trace_id) { + this.msg_type = 'Snapshot'; + this.sender = 0; + this.devices = devices; + this.links = links; + this.groups = groups; + this.streams = streams; + this.order = order; + this.trace_id = trace_id; +} +exports.Snapshot = Snapshot; + +function EnableTest() { + this.msg_type = "EnableTest"; +} +exports.EnableTest = EnableTest; + +function DisableTest() { + this.msg_type = "DisableTest"; +} +exports.DisableTest = DisableTest; + +function StartTest() { + this.msg_type = "StartTest"; +} +exports.StartTest = StartTest; + +function TestCompleted() { + this.msg_type = "TestCompleted"; +} +exports.TestCompleted = TestCompleted; + +function TestResult(sender, id, name, result, date, code_under_test) { + this.msg_type = "TestResult"; + this.sender = sender; + this.id = id; + this.name = name; + this.result = result; + this.date = date; + this.code_under_test = code_under_test; +} +exports.TestResult = TestResult; + +function Coverage(sender, coverage, result_id) { + this.msg_type = "Coverage"; + this.sender = sender; + this.coverage = coverage; + this.result_id = result_id; +} +exports.Coverage = Coverage; diff --git a/awx/ui/client/src/network-ui/models.js b/awx/ui/client/src/network-ui/models.js index edb9405012..c6f6cf541f 100644 --- a/awx/ui/client/src/network-ui/models.js +++ b/awx/ui/client/src/network-ui/models.js @@ -36,8 +36,12 @@ Device.prototype.toJSON = function () { x: this.x, y: this.y, type: this.type, - interfaces: this.interfaces, - processes: this.processes}; + interfaces: this.interfaces.map(function (x) { + return x.toJSON(); + }), + processes: this.processes.map(function (x) { + return x.toJSON(); + })}; }; Device.prototype.is_selected = function (x, y) { @@ -726,6 +730,11 @@ function Process(id, name, type, x, y) { } exports.Process = Process; +Process.prototype.toJSON = function () { + return {id: this.id, + name: this.name}; +}; + function Stream(id, from_device, to_device, label) { this.id = id; this.from_device = from_device; @@ -919,3 +928,21 @@ Stream.prototype.start_arc_angle_rad = function () { Stream.prototype.start_arc_angle = function () { return this.start_arc_angle_rad() * 180 / Math.PI; }; + +function Test(name, event_trace, fsm_trace, pre_test_snapshot, post_test_snapshot) { + this.name = name; + this.event_trace = event_trace; + this.fsm_trace = fsm_trace; + this.pre_test_snapshot = pre_test_snapshot; + this.post_test_snapshot = post_test_snapshot; +} +exports.Test = Test; + +function TestResult(id, name, result, date, code_under_test) { + this.id = id; + this.name = name; + this.result = result; + this.date = date; + this.code_under_test = code_under_test; +} +exports.TestResult = TestResult; diff --git a/awx/ui/client/src/network-ui/network.ui.app.js b/awx/ui/client/src/network-ui/network.ui.app.js index da11b56090..78bf89fc6f 100644 --- a/awx/ui/client/src/network-ui/network.ui.app.js +++ b/awx/ui/client/src/network-ui/network.ui.app.js @@ -33,6 +33,7 @@ var inventoryToolboxClipPath = require('./inventory.toolbox.clip.path.directive. var statusLight = require('./status.light.directive.js'); var taskStatus = require('./task.status.directive.js'); var debug = require('./debug.directive.js'); +var test_results = require('./test_results.directive.js'); var awxNetworkUI = require('./network.ui.directive.js'); var networkUI = angular.module('networkUI', [ @@ -70,6 +71,7 @@ var networkUI = angular.module('networkUI', [ .directive('awxNetInventoryToolboxClipPath', inventoryToolboxClipPath.inventoryToolboxClipPath) .directive('awxNetStatusLight', statusLight.statusLight) .directive('awxNetTaskStatus', taskStatus.taskStatus) + .directive('awxNetTestResults', test_results.test_results) .directive('awxNetworkUi', awxNetworkUI.awxNetworkUI); exports.networkUI = networkUI; diff --git a/awx/ui/client/src/network-ui/network.ui.controller.js b/awx/ui/client/src/network-ui/network.ui.controller.js index 93931b0fed..9a40a301f0 100644 --- a/awx/ui/client/src/network-ui/network.ui.controller.js +++ b/awx/ui/client/src/network-ui/network.ui.controller.js @@ -15,6 +15,7 @@ var stream_fsm = require('./stream.fsm.js'); var group = require('./group.js'); var buttons = require('./buttons.js'); var time = require('./time.js'); +var test_fsm = require('./test.fsm.js'); var util = require('./util.js'); var models = require('./models.js'); var messages = require('./messages.js'); @@ -22,7 +23,7 @@ var svg_crowbar = require('./svg-crowbar.js'); var ReconnectingWebSocket = require('reconnectingwebsocket'); var NetworkUIController = function($scope, $document, $location, $window, $http, - $q, $state, ProcessErrors) { + $q, $state, ProcessErrors, ConfigService) { window.scope = $scope; var i = 0; @@ -96,6 +97,7 @@ var NetworkUIController = function($scope, $document, $location, $window, $http, $scope.group_id_seq = util.natural_numbers(0); $scope.message_id_seq = util.natural_numbers(0); $scope.stream_id_seq = util.natural_numbers(0); + $scope.test_result_id_seq = util.natural_numbers(0); $scope.overall_toolbox_collapsed = false; $scope.time_pointer = -1; $scope.frame = 0; @@ -108,6 +110,14 @@ var NetworkUIController = function($scope, $document, $location, $window, $http, $scope.groups = []; $scope.processes = []; $scope.configurations = []; + $scope.tests = []; + $scope.current_tests = []; + $scope.current_test = null; + $scope.testing = false; + $scope.version = null; + $scope.test_events = []; + $scope.test_results = []; + $scope.test_errors = []; $scope.streams = []; $scope.view_port = {'x': 0, 'y': 0, @@ -118,7 +128,9 @@ var NetworkUIController = function($scope, $document, $location, $window, $http, $scope.trace_id = $scope.trace_id_seq(); $scope.send_trace_message = function (message) { - console.log(message); + if (!$scope.recording) { + return; + } message.sender = $scope.client_id; message.trace_id = $scope.trace_id; message.message_id = $scope.message_id_seq(); @@ -148,6 +160,7 @@ var NetworkUIController = function($scope, $document, $location, $window, $http, $scope.site_controller = new fsm.FSMController($scope, "site_fsm", site_fsm.Disable, $scope); $scope.buttons_controller = new fsm.FSMController($scope, "buttons_fsm", buttons.Start, $scope); $scope.time_controller = new fsm.FSMController($scope, "time_fsm", time.Start, $scope); + $scope.test_controller = new fsm.FSMController($scope, "test_fsm", test_fsm.Start, $scope); $scope.app_toolbox_controller = new fsm.FSMController($scope, "toolbox_fsm", toolbox_fsm.Start, $scope); //App Toolbox Setup @@ -185,6 +198,12 @@ var NetworkUIController = function($scope, $document, $location, $window, $http, let host = hosts[i]; console.log(host); host.data = jsyaml.safeLoad(host.variables); + if (host.data.type == undefined) { + host.data.type = 'unknown'; + } + if (host.data.name == undefined) { + host.data.name = host.name; + } var device = new models.Device(0, host.data.name, 0, 0, host.data.type, host.id, host.variables); device.icon = true; $scope.inventory_toolbox.items.push(device); @@ -288,9 +307,13 @@ var NetworkUIController = function($scope, $document, $location, $window, $http, $scope.mode_controller.delegate_channel = new fsm.Channel($scope.mode_controller, $scope.time_controller, $scope); + $scope.test_controller.delegate_channel = new fsm.Channel($scope.test_controller, + $scope.mode_controller, + $scope); + $scope.first_channel = new fsm.Channel(null, - $scope.mode_controller, + $scope.test_controller, $scope); var getMouseEventResult = function (mouseEvent) { @@ -464,7 +487,7 @@ var NetworkUIController = function($scope, $document, $location, $window, $http, $scope.onMouseDown = function ($event) { $scope.normalize_mouse_event($event); if ($scope.recording) { - $scope.send_control_message(new messages.MouseEvent($scope.client_id, $event.x, $event.y, $event.type)); + $scope.send_control_message(new messages.MouseEvent($scope.client_id, $event.x, $event.y, $event.type, $scope.trace_id)); } $scope.last_event = $event; $scope.first_channel.send('MouseDown', $event); @@ -475,7 +498,7 @@ var NetworkUIController = function($scope, $document, $location, $window, $http, $scope.onMouseUp = function ($event) { $scope.normalize_mouse_event($event); if ($scope.recording) { - $scope.send_control_message(new messages.MouseEvent($scope.client_id, $event.x, $event.y, $event.type)); + $scope.send_control_message(new messages.MouseEvent($scope.client_id, $event.x, $event.y, $event.type, $scope.trace_id)); } $scope.last_event = $event; $scope.first_channel.send('MouseUp', $event); @@ -486,7 +509,7 @@ var NetworkUIController = function($scope, $document, $location, $window, $http, $scope.onMouseLeave = function ($event) { $scope.normalize_mouse_event($event); if ($scope.recording) { - $scope.send_control_message(new messages.MouseEvent($scope.client_id, $event.x, $event.y, $event.type)); + $scope.send_control_message(new messages.MouseEvent($scope.client_id, $event.x, $event.y, $event.type, $scope.trace_id)); } $scope.onMouseLeaveResult = getMouseEventResult($event); $scope.cursor.hidden = true; @@ -496,7 +519,7 @@ var NetworkUIController = function($scope, $document, $location, $window, $http, $scope.onMouseMove = function ($event) { $scope.normalize_mouse_event($event); if ($scope.recording) { - $scope.send_control_message(new messages.MouseEvent($scope.client_id, $event.x, $event.y, $event.type)); + $scope.send_control_message(new messages.MouseEvent($scope.client_id, $event.x, $event.y, $event.type, $scope.trace_id)); } //var coords = getCrossBrowserElementCoords($event); $scope.cursor.hidden = false; @@ -513,7 +536,7 @@ var NetworkUIController = function($scope, $document, $location, $window, $http, $scope.onMouseOver = function ($event) { $scope.normalize_mouse_event($event); if ($scope.recording) { - $scope.send_control_message(new messages.MouseEvent($scope.client_id, $event.x, $event.y, $event.type)); + $scope.send_control_message(new messages.MouseEvent($scope.client_id, $event.x, $event.y, $event.type, $scope.trace_id)); } $scope.onMouseOverResult = getMouseEventResult($event); $scope.cursor.hidden = false; @@ -529,11 +552,11 @@ var NetworkUIController = function($scope, $document, $location, $window, $http, var deltaY = $event.deltaY; // console.log([$event, delta, deltaX, deltaY]); if ($scope.recording) { - $scope.send_control_message(new messages.MouseWheelEvent($scope.client_id, delta, deltaX, deltaY, $event.type, $event.originalEvent.metaKey)); + $scope.send_control_message(new messages.MouseWheelEvent($scope.client_id, delta, deltaX, deltaY, $event.type, $event.originalEvent.metaKey, $scope.trace_id)); } $scope.last_event = $event; $scope.first_channel.send('MouseWheel', [$event, delta, deltaX, deltaY]); - event.preventDefault(); + $event.preventDefault(); }; $scope.onKeyDown = function ($event) { @@ -545,7 +568,8 @@ var NetworkUIController = function($scope, $document, $location, $window, $http, $event.altKey, $event.shiftKey, $event.ctrlKey, - $event.metaKey)); + $event.metaKey, + $scope.trace_id)); } $scope.last_event = $event; $scope.last_key = $event.key; @@ -653,6 +677,7 @@ var NetworkUIController = function($scope, $document, $location, $window, $http, }; $scope.onRenameContextButton = function (button) { + console.log(button.name); $scope.context_menus[0].enabled = false; $scope.first_channel.send("LabelEdit", {}); }; @@ -690,14 +715,31 @@ var NetworkUIController = function($scope, $document, $location, $window, $http, console.log(button.name); $scope.recording = ! $scope.recording; if ($scope.recording) { + $scope.trace_id = $scope.trace_id_seq(); $scope.send_control_message(new messages.MultipleMessage($scope.client_id, - [new messages.StartRecording($scope.client_id), + [new messages.StartRecording($scope.client_id, $scope.trace_id), new messages.ViewPort($scope.client_id, $scope.current_scale, $scope.panX, - $scope.panY)])); + $scope.panY, + $scope.trace_id), + new messages.Snapshot($scope.client_id, + $scope.devices, + $scope.links, + $scope.groups, + $scope.streams, + 0, + $scope.trace_id)])); } else { - $scope.send_control_message(new messages.StopRecording($scope.client_id)); + $scope.send_control_message(new messages.MultipleMessage($scope.client_id, + [new messages.Snapshot($scope.client_id, + $scope.devices, + $scope.links, + $scope.groups, + $scope.streams, + 1, + $scope.trace_id), + new messages.StopRecording($scope.client_id, $scope.trace_id)])); } }; @@ -789,6 +831,25 @@ var NetworkUIController = function($scope, $document, $location, $window, $http, console.log(button.label); window.open("/network_ui/download_trace?topology_id=" + $scope.topology_id + "&trace_id=" + $scope.trace_id + "&client_id=" + $scope.client_id); }; + + $scope.onDownloadRecordingButton = function (button) { + console.log(button.label); + window.open("/network_ui/download_recording?topology_id=" + $scope.topology_id + "&trace_id=" + $scope.trace_id + "&client_id=" + $scope.client_id); + }; + + $scope.onUploadTestButton = function (button) { + console.log(button.name); + window.open("/network_ui/upload_test", "_top"); + }; + + $scope.onRunTestsButton = function (button) { + console.log(button.name); + + $scope.test_results = []; + $scope.current_tests = $scope.tests.slice(); + $scope.first_channel.send("EnableTest", new messages.EnableTest()); + }; + // Buttons var button_offset = 200; @@ -802,6 +863,9 @@ var NetworkUIController = function($scope, $document, $location, $window, $http, new models.Button("CONFIGURE", button_offset + 520, 48, 90, 30, $scope.onConfigureButton, $scope), new models.Button("EXPORT YAML", button_offset + 620, 48, 120, 30, $scope.onExportYamlButton, $scope), new models.Button("DOWNLOAD TRACE", button_offset + 750, 48, 150, 30, $scope.onDownloadTraceButton, $scope), + new models.Button("DOWNLOAD RECORDING", button_offset + 910, 48, 170, 30, $scope.onDownloadRecordingButton, $scope), + new models.Button("UPLOAD TEST", button_offset + 10, 88, 100, 30, $scope.onUploadTestButton, $scope), + new models.Button("RUN TESTS", button_offset + 120, 88, 100, 30, $scope.onRunTestsButton, $scope), ]; var LAYERS_X = 160; @@ -1429,6 +1493,8 @@ var NetworkUIController = function($scope, $document, $location, $window, $http, //Erase the existing state $scope.devices = []; $scope.links = []; + $scope.groups = []; + $scope.streams = []; var device_map = {}; var device_interface_map = {}; @@ -1634,12 +1700,6 @@ var NetworkUIController = function($scope, $document, $location, $window, $http, } }; - $scope.send_coverage = function () { - console.log("Sending coverage"); - if (typeof(window.__coverage__) !== "undefined" && window.__coverage__ !== null) { - $scope.send_control_message(new messages.Coverage($scope.client_id, window.__coverage__)); - } - }; $scope.control_socket.onmessage = function(message) { @@ -1743,6 +1803,112 @@ var NetworkUIController = function($scope, $document, $location, $window, $http, map.set(key, stream.offset + 1); } }; + + setInterval( function () { + var test_event = null; + if ($scope.test_events.length > 0) { + test_event = $scope.test_events.shift(); + console.log(test_event); + test_event.sender = 0; + try { + $scope.first_channel.send(test_event.msg_type, test_event); + } catch (err) { + $scope.test_errors.push(err); + } + } + $scope.$apply(); + }, 10); + + ConfigService + .getConfig() + .then(function(config){ + $scope.version = config.version; + }); + + $scope.reset_coverage = function() { + var i = null; + var coverage = null; + var f = null; + if (typeof(window.__coverage__) !== "undefined" && window.__coverage__ !== null) { + for (f in window.__coverage__) { + coverage = window.__coverage__[f]; + for (i in coverage.b) { + coverage.b[i] = [0, 0]; + } + for (i in coverage.f) { + coverage.f[i] = 0; + } + for (i in coverage.s) { + coverage.s[i] = 0; + } + } + } + }; + + $scope.reset_flags = function () { + $scope.debug = {'hidden': true}; + $scope.hide_buttons = false; + $scope.hide_links = false; + $scope.hide_interfaces = false; + $scope.hide_groups = false; + }; + + + $scope.reset_fsm_state = function () { + $scope.null_controller.state = null_fsm.Start; + $scope.null_controller.state.start($scope.null_controller); + $scope.hotkeys_controller.state = hotkeys.Start; + $scope.hotkeys_controller.state.start($scope.hotkeys_controller); + $scope.view_controller.state = view.Start; + $scope.view_controller.state.start($scope.view_controller); + $scope.device_detail_controller.state = device_detail_fsm.Start; + $scope.device_detail_controller.state.start($scope.device_detail_controller); + $scope.move_controller.state = move.Start; + $scope.move_controller.state.start($scope.move_controller); + $scope.link_controller.state = link.Start; + $scope.link_controller.state.start($scope.link_controller); + $scope.stream_controller.state = stream_fsm.Start; + $scope.stream_controller.state.start($scope.stream_controller); + $scope.group_controller.state = group.Start; + $scope.group_controller.state.start($scope.group_controller); + $scope.rack_controller.state = rack_fsm.Disable; + $scope.rack_controller.state.start($scope.rack_controller); + $scope.site_controller.state = site_fsm.Disable; + $scope.site_controller.state.start($scope.site_controller); + $scope.buttons_controller.state = buttons.Start; + $scope.buttons_controller.state.start($scope.buttons_controller); + $scope.time_controller.state = time.Start; + $scope.time_controller.state.start($scope.time_controller); + $scope.app_toolbox_controller.state = toolbox_fsm.Start; + $scope.app_toolbox_controller.state.start($scope.app_toolbox_controller); + $scope.inventory_toolbox_controller.state = toolbox_fsm.Start; + $scope.inventory_toolbox_controller.state.start($scope.inventory_toolbox_controller); + $scope.rack_toolbox_controller.state = toolbox_fsm.Start; + $scope.rack_toolbox_controller.state.start($scope.rack_toolbox_controller); + $scope.site_toolbox_controller.state = toolbox_fsm.Start; + $scope.site_toolbox_controller.state.start($scope.site_toolbox_controller); + $scope.mode_controller.state = mode_fsm.Start; + $scope.mode_controller.state.start($scope.mode_controller); + }; + + $scope.reset_history = function () { + $scope.history = []; + }; + + $scope.reset_toolboxes = function () { + $scope.app_toolbox.items = []; + $scope.app_toolbox.items.push(new models.Process(0, 'BGP', 'process', 0, 0)); + $scope.app_toolbox.items.push(new models.Process(0, 'OSPF', 'process', 0, 0)); + $scope.app_toolbox.items.push(new models.Process(0, 'STP', 'process', 0, 0)); + $scope.app_toolbox.items.push(new models.Process(0, 'Zero Pipeline', 'process', 0, 0)); + + for(i = 0; i < $scope.app_toolbox.items.length; i++) { + $scope.app_toolbox.items[i].icon = true; + } + $scope.inventory_toolbox.items = []; + $scope.rack_toolbox.items = []; + $scope.site_toolbox.items = []; + }; }; exports.NetworkUIController = NetworkUIController; diff --git a/awx/ui/client/src/network-ui/network_ui.partial.svg b/awx/ui/client/src/network-ui/network_ui.partial.svg index 848977db7d..3cb4871449 100644 --- a/awx/ui/client/src/network-ui/network_ui.partial.svg +++ b/awx/ui/client/src/network-ui/network_ui.partial.svg @@ -116,6 +116,7 @@ ng-attr-transform="translate({{context_menus[0].x}}, {{context_menus[0].y}})"> + diff --git a/awx/ui/client/src/network-ui/style.less b/awx/ui/client/src/network-ui/style.less index 9d2589ccc5..1d69f9ae4f 100644 --- a/awx/ui/client/src/network-ui/style.less +++ b/awx/ui/client/src/network-ui/style.less @@ -766,3 +766,8 @@ .NetworkUI__contextMenuButton-pressed{ fill:@button-body-hover; } + +.NetworkUI__test_results { + fill: @light-background; + stroke: @dark-widget-detail; +} diff --git a/awx/ui/client/src/network-ui/test.fsm.js b/awx/ui/client/src/network-ui/test.fsm.js new file mode 100644 index 0000000000..ebd842d15b --- /dev/null +++ b/awx/ui/client/src/network-ui/test.fsm.js @@ -0,0 +1,160 @@ +var inherits = require('inherits'); +var fsm = require('./fsm.js'); +var messages = require('./messages.js'); +var models = require('./models.js'); + +function _State () { +} +inherits(_State, fsm._State); + + +function _Disabled () { + this.name = 'Disabled'; +} +inherits(_Disabled, _State); +var Disabled = new _Disabled(); +exports.Disabled = Disabled; + +function _Start () { + this.name = 'Start'; +} +inherits(_Start, _State); +var Start = new _Start(); +exports.Start = Start; + +function _Running () { + this.name = 'Running'; +} +inherits(_Running, _State); +var Running = new _Running(); +exports.Running = Running; + +function _Loading () { + this.name = 'Loading'; +} +inherits(_Loading, _State); +var Loading = new _Loading(); +exports.Loading = Loading; + +function _Ready () { + this.name = 'Ready'; +} +inherits(_Ready, _State); +var Ready = new _Ready(); +exports.Ready = Ready; + +function _Reporting () { + this.name = 'Reporting'; +} +inherits(_Reporting, _State); +var Reporting = new _Reporting(); +exports.Reporting = Reporting; + + + + +_Disabled.prototype.onEnableTest = function (controller) { + + controller.changeState(Ready); +}; +_Disabled.prototype.onEnableTest.transitions = ['Ready']; + + + +_Start.prototype.start = function (controller) { + + controller.changeState(Disabled); + +}; +_Start.prototype.start.transitions = ['Disabled']; + + + +_Running.prototype.onTestCompleted = function (controller) { + + controller.changeState(Reporting); +}; +_Running.prototype.onTestCompleted.transitions = ['Reporting']; + +_Reporting.prototype.start = function (controller) { + + var test_result = null; + controller.scope.replay = false; + controller.scope.disconnected = false; + controller.scope.recording = false; + var result = "passed"; + if (controller.scope.test_errors.length > 0) { + result = "errored"; + } + test_result = new models.TestResult(controller.scope.test_result_id_seq(), + controller.scope.current_test.name, + result, + new Date().toISOString(), + controller.scope.version); + controller.scope.test_results.push(test_result); + console.log(["Reporting test", test_result.name, test_result.id]); + controller.scope.send_control_message(new messages.TestResult(controller.scope.client_id, + test_result.id, + test_result.name, + test_result.result, + test_result.date, + test_result.code_under_test)); + if (typeof(window.__coverage__) !== "undefined" && window.__coverage__ !== null) { + console.log(["Reporting coverage", test_result.name, test_result.id]); + controller.scope.send_control_message(new messages.Coverage(controller.scope.client_id, window.__coverage__, test_result.id)); + } + controller.changeState(Loading); +}; +_Reporting.prototype.start.transitions = ['Loading']; + + +_Loading.prototype.start = function (controller) { + + if (controller.scope.current_tests.length === 0) { + controller.changeState(Disabled); + } else { + console.log("Starting test"); + controller.scope.current_test = controller.scope.current_tests.shift(); + controller.scope.onSnapshot(controller.scope.current_test.pre_test_snapshot); + controller.scope.replay = true; + controller.scope.disconnected = true; + controller.scope.test_errors = []; + controller.scope.test_events = controller.scope.current_test.event_trace.slice(); + controller.scope.test_events.push(new messages.TestCompleted()); + controller.scope.reset_coverage(); + controller.scope.reset_flags(); + controller.scope.reset_fsm_state(); + controller.scope.reset_history(); + controller.scope.reset_toolboxes(); + controller.changeState(Running); + } +}; +_Loading.prototype.start.transitions = ['Running']; + + + +_Ready.prototype.onDisableTest = function (controller) { + + controller.changeState(Disabled); +}; +_Ready.prototype.onDisableTest.transitions = ['Disabled']; + +_Ready.prototype.start = function (controller) { + + var load_id = controller.scope.test_result_id_seq(); + + console.log(["Reporting Load", load_id]); + controller.scope.send_control_message(new messages.TestResult(controller.scope.client_id, + load_id, + "Load", + "passed", + new Date().toISOString(), + controller.scope.version)); + if (typeof(window.__coverage__) !== "undefined" && window.__coverage__ !== null) { + console.log(["Reporting Load Coverage", load_id]); + controller.scope.send_control_message(new messages.Coverage(controller.scope.client_id, window.__coverage__, load_id)); + } + + controller.changeState(Loading); +}; +_Ready.prototype.start.transitions = ['Loading']; diff --git a/awx/ui/client/src/network-ui/test_results.directive.js b/awx/ui/client/src/network-ui/test_results.directive.js new file mode 100644 index 0000000000..9430ad4056 --- /dev/null +++ b/awx/ui/client/src/network-ui/test_results.directive.js @@ -0,0 +1,8 @@ +/* Copyright (c) 2018 Red Hat, Inc. */ + +const templateUrl = require('~network-ui/test_results.partial.svg'); + +function test_results () { + return { restrict: 'A', templateUrl}; +} +exports.test_results = test_results; diff --git a/awx/ui/client/src/network-ui/test_results.partial.svg b/awx/ui/client/src/network-ui/test_results.partial.svg new file mode 100644 index 0000000000..159808bb3d --- /dev/null +++ b/awx/ui/client/src/network-ui/test_results.partial.svg @@ -0,0 +1,14 @@ + + + + + +Test Results {{version}} + + +{{result.name}} - {{result.result}} + + + + + diff --git a/awx/ui/client/src/network-ui/time.js b/awx/ui/client/src/network-ui/time.js index 349a606791..19aca872a8 100644 --- a/awx/ui/client/src/network-ui/time.js +++ b/awx/ui/client/src/network-ui/time.js @@ -3,6 +3,7 @@ var inherits = require('inherits'); var fsm = require('./fsm.js'); var messages = require('./messages.js'); var util = require('./util.js'); +var models = require('./models.js'); function _State () { } @@ -539,3 +540,23 @@ _Present.prototype.undo = function(controller) { controller.changeState(Past); } }; + + +_Present.prototype.onTestCase = function(controller, msg_type, message) { + console.log([msg_type, message]); + if ('runnable' in message[1]) { + if (!message[1].runnable) { + return; + } + } + controller.scope.tests.push(new models.Test(message[0], + message[1].event_trace, + [], + message[1].snapshots[0], + message[1].snapshots[1])); +}; + +_Present.prototype.onError = function(controller, msg_type, message) { + console.log(["onError", msg_type, message]); + throw new Error("ServerError: " + message); +}; diff --git a/awx/ui/client/src/network-ui/tower.app.js b/awx/ui/client/src/network-ui/tower.app.js index 5b0ccb0b31..fd4846eab8 100644 --- a/awx/ui/client/src/network-ui/tower.app.js +++ b/awx/ui/client/src/network-ui/tower.app.js @@ -2,7 +2,7 @@ var angular = require('angular'); -var tower = angular.module('tower', ['tablesUI', 'networkUI', 'ui.router']); +var tower = angular.module('tower', ['networkUI', 'ui.router']); tower.config(function($stateProvider, $urlRouterProvider) { diff --git a/awx/ui/package.json b/awx/ui/package.json index 787aae80f9..5a0297d0d4 100644 --- a/awx/ui/package.json +++ b/awx/ui/package.json @@ -61,6 +61,7 @@ "html-loader": "^0.5.1", "html-webpack-harddisk-plugin": "^0.1.0", "html-webpack-plugin": "^2.30.1", + "istanbul-instrumenter-loader": "^3.0.0", "jasmine-core": "^2.5.2", "jshint": "^2.9.4", "jshint-stylish": "^2.2.0",