def test_failure_after_side_effect(self): # Verify that when a formula fails after a side-effect, the effect is reverted. self.load_sample(self.sample) formula = 'Schools.lookupOrAddDerived(city="TESTCITY")\nraise Exception("test-error")' out_actions = self.apply_user_action( ['AddColumn', 'Address', "A", { 'formula': formula }]) self.assertPartialOutActions( out_actions, { "stored": [ [ "AddColumn", "Address", "A", { "formula": formula, "isFormula": True, "type": "Any" } ], [ "AddRecord", "_grist_Tables_column", 13, { "colId": "A", "formula": formula, "isFormula": True, "label": "A", "parentId": 1, "parentPos": 4.0, "type": "Any", "widgetOptions": "" } ], [ "BulkUpdateRecord", "Address", [21, 22], { "A": [["E", "Exception"], ["E", "Exception"]] } ], # The thing to note here is that while lookupOrAddDerived() should have added a row to # Schools, the Exception negated it, and there is no action to add that row. ] }) # Check that data is as expected: no new records in Schools, one new column in Address. self.assertTableData('Schools', cols="all", data=self.schools_table_data) self.assertTableData( 'Address', cols="all", data=[ ["id", "city", "state", "amount", "A"], [ 21, "New York", "NY", 1, objtypes.RaisedException(Exception()) ], [22, "Albany", "NY", 2, objtypes.RaisedException(Exception())], ])
def test_loop(self): sample = testutil.parse_test_sample({ "SCHEMA": [[ 1, "Table1", [ [31, "A", "Numeric", False, "", "", ""], [31, "B", "Numeric", True, "$C", "", ""], [32, "C", "Numeric", True, "$B", "", ""], ] ]], "DATA": { "Table1": [ ["id", "A"], [1, 1], [2, 2], [3, 3], ] } }) self.load_sample(sample) circle = objtypes.RaisedException(depend.CircularRefError()) self.assertTableData('Table1', data=[ ['id', 'A', 'B', 'C'], [1, 1, circle, circle], [2, 2, circle, circle], [3, 3, circle, circle], ])
def test_cycle(self): sample = testutil.parse_test_sample({ "SCHEMA": [[ 1, "Table1", [ [30, "A", "Numeric", False, "", "", ""], [31, "Principal", "Numeric", True, "$Interest", "", ""], [32, "Interest", "Numeric", True, "$Principal", "", ""], [33, "A2", "Numeric", True, "$A", "", ""], ] ]], "DATA": { "Table1": [ ["id", "A"], [1, 1], [2, 2], [3, 3], ] } }) self.load_sample(sample) circle = objtypes.RaisedException(depend.CircularRefError()) self.assertTableData('Table1', data=[ ['id', 'A', 'Principal', 'Interest', 'A2'], [1, 1, circle, circle, 1], [2, 2, circle, circle, 2], [3, 3, circle, circle, 3], ])
def test_cycle_and_reference(self): sample = testutil.parse_test_sample({ "SCHEMA": [ [ 2, "ATable", [ [32, "A", "Ref:ZTable", False, "", "", ""], [33, "B", "Numeric", True, "$A.B", "", ""], ] ], [ 1, "ZTable", [ [31, "A", "Numeric", False, "", "", ""], [31, "B", "Numeric", True, "$B", "", ""], ] ], ], "DATA": { "ATable": [ ["id", "A"], [1, 1], [2, 2], [3, 3], ], "ZTable": [ ["id", "A"], [1, 6], [2, 7], [3, 8], ] } }) self.load_sample(sample) circle = objtypes.RaisedException(depend.CircularRefError()) self.assertTableData('ATable', data=[ ['id', 'A', 'B'], [1, 1, circle], [2, 2, circle], [3, 3, circle], ]) self.assertTableData('ZTable', data=[ ['id', 'A', 'B'], [1, 6, circle], [2, 7, circle], [3, 8, circle], ])
def test_cumulative_formula_with_references(self): top = 100 formula = "max($Prev.Principal + $Prev.Interest, 1000)" sample = testutil.parse_test_sample({ "SCHEMA": [[ 1, "Table1", [ [41, "Prev", "Ref:Table1", True, "$id - 1", "", ""], [42, "Principal", "Numeric", True, formula, "", ""], [ 43, "Interest", "Numeric", True, "int($Principal * 0.1)", "", "" ], ] ], [ 2, "Readout", [ [ 46, "LastPrincipal", "Numeric", True, "Table1.lookupOne(id=%d).Principal" % top, "", "" ], ] ]], "DATA": { "Table1": [["id"]] + [[r] for r in range(1, top + 1)], "Readout": [["id"], [1]], } }) self.load_sample(sample) self.assertTableData('Readout', data=[ ['id', 'LastPrincipal'], [1, 12494908.0], ]) self.modify_column("Table1", "Prev", formula="$id - 1 if $id > 1 else 100") self.assertTableData( 'Readout', data=[ ['id', 'LastPrincipal'], [1, objtypes.RaisedException(depend.CircularRefError())], ])
def test_attribute_error(self): sample = testutil.parse_test_sample({ "SCHEMA": [[ 1, "AttrTest", [ [30, "A", "Numeric", False, "", "", ""], [31, "B", "Numeric", True, "$AA", "", ""], [32, "C", "Numeric", True, "$B", "", ""], ] ]], "DATA": { "AttrTest": [ ["id", "A"], [1, 1], [2, 2], ] } }) self.load_sample(sample) errVal = objtypes.RaisedException(AttributeError()) self.assertTableData('AttrTest', data=[ ['id', 'A', 'B', 'C'], [1, 1, errVal, errVal], [2, 2, errVal, errVal], ]) self.assertFormulaError( self.engine.get_formula_error('AttrTest', 'B', 1), AttributeError, "Table 'AttrTest' has no column 'AA'", r"AttributeError: Table 'AttrTest' has no column 'AA'") cell_error = self.engine.get_formula_error('AttrTest', 'C', 1) self.assertFormulaError( cell_error, objtypes.CellError, "AttributeError in referenced cell AttrTest[1].B", r"CellError: AttributeError in referenced cell AttrTest\[1\].B") self.assertEqual(objtypes.encode_object(cell_error), [ 'E', 'AttributeError', "Table 'AttrTest' has no column 'AA'\n" "(in referenced cell AttrTest[1].B)", cell_error.details ])
def test_record_bad_calls(self): self.load_sample(testsamples.sample_students) self.add_column("Schools", "Foo", formula='repr(RECORD($name))') self.assertPartialData("Schools", ["id", "Foo"], [ [1, objtypes.RaisedException(ValueError())], [2, objtypes.RaisedException(ValueError())], [3, objtypes.RaisedException(ValueError())], [4, objtypes.RaisedException(ValueError())], ]) self.modify_column("Schools", "Foo", formula='repr(RECORD([rec] if $id == 2 else $id))') self.assertPartialData("Schools", ["id", "Foo"], [ [1, objtypes.RaisedException(ValueError())], [2, "[{'address': Address[12], 'id': 2, 'name': 'Columbia'}]"], [3, objtypes.RaisedException(ValueError())], [4, objtypes.RaisedException(ValueError())], ]) self.assertEqual( self.engine.get_formula_error('Schools', 'Foo', 1).error.message, 'RECORD() requires a Record or an iterable of Records')
def test_record_bad_calls(self): self.load_sample(testsamples.sample_students) self.add_column("Schools", "Foo", formula='repr(RECORD($name))') self.assertPartialData("Schools", ["id", "Foo"], [ [1, objtypes.RaisedException(ValueError())], [2, objtypes.RaisedException(ValueError())], [3, objtypes.RaisedException(ValueError())], [4, objtypes.RaisedException(ValueError())], ]) self.modify_column( "Schools", "Foo", formula='repr(sorted(RECORD(rec if $id == 2 else $id).items()))') self.assertPartialData("Schools", ["id", "Foo"], [ [1, objtypes.RaisedException(ValueError())], [2, "[('address', Address[12]), ('id', 2), ('name', 'Columbia')]"], [3, objtypes.RaisedException(ValueError())], [4, objtypes.RaisedException(ValueError())], ]) self.assertEqual( str(self.engine.get_formula_error('Schools', 'Foo', 1).error), 'RECORD() requires a Record or an iterable of Records')
def test_attribute_error(self): sample = testutil.parse_test_sample({ "SCHEMA": [[ 1, "AttrTest", [ [30, "A", "Numeric", False, "", "", ""], [31, "B", "Numeric", True, "$AA", "", ""], [32, "C", "Numeric", True, "$B", "", ""], ] ]], "DATA": { "AttrTest": [ ["id", "A"], [1, 1], [2, 2], ] } }) self.load_sample(sample) errVal = objtypes.RaisedException(AttributeError()) self.assertTableData('AttrTest', data=[ ['id', 'A', 'B', 'C'], [1, 1, errVal, errVal], [2, 2, errVal, errVal], ]) self.assertFormulaError( self.engine.get_formula_error('AttrTest', 'B', 1), AttributeError, "Table 'AttrTest' has no column 'AA'", r"AttributeError: Table 'AttrTest' has no column 'AA'") self.assertFormulaError( self.engine.get_formula_error('AttrTest', 'C', 1), AttributeError, "Table 'AttrTest' has no column 'AA'", r"AttributeError: Table 'AttrTest' has no column 'AA'")
def test_catch_all_in_formula(self): sample = testutil.parse_test_sample({ "SCHEMA": [ [ 1, "Table1", [ [51, "A", "Numeric", False, "", "", ""], [ 52, "B1", "Numeric", True, "try:\n return $A+$C\nexcept:\n return 42", "", "" ], [ 53, "B2", "Numeric", True, "try:\n return $D+None\nexcept:\n return 42", "", "" ], [ 54, "B3", "Numeric", True, "try:\n return $A+$B4+$D\nexcept:\n return 42", "", "" ], [ 55, "B4", "Numeric", True, "try:\n return $A+$B3+$D\nexcept:\n return 42", "", "" ], [ 56, "B5", "Numeric", True, "try:\n return $E+1\nexcept:\n raise Exception('monkeys!')", "", "" ], [ 56, "B6", "Numeric", True, "try:\n return $F+1\nexcept Exception as e:\n e.node = e.row_id = 'monkey'", "", "" ], [57, "C", "Numeric", False, "", "", ""], [58, "D", "Numeric", True, "$A", "", ""], [59, "E", "Numeric", True, "$A", "", ""], [59, "F", "Numeric", True, "$A", "", ""], ] ], ], "DATA": { "Table1": [["id", "A", "C"], [1, 1, 2], [2, 20, 10]], } }) self.load_sample(sample) circle = objtypes.RaisedException(depend.CircularRefError()) # B4 is a subtle case. B3 and B4 refer to each other. B3 is recomputed first, # and cells evaluate to a CircularRefError. Now B3 has a value, so B4 can be # evaluated, and results in 42 when addition of an integer and an exception value # fails. self.assertTableData( 'Table1', data=[ [ 'id', 'A', 'B1', 'B2', 'B3', 'B4', 'B5', 'B6', 'C', 'D', 'E', 'F' ], [1, 1, 3, 42, circle, 42, 2, 2, 2, 1, 1, 1], [2, 20, 30, 42, circle, 42, 21, 21, 10, 20, 20, 20], ])
def test_formula_reading_from_an_errored_formula(self): # There was a bug whereby if one formula (call it D) referred to # another (call it T), and that other formula was in error, the # error values of that second formula would not be passed on the # client as a BulkUpdateRecord. The bug was dependent on order of # evaluation of columns. D would be evaluated first, and evaluate # T in a nested way. When evaluating T, a BulkUpdateRecord would # be prepared correctly, and when popping back to evaluate D, # the BulkUpdateRecord for D would be prepared correctly, but since # D was an error, any nested actions would be reverted (this is # logic related to undoing potential side-effects on failure). # First, set up a table with a sequence in A, a formula to do cumulative sums in T, # and a formula D to copy T. formula = "recs = UpdateTest.lookupRecords()\nsum(r.A for r in recs if r.A <= $A)" sample = testutil.parse_test_sample({ "SCHEMA": [[ 1, "UpdateTest", [ [20, "A", "Numeric", False, "", "", ""], [21, "T", "Numeric", True, formula, "", ""], [22, "D", "Numeric", True, "$T", "", ""], ] ]], "DATA": { "UpdateTest": [ ["id", "A"], [1, 1], [2, 2], [3, 3], ] } }) # Check the setup is working correctly. self.load_sample(sample) self.assertTableData('UpdateTest', data=[ ['id', 'A', 'T', 'D'], [1, 1., 1., 1.], [2, 2., 3., 3.], [3, 3., 6., 6.], ]) # Now rename the data column. This rename results in a partial # update to the T formula that leaves it broken (not all the As are caught). out_actions = self.apply_user_action( ["RenameColumn", "UpdateTest", "A", "AA"]) # Make sure the we have bulk updates for both T and D, and not just D. err = ["E", "AttributeError"] self.assertPartialOutActions( out_actions, { "stored": [ ["RenameColumn", "UpdateTest", "A", "AA"], [ "ModifyColumn", "UpdateTest", "T", { "formula": "recs = UpdateTest.lookupRecords()\nsum(r.A for r in recs if r.A <= $AA)" } ], [ "BulkUpdateRecord", "_grist_Tables_column", [20, 21], { "colId": ["AA", "T"], "formula": [ "", "recs = UpdateTest.lookupRecords()\nsum(r.A for r in recs if r.A <= $AA)" ] } ], [ "BulkUpdateRecord", "UpdateTest", [1, 2, 3], { "D": [err, err, err] } ], [ "BulkUpdateRecord", "UpdateTest", [1, 2, 3], { "T": [err, err, err] } ], ] }) # Make sure the table is in the correct state. errVal = objtypes.RaisedException(AttributeError()) self.assertTableData('UpdateTest', data=[ ['id', 'AA', 'T', 'D'], [1, 1., errVal, errVal], [2, 2., errVal, errVal], [3, 3., errVal, errVal], ])
def test_lookup_state(self): # Bug https://phab.getgrist.com/T297 was caused by lookup maps getting corrupted while # re-evaluating a formula for the sake of getting error details. This test case reproduces the # bug in the old code and verifies that it is fixed. sample = testutil.parse_test_sample({ "SCHEMA": [[ 1, "LookupTest", [ [11, "A", "Numeric", False, "", "", ""], [ 12, "B", "Text", True, "LookupTest.lookupOne(A=2).x.upper()", "", "" ], ] ]], "DATA": { "LookupTest": [ ["id", "A"], [7, 2], ] } }) self.load_sample(sample) self.assertTableData( 'LookupTest', data=[ ['id', 'A', 'B'], [7, 2., objtypes.RaisedException(AttributeError())], ]) # Updating a dependency shouldn't cause problems. self.update_record('LookupTest', 7, A=3) self.assertTableData( 'LookupTest', data=[ ['id', 'A', 'B'], [7, 3., objtypes.RaisedException(AttributeError())], ]) # Fetch the error details. self.assertFormulaError( self.engine.get_formula_error('LookupTest', 'B', 7), AttributeError, "Table 'LookupTest' has no column 'x'") # Updating a dependency after the fetch used to cause the error # "AttributeError: 'Table' object has no attribute 'col_id'". Check that it's fixed. self.update_record('LookupTest', 7, A=2) # Should NOT raise an exception. self.assertTableData( 'LookupTest', data=[ ['id', 'A', 'B'], [7, 2., objtypes.RaisedException(AttributeError())], ]) # Add the column that will fix the attribute error. self.add_column('LookupTest', 'x', type='Text') self.assertTableData('LookupTest', data=[ ['id', 'A', 'x', 'B'], [7, 2., '', ''], ]) # And check that the dependency still works and is recomputed. self.update_record('LookupTest', 7, x='hello') self.assertTableData('LookupTest', data=[ ['id', 'A', 'x', 'B'], [7, 2., 'hello', 'HELLO'], ]) self.update_record('LookupTest', 7, A=3) self.assertTableData('LookupTest', data=[ ['id', 'A', 'x', 'B'], [7, 3., 'hello', ''], ])