Implement all tests and functionality in Javascript

This commit is contained in:
Yingtong Li 2017-09-25 15:19:48 +10:00
parent 506e1c287f
commit 0a6831b366
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
10 changed files with 264 additions and 56 deletions

View File

@ -15,12 +15,11 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
set +e
FLAGS=-k
for f in eos.js eos.js_tests; do
transcrypt -b -n $FLAGS $f.py
#for f in eos.js eos.js_tests; do
for f in eos.js_tests; do
transcrypt -b -n -o $FLAGS $f.py || exit 1
# Javascript identifiers cannot contain dots
perl -0777 -pi -e 's/eos.js/eosjs/g' eos/__javascript__/$f.js
@ -30,4 +29,7 @@ for f in eos.js eos.js_tests; do
# Transcrypt by default suppresses stack traces for some reason??
perl -0777 -pi -e "s/__except0__.__cause__ = null;//g" eos/__javascript__/$f.js
# Disable handling of special attributes
perl -0777 -pi -e "s/var __specialattrib__ = function \(attrib\) \{/var __specialattrib__ = function (attrib) { return false;/g" eos/__javascript__/$f.js
done

View File

@ -14,7 +14,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from eos.core.tests import EosTestCase
from eos.core.tests import *
from eos.base.election import *
from eos.base.workflow import *
@ -23,7 +23,8 @@ from eos.core.objects import *
class ElectionTestCase(EosTestCase):
@classmethod
def setUpClass(cls):
client.drop_database('test')
if is_python:
client.drop_database('test')
def exit_task_assert(self, election, task, next_task):
self.assertEqual(election.workflow.get_task(task).status, WorkflowTask.Status.READY)
@ -32,6 +33,11 @@ class ElectionTestCase(EosTestCase):
self.assertEqual(election.workflow.get_task(task).status, WorkflowTask.Status.EXITED)
self.assertEqual(election.workflow.get_task(next_task).status, WorkflowTask.Status.READY)
def save_if_python(self, obj):
if is_python:
obj.save()
@py_only
def test_run_election(self):
# Set up election
election = Election()
@ -59,15 +65,18 @@ class ElectionTestCase(EosTestCase):
question = ApprovalQuestion(prompt='Chairman', choices=['John Doe', 'Andrew Citizen'])
election.questions.append(question)
election.save()
self.save_if_python(election)
# Check that it saved
self.assertEqual(db[Election._name].find_one()['value'], election.serialise())
self.assertEqual(EosObject.deserialise_and_unwrap(db[Election._name].find_one()).serialise(), election.serialise())
if is_python:
self.assertEqual(db[Election._name].find_one()['value'], election.serialise())
self.assertEqual(EosObject.deserialise_and_unwrap(db[Election._name].find_one()).serialise(), election.serialise())
self.assertEqualJSON(EosObject.deserialise_and_unwrap(EosObject.serialise_and_wrap(election)).serialise(), election.serialise())
# Freeze election
self.exit_task_assert(election, 'eos.base.workflow.TaskConfigureElection', 'eos.base.workflow.TaskOpenVoting')
election.save()
self.save_if_python(election)
# Try to freeze it again
try:
@ -87,11 +96,11 @@ class ElectionTestCase(EosTestCase):
ballot.encrypted_answers.append(encrypted_answer)
election.voters[i].ballots.append(ballot)
election.save()
self.save_if_python(election)
# Close voting
self.exit_task_assert(election, 'eos.base.workflow.TaskOpenVoting', 'eos.base.workflow.TaskCloseVoting')
election.save()
self.save_if_python(election)
# Compute result
election.results = [None, None]
@ -99,7 +108,7 @@ class ElectionTestCase(EosTestCase):
result = election.questions[i].compute_result()
election.results[i] = result
election.save()
self.save_if_python(election)
self.assertEqual(election.results[0].choices, [2, 1, 1])
self.assertEqual(election.results[1].choices, [2, 1])

View File

@ -68,7 +68,7 @@ class WorkflowTask(EmbeddedObject):
def exit(self):
if self.status is not WorkflowTask.Status.READY:
raise Exception()
raise Exception('Attempted to exit a task when not ready')
self.status = WorkflowTask.Status.EXITED
self.fire_event('exit')
@ -83,7 +83,11 @@ class Workflow(EmbeddedObject):
super().__init__(*args, **kwargs)
def get_tasks(self, descriptor):
yield from (task for task in self.tasks if task.satisfies(descriptor))
#yield from (task for task in self.tasks if task.satisfies(descriptor))
for i in range(len(self.tasks)):
task = self.tasks[i]
if task.satisfies(descriptor):
yield task
def get_task(self, descriptor):
try:

