mirror of
https://github.com/ansible/awx.git
synced 2024-11-01 08:21:15 +03:00
Moves network_ui_test to an external repo
This commit is contained in:
parent
abb95fdad6
commit
96b3ebd31e
@ -1,6 +1,5 @@
|
||||
from channels.routing import route
|
||||
from awx.network_ui.routing import channel_routing as network_ui_routing
|
||||
from awx.network_ui_test.routing import channel_routing as network_ui_test_routing
|
||||
|
||||
|
||||
channel_routing = [
|
||||
@ -11,4 +10,3 @@ channel_routing = [
|
||||
|
||||
|
||||
channel_routing += network_ui_routing
|
||||
channel_routing += network_ui_test_routing
|
||||
|
@ -1,221 +0,0 @@
|
||||
|
||||
Network UI Test
|
||||
===============
|
||||
|
||||
Network UI Test is an event driven test framework for testing the Network UI.
|
||||
This tool works by setting up the UI with a pre-test run snapshot, replaying a
|
||||
set of events to the Network UI, catching exceptions and then comparing the
|
||||
state of the system after the events to a post test run snapshot. Test results
|
||||
and test code coverage are stored for each test run through the system. This
|
||||
allows us to determine which lines of test are run during each test and which
|
||||
lines are run under any test. This can be very helpful during development to
|
||||
determine that the code is executed as expected given a certain input, to find
|
||||
code that needs additional tests, and to find code that is not be run under any
|
||||
of the given inputs (a.k.a. dead code).
|
||||
|
||||
Using this test framework it is fairly easy to achieve 90%+ code coverage under
|
||||
test with a few days of work recording and crafting tests.
|
||||
|
||||
Test Steps
|
||||
----------
|
||||
|
||||
The tests suppported by this test framework perform the following steps:
|
||||
|
||||
* Reset code coverage records
|
||||
* Recreate a pre-test snapshot on the UI
|
||||
* Replay a set of events to the UI
|
||||
* Check for exceptions thrown by the UI
|
||||
* Check the state of the system based on the post-test snapshot
|
||||
* Report the pass/fail/error status of the test by test name, code version, and client id
|
||||
* Report the code coverage per test result
|
||||
* Repeat for next test
|
||||
|
||||
Test Case Data Schema
|
||||
---------------------
|
||||
|
||||
The tests are completely data driven and the data for the test snapshots and events are stored in a JSON
|
||||
structure in the `TestCase` model under the `test_case_data` field that has the following structure:
|
||||
|
||||
{
|
||||
"event_trace": [],
|
||||
"fsm_trace": [],
|
||||
"snapshots": [
|
||||
{
|
||||
"devices": [],
|
||||
"inventory_toolbox": [],
|
||||
"links": [],
|
||||
"message_id": 4,
|
||||
"msg_type": "Snapshot",
|
||||
"order": 0,
|
||||
"sender": 0,
|
||||
"trace_id": 2
|
||||
},
|
||||
{
|
||||
"devices": [],
|
||||
"inventory_toolbox": [],
|
||||
"links": [],
|
||||
"message_id": 31,
|
||||
"msg_type": "Snapshot",
|
||||
"order": 1,
|
||||
"sender": 0,
|
||||
"trace_id": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
The pre-test snapshot has order 0 and the post-test snapshot has order 1.
|
||||
|
||||
|
||||
|
||||
Network UI Test Models
|
||||
----------------------
|
||||
|
||||
![Models](designs/models.png)
|
||||
|
||||
* FSMTrace - The record of an event that happend during an FSM trace. This is used in the recording phase of test case data.
|
||||
* EventTrace - The record of an event that happened during an event trace. This is used in the recording phase of test case data.
|
||||
* Coverage - Per-line test case coverage as returned by istanbul JS code coverage tool.
|
||||
* TopologySnapshot - A snapshot of the state of the network UI before or after the test was run.
|
||||
* TestCase - The definition of a test case with a given name using the test case data schema defined above.
|
||||
* Result - One of passed, failed, errored, skipped, aborted, not run, or blocked.
|
||||
* TestResult - The record of one test case being run at a certain code location by a client at a certain time.
|
||||
* CodeUnderTest - The record of what exactly was the code being tested at the time the test was run.
|
||||
|
||||
|
||||
Messages
|
||||
--------
|
||||
|
||||
JSON messages are passed over the `/network_ui/test` websocket between the test client and the test server.
|
||||
The protocol that is used for all messages is in ABNF (RFC5234):
|
||||
|
||||
|
||||
message_type = 'MultipleMessage' / 'MouseEvent' / 'MouseWheelEvent' / 'KeyEvent' / 'StartRecording' / 'StopRecording' / 'ViewPort' / 'FSMTrace' / 'ChannelTrace' / 'Snapshot' / 'EnableTest' / 'DisableTest' / 'StartTest' / 'TestCompleted' / 'TestResult' / 'Coverage'
|
||||
message_data = '{' 'msg_type' ': ' message_type ', ' key-value *( ', ' key-value ) '}'
|
||||
message = '[ id , ' posint ']' / '[ topology_id , ' posint ']' / '[' message_type ', ' message_data ']'
|
||||
|
||||
See https://github.com/AndyA/abnfgen/blob/master/andy/json.abnf for the rest of the JSON ABNF.
|
||||
|
||||
See [designs/messages.yml](designs/messages.yml) for the allowable keys and values for each message type.
|
||||
|
||||
|
||||
|
||||
Loading Tests
|
||||
-------------
|
||||
|
||||
Tests can be imported from exported data dumps from the `awx-manage datadump` command. From the Tower shell run
|
||||
the following command:
|
||||
|
||||
awx-manage loaddata /awx_devel/network_ui_test_cases_2018_03_12.json
|
||||
|
||||
This will load the tests from the `network_ui_test_cases_2018_03_12.json` file.
|
||||
|
||||
Exporting Tests
|
||||
---------------
|
||||
|
||||
Use the standard Django dumpdata command to dump the test case data to a file:
|
||||
|
||||
awx-manage dumpdata network_ui_test.TestCase > /awx_devel/network_ui_test_cases_YYYY_MM_DD.json
|
||||
|
||||
|
||||
Writing Tests Manually or Generating Tests
|
||||
------------------------------------------
|
||||
|
||||
Use the empty test case schema above and add messages to the event_trace list. Then upload the
|
||||
JSON test data to the upload test URL below.
|
||||
|
||||
Recording Tests
|
||||
---------------
|
||||
|
||||
Tests can be reruns of previously recorded manual interactions with the network UI. To start a recording
|
||||
open the JavaScript console and run this command:
|
||||
|
||||
scope.onRecordButton()
|
||||
|
||||
To stop the recording run this command:
|
||||
|
||||
scope.onRecordButton()
|
||||
|
||||
To download the recording use this command:
|
||||
|
||||
scope.onDownloadRecordingButton()
|
||||
|
||||
|
||||
After downloading the recording upload the data using the following URL to make a test of it.
|
||||
|
||||
Uploading Tests
|
||||
---------------
|
||||
|
||||
Go to the URL: https://SERVER:PORT/network_ui_test/upload_test and use the form to upload test data
|
||||
and choose a test case name.
|
||||
|
||||
Instrumenting a UI Build
|
||||
------------------------
|
||||
|
||||
Code coverage is collected automatically if the UI is built with instrumented code. Add this section to the
|
||||
rules in awx/ui/build/webpack.base.js to build instrumented code.
|
||||
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: {
|
||||
loader: 'istanbul-instrumenter-loader',
|
||||
options: { esModules: true }
|
||||
},
|
||||
enforce: 'pre',
|
||||
include: [
|
||||
/src\/network-ui\//
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
Then run:
|
||||
|
||||
make ui-devel
|
||||
|
||||
or:
|
||||
|
||||
make ui-docker
|
||||
|
||||
|
||||
To rebuild the code with istanbul instrumentation.
|
||||
|
||||
|
||||
Running Tests
|
||||
-------------
|
||||
|
||||
|
||||
To kick off tests in the web browser navigate to the Network UI page under an inventory and open the JavaScript console.
|
||||
Run this command in the JavaScript console:
|
||||
|
||||
scope.onRunTestsButton();
|
||||
|
||||
|
||||
Building a Coverage Report
|
||||
--------------------------
|
||||
|
||||
To build a coverage report for the last set of tests that were run as in above. Edit the `tools/Makefile` and change
|
||||
`SERVER` to match your Tower server. Then run the following command from the `tools` directory:
|
||||
|
||||
make coverage
|
||||
|
||||
This will download all the coverage data from all the test results that were previously recorded and build a coverage
|
||||
report with istantbul. You can then view the report at the address http://localhost:9000 or http://server:PORT
|
||||
where you executed the command.
|
||||
|
||||
http://localhost:9000/coverage will contain the coverage for all test cases merged together.
|
||||
http://localhost:9000/Load/coverage will contain the coverage for just loading the page.
|
||||
http://localhost:9000/TestX/coverage will contain the coverage for just the TestX test where
|
||||
TestX is the name of one of the tests run previously.
|
||||
|
||||
|
||||
|
||||
3.3 Hardening Road Map
|
||||
----------------------
|
||||
|
||||
* Add post-test snapshot comparison
|
||||
* Add FSM trace comparison
|
||||
* Add an ability to run a single test
|
||||
* Add an ability to run a subset of the tests
|
||||
* Add a big red light when event recording is on.
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
# Copyright (c) 2017 Red Hat, Inc
|
@ -1,184 +0,0 @@
|
||||
# Copyright (c) 2018 Red Hat, Inc
|
||||
# In consumers.py
|
||||
from channels import Group, Channel
|
||||
from channels.sessions import channel_session
|
||||
from awx.network_ui.models import Topology, Client
|
||||
from awx.network_ui.models import TopologyInventory
|
||||
from awx.network_ui_test.models import FSMTrace, EventTrace, Coverage, TopologySnapshot
|
||||
from awx.network_ui_test.models import TestCase, TestResult, CodeUnderTest, Result
|
||||
import urlparse
|
||||
import logging
|
||||
from django.utils.dateparse import parse_datetime
|
||||
|
||||
|
||||
import json
|
||||
# Connected to websocket.connect
|
||||
|
||||
logger = logging.getLogger("awx.network_ui_test.consumers")
|
||||
|
||||
|
||||
def parse_inventory_id(data):
|
||||
inventory_id = data.get('inventory_id', ['null'])
|
||||
try:
|
||||
inventory_id = int(inventory_id[0])
|
||||
except ValueError:
|
||||
inventory_id = None
|
||||
if not inventory_id:
|
||||
inventory_id = None
|
||||
return inventory_id
|
||||
|
||||
|
||||
class TestPersistence(object):
|
||||
|
||||
def parse_message_text(self, message_text, client_id):
|
||||
data = json.loads(message_text)
|
||||
if len(data) == 2:
|
||||
message_type = data.pop(0)
|
||||
message_value = data.pop(0)
|
||||
if isinstance(message_value, list):
|
||||
logger.error("Message has no sender")
|
||||
return None, None
|
||||
if isinstance(message_value, dict) and client_id != message_value.get('sender'):
|
||||
logger.error("client_id mismatch expected: %s actual %s", client_id, message_value.get('sender'))
|
||||
return None, None
|
||||
return message_type, message_value
|
||||
else:
|
||||
logger.error("Invalid message text")
|
||||
return None, None
|
||||
|
||||
def handle(self, message):
|
||||
topology_id = message.get('topology')
|
||||
assert topology_id is not None, "No topology_id"
|
||||
client_id = message.get('client')
|
||||
assert client_id is not None, "No client_id"
|
||||
message_type, message_value = self.parse_message_text(message['text'], client_id)
|
||||
if message_type is None:
|
||||
return
|
||||
handler = self.get_handler(message_type)
|
||||
if handler is not None:
|
||||
try:
|
||||
handler(message_value, topology_id, client_id)
|
||||
except Exception:
|
||||
Group("client-%s" % client_id).send({"text": json.dumps(["Error", "Server Error"])})
|
||||
raise
|
||||
else:
|
||||
logger.warning("Unsupported message %s: no handler", message_type)
|
||||
|
||||
def get_handler(self, message_type):
|
||||
return getattr(self, "on{0}".format(message_type), None)
|
||||
|
||||
def onMultipleMessage(self, message_value, topology_id, client_id):
|
||||
for message in message_value['messages']:
|
||||
handler = self.get_handler(message['msg_type'])
|
||||
if handler is not None:
|
||||
handler(message, topology_id, client_id)
|
||||
else:
|
||||
logger.warning("Unsupported message %s", message['msg_type'])
|
||||
|
||||
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')
|
||||
|
||||
x, y, z = [int(i) for i in xyz.split('.')]
|
||||
|
||||
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)
|
||||
|
||||
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()
|
||||
|
||||
|
||||
def onCoverage(self, coverage, topology_id, client_id):
|
||||
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
|
||||
|
||||
def onStopRecording(self, recording, topology_id, client_id):
|
||||
pass
|
||||
|
||||
def write_event(self, event, topology_id, client_id):
|
||||
if event.get('save', True):
|
||||
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
|
||||
onTouchEvent = write_event
|
||||
onMouseWheelEvent = write_event
|
||||
onKeyEvent = write_event
|
||||
|
||||
def onFSMTrace(self, message_value, diagram_id, client_id):
|
||||
FSMTrace(trace_session_id=message_value['trace_id'],
|
||||
fsm_name=message_value['fsm_name'],
|
||||
from_state=message_value['from_state'],
|
||||
to_state=message_value['to_state'],
|
||||
order=message_value['order'],
|
||||
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()
|
||||
|
||||
|
||||
@channel_session
|
||||
def ws_connect(message):
|
||||
# Accept connection
|
||||
data = urlparse.parse_qs(message.content['query_string'])
|
||||
inventory_id = parse_inventory_id(data)
|
||||
topology_ids = list(TopologyInventory.objects.filter(inventory_id=inventory_id).values_list('topology_id', flat=True))
|
||||
topology_id = None
|
||||
if len(topology_ids) > 0:
|
||||
topology_id = topology_ids[0]
|
||||
if topology_id is not None:
|
||||
topology = Topology.objects.get(topology_id=topology_id)
|
||||
else:
|
||||
topology = Topology(name="topology", scale=1.0, panX=0, panY=0)
|
||||
topology.save()
|
||||
TopologyInventory(inventory_id=inventory_id, topology_id=topology.topology_id).save()
|
||||
topology_id = topology.topology_id
|
||||
message.channel_session['topology_id'] = topology_id
|
||||
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])})
|
||||
send_tests(message.reply_channel)
|
||||
|
||||
|
||||
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)]])})
|
||||
|
||||
|
||||
@channel_session
|
||||
def ws_message(message):
|
||||
Channel('test_persistence').send({"text": message['text'],
|
||||
"topology": message.channel_session['topology_id'],
|
||||
"client": message.channel_session['client_id']})
|
||||
|
||||
|
||||
@channel_session
|
||||
def ws_disconnect(message):
|
||||
pass
|
||||
|
||||
|
@ -1,18 +0,0 @@
|
||||
messages:
|
||||
- {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: 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: 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]}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 156 KiB |
@ -1,168 +0,0 @@
|
||||
app: awx.network_ui_test
|
||||
external_models: []
|
||||
models:
|
||||
- fields:
|
||||
- name: client_id
|
||||
pk: true
|
||||
type: AutoField
|
||||
name: Client
|
||||
x: -518
|
||||
y: 138
|
||||
- fields:
|
||||
- name: fsm_trace_id
|
||||
pk: true
|
||||
type: AutoField
|
||||
- len: 200
|
||||
name: fsm_name
|
||||
type: CharField
|
||||
- len: 200
|
||||
name: from_state
|
||||
type: CharField
|
||||
- len: 200
|
||||
name: to_state
|
||||
type: CharField
|
||||
- len: 200
|
||||
name: message_type
|
||||
type: CharField
|
||||
- name: client
|
||||
ref: Client
|
||||
ref_field: client_id
|
||||
type: ForeignKey
|
||||
- default: 0
|
||||
name: trace_session_id
|
||||
type: IntegerField
|
||||
- default: 0
|
||||
name: order
|
||||
type: IntegerField
|
||||
name: FSMTrace
|
||||
x: -872
|
||||
y: 507
|
||||
- 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
|
||||
panY: 189.446959094643
|
||||
scaleXY: 0.69
|
@ -1,102 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.7 on 2018-03-06 18:40
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('network_ui', '0001_initial'),
|
||||
]
|
||||
|
||||
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(blank=True, max_length=40)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Coverage',
|
||||
fields=[
|
||||
('coverage_id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('coverage_data', models.TextField()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EventTrace',
|
||||
fields=[
|
||||
('event_trace_id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('trace_session_id', models.IntegerField(default=0)),
|
||||
('event_data', models.TextField()),
|
||||
('message_id', models.IntegerField()),
|
||||
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='network_ui.Client')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FSMTrace',
|
||||
fields=[
|
||||
('fsm_trace_id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('fsm_name', models.CharField(blank=True, max_length=200)),
|
||||
('from_state', models.CharField(blank=True, max_length=200)),
|
||||
('to_state', models.CharField(blank=True, max_length=200)),
|
||||
('message_type', models.CharField(blank=True, max_length=200)),
|
||||
('trace_session_id', models.IntegerField(default=0)),
|
||||
('order', models.IntegerField(default=0)),
|
||||
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='network_ui.Client')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Result',
|
||||
fields=[
|
||||
('result_id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(blank=True, max_length=20)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TestCase',
|
||||
fields=[
|
||||
('test_case_id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(blank=True, 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()),
|
||||
('id', models.IntegerField(default=0)),
|
||||
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='network_ui.Client')),
|
||||
('code_under_test', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='network_ui_test.CodeUnderTest')),
|
||||
('result', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='network_ui_test.Result')),
|
||||
('test_case', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='network_ui_test.TestCase')),
|
||||
],
|
||||
),
|
||||
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')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='coverage',
|
||||
name='test_result',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='network_ui_test.TestResult'),
|
||||
),
|
||||
]
|
@ -1,29 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.7 on 2018-03-06 22:43
|
||||
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_test', 'Result')
|
||||
for result in results:
|
||||
Result.objects.get_or_create(name=result)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('network_ui_test', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
code=populate_result_types,
|
||||
),
|
||||
]
|
@ -1,26 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.7 on 2018-03-06 23:06
|
||||
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_test', 'TestCase')
|
||||
TestCase.objects.get_or_create(name="Load", test_case_data=json.dumps(dict(runnable=False)))
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('network_ui_test', '0002_auto_20180306_2243'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
code=add_load_test_case,
|
||||
),
|
||||
]
|
@ -1,73 +0,0 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class FSMTrace(models.Model):
|
||||
|
||||
fsm_trace_id = models.AutoField(primary_key=True,)
|
||||
fsm_name = models.CharField(max_length=200, blank=True)
|
||||
from_state = models.CharField(max_length=200, blank=True)
|
||||
to_state = models.CharField(max_length=200, blank=True)
|
||||
message_type = models.CharField(max_length=200, blank=True)
|
||||
client = models.ForeignKey('network_ui.Client',)
|
||||
trace_session_id = models.IntegerField(default=0,)
|
||||
order = models.IntegerField(default=0,)
|
||||
|
||||
|
||||
class EventTrace(models.Model):
|
||||
|
||||
event_trace_id = models.AutoField(primary_key=True,)
|
||||
client = models.ForeignKey('network_ui.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('network_ui.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, blank=True)
|
||||
test_case_data = models.TextField()
|
||||
|
||||
|
||||
class Result(models.Model):
|
||||
|
||||
result_id = models.AutoField(primary_key=True,)
|
||||
name = models.CharField(max_length=20, blank=True)
|
||||
|
||||
|
||||
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, blank=True)
|
||||
|
||||
|
||||
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('network_ui.Client',)
|
@ -1,12 +0,0 @@
|
||||
# Copyright (c) 2017 Red Hat, Inc
|
||||
from channels.routing import route
|
||||
from awx.network_ui_test.consumers import ws_connect, ws_message, ws_disconnect, TestPersistence
|
||||
|
||||
|
||||
channel_routing = [
|
||||
route("websocket.connect", ws_connect, path=r"^/network_ui/test"),
|
||||
route("websocket.receive", ws_message, path=r"^/network_ui/test"),
|
||||
route("websocket.disconnect", ws_disconnect, path=r"^/network_ui/test"),
|
||||
route("test_persistence", TestPersistence().handle),
|
||||
]
|
||||
|
@ -1,5 +0,0 @@
|
||||
<form action="/network_ui_test/upload_test" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{form}}
|
||||
<button type="submit">Upload Test</button>
|
||||
</form>
|
@ -1,13 +0,0 @@
|
||||
|
||||
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}
|
@ -1,51 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Usage:
|
||||
coverage_report [options] <server>
|
||||
|
||||
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_test/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)
|
||||
|
||||
server = parsed_args['<server>']
|
||||
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__':
|
||||
sys.exit(main(sys.argv[1:]))
|
@ -1,2 +0,0 @@
|
||||
requests
|
||||
docopt
|
@ -1,14 +0,0 @@
|
||||
# Copyright (c) 2017 Red Hat, Inc
|
||||
from django.conf.urls import url
|
||||
|
||||
from awx.network_ui_test import views
|
||||
|
||||
app_name = 'network_ui_test'
|
||||
urlpatterns = [
|
||||
url(r'^tests$', views.tests, name='tests'),
|
||||
url(r'^upload_test$', views.upload_test, name='upload_test'),
|
||||
url(r'^download_coverage/(?P<pk>[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'),
|
||||
]
|
||||
|
@ -1,112 +0,0 @@
|
||||
# Copyright (c) 2017 Red Hat, Inc
|
||||
from django.shortcuts import render
|
||||
from django import forms
|
||||
from django.http import JsonResponse, HttpResponse, HttpResponseRedirect
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
import yaml
|
||||
|
||||
import json
|
||||
|
||||
|
||||
# Create your views here.
|
||||
from .models import FSMTrace, EventTrace, TopologySnapshot
|
||||
from .models import TestCase, TestResult, Coverage
|
||||
|
||||
|
||||
class FSMTraceForm(forms.Form):
|
||||
topology_id = forms.IntegerField()
|
||||
trace_id = forms.IntegerField()
|
||||
client_id = forms.IntegerField()
|
||||
|
||||
|
||||
def download_trace(request):
|
||||
form = FSMTraceForm(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 = list(FSMTrace.objects.filter(trace_session_id=trace_id,
|
||||
client_id=client_id).order_by('order').values())
|
||||
response = HttpResponse(yaml.safe_dump(data, default_flow_style=False),
|
||||
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)
|
||||
|
||||
|
||||
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_test/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_test/tests')
|
||||
else:
|
||||
form = UploadTestForm()
|
||||
return render(request, 'network_ui_test/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
|
||||
|
@ -279,8 +279,7 @@ INSTALLED_APPS = (
|
||||
'awx.ui',
|
||||
'awx.sso',
|
||||
'solo',
|
||||
'awx.network_ui',
|
||||
'awx.network_ui_test',
|
||||
'awx.network_ui'
|
||||
)
|
||||
|
||||
INTERNAL_IPS = ('127.0.0.1',)
|
||||
|
@ -35,6 +35,7 @@ var NetworkUIController = function($scope,
|
||||
|
||||
$scope.api_token = '';
|
||||
$scope.disconnected = false;
|
||||
$scope.tests_enabled = false;
|
||||
|
||||
$scope.topology_id = 0;
|
||||
// Create a web socket to connect to the backend server
|
||||
@ -54,9 +55,17 @@ var NetworkUIController = function($scope,
|
||||
$scope.control_socket = new ReconnectingWebSocket(protocol + "://" + window.location.host + "/network_ui/topology?inventory_id=" + $scope.inventory_id,
|
||||
null,
|
||||
{debug: false, reconnectInterval: 300});
|
||||
$scope.test_socket = new ReconnectingWebSocket(protocol + "://" + window.location.host + "/network_ui/test?inventory_id=" + $scope.inventory_id,
|
||||
null,
|
||||
{debug: false, reconnectInterval: 300});
|
||||
if ($scope.tests_enabled) {
|
||||
$scope.test_socket = new ReconnectingWebSocket(protocol + "://" + window.location.host + "/network_ui/test?inventory_id=" + $scope.inventory_id,
|
||||
null,
|
||||
{debug: false, reconnectInterval: 300});
|
||||
} else {
|
||||
$scope.test_socket = {
|
||||
on_message: util.noop,
|
||||
send: util.noop
|
||||
};
|
||||
}
|
||||
|
||||
} else {
|
||||
$scope.control_socket = {
|
||||
on_message: util.noop
|
||||
|
@ -16,7 +16,6 @@ urlpatterns = [
|
||||
url(r'^sso/', include('awx.sso.urls', namespace='sso')),
|
||||
url(r'^sso/', include('social_django.urls', namespace='social')),
|
||||
url(r'^network_ui/', include('awx.network_ui.urls', namespace='network_uiui', app_name='network_ui')),
|
||||
url(r'^network_ui_test/', include('awx.network_ui_test.urls', namespace='network_ui_test', app_name='network_ui_test')),
|
||||
url(r'^(?:api/)?400.html$', handle_400),
|
||||
url(r'^(?:api/)?403.html$', handle_403),
|
||||
url(r'^(?:api/)?404.html$', handle_404),
|
||||
|
Loading…
Reference in New Issue
Block a user