diff --git a/build_js.sh b/build_js.sh
index 841e716..ce435f8 100755
--- a/build_js.sh
+++ b/build_js.sh
@@ -15,12 +15,11 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-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
diff --git a/eos/base/tests.py b/eos/base/tests.py
index 1220e2d..1055e6e 100644
--- a/eos/base/tests.py
+++ b/eos/base/tests.py
@@ -14,7 +14,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-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])
diff --git a/eos/base/workflow.py b/eos/base/workflow.py
index b75a6ef..db1369c 100644
--- a/eos/base/workflow.py
+++ b/eos/base/workflow.py
@@ -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:
diff --git a/eos/core/bigint/js.py b/eos/core/bigint/js.py
index 1ebbe92..72b9097 100644
--- a/eos/core/bigint/js.py
+++ b/eos/core/bigint/js.py
@@ -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)
diff --git a/eos/core/objects/js.py b/eos/core/objects/js.py
index 2c16efa..490a542 100644
--- a/eos/core/objects/js.py
+++ b/eos/core/objects/js.py
@@ -14,12 +14,22 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+# 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
diff --git a/eos/core/objects/json.js b/eos/core/objects/json.js
new file mode 100644
index 0000000..82b9202
--- /dev/null
+++ b/eos/core/objects/json.js
@@ -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);
+}
diff --git a/eos/core/objects/python.py b/eos/core/objects/python.py
index c37c8b2..793b242 100644
--- a/eos/core/objects/python.py
+++ b/eos/core/objects/python.py
@@ -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)
diff --git a/eos/core/tests.py b/eos/core/tests.py
index 28ff546..c2eecab 100644
--- a/eos/core/tests.py
+++ b/eos/core/tests.py
@@ -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..Person', 'value': expect1}
+ #expect1a = {'type': 'eos.core.tests.ObjectTestCase.setUpClass..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):
diff --git a/eos/js_tests.py b/eos/js_tests.py
index 3187661..dd04f60 100644
--- a/eos/js_tests.py
+++ b/eos/js_tests.py
@@ -16,4 +16,5 @@
import eos.js
-import eos.core.test_code
+import eos.core.tests
+import eos.base.tests
diff --git a/eos/tests.py b/eos/tests.py
index 3f71b8e..d1fc490 100644
--- a/eos/tests.py
+++ b/eos/tests.py
@@ -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)