View File

@ -93,3 +93,9 @@ class BigInt(EosObject):
if not isinstance(modulo, BigInt):
modulo = BigInt(modulo)
return BigInt(self.impl.modPow(other.impl, modulo.impl))
# TNYI: No native pow() support
def pow(a, b, c=None):
if not isinstance(a, BigInt):
a = BigInt(a)
return a.__pow__(b, c)

View File

@ -14,12 +14,22 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Load json.js
lib = __pragma__('js', '''
(function() {{
var exports = {{}};
{}
exports.stringify = stringify_main;
return exports;
}})()''', __include__('eos/core/objects/json.js'))
# Fields
# ======
class Field:
def __init__(self, *args, **kwargs):
self.default = kwargs.get('default', None)
#console.log(kwargs.get('hashed', None))
self.default = kwargs['py_default'] if kwargs.hasOwnProperty('py_default') else None
self.hashed = kwargs.get('hashed', True)
class PrimitiveField(Field):
@ -74,7 +84,7 @@ class EmbeddedObjectListField(Field):
class EosObjectType(type):
def __new__(meta, name, bases, attrs):
cls = type.__new__(meta, name, bases, attrs)
cls._name = cls.__module__ + '.' + cls.__qualname__
cls._name = (meta.__next_class_module__ + '.' + cls.__name__).replace('.js.', '.') #TNYI: module and qualname
if name != 'EosObject':
EosObject.objects[cls._name] = cls
return cls
@ -106,15 +116,39 @@ class EosObject(metaclass=EosObjectType):
def deserialise_and_unwrap(value, object_type=None):
if object_type:
return object_type.deserialise(value)
print(value['type'])
return EosObject.objects[value['type']].deserialise(value['value'])
# Different to Python
@staticmethod
def to_json(value):
return lib.stringify(value)
@staticmethod
def from_json(value):
return JSON.parse(value)
class EosList(EosObject, list):
class EosList(EosObject):
def __init__(self, *args):
self.impl = list(*args)
# Diferent to Python
# Lists are implemented as native JS Arrays, so no cheating here :(
def __len__(self):
return len(self.impl)
def __getitem__(self, idx):
return self.impl[idx]
def __setitem__(self, idx, val):
self.impl[idx] = val
def __contains__(self, val):
return val in self.impl
def append(self, value):
if isinstance(value, EosObject):
value._instance = (self, len(self))
if not value._inited:
value.post_init()
return super().append(value)
return self.impl.append(value)
class DocumentObjectType(EosObjectType):
def __new__(meta, name, bases, attrs):
@ -131,20 +165,7 @@ class DocumentObjectType(EosObjectType):
cls._fields = fields
# Make properties
def make_property(name, field):
def field_getter(self):
return self._field_values[name]
def field_setter(self, value):
if isinstance(value, EosObject):
value._instance = (self, name)
if not value._inited:
value.post_init()
self._field_values[name] = value
return property(field_getter, field_setter)
for attr, val in fields.items():
setattr(cls, attr, make_property(attr, val))
# Different to Python: This is handled at the instance level
return cls
@ -156,21 +177,41 @@ class DocumentObject(EosObject, metaclass=DocumentObjectType):
self._field_values = {}
# Different to Python
for attr, val in self._fields.items():
def make_property(name, field):
def field_getter():
return self._field_values[name]
def field_setter(value):
if isinstance(value, EosObject):
value._instance = (self, name)
if not value._inited:
value.post_init()
self._field_values[name] = value
return (field_getter, field_setter)
prop = make_property(attr, val)
Object.defineProperty(self, attr, {
'get': prop[0],
'set': prop[1]
})
if attr in kwargs:
setattr(self, attr, kwargs[attr])
else:
default = val.default
if callable(default):
if default is not None and callable(default):
default = default()
setattr(self, attr, default)
# Different to Python
# TNYI: Strange things happen with py_ attributes
def serialise(self):
return {attr: val.serialise(getattr(self, attr)) for attr, val in self._fields.items()}
return {(attr[3:] if attr.startswith('py_') else attr): val.serialise(getattr(self, attr)) for attr, val in self._fields.items()}
@classmethod
def deserialise(cls, value):
return cls(**{attr: val.deserialise(value[attr]) for attr, val in cls._fields.items()})
return cls(**{attr: val.deserialise(value[attr[3:] if attr.startswith('py_') else attr]) for attr, val in cls._fields.items()})
class TopLevelObject(DocumentObject):
pass

