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)