diff --git a/sandbox/grist/engine.py b/sandbox/grist/engine.py index 4b2a3fdb..cbf9143d 100644 --- a/sandbox/grist/engine.py +++ b/sandbox/grist/engine.py @@ -1093,11 +1093,11 @@ class Engine(object): # Without being very smart, if trigger-formula dependencies change for any columns, rebuild # them for all columns. Specifically, we will create nodes and edges in the dependency graph. - for table_id, table in self.tables.iteritems(): + for table_id, table in six.iteritems(self.tables): if table_id.startswith('_grist_'): # We can skip metadata tables, there are no trigger-formulas there. continue - for col_id, col_obj in table.all_columns.iteritems(): + for col_id, col_obj in six.iteritems(table.all_columns): if col_obj.is_formula() or not col_obj.has_formula(): continue col_rec = self.docmodel.columns.lookupOne(tableId=table_id, colId=col_id) diff --git a/sandbox/grist/useractions.py b/sandbox/grist/useractions.py index 8465ead3..ceaefed9 100644 --- a/sandbox/grist/useractions.py +++ b/sandbox/grist/useractions.py @@ -338,9 +338,10 @@ class UserActions(object): for col_id in table.all_columns: if col_id in column_values: continue - col_rec = self._docmodel.columns.lookupOne(tableId=table_id, colId=col_id) - if col_rec.recalcWhen == RecalcWhen.NEVER: - continue + if not table_id.startswith('_grist_'): + col_rec = self._docmodel.columns.lookupOne(tableId=table_id, colId=col_id) + if col_rec.recalcWhen == RecalcWhen.NEVER: + continue recalc_cols.add(col_id) self._engine.invalidate_records(table_id, filled_row_ids, data_cols_to_recompute=recalc_cols) @@ -376,7 +377,7 @@ class UserActions(object): table = self._engine.tables[table_id] column_values = action[2] if column_values: # Only if this is a non-trivial update. - for col_id, col_obj in table.all_columns.iteritems(): + for col_id, col_obj in six.iteritems(table.all_columns): if col_obj.is_formula() or not col_obj.has_formula(): continue col_rec = self._docmodel.columns.lookupOne(tableId=table_id, colId=col_id) diff --git a/sandbox/grist/xmlrunner.py b/sandbox/grist/xmlrunner.py new file mode 100644 index 00000000..e460190c --- /dev/null +++ b/sandbox/grist/xmlrunner.py @@ -0,0 +1,404 @@ +# -*- coding: utf-8 -*- + +""" +XML Test Runner for PyUnit +""" + +# Written by Sebastian Rittau and placed in +# the Public Domain. With contributions by Paolo Borelli and others. + +from __future__ import unicode_literals + +__version__ = "0.3" + +import os.path +import re +import sys +import time +import traceback +import unittest +import unittest.util +from xml.sax.saxutils import escape + +from io import StringIO, BytesIO + + +class _TestInfo(object): + + """Information about a particular test. + + Used by _XMLTestResult. + + """ + + def __init__(self, test, time): + (self._class, self._method) = test.id().rsplit(".", 1) + self._time = time + self._error = None + self._failure = None + + @staticmethod + def create_success(test, time): + """Create a _TestInfo instance for a successful test.""" + return _TestInfo(test, time) + + @staticmethod + def create_failure(test, time, failure): + """Create a _TestInfo instance for a failed test.""" + info = _TestInfo(test, time) + info._failure = failure + return info + + @staticmethod + def create_error(test, time, error): + """Create a _TestInfo instance for an erroneous test.""" + info = _TestInfo(test, time) + info._error = error + return info + + def print_report(self, stream): + """Print information about this test case in XML format to the + supplied stream. + + """ + tag_template = (' ') + stream.write(tag_template.format(class_=self._class, + method=self._method, + time=self._time)) + if self._failure is not None: + self._print_error(stream, 'failure', self._failure) + if self._error is not None: + self._print_error(stream, 'error', self._error) + stream.write('\n') + + @staticmethod + def _print_error(stream, tag_name, error): + """Print information from a failure or error to the supplied stream.""" + str_ = str if sys.version_info[0] >= 3 else unicode + io_class = StringIO if sys.version_info[0] >= 3 else BytesIO + text = escape(str_(error[1])) + class_name = unittest.util.strclass(error[0]) + stream.write('\n') + stream.write(' <{tag} type="{class_}">{text}\n'.format( + tag=tag_name, class_= class_name, text=text)) + tb_stream = io_class() + traceback.print_tb(error[2], None, tb_stream) + tb_string = tb_stream.getvalue() + if sys.version_info[0] < 3: + tb_string = tb_string.decode("utf-8") + stream.write(escape(tb_string)) + stream.write(' \n'.format(tag=tag_name)) + stream.write(' ') + + +def _clsname(cls): + return cls.__module__ + "." + cls.__name__ + + +class _XMLTestResult(unittest.TestResult): + + """A test result class that stores result as XML. + + Used by XMLTestRunner. + + """ + + def __init__(self, class_name): + unittest.TestResult.__init__(self) + self._test_name = class_name + self._start_time = None + self._tests = [] + self._error = None + self._failure = None + + def startTest(self, test): + unittest.TestResult.startTest(self, test) + self._error = None + self._failure = None + self._start_time = time.time() + + def stopTest(self, test): + time_taken = time.time() - self._start_time + unittest.TestResult.stopTest(self, test) + if self._error: + info = _TestInfo.create_error(test, time_taken, self._error) + elif self._failure: + info = _TestInfo.create_failure(test, time_taken, self._failure) + else: + info = _TestInfo.create_success(test, time_taken) + self._tests.append(info) + + def addError(self, test, err): + unittest.TestResult.addError(self, test, err) + self._error = err + + def addFailure(self, test, err): + unittest.TestResult.addFailure(self, test, err) + self._failure = err + + def print_report(self, stream, time_taken, out, err): + """Prints the XML report to the supplied stream. + + The time the tests took to perform as well as the captured standard + output and standard error streams must be passed in.a + + """ + tag_template = ('\n') + stream.write(tag_template.format(name=self._test_name, + total=self.testsRun, + errors=len(self.errors), + failures=len(self.failures), + time=time_taken)) + for info in self._tests: + info.print_report(stream) + stream.write(' \n'.format( + out)) + stream.write(' \n'.format( + err)) + stream.write('\n') + + +class XMLTestRunner(object): + + """A test runner that stores results in XML format compatible with JUnit. + + XMLTestRunner(stream=None) -> XML test runner + + The XML file is written to the supplied stream. If stream is None, the + results are stored in a file called TEST-..xml in the + current working directory (if not overridden with the path property), + where and are the module and class name of the test class. + + """ + + def __init__(self, stream=None): + self._stream = stream + self._path = "." + + def run(self, test): + """Run the given test case or test suite.""" + class_ = test.__class__ + class_name = class_.__module__ + "." + class_.__name__ + if self._stream is None: + filename = "TEST-{0}.xml".format(class_name) + stream = open(os.path.join(self._path, filename), "w") + stream.write('\n') + else: + stream = self._stream + + result = _XMLTestResult(class_name) + start_time = time.time() + + with _FakeStdStreams(): + test(result) + try: + out_s = sys.stdout.getvalue() + except AttributeError: + out_s = "" + try: + err_s = sys.stderr.getvalue() + except AttributeError: + err_s = "" + + time_taken = time.time() - start_time + result.print_report(stream, time_taken, out_s, err_s) + if self._stream is None: + stream.close() + + return result + + def _set_path(self, path): + self._path = path + + path = property( + lambda self: self._path, _set_path, None, + """The path where the XML files are stored. + + This property is ignored when the XML file is written to a file + stream.""") + + +class _FakeStdStreams(object): + + def __enter__(self): + self._orig_stdout = sys.stdout + self._orig_stderr = sys.stderr + sys.stdout = StringIO() + sys.stderr = StringIO() + + def __exit__(self, exc_type, exc_val, exc_tb): + sys.stdout = self._orig_stdout + sys.stderr = self._orig_stderr + + +class XMLTestRunnerTest(unittest.TestCase): + + def setUp(self): + self._stream = StringIO() + + def _try_test_run(self, test_class, expected): + + """Run the test suite against the supplied test class and compare the + XML result against the expected XML string. Fail if the expected + string doesn't match the actual string. All time attributes in the + expected string should have the value "0.000". All error and failure + messages are reduced to "Foobar". + + """ + + self._run_test_class(test_class) + + got = self._stream.getvalue() + # Replace all time="X.YYY" attributes by time="0.000" to enable a + # simple string comparison. + got = re.sub(r'time="\d+\.\d+"', 'time="0.000"', got) + # Likewise, replace all failure and error messages by a simple "Foobar" + # string. + got = re.sub(r'(?s).*?', + r'Foobar', got) + got = re.sub(r'(?s).*?', + r'Foobar', got) + # And finally Python 3 compatibility. + got = got.replace('type="builtins.', 'type="exceptions.') + + self.assertEqual(expected, got) + + def _run_test_class(self, test_class): + runner = XMLTestRunner(self._stream) + runner.run(unittest.makeSuite(test_class)) + + def test_no_tests(self): + """Regression test: Check whether a test run without any tests + matches a previous run. + + """ + class TestTest(unittest.TestCase): + pass + self._try_test_run(TestTest, """ + + + +""") + + def test_success(self): + """Regression test: Check whether a test run with a successful test + matches a previous run. + + """ + class TestTest(unittest.TestCase): + def test_foo(self): + pass + self._try_test_run(TestTest, """ + + + + +""") + + def test_failure(self): + """Regression test: Check whether a test run with a failing test + matches a previous run. + + """ + class TestTest(unittest.TestCase): + def test_foo(self): + self.assertTrue(False) + self._try_test_run(TestTest, """ + + Foobar + + + + +""") + + def test_error(self): + """Regression test: Check whether a test run with a erroneous test + matches a previous run. + + """ + class TestTest(unittest.TestCase): + def test_foo(self): + raise IndexError() + self._try_test_run(TestTest, """ + + Foobar + + + + +""") + + def test_non_ascii_characters_in_traceback(self): + """Test umlauts in traceback exception messages.""" + class TestTest(unittest.TestCase): + def test_foo(self): + raise Exception("Test äöü") + self._run_test_class(TestTest) + + def test_stdout_capture(self): + """Regression test: Check whether a test run with output to stdout + matches a previous run. + + """ + class TestTest(unittest.TestCase): + def test_foo(self): + sys.stdout.write("Test\n") + self._try_test_run(TestTest, """ + + + + +""") + + def test_stderr_capture(self): + """Regression test: Check whether a test run with output to stderr + matches a previous run. + + """ + class TestTest(unittest.TestCase): + def test_foo(self): + sys.stderr.write("Test\n") + self._try_test_run(TestTest, """ + + + + +""") + + class NullStream(object): + """A file-like object that discards everything written to it.""" + def write(self, buffer): + pass + + def test_unittests_changing_stdout(self): + """Check whether the XMLTestRunner recovers gracefully from unit tests + that change stdout, but don't change it back properly. + + """ + class TestTest(unittest.TestCase): + def test_foo(self): + sys.stdout = XMLTestRunnerTest.NullStream() + + runner = XMLTestRunner(self._stream) + runner.run(unittest.makeSuite(TestTest)) + + def test_unittests_changing_stderr(self): + """Check whether the XMLTestRunner recovers gracefully from unit tests + that change stderr, but don't change it back properly. + + """ + class TestTest(unittest.TestCase): + def test_foo(self): + sys.stderr = XMLTestRunnerTest.NullStream() + + runner = XMLTestRunner(self._stream) + runner.run(unittest.makeSuite(TestTest)) + + +if __name__ == "__main__": + unittest.main()