84
eos/core/objects/json.js Normal file
View File

@ -0,0 +1,84 @@
/*
Copyright © 2017 RunasSudo (Yingtong Li)
Based on json-stable-stringify by substack, licensed under the MIT License.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
var isArray = Array.isArray || function (x) {
return {}.toString.call(x) === '[object Array]';
};
var objectKeys = Object.keys || function (obj) {
var has = Object.prototype.hasOwnProperty || function () { return true };
var keys = [];
for (var key in obj) {
if (has.call(obj, key)) keys.push(key);
}
return keys;
};
var seen = [];
function stringify(parent, key, node, level) {
if (node && node.toJSON && typeof node.toJSON === 'function') {
node = node.toJSON();
}
if (node === undefined) {
return;
}
if (typeof node !== 'object' || node === null) {
return JSON.stringify(node);
}
if (isArray(node)) {
var out = [];
for (var i = 0; i < node.length; i++) {
var item = stringify(node, i, node[i], level+1) || JSON.stringify(null);
out.push(item);
}
return '[' + out.join(', ') + ']';
} else {
if (seen.indexOf(node) !== -1) {
throw new TypeError('Converting circular structure to JSON');
} else {
seen.push(node);
}
var keys = objectKeys(node).sort();
var out = [];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var value = stringify(node, key, node[key], level+1);
if(!value) {
continue;
}
var keyValue = JSON.stringify(key) + ': ' + value;
out.push(keyValue);
}
seen.splice(seen.indexOf(node), 1);
return '{' + out.join(', ') + '}';
}
};
function stringify_main(obj) {
return stringify({ '': obj }, '', obj, 0);
}

View File

@ -17,6 +17,7 @@
import pymongo
from bson.binary import UUIDLegacy
import json
import uuid
# Database
@ -94,7 +95,7 @@ class UUIDField(Field):
class EosObjectType(type):
def __new__(meta, name, bases, attrs):
cls = type.__new__(meta, name, bases, attrs)
cls._name = cls.__module__ + '.' + cls.__qualname__
cls._name = (cls.__module__ + '.' + cls.__name__).replace('.js.', '.') #TNYI: qualname
if name != 'EosObject':
EosObject.objects[cls._name] = cls
return cls
@ -127,6 +128,14 @@ class EosObject(metaclass=EosObjectType):
if object_type:
return object_type.deserialise(value)
return EosObject.objects[value['type']].deserialise(value['value'])
@staticmethod
def to_json(value):
return json.dumps(value, sort_keys=True)
@staticmethod
def from_json(value):
return json.loads(value)
class EosList(EosObject, list):
def append(self, value):
@ -181,7 +190,7 @@ class DocumentObject(EosObject, metaclass=DocumentObjectType):
setattr(self, attr, kwargs[attr])
else:
default = val.default
if callable(default):
if default is not None and callable(default):
default = default()
setattr(self, attr, default)

View File

@ -18,13 +18,35 @@ from eos.core.bigint import *
from eos.core.bitstring import *
from eos.core.objects import *
# Common library things
# ===================
class EosTestCase:
@classmethod
def setUpClass(cls):
pass
def assertEqual(self, a, b):
self.impl.assertEqual(a, b)
if is_python:
self.impl.assertEqual(a, b)
else:
if a is None:
if b is not None:
raise Error('Assertion failed: ' + str(a) + ' != ' + str(b))
else:
if a != b:
raise Error('Assertion failed: ' + str(a) + ' != ' + str(b))
def assertEqualJSON(self, a, b):
self.assertEqual(EosObject.to_json(a), EosObject.to_json(b))
def py_only(func):
func._py_only = True
def js_only(func):
func._js_only = True
# eos.core tests
# ==============
class ObjectTestCase(EosTestCase):
@classmethod
@ -49,14 +71,15 @@ class ObjectTestCase(EosTestCase):
def test_serialise(self):
person1 = self.Person(name='John', address='Address 1')
expect1 = {'_ver': '0.1', 'name': 'John', 'address': 'Address 1'}
expect1a = {'type': 'eos.core.tests.ObjectTestCase.setUpClass.<locals>.Person', 'value': expect1}
#expect1a = {'type': 'eos.core.tests.ObjectTestCase.setUpClass.<locals>.Person', 'value': expect1}
expect1a = {'type': 'eos.core.tests.Person', 'value': expect1}
self.assertEqual(person1.serialise(), expect1)
self.assertEqual(EosObject.serialise_and_wrap(person1, self.Person), expect1)
self.assertEqual(EosObject.serialise_and_wrap(person1), expect1a)
self.assertEqualJSON(person1.serialise(), expect1)
self.assertEqualJSON(EosObject.serialise_and_wrap(person1, self.Person), expect1)
self.assertEqualJSON(EosObject.serialise_and_wrap(person1), expect1a)
#self.assertEqual(EosObject.deserialise_and_unwrap(expect1a), person1)
self.assertEqual(EosObject.deserialise_and_unwrap(expect1a).serialise(), person1.serialise())
self.assertEqualJSON(EosObject.deserialise_and_unwrap(expect1a).serialise(), person1.serialise())
class BigIntTestCase(EosTestCase):
def test_basic(self):

View File

@ -16,4 +16,5 @@
import eos.js
import eos.core.test_code
import eos.core.tests
import eos.base.tests

View File

@ -21,14 +21,16 @@ from eos.core.bigint import *
from eos.core.bitstring import *
from eos.core.objects import *
import execjs
import importlib
import os
import types
test_suite = TestSuite()
# All the TestCase's we dynamically generate inherit from this class
class BaseTestCase(TestCase):
# All the TestCase's we dynamically generate inherit from these classes
class BasePyTestCase(TestCase):
@classmethod
def setUpClass(cls):
cls.impl.setUpClass()
@ -41,6 +43,20 @@ class BaseTestCase(TestCase):
return func(*args)
setattr(cls, method, call_method)
class BaseJSTestCase(TestCase):
@classmethod
def setUpClass(cls):
with open('eos/__javascript__/eos.js_tests.js', 'r') as f:
code = f.read()
cls.ctx = execjs.get().compile('var window={},navigator={};' + code + 'var test=window.eosjs_tests.' + cls.module + '.__all__.' + cls.name + '();test.setUpClass();')
@classmethod
def add_method(cls, method):
def call_method(self, *args):
# TODO: args
return cls.ctx.eval('test.' + method + '()')
setattr(cls, method, call_method)
# Test discovery
import eos.core.tests
for dirpath, dirnames, filenames in os.walk('eos'):
@ -48,17 +64,30 @@ for dirpath, dirnames, filenames in os.walk('eos'):
# Skip this file
continue
if 'tests.py' in filenames:
module = importlib.import_module(dirpath.replace('/', '.') + '.tests')
module_name = dirpath.replace('/', '.') + '.tests'
module = importlib.import_module(module_name)
for name in dir(module):
obj = getattr(module, name)
if isinstance(obj, type):
if issubclass(obj, eos.core.tests.EosTestCase):
cls = type(name + 'Impl', (BaseTestCase,), {'impl': obj()})
for method in dir(cls.impl):
if isinstance(getattr(cls.impl, method), types.MethodType) and not hasattr(cls, method):
cls.add_method(method)
if method.startswith('test_'):
test_case = cls(method)
test_suite.addTest(test_case)
impl = obj()
cls_py = type(name + 'ImplPy', (BasePyTestCase,), {'impl': impl})
cls_js = type(name + 'ImplJS', (BaseJSTestCase,), {'module': module_name, 'name': name})
for method in dir(impl):
method_val = getattr(impl, method)
if isinstance(method_val, types.MethodType) and not hasattr(cls_py, method):
# Python
if not getattr(method_val, '_js_only', False):
cls_py.add_method(method)
if method.startswith('test_'):
test_case = cls_py(method)
test_suite.addTest(test_case)
# Javascript
if not getattr(method_val, '_py_only', False):
if method.startswith('test_'):
cls_js.add_method(method)
test_case = cls_js(method)
test_suite.addTest(test_case)
TextTestRunner(verbosity=3).run(test_suite)
TextTestRunner(verbosity=3, failfast=True).run(test_suite)