feat: add basic support for execute upgrades, download stdout/stderr
upgrade progress is still not being shown yet
This commit is contained in:
parent
1a8900c9f4
commit
e5e31a7d32
17 changed files with 805 additions and 12 deletions
|
@ -316,3 +316,18 @@ class TestPermissions(DataTestCase):
|
|||
widget = typ.widget_maker()
|
||||
self.assertEqual(len(widget.values), 1)
|
||||
self.assertEqual(widget.values[0], ('widgets.polish', "Polish the widgets"))
|
||||
|
||||
|
||||
class TestFileDownload(DataTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.setup_db()
|
||||
self.request = testing.DummyRequest(wutta_config=self.config)
|
||||
|
||||
def test_widget_maker(self):
|
||||
|
||||
# sanity / coverage check
|
||||
typ = mod.FileDownload(self.request, url='/foo')
|
||||
widget = typ.widget_maker()
|
||||
self.assertIsInstance(widget, widgets.FileDownloadWidget)
|
||||
self.assertEqual(widget.url, '/foo')
|
||||
|
|
|
@ -7,7 +7,7 @@ import deform
|
|||
from pyramid import testing
|
||||
|
||||
from wuttaweb.forms import widgets as mod
|
||||
from wuttaweb.forms.schema import PersonRef, RoleRefs, UserRefs, Permissions
|
||||
from wuttaweb.forms.schema import FileDownload, PersonRef, RoleRefs, UserRefs, Permissions
|
||||
from tests.util import WebTestCase
|
||||
|
||||
|
||||
|
@ -52,6 +52,55 @@ class TestObjectRefWidget(WebTestCase):
|
|||
self.assertIn('href="/foo"', html)
|
||||
|
||||
|
||||
class TestFileDownloadWidget(WebTestCase):
|
||||
|
||||
def make_field(self, node, **kwargs):
|
||||
# TODO: not sure why default renderer is in use even though
|
||||
# pyramid_deform was included in setup? but this works..
|
||||
kwargs.setdefault('renderer', deform.Form.default_renderer)
|
||||
return deform.Field(node, **kwargs)
|
||||
|
||||
def test_serialize(self):
|
||||
|
||||
# nb. we let the field construct the widget via our type
|
||||
# (nb. at first we do not provide a url)
|
||||
node = colander.SchemaNode(FileDownload(self.request))
|
||||
field = self.make_field(node)
|
||||
widget = field.widget
|
||||
|
||||
# null value
|
||||
html = widget.serialize(field, None, readonly=True)
|
||||
self.assertNotIn('<a ', html)
|
||||
self.assertIn('<span>', html)
|
||||
|
||||
# path to nonexistent file
|
||||
html = widget.serialize(field, '/this/path/does/not/exist', readonly=True)
|
||||
self.assertNotIn('<a ', html)
|
||||
self.assertIn('<span>', html)
|
||||
|
||||
# path to actual file
|
||||
datfile = self.write_file('data.txt', "hello\n" * 1000)
|
||||
html = widget.serialize(field, datfile, readonly=True)
|
||||
self.assertNotIn('<a ', html)
|
||||
self.assertIn('<span>', html)
|
||||
self.assertIn('data.txt', html)
|
||||
self.assertIn('kB)', html)
|
||||
|
||||
# path to file, w/ url
|
||||
node = colander.SchemaNode(FileDownload(self.request, url='/download/blarg'))
|
||||
field = self.make_field(node)
|
||||
widget = field.widget
|
||||
html = widget.serialize(field, datfile, readonly=True)
|
||||
self.assertNotIn('<span>', html)
|
||||
self.assertIn('<a href="/download/blarg">', html)
|
||||
self.assertIn('data.txt', html)
|
||||
self.assertIn('kB)', html)
|
||||
|
||||
# nb. same readonly output even if we ask for editable
|
||||
html2 = widget.serialize(field, datfile, readonly=False)
|
||||
self.assertEqual(html2, html)
|
||||
|
||||
|
||||
class TestRoleRefsWidget(WebTestCase):
|
||||
|
||||
def make_field(self, node, **kwargs):
|
||||
|
@ -113,7 +162,7 @@ class TestUserRefsWidget(WebTestCase):
|
|||
|
||||
# empty
|
||||
html = widget.serialize(field, set(), readonly=True)
|
||||
self.assertIn('<b-table ', html)
|
||||
self.assertEqual(html, '<span></span>')
|
||||
|
||||
# with data, no actions
|
||||
user = model.User(username='barney')
|
||||
|
|
|
@ -6,11 +6,12 @@ from unittest.mock import MagicMock
|
|||
from pyramid import testing
|
||||
|
||||
from wuttjamaican.conf import WuttaConfig
|
||||
from wuttjamaican.testing import FileConfigTestCase
|
||||
from wuttaweb import subscribers
|
||||
from wuttaweb.menus import MenuHandler
|
||||
|
||||
|
||||
class DataTestCase(TestCase):
|
||||
class DataTestCase(FileConfigTestCase):
|
||||
"""
|
||||
Base class for test suites requiring a full (typical) database.
|
||||
"""
|
||||
|
@ -19,6 +20,7 @@ class DataTestCase(TestCase):
|
|||
self.setup_db()
|
||||
|
||||
def setup_db(self):
|
||||
self.setup_files()
|
||||
self.config = WuttaConfig(defaults={
|
||||
'wutta.db.default.url': 'sqlite://',
|
||||
})
|
||||
|
@ -33,7 +35,7 @@ class DataTestCase(TestCase):
|
|||
self.teardown_db()
|
||||
|
||||
def teardown_db(self):
|
||||
pass
|
||||
self.teardown_files()
|
||||
|
||||
|
||||
class WebTestCase(DataTestCase):
|
||||
|
|
|
@ -50,6 +50,26 @@ class TestView(WebTestCase):
|
|||
self.assertIsInstance(error, HTTPFound)
|
||||
self.assertEqual(error.location, '/')
|
||||
|
||||
def test_file_response(self):
|
||||
view = self.make_view()
|
||||
|
||||
# default uses attachment behavior
|
||||
datfile = self.write_file('dat.txt', 'hello')
|
||||
response = view.file_response(datfile)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.content_disposition, 'attachment; filename="dat.txt"')
|
||||
|
||||
# but can disable attachment behavior
|
||||
datfile = self.write_file('dat.txt', 'hello')
|
||||
response = view.file_response(datfile, attachment=False)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsNone(response.content_disposition)
|
||||
|
||||
# path not found
|
||||
crapfile = '/does/not/exist'
|
||||
response = view.file_response(crapfile)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_json_response(self):
|
||||
view = self.make_view()
|
||||
response = view.json_response({'foo': 'bar'})
|
||||
|
|
|
@ -30,6 +30,8 @@ class TestMasterView(WebTestCase):
|
|||
model_key='uuid',
|
||||
deletable_bulk=True,
|
||||
has_autocomplete=True,
|
||||
downloadable=True,
|
||||
executable=True,
|
||||
configurable=True):
|
||||
mod.MasterView.defaults(self.pyramid_config)
|
||||
|
||||
|
@ -1310,6 +1312,59 @@ class TestMasterView(WebTestCase):
|
|||
self.assertEqual(normal, {'value': 'bogus',
|
||||
'label': "Betty Boop"})
|
||||
|
||||
def test_download(self):
|
||||
model = self.app.model
|
||||
self.app.save_setting(self.session, 'foo', 'bar')
|
||||
self.session.commit()
|
||||
|
||||
with patch.multiple(mod.MasterView, create=True,
|
||||
model_class=model.Setting,
|
||||
model_key='name',
|
||||
Session=MagicMock(return_value=self.session)):
|
||||
view = self.make_view()
|
||||
self.request.matchdict = {'name': 'foo'}
|
||||
|
||||
# 404 if no filename
|
||||
response = view.download()
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# 404 if bad filename
|
||||
self.request.GET = {'filename': 'doesnotexist'}
|
||||
response = view.download()
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# 200 if good filename
|
||||
foofile = self.write_file('foo.txt', 'foo')
|
||||
with patch.object(view, 'download_path', return_value=foofile):
|
||||
response = view.download()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.content_disposition, 'attachment; filename="foo.txt"')
|
||||
|
||||
def test_execute(self):
|
||||
self.pyramid_config.add_route('settings.view', '/settings/{name}')
|
||||
model = self.app.model
|
||||
self.app.save_setting(self.session, 'foo', 'bar')
|
||||
self.session.commit()
|
||||
|
||||
with patch.multiple(mod.MasterView, create=True,
|
||||
model_class=model.Setting,
|
||||
model_key='name',
|
||||
Session=MagicMock(return_value=self.session)):
|
||||
view = self.make_view()
|
||||
self.request.matchdict = {'name': 'foo'}
|
||||
|
||||
# basic usage, redirects to view obj url
|
||||
response = view.execute()
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(self.request.session.pop_flash(), ["Setting was executed."])
|
||||
|
||||
# execution error
|
||||
with patch.object(view, 'execute_instance', side_effect=RuntimeError):
|
||||
response = view.execute()
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(self.request.session.pop_flash(), [])
|
||||
self.assertEqual(self.request.session.pop_flash('error'), ["RuntimeError"])
|
||||
|
||||
def test_configure(self):
|
||||
self.pyramid_config.include('wuttaweb.views.common')
|
||||
self.pyramid_config.include('wuttaweb.views.auth')
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from wuttaweb.views import upgrades as mod
|
||||
from wuttjamaican.exc import ConfigurationError
|
||||
from tests.util import WebTestCase
|
||||
|
||||
|
||||
|
@ -42,6 +45,7 @@ class TestUpgradeView(WebTestCase):
|
|||
self.assertEqual(view.grid_row_class(upgrade, data, 1), 'has-background-warning')
|
||||
|
||||
def test_configure_form(self):
|
||||
self.pyramid_config.add_route('upgrades.download', '/upgrades/{uuid}/download')
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
user = model.User(username='barney')
|
||||
|
@ -66,7 +70,7 @@ class TestUpgradeView(WebTestCase):
|
|||
view.configure_form(form)
|
||||
self.assertNotIn('created', form)
|
||||
|
||||
# test executed field when viewing
|
||||
# test executed, stdout/stderr when viewing
|
||||
with patch.object(view, 'viewing', new=True):
|
||||
|
||||
# executed is *not* shown by default
|
||||
|
@ -74,13 +78,18 @@ class TestUpgradeView(WebTestCase):
|
|||
self.assertIn('executed', form)
|
||||
view.configure_form(form)
|
||||
self.assertNotIn('executed', form)
|
||||
self.assertNotIn('stdout_file', form)
|
||||
self.assertNotIn('stderr_file', form)
|
||||
|
||||
# but it *is* shown if upgrade is executed
|
||||
upgrade.executed = datetime.datetime.now()
|
||||
upgrade.status = enum.UpgradeStatus.SUCCESS
|
||||
form = view.make_form(model_class=model.Upgrade, model_instance=upgrade)
|
||||
self.assertIn('executed', form)
|
||||
view.configure_form(form)
|
||||
self.assertIn('executed', form)
|
||||
self.assertIn('stdout_file', form)
|
||||
self.assertIn('stderr_file', form)
|
||||
|
||||
def test_objectify(self):
|
||||
model = self.app.model
|
||||
|
@ -101,3 +110,167 @@ class TestUpgradeView(WebTestCase):
|
|||
self.assertEqual(upgrade.description, "new one")
|
||||
self.assertIs(upgrade.created_by, user)
|
||||
self.assertEqual(upgrade.status, enum.UpgradeStatus.PENDING)
|
||||
|
||||
def test_download_path(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
|
||||
appdir = self.mkdir('app')
|
||||
self.config.setdefault('wutta.appdir', appdir)
|
||||
self.assertEqual(self.app.get_appdir(), appdir)
|
||||
|
||||
user = model.User(username='barney')
|
||||
upgrade = model.Upgrade(description='test', created_by=user,
|
||||
status=enum.UpgradeStatus.PENDING)
|
||||
self.session.add(upgrade)
|
||||
self.session.commit()
|
||||
|
||||
view = self.make_view()
|
||||
uuid = upgrade.uuid
|
||||
|
||||
# no filename
|
||||
path = view.download_path(upgrade, None)
|
||||
self.assertIsNone(path)
|
||||
|
||||
# with filename
|
||||
path = view.download_path(upgrade, 'foo.txt')
|
||||
self.assertEqual(path, os.path.join(appdir, 'data', 'upgrades',
|
||||
uuid[:2], uuid[2:], 'foo.txt'))
|
||||
|
||||
def test_get_upgrade_filepath(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
|
||||
appdir = self.mkdir('app')
|
||||
self.config.setdefault('wutta.appdir', appdir)
|
||||
self.assertEqual(self.app.get_appdir(), appdir)
|
||||
|
||||
user = model.User(username='barney')
|
||||
upgrade = model.Upgrade(description='test', created_by=user,
|
||||
status=enum.UpgradeStatus.PENDING)
|
||||
self.session.add(upgrade)
|
||||
self.session.commit()
|
||||
|
||||
view = self.make_view()
|
||||
uuid = upgrade.uuid
|
||||
|
||||
# no filename
|
||||
path = view.get_upgrade_filepath(upgrade)
|
||||
self.assertEqual(path, os.path.join(appdir, 'data', 'upgrades',
|
||||
uuid[:2], uuid[2:]))
|
||||
|
||||
# with filename
|
||||
path = view.get_upgrade_filepath(upgrade, 'foo.txt')
|
||||
self.assertEqual(path, os.path.join(appdir, 'data', 'upgrades',
|
||||
uuid[:2], uuid[2:], 'foo.txt'))
|
||||
|
||||
def test_delete_instance(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
|
||||
appdir = self.mkdir('app')
|
||||
self.config.setdefault('wutta.appdir', appdir)
|
||||
self.assertEqual(self.app.get_appdir(), appdir)
|
||||
|
||||
user = model.User(username='barney')
|
||||
upgrade = model.Upgrade(description='test', created_by=user,
|
||||
status=enum.UpgradeStatus.PENDING)
|
||||
self.session.add(upgrade)
|
||||
self.session.commit()
|
||||
|
||||
view = self.make_view()
|
||||
|
||||
# mock stdout/stderr files
|
||||
upgrade_dir = view.get_upgrade_filepath(upgrade)
|
||||
stdout = view.get_upgrade_filepath(upgrade, 'stdout.log')
|
||||
with open(stdout, 'w') as f:
|
||||
f.write('stdout')
|
||||
stderr = view.get_upgrade_filepath(upgrade, 'stderr.log')
|
||||
with open(stderr, 'w') as f:
|
||||
f.write('stderr')
|
||||
|
||||
# both upgrade and files are deleted
|
||||
self.assertTrue(os.path.exists(upgrade_dir))
|
||||
self.assertTrue(os.path.exists(stdout))
|
||||
self.assertTrue(os.path.exists(stderr))
|
||||
self.assertEqual(self.session.query(model.Upgrade).count(), 1)
|
||||
with patch.object(view, 'Session', return_value=self.session):
|
||||
view.delete_instance(upgrade)
|
||||
self.assertFalse(os.path.exists(upgrade_dir))
|
||||
self.assertFalse(os.path.exists(stdout))
|
||||
self.assertFalse(os.path.exists(stderr))
|
||||
self.assertEqual(self.session.query(model.Upgrade).count(), 0)
|
||||
|
||||
def test_execute_instance(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
|
||||
appdir = self.mkdir('app')
|
||||
self.config.setdefault('wutta.appdir', appdir)
|
||||
self.assertEqual(self.app.get_appdir(), appdir)
|
||||
|
||||
user = model.User(username='barney')
|
||||
upgrade = model.Upgrade(description='test', created_by=user,
|
||||
status=enum.UpgradeStatus.PENDING)
|
||||
self.session.add(upgrade)
|
||||
self.session.commit()
|
||||
|
||||
view = self.make_view()
|
||||
self.request.user = user
|
||||
python = sys.executable
|
||||
|
||||
# script not yet confiugred
|
||||
self.assertRaises(ConfigurationError, view.execute_instance, upgrade)
|
||||
|
||||
# script w/ success
|
||||
goodpy = self.write_file('good.py', """
|
||||
import sys
|
||||
sys.stdout.write('hello from good.py')
|
||||
sys.exit(0)
|
||||
""")
|
||||
self.app.save_setting(self.session, 'wutta.upgrades.command', f'{python} {goodpy}')
|
||||
self.assertIsNone(upgrade.executed)
|
||||
self.assertIsNone(upgrade.executed_by)
|
||||
self.assertEqual(upgrade.status, enum.UpgradeStatus.PENDING)
|
||||
with patch.object(view, 'Session', return_value=self.session):
|
||||
with patch.object(self.config, 'usedb', new=True):
|
||||
view.execute_instance(upgrade)
|
||||
self.assertIsNotNone(upgrade.executed)
|
||||
self.assertIs(upgrade.executed_by, user)
|
||||
self.assertEqual(upgrade.status, enum.UpgradeStatus.SUCCESS)
|
||||
with open(view.get_upgrade_filepath(upgrade, 'stdout.log')) as f:
|
||||
self.assertEqual(f.read(), 'hello from good.py')
|
||||
with open(view.get_upgrade_filepath(upgrade, 'stderr.log')) as f:
|
||||
self.assertEqual(f.read(), '')
|
||||
|
||||
# need a new record for next test
|
||||
upgrade = model.Upgrade(description='test', created_by=user,
|
||||
status=enum.UpgradeStatus.PENDING)
|
||||
self.session.add(upgrade)
|
||||
self.session.commit()
|
||||
|
||||
# script w/ failure
|
||||
badpy = self.write_file('bad.py', """
|
||||
import sys
|
||||
sys.stderr.write('hello from bad.py')
|
||||
sys.exit(42)
|
||||
""")
|
||||
self.app.save_setting(self.session, 'wutta.upgrades.command', f'{python} {badpy}')
|
||||
self.assertIsNone(upgrade.executed)
|
||||
self.assertIsNone(upgrade.executed_by)
|
||||
self.assertEqual(upgrade.status, enum.UpgradeStatus.PENDING)
|
||||
with patch.object(view, 'Session', return_value=self.session):
|
||||
with patch.object(self.config, 'usedb', new=True):
|
||||
view.execute_instance(upgrade)
|
||||
self.assertIsNotNone(upgrade.executed)
|
||||
self.assertIs(upgrade.executed_by, user)
|
||||
self.assertEqual(upgrade.status, enum.UpgradeStatus.FAILURE)
|
||||
with open(view.get_upgrade_filepath(upgrade, 'stdout.log')) as f:
|
||||
self.assertEqual(f.read(), '')
|
||||
with open(view.get_upgrade_filepath(upgrade, 'stderr.log')) as f:
|
||||
self.assertEqual(f.read(), 'hello from bad.py')
|
||||
|
||||
def test_configure_get_simple_settings(self):
|
||||
# sanity/coverage check
|
||||
view = self.make_view()
|
||||
simple = view.configure_get_simple_settings()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue