You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
590 lines
21 KiB
590 lines
21 KiB
import logging
|
|
import sys
|
|
import unittest
|
|
import json
|
|
import os
|
|
import copy
|
|
import threading
|
|
import time
|
|
import requests
|
|
import re
|
|
import random
|
|
|
|
|
|
def gen_random_file(fname, size=1024):
|
|
with open(fname, 'wb') as fh:
|
|
for i in range(size):
|
|
fh.write(chr(random.randint(0, 255)).encode('utf-8'))
|
|
|
|
|
|
class FormConfigTestCase(unittest.TestCase):
|
|
"""
|
|
Test the proper low-level handling of form configurations such as loading,
|
|
callbacks, etc.
|
|
"""
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
if os.path.exists('tmp_stdout'):
|
|
os.unlink('tmp_stdout')
|
|
if os.path.exists('tmp_stderr'):
|
|
os.unlink('tmp_stderr')
|
|
|
|
def testNoSuchForm(self):
|
|
"""Getting non-existing form should raise ValueError"""
|
|
sf = scriptform.ScriptForm('test_formconfig_hidden.json')
|
|
fc = sf.get_form_config()
|
|
self.assertRaises(ValueError, fc.get_form_def, 'nonexisting')
|
|
|
|
def testMissingScript(self):
|
|
"""Missing script callbacks should raise an OSError"""
|
|
self.assertRaises(OSError, scriptform.ScriptForm, 'test_formconfig_missingscript.json')
|
|
|
|
def testNoExec(self):
|
|
"""Non-executable script callbacks should raise an FormConfigError"""
|
|
from formconfig import FormConfigError
|
|
self.assertRaises(FormConfigError, scriptform.ScriptForm, 'test_formconfig_noexec.json')
|
|
|
|
def testHidden(self):
|
|
"""Hidden forms should not show up in the list of forms"""
|
|
sf = scriptform.ScriptForm('test_formconfig_hidden.json')
|
|
fc = sf.get_form_config()
|
|
self.assertTrue(fc.get_visible_forms() == [])
|
|
|
|
def testCallbackStore(self):
|
|
"""Test a callback that returns output in strings"""
|
|
sf = scriptform.ScriptForm('test_formconfig_callback.json')
|
|
fc = sf.get_form_config()
|
|
fd = fc.get_form_def('test_store')
|
|
res = runscript.run_script(fd, {}, {})
|
|
self.assertEqual(res['exitcode'], 33)
|
|
self.assertTrue(b'stdout' in res['stdout'])
|
|
self.assertTrue(b'stderr' in res['stderr'])
|
|
|
|
def testCallbackRaw(self):
|
|
"""Test a callback that returns raw output"""
|
|
sf = scriptform.ScriptForm('test_formconfig_callback.json')
|
|
fc = sf.get_form_config()
|
|
fd = fc.get_form_def('test_raw')
|
|
stdout = open('tmp_stdout', 'w+') # can't use StringIO
|
|
stderr = open('tmp_stderr', 'w+')
|
|
exitcode = runscript.run_script(fd, {}, {}, stdout, stderr)
|
|
stdout.seek(0)
|
|
stderr.seek(0)
|
|
self.assertTrue(exitcode == 33)
|
|
self.assertTrue('stdout' in stdout.read())
|
|
stdout.close()
|
|
stderr.close()
|
|
|
|
def testCallbackMissingParams(self):
|
|
"""
|
|
"""
|
|
sf = scriptform.ScriptForm('test_formconfig_callback.json')
|
|
fc = sf.get_form_config()
|
|
fd = fc.get_form_def('test_raw')
|
|
self.assertRaises(ValueError, runscript.run_script, fd, {}, {})
|
|
|
|
|
|
class FormDefinitionTest(unittest.TestCase):
|
|
"""
|
|
Form Definition tests. Mostly directly testing if validations work.
|
|
"""
|
|
def setUp(self):
|
|
self.sf = scriptform.ScriptForm('test_formdefinition_validate.json')
|
|
self.fc = self.sf.get_form_config()
|
|
|
|
def testUnknownFieldError(self):
|
|
fd = self.fc.get_form_def('test_required')
|
|
self.assertRaises(KeyError, fd.get_field_def, 'nosuchfield')
|
|
|
|
def testRequired(self):
|
|
fd = self.fc.get_form_def('test_required')
|
|
form_values = {}
|
|
errors, values = fd.validate(form_values)
|
|
self.assertIn('string', errors)
|
|
self.assertIn('required', errors['string'][0])
|
|
|
|
def testValidateStringMin(self):
|
|
fd = self.fc.get_form_def('test_val_string')
|
|
form_values = {"val_string": "123"}
|
|
errors, values = fd.validate(form_values)
|
|
self.assertIn('val_string', errors)
|
|
self.assertIn('Minimum', errors['val_string'][0])
|
|
|
|
def testValidateStringMax(self):
|
|
fd = self.fc.get_form_def('test_val_string')
|
|
form_values = {"val_string": "1234567"}
|
|
errors, values = fd.validate(form_values)
|
|
self.assertIn('val_string', errors)
|
|
self.assertIn('Maximum', errors['val_string'][0])
|
|
|
|
def testValidateStringValue(self):
|
|
fd = self.fc.get_form_def('test_val_string')
|
|
form_values = {"val_string": "1234"}
|
|
errors, values = fd.validate(form_values)
|
|
self.assertNotIn('val_string', errors)
|
|
self.assertEqual(values['val_string'], "1234")
|
|
|
|
def testValidateIntegerInvalid(self):
|
|
fd = self.fc.get_form_def('test_val_integer')
|
|
form_values = {"val_integer": 'three'}
|
|
errors, values = fd.validate(form_values)
|
|
self.assertIn('val_integer', errors)
|
|
self.assertIn('Must be a', errors['val_integer'][0])
|
|
|
|
def testValidateIntegerMin(self):
|
|
fd = self.fc.get_form_def('test_val_integer')
|
|
form_values = {"val_integer": 3}
|
|
errors, values = fd.validate(form_values)
|
|
self.assertIn('val_integer', errors)
|
|
self.assertIn('Minimum', errors['val_integer'][0])
|
|
|
|
def testValidateIntegerMax(self):
|
|
fd = self.fc.get_form_def('test_val_integer')
|
|
form_values = {"val_integer": 7}
|
|
errors, values = fd.validate(form_values)
|
|
self.assertIn('val_integer', errors)
|
|
self.assertIn('Maximum', errors['val_integer'][0])
|
|
|
|
def testValidateIntegerValue(self):
|
|
fd = self.fc.get_form_def('test_val_integer')
|
|
form_values = {"val_integer": 6}
|
|
errors, values = fd.validate(form_values)
|
|
self.assertNotIn('val_integer', errors)
|
|
self.assertEqual(values['val_integer'], 6)
|
|
|
|
def testValidateFloatInvalid(self):
|
|
fd = self.fc.get_form_def('test_val_float')
|
|
form_values = {"val_float": 'four'}
|
|
errors, values = fd.validate(form_values)
|
|
self.assertTrue('val_float' in errors)
|
|
self.assertTrue('Must be a' in errors['val_float'][0])
|
|
|
|
def testValidateFloatMin(self):
|
|
fd = self.fc.get_form_def('test_val_float')
|
|
form_values = {"val_float": 2.05}
|
|
errors, values = fd.validate(form_values)
|
|
self.assertTrue('val_float' in errors)
|
|
self.assertTrue('Minimum' in errors['val_float'][0])
|
|
|
|
def testValidateFloatMax(self):
|
|
fd = self.fc.get_form_def('test_val_float')
|
|
form_values = {"val_float": 2.31}
|
|
errors, values = fd.validate(form_values)
|
|
self.assertIn('val_float', errors)
|
|
self.assertIn('Maximum', errors['val_float'][0])
|
|
|
|
def testValidateFloatValue(self):
|
|
fd = self.fc.get_form_def('test_val_float')
|
|
form_values = {"val_float": 2.29}
|
|
errors, values = fd.validate(form_values)
|
|
self.assertNotIn('val_float', errors)
|
|
self.assertEqual(values['val_float'], 2.29)
|
|
|
|
def testValidateDateInvalid(self):
|
|
fd = self.fc.get_form_def('test_val_date')
|
|
form_values = {"val_date": '2015-001'}
|
|
errors, values = fd.validate(form_values)
|
|
self.assertIn('val_date', errors)
|
|
self.assertIn('Invalid date', errors['val_date'][0])
|
|
|
|
def testValidateDateMin(self):
|
|
fd = self.fc.get_form_def('test_val_date')
|
|
form_values = {"val_date": '2015-03-01'}
|
|
errors, values = fd.validate(form_values)
|
|
self.assertIn('val_date', errors)
|
|
self.assertIn('Minimum', errors['val_date'][0])
|
|
|
|
def testValidateDateMax(self):
|
|
fd = self.fc.get_form_def('test_val_date')
|
|
form_values = {"val_date": '2015-03-06'}
|
|
errors, values = fd.validate(form_values)
|
|
self.assertIn('val_date', errors)
|
|
self.assertIn('Maximum', errors['val_date'][0])
|
|
|
|
def testValidateDateValue(self):
|
|
import datetime
|
|
fd = self.fc.get_form_def('test_val_date')
|
|
form_values = {"val_date": '2015-03-03'}
|
|
errors, values = fd.validate(form_values)
|
|
self.assertNotIn('val_date', errors)
|
|
self.assertEqual(values['val_date'], datetime.date(2015, 3, 3))
|
|
|
|
def testValidateSelectValue(self):
|
|
fd = self.fc.get_form_def('test_val_select')
|
|
form_values = {"val_select": 'option_a'}
|
|
errors, values = fd.validate(form_values)
|
|
self.assertNotIn('val_select', errors)
|
|
self.assertEqual(values['val_select'], 'option_a')
|
|
|
|
def testValidateSelectInvalid(self):
|
|
fd = self.fc.get_form_def('test_val_select')
|
|
form_values = {"val_select": 'option_c'}
|
|
errors, values = fd.validate(form_values)
|
|
self.assertIn('val_select', errors)
|
|
self.assertIn('Invalid value', errors['val_select'][0])
|
|
|
|
def testValidateCheckbox(self):
|
|
fd = self.fc.get_form_def('test_val_checkbox')
|
|
form_values = {"val_checkbox": 'on'}
|
|
errors, values = fd.validate(form_values)
|
|
self.assertNotIn('val_checkbox', errors)
|
|
self.assertEqual(values['val_checkbox'], 'on')
|
|
|
|
def testValidateCheckboxDefaultOn(self):
|
|
fd = self.fc.get_form_def('test_val_checkbox_on')
|
|
form_values = {"val_checkbox_on": 'off'}
|
|
errors, values = fd.validate(form_values)
|
|
self.assertNotIn('val_checkbox_on', errors)
|
|
self.assertEqual(values['val_checkbox_on'], 'off')
|
|
|
|
def testValidateCheckboxInvalid(self):
|
|
fd = self.fc.get_form_def('test_val_checkbox')
|
|
form_values = {"val_checkbox": 'true'}
|
|
errors, values = fd.validate(form_values)
|
|
self.assertIn('val_checkbox', errors)
|
|
self.assertIn('Invalid value', errors['val_checkbox'][0])
|
|
|
|
def testValidateTextMin(self):
|
|
fd = self.fc.get_form_def('test_val_text')
|
|
form_values = {"val_text": '1234'}
|
|
errors, values = fd.validate(form_values)
|
|
self.assertIn('val_text', errors)
|
|
self.assertIn('Minimum', errors['val_text'][0])
|
|
|
|
def testValidateTextMax(self):
|
|
fd = self.fc.get_form_def('test_val_text')
|
|
form_values = {"val_text": '12345678901'}
|
|
errors, values = fd.validate(form_values)
|
|
self.assertIn('val_text', errors)
|
|
self.assertIn('Maximum', errors['val_text'][0])
|
|
|
|
def testValidateFileMissingFile(self):
|
|
fd = self.fc.get_form_def('test_val_file')
|
|
form_values = {}
|
|
errors, values = fd.validate(form_values)
|
|
self.assertIn('val_file', errors)
|
|
self.assertIn('required', errors['val_file'][0])
|
|
|
|
def testValidateFileMissingFileName(self):
|
|
fd = self.fc.get_form_def('test_val_file')
|
|
form_values = {'val_file': 'foo'}
|
|
self.assertRaises(KeyError, fd.validate, form_values)
|
|
|
|
|
|
class FormDefinitionFieldMissingProperty(unittest.TestCase):
|
|
"""
|
|
"""
|
|
def testMissing(self):
|
|
self.assertRaises(KeyError, scriptform.ScriptForm, 'test_formdefinition_missing_title.json')
|
|
|
|
|
|
class WebAppTest(unittest.TestCase):
|
|
"""
|
|
Test the web app by actually running the server and making web calls to it.
|
|
"""
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.auth_admin = requests.auth.HTTPBasicAuth('admin', 'admin')
|
|
cls.auth_user = requests.auth.HTTPBasicAuth('user', 'user')
|
|
|
|
# Run the server in a thread, so we can execute the tests in the main
|
|
# program.
|
|
def server_thread(sf):
|
|
sf.run(listen_port=8002)
|
|
cls.sf = scriptform.ScriptForm('test_webapp.json')
|
|
|
|
thread = threading.Thread(target=server_thread, args=(cls.sf,))
|
|
thread.start()
|
|
|
|
while True:
|
|
time.sleep(0.1)
|
|
if cls.sf.running is True:
|
|
break
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
# Shut down the webserver and wait until it has shut down.
|
|
cls.sf.shutdown()
|
|
while True:
|
|
time.sleep(0.1)
|
|
if cls.sf.running is False:
|
|
break
|
|
|
|
def testError404(self):
|
|
r = requests.get('http://localhost:8002/nosuchurl')
|
|
self.assertEqual(r.status_code, 404)
|
|
self.assertIn('Not found', r.text)
|
|
|
|
def testError401(self):
|
|
r = requests.get('http://localhost:8002/')
|
|
self.assertEqual(r.status_code, 401)
|
|
|
|
def testAuthFormNoAuthGet(self):
|
|
r = requests.get('http://localhost:8002/form?form_name=admin_only')
|
|
self.assertEqual(r.status_code, 401)
|
|
|
|
def testAuthFormNoAuthPost(self):
|
|
data = {"form_name": 'admin_only'}
|
|
r = requests.post('http://localhost:8002/submit', data)
|
|
self.assertEqual(r.status_code, 401)
|
|
|
|
def testAuthFormUnauthorizedGet(self):
|
|
r = requests.get('http://localhost:8002/form?form_name=admin_only', auth=self.auth_user)
|
|
self.assertEqual(r.status_code, 403)
|
|
|
|
def testAuthFormUnauthorizedPost(self):
|
|
data = {"form_name": 'admin_only'}
|
|
r = requests.post('http://localhost:8002/submit', data, auth=self.auth_user)
|
|
self.assertEqual(r.status_code, 403)
|
|
|
|
def testHidden(self):
|
|
"""Hidden forms shouldn't appear in the output"""
|
|
r = requests.get('http://localhost:8002/', auth=self.auth_user)
|
|
self.assertNotIn('Hidden form', r.text)
|
|
|
|
def testShown(self):
|
|
"""Non-hidden forms should appear in the output"""
|
|
r = requests.get('http://localhost:8002/', auth=self.auth_user)
|
|
self.assertIn('Output escaped', r.text)
|
|
|
|
def testRender(self):
|
|
r = requests.get('http://localhost:8002/form?form_name=validate', auth=self.auth_user)
|
|
self.assertIn('Validated form', r.text)
|
|
self.assertIn('This form is heavily validated', r.text)
|
|
self.assertIn('name="string"', r.text)
|
|
|
|
def testValidateCorrectData(self):
|
|
data = {
|
|
"form_name": 'validate',
|
|
"string": "12345",
|
|
"integer": "12",
|
|
"float": "0.6",
|
|
"date": "2015-01-02",
|
|
"text": "1234567890",
|
|
"password": "12345",
|
|
"radio": "One",
|
|
"checkbox": "on",
|
|
"select": "option_a",
|
|
}
|
|
|
|
gen_random_file('data.csv')
|
|
|
|
with open('data.csv', 'rb') as fh:
|
|
files = {'file': fh}
|
|
r = requests.post("http://localhost:8002/submit", data=data, files=files, auth=self.auth_user)
|
|
|
|
self.assertIn('string=12345', r.text)
|
|
self.assertIn('integer=12', r.text)
|
|
self.assertIn('float=0.6', r.text)
|
|
self.assertIn('date=2015-01-02', r.text)
|
|
self.assertIn('text=1234567890', r.text)
|
|
self.assertIn('password=12345', r.text)
|
|
self.assertIn('radio=One', r.text)
|
|
self.assertIn('checkbox=on', r.text)
|
|
self.assertIn('select=option_a', r.text)
|
|
|
|
os.unlink('data.csv')
|
|
|
|
def testValidateIncorrectData(self):
|
|
data = {
|
|
"form_name": 'validate',
|
|
"string": "12345678",
|
|
"integer": "9",
|
|
"float": "1.1",
|
|
"date": "2015-02-02",
|
|
"radio": "Ten",
|
|
"text": "123456789",
|
|
"password": "1234",
|
|
"checkbox": "invalidvalue",
|
|
"select": "invalidvalue",
|
|
}
|
|
|
|
gen_random_file('data.txt')
|
|
|
|
with open('data.txt', 'rb') as fh:
|
|
files = {'file': fh}
|
|
r = requests.post("http://localhost:8002/submit", data=data, files=files, auth=self.auth_user)
|
|
|
|
self.assertIn('Maximum length is 7', r.text)
|
|
self.assertIn('Minimum value is 10', r.text)
|
|
self.assertIn('Maximum value is 1.0', r.text)
|
|
self.assertIn('Maximum value is 2015-02-01', r.text)
|
|
self.assertIn('Invalid value for radio button: Ten', r.text)
|
|
self.assertIn('Minimum length is 10', r.text)
|
|
self.assertIn('Minimum length is 5', r.text)
|
|
self.assertIn('Only file types allowed: csv', r.text)
|
|
self.assertIn('Invalid value for radio button', r.text)
|
|
self.assertIn('Invalid value for dropdown', r.text)
|
|
|
|
os.unlink('data.txt')
|
|
|
|
def testValidateRefill(self):
|
|
"""
|
|
Ensure that field values are properly repopulated if there were any
|
|
errors in validation.
|
|
"""
|
|
data = {
|
|
"form_name": 'validate',
|
|
"string": "123",
|
|
"integer": "12",
|
|
"float": "0.6",
|
|
"date": "2015-01-02",
|
|
"text": "1234567890",
|
|
"password": "12345",
|
|
"radio": "One",
|
|
"checkbox": "on",
|
|
"select": "option_b",
|
|
}
|
|
|
|
gen_random_file('data.txt')
|
|
|
|
with open ('data.txt', 'rb') as fh:
|
|
files = {'file': fh}
|
|
r = requests.post("http://localhost:8002/submit", data=data, files=files, auth=self.auth_user)
|
|
self.assertIn('value="123"', r.text)
|
|
self.assertIn('value="12"', r.text)
|
|
self.assertIn('value="0.6"', r.text)
|
|
self.assertIn('value="2015-01-02"', r.text)
|
|
self.assertIn('>1234567890<', r.text)
|
|
self.assertIn('value="12345"', r.text)
|
|
self.assertIn('value="on"', r.text)
|
|
self.assertIn('selected>Option B', r.text)
|
|
|
|
os.unlink('data.txt')
|
|
|
|
def testOutputEscaped(self):
|
|
"""Form with 'escaped' output should have HTML entities escaped"""
|
|
data = {
|
|
"form_name": 'output_escaped',
|
|
"string": '<foo>'
|
|
}
|
|
r = requests.post('http://localhost:8002/submit', data, auth=self.auth_user)
|
|
self.assertIn('string=<foo>', r.text)
|
|
|
|
def testOutputRaw(self):
|
|
data = {
|
|
"form_name": 'output_raw',
|
|
"string": '<foo>'
|
|
}
|
|
r = requests.post('http://localhost:8002/submit', data, auth=self.auth_user)
|
|
self.assertIn('string=<foo>', r.text)
|
|
|
|
def testOutputHTML(self):
|
|
data = {
|
|
"form_name": 'output_html',
|
|
"string": '<foo>'
|
|
}
|
|
r = requests.post('http://localhost:8002/submit', data, auth=self.auth_user)
|
|
self.assertIn('string=<foo>', r.text)
|
|
|
|
def testUpload(self):
|
|
gen_random_file('data.raw')
|
|
|
|
data = {
|
|
"form_name": "upload"
|
|
}
|
|
with open('data.raw', 'rb') as fh:
|
|
files = {'file': fh}
|
|
r = requests.post("http://localhost:8002/submit", files=files, data=data, auth=self.auth_user)
|
|
self.assertIn('SAME', r.text)
|
|
os.unlink('data.raw')
|
|
|
|
def testStaticValid(self):
|
|
r = requests.get("http://localhost:8002/static?fname=ssh_server.png", auth=self.auth_user)
|
|
self.assertEqual(r.status_code, 200)
|
|
f_served = b''
|
|
for c in r.iter_content():
|
|
f_served += c
|
|
|
|
with open('static/ssh_server.png', 'rb')as fh:
|
|
f_orig = fh.read()
|
|
self.assertEqual(f_orig, f_served)
|
|
|
|
def testStaticInvalidFilename(self):
|
|
r = requests.get("http://localhost:8002/static?fname=../../ssh_server.png", auth=self.auth_user)
|
|
self.assertEqual(r.status_code, 403)
|
|
|
|
def testStaticInvalidNotFound(self):
|
|
r = requests.get("http://localhost:8002/static?fname=nosuchfile.png", auth=self.auth_user)
|
|
self.assertEqual(r.status_code, 404)
|
|
|
|
def testHiddenField(self):
|
|
r = requests.get('http://localhost:8002/form?form_name=hidden_field', auth=self.auth_user)
|
|
self.assertIn('class="hidden"', r.text)
|
|
|
|
def testCallbackFail(self):
|
|
data = {
|
|
"form_name": "callback_fail"
|
|
}
|
|
r = requests.post("http://localhost:8002/submit", data=data, auth=self.auth_user)
|
|
self.assertIn('<span class="error">stderr output\n</span>', r.text)
|
|
|
|
|
|
class WebAppSingleTest(unittest.TestCase):
|
|
"""
|
|
Test that Scriptform doesn't show us a list of forms, but directly shows us
|
|
the form is there's only one.
|
|
"""
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
# Run the server in a thread, so we can execute the tests in the main
|
|
# program.
|
|
def server_thread(sf):
|
|
sf.run(listen_port=8002)
|
|
cls.sf = scriptform.ScriptForm('test_webapp_singleform.json')
|
|
|
|
thread = threading.Thread(target=server_thread, args=(cls.sf,))
|
|
thread.start()
|
|
|
|
while True:
|
|
time.sleep(0.1)
|
|
if cls.sf.running is True:
|
|
break
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
cls.sf.shutdown()
|
|
while True:
|
|
time.sleep(0.1)
|
|
if not cls.sf.running:
|
|
break
|
|
|
|
def testSingleForm(self):
|
|
"""
|
|
Ensure that Scriptform directly shows the form if there is only one.
|
|
"""
|
|
r = requests.get("http://localhost:8002/")
|
|
self.assertIn('only_form', r.text)
|
|
|
|
def testStaticDisabled(self):
|
|
"""
|
|
"""
|
|
r = requests.get("http://localhost:8002/static?fname=nosuchfile.png")
|
|
self.assertEqual(r.status_code, 501)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
logging.basicConfig(level=logging.FATAL,
|
|
format='%(asctime)s:%(name)s:%(levelname)s:%(message)s',
|
|
filename='test.log',
|
|
filemode='a')
|
|
import coverage
|
|
cov = coverage.coverage(omit=['*test*', 'main', '*/lib/python*'])
|
|
cov.start()
|
|
|
|
sys.path.insert(0, '../src')
|
|
import scriptform
|
|
import runscript
|
|
unittest.main(exit=True)
|
|
|
|
cov.stop()
|
|
cov.save()
|
|
|
|
print(cov.report())
|
|
try:
|
|
print(cov.html_report())
|
|
except coverage.misc.CoverageException as err:
|
|
if "Couldn't find static file 'jquery.hotkeys.js'" in err.message:
|
|
pass
|
|
else:
|
|
raise
|
|
|