test runner working in docker

This commit is contained in:
Yusef Napora 2020-06-22 14:24:27 -04:00
parent c9dbf908b5
commit 9e348c049b
9 changed files with 173 additions and 114 deletions

View File

@ -17,6 +17,7 @@ RUN mkdir /testground && chmod 777 /testground
ENV HOME /composer ENV HOME /composer
ENV TESTGROUND_HOME /testground ENV TESTGROUND_HOME /testground
ENV LISTEN_PORT 5006 ENV LISTEN_PORT 5006
ENV TESTGROUND_DAEMON_HOST host.docker.internal
VOLUME /testground/plans VOLUME /testground/plans

View File

@ -3,10 +3,11 @@ import panel as pn
import toml import toml
from .util import get_plans, get_manifest from .util import get_plans, get_manifest
from .composition import Composition from .composition import Composition
from .runner import TestRunner
STAGE_WELCOME = 'Welcome' STAGE_WELCOME = 'Welcome'
STAGE_CONFIG_COMPOSITION = 'Configure' STAGE_CONFIG_COMPOSITION = 'Configure'
STAGE_RUN_TEST = 'Run'
class Welcome(param.Parameterized): class Welcome(param.Parameterized):
composition = param.Parameter() composition = param.Parameter()
@ -45,7 +46,7 @@ class Welcome(param.Parameterized):
return return
print('plan selected: {}'.format(evt.new)) print('plan selected: {}'.format(evt.new))
manifest = get_manifest(evt.new) manifest = get_manifest(evt.new)
self.composition = Composition(manifest=manifest) self.composition = Composition(manifest=manifest, add_default_group=True)
print('new composition: ', self.composition) print('new composition: ', self.composition)
self.ready = True self.ready = True
@ -65,7 +66,8 @@ class WorkflowPipeline(object):
def __init__(self): def __init__(self):
stages = [ stages = [
(STAGE_WELCOME, Welcome(), dict(ready_parameter='ready')), (STAGE_WELCOME, Welcome(), dict(ready_parameter='ready')),
(STAGE_CONFIG_COMPOSITION, ConfigureComposition()) (STAGE_CONFIG_COMPOSITION, ConfigureComposition()),
(STAGE_RUN_TEST, TestRunner()),
] ]
self.pipeline = pn.pipeline.Pipeline(debug=True, stages=stages) self.pipeline = pn.pipeline.Pipeline(debug=True, stages=stages)
@ -79,6 +81,7 @@ class WorkflowPipeline(object):
self.pipeline.next_button, self.pipeline.next_button,
), ),
self.pipeline.stage, self.pipeline.stage,
sizing_mode='stretch_width',
) )

View File

@ -188,11 +188,13 @@ class Composition(param.Parameterized):
groups_ui = None groups_ui = None
def __init__(self, manifest=None, **params): def __init__(self, manifest=None, add_default_group=False, **params):
super(Composition, self).__init__(**params) super(Composition, self).__init__(**params)
self.manifest = manifest self.manifest = manifest
self.testcase_param_classes = dict() self.testcase_param_classes = dict()
self._set_manifest(manifest) self._set_manifest(manifest)
if add_default_group:
self._add_group()
@classmethod @classmethod
def from_dict(cls, d, manifest=None): def from_dict(cls, d, manifest=None):
@ -262,7 +264,7 @@ class Composition(param.Parameterized):
print_err("No testcase found in manifest named " + case) print_err("No testcase found in manifest named " + case)
return cls return cls
def _add_group(self, evt): def _add_group(self, *args):
g = Group(id='New Group', params_class=self._params_class_for_current_testcase()) g = Group(id='New Group', params_class=self._params_class_for_current_testcase())
groups = self.param['groups'].objects groups = self.param['groups'].objects
groups.append(g) groups.append(g)

111
composer/app/runner.py Normal file
View File

@ -0,0 +1,111 @@
import os
import panel as pn
import param
from panel.io.server import unlocked
from tornado.ioloop import IOLoop, PeriodicCallback
from tornado.process import Subprocess
from subprocess import STDOUT
from bokeh.models.widgets import Div
from ansi2html import Ansi2HTMLConverter
from .composition import Composition
TESTGROUND = 'testground'
class AnsiColorText(pn.widgets.Widget):
style = param.Dict(default=None, doc="""
Dictionary of CSS property:value pairs to apply to this Div.""")
value = param.Parameter(default=None)
_format = '<div>{value}</div>'
_rename = {'name': None, 'value': 'text'}
# _target_transforms = {'value': 'target.text.split(": ")[0]+": "+value'}
#
# _source_transforms = {'value': 'value.split(": ")[1]'}
_widget_type = Div
_converter = Ansi2HTMLConverter(inline=True)
def _process_param_change(self, msg):
msg = super(AnsiColorText, self)._process_property_change(msg)
if 'value' in msg:
text = str(msg.pop('value'))
text = self._converter.convert(text)
msg['text'] = text
return msg
def scroll_down(self):
# TODO: figure out how to automatically scroll down as text is added
pass
class CommandRunner(param.Parameterized):
command_output = param.String()
def __init__(self, **params):
super().__init__(**params)
self._output_lines = []
self.proc = None
self._updater = PeriodicCallback(self._refresh_output, callback_time=1000)
@pn.depends('command_output')
def panel(self):
return pn.Param(self.param, show_name=False, sizing_mode='stretch_width', widgets={
'command_output': dict(
type=AnsiColorText,
sizing_mode='stretch_width',
height=800)
})
def run(self, *cmd):
self.command_output = ''
self._output_lines = []
self.proc = Subprocess(cmd, stdout=Subprocess.STREAM, stderr=STDOUT)
self._get_next_line()
self._updater.start()
def _get_next_line(self):
if self.proc is None:
return
loop = IOLoop.current()
loop.add_future(self.proc.stdout.read_until(bytes('\n', encoding='utf8')), self._append_output)
def _append_output(self, future):
self._output_lines.append(future.result().decode('utf8'))
self._get_next_line()
def _refresh_output(self):
text = ''.join(self._output_lines)
if len(text) != len(self.command_output):
with unlocked():
self.command_output = text
class TestRunner(param.Parameterized):
composition = param.ClassSelector(class_=Composition, precedence=-1)
testground_daemon_endpoint = param.String(default="{}:8042".format(os.environ.get('TESTGROUND_DAEMON_HOST', 'localhost')))
run_test = param.Action(lambda self: self.run())
runner = CommandRunner()
def __init__(self, **params):
super().__init__(**params)
def run(self):
# TODO: temp file management - maybe we should mount a volume and save there?
filename = '/tmp/composition.toml'
self.composition.write_to_file(filename)
self.runner.run(TESTGROUND, '--endpoint', self.testground_daemon_endpoint, 'run', 'composition', '-f', filename)
def panel(self):
return pn.Column(
self.param['testground_daemon_endpoint'],
self.param['run_test'],
self.runner.panel(),
sizing_mode='stretch_width',
)

File diff suppressed because one or more lines are too long

View File

@ -24,30 +24,15 @@ require_cmds() {
done done
} }
get_manifest_paths() { update_plans() {
find -L $tg_home/plans -name manifest.toml
}
update_manifests() {
local dest_dir=$1 local dest_dir=$1
mkdir -p $dest_dir rsync -avzh --quiet --copy-links "${tg_home}/plans/" ${dest_dir}
for m in $(get_manifest_paths); do
local plan=$(basename $(dirname $m))
local dest="$dest_dir/$plan/manifest.toml"
mkdir -p "$dest_dir/$plan"
# only copy if source manifest is newer than dest (or dest doesn't exist)
if [[ ! -e $dest || $m -nt $dest ]]; then
cp $m $dest
fi
done
} }
watch_manifests() { watch_plans() {
local manifest_dest=$1 local plans_dest=$1
while true; do while true; do
update_manifests ${manifest_dest} update_plans ${plans_dest}
sleep $poll_interval sleep $poll_interval
done done
} }
@ -71,16 +56,23 @@ cleanup () {
docker stop ${container_id} >/dev/null docker stop ${container_id} >/dev/null
fi fi
if [[ -d "$temp_manifest_dir" ]]; then if [[ -d "$temp_plans_dir" ]]; then
rm -rf ${temp_manifest_dir} rm -rf ${temp_plans_dir}
fi fi
} }
get_host_ip() {
# get interface of default route
local net_if=$(netstat -rn | awk '/^0.0.0.0/ {thif=substr($0,74,10); print thif;} /^default.*UG/ {thif=substr($0,65,10); print thif;}')
# use ifconfig to get addr of that interface
ifconfig ${net_if} | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1'
}
# run cleanup on exit # run cleanup on exit
trap "{ cleanup; }" EXIT trap "{ cleanup; }" EXIT
# make sure we have the commands we need # make sure we have the commands we need
require_cmds jq docker require_cmds jq docker rsync
# make temp dir for manifests # make temp dir for manifests
temp_base="/tmp" temp_base="/tmp"
@ -88,14 +80,19 @@ if [[ "$TEMP" != "" ]]; then
temp_base=$TEMP temp_base=$TEMP
fi fi
temp_manifest_dir="$(mktemp -d ${temp_base}/testground-composer-XXXX)" temp_plans_dir="$(mktemp -d ${temp_base}/testground-composer-XXXX)"
echo "temp manifest dir: $temp_manifest_dir" echo "temp plans dir: $temp_plans_dir"
# copy the manifests to the temp dir # copy testplans from $TESTGROUND_HOME/plans to the temp dir
update_manifests ${temp_manifest_dir} update_plans ${temp_plans_dir}
# run the container in detached mode and grab the id # run the container in detached mode and grab the id
container_id=$(docker run -d --user $(id -u):$(id -g) -p ${panel_port}:5006 -v ${temp_manifest_dir}:${container_plans_dir}:ro $image_full_name) container_id=$(docker run -d \
-e TESTGROUND_DAEMON_HOST=$(get_host_ip) \
--user $(id -u):$(id -g) \
-p ${panel_port}:5006 \
-v ${temp_plans_dir}:${container_plans_dir}:ro \
$image_full_name)
echo "container $container_id started" echo "container $container_id started"
# print the log output # print the log output
@ -108,5 +105,5 @@ sleep 2
panel_url="http://localhost:${panel_port}" panel_url="http://localhost:${panel_port}"
open_url $panel_url open_url $panel_url
# poll & check for manifest changes every few seconds # poll & sync testplan changes every few seconds
watch_manifests ${temp_manifest_dir} watch_plans ${temp_plans_dir}

View File

@ -1,63 +0,0 @@
name = "example"
[defaults]
builder = "exec:go"
runner = "local:exec"
[builders."docker:generic"]
enabled = true
[builders."docker:generic".build_args]
build_image = "golang:alpine"
run_image = "scratch"
[builders."docker:go"]
enabled = true
[builders."exec:go"]
enabled = true
[runners."local:docker"]
enabled = true
[runners."local:exec"]
enabled = true
[runners."cluster:swarm"]
enabled = true
[runners."cluster:k8s"]
enabled = true
[[testcases]]
name = "output"
instances = { min = 1, max = 200, default = 1 }
[[testcases]]
name = "failure"
instances = { min = 1, max = 200, default = 1 }
[[testcases]]
name = "panic"
instances = { min = 1, max = 200, default = 1 }
[[testcases]]
name = "params"
instances = { min = 1, max = 200, default = 1 }
[testcases.params]
param1 = { type = "int", desc = "some param 1", unit = "widgets", default=1 }
param2 = { type = "int", desc = "some param 2", unit = "widgets", default=2 }
param3 = { type = "int", desc = "some param 3", unit = "widgets", default=3 }
[[testcases]]
name = "sync"
instances = { min = 2, max = 200, default = 5 }
[[testcases]]
name = "metrics"
instances = { min = 1, max = 200, default = 5 }
[[testcases]]
name = "artifact"
instances = { min = 1, max = 200, default = 5 }

View File

@ -0,0 +1,14 @@
[metadata]
name = "ping-pong-local"
author = "yusef"
[global]
plan = "network"
case = "ping-pong"
total_instances = 2
builder = "docker:go"
runner = "local:docker"
[[groups]]
id = "nodes"
instances = { count = 2 }

View File

@ -3,3 +3,4 @@ toml
jupyter jupyter
panel panel
holoviews holoviews
ansi2html