From 014b562ca9693012b03a2c1389669944feba0f11 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Thu, 19 Dec 2024 12:59:35 +0100 Subject: [PATCH] Promote broken values instead of ignoring them (#11777) Partially fixes #5430 by propagating `DataflowError`s found during statement execution out of the method. # Important Notes This change [may affect behavior](https://github.com/enso-org/enso/pull/11673/files#r1871128327) of existing methods that ignore `DataflowError` as [discussed here](https://github.com/enso-org/enso/pull/11673/files#r1871128327). --- CHANGELOG.md | 2 + .../0.0.0-dev/src/Data/Index_Sub_Range.enso | 10 +- .../Base/0.0.0-dev/src/Data/Range.enso | 5 +- .../0.0.0-dev/src/Data/Time/Date_Range.enso | 5 +- .../src/Internal/Array_Like_Helpers.enso | 2 +- .../Test/0.0.0-dev/src/Extensions.enso | 105 ++++++++------ .../0.0.0-dev/src/Extensions_Helpers.enso | 14 ++ docs/types/errors.md | 134 ++++++++++++------ .../DataflowErrorPropagationTest.java | 86 +++++++++++ .../node/callable/function/BlockNode.java | 19 ++- test/Base_Tests/src/Data/Decimal_Spec.enso | 4 +- test/Base_Tests/src/Data/Function_Spec.enso | 1 - test/Base_Tests/src/Data/Range_Spec.enso | 32 +++-- .../src/Data/Time/Date_Range_Spec.enso | 8 +- test/Base_Tests/src/Random_Spec.enso | 1 - test/Base_Tests/src/System/File_Spec.enso | 2 +- .../Cross_Tab_Spec.enso | 1 - .../Common_Table_Operations/Nothing_Spec.enso | 3 +- .../Table_Tests/src/Database/SQLite_Spec.enso | 27 ++-- .../Table_Tests/src/Database/Upload_Spec.enso | 2 +- test/Table_Tests/src/IO/Fetch_Spec.enso | 16 ++- .../src/In_Memory/Split_Tokenize_Spec.enso | 11 +- test/Table_Tests/src/Util.enso | 18 ++- test/Table_Tests/src/Util_Spec.enso | 66 ++++++++- test/Test_Tests/package.yaml | 7 + test/Test_Tests/src/Extensions_Spec.enso | 86 +++++++++++ test/Test_Tests/src/Helpers.enso | 17 +++ test/Test_Tests/src/Main.enso | 13 ++ 28 files changed, 540 insertions(+), 157 deletions(-) create mode 100644 distribution/lib/Standard/Test/0.0.0-dev/src/Extensions_Helpers.enso create mode 100644 engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/semantic/DataflowErrorPropagationTest.java create mode 100644 test/Test_Tests/package.yaml create mode 100644 test/Test_Tests/src/Extensions_Spec.enso create mode 100644 test/Test_Tests/src/Helpers.enso create mode 100644 test/Test_Tests/src/Main.enso diff --git a/CHANGELOG.md b/CHANGELOG.md index b1e53b92c712..1d4db090dd76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,11 +12,13 @@ #### Enso Language & Runtime +- [Promote broken values instead of ignoring them][11777]. - [Intersection types & type checks][11600] - A constructor or type definition with a single inline argument definition was previously allowed to use spaces in the argument definition without parentheses. [This is now a syntax error.][11856] +[11777]: https://github.com/enso-org/enso/pull/11777 [11600]: https://github.com/enso-org/enso/pull/11600 [11856]: https://github.com/enso-org/enso/pull/11856 diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Index_Sub_Range.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Index_Sub_Range.enso index 95dc0af8ac17..360497b52657 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Index_Sub_Range.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Index_Sub_Range.enso @@ -204,9 +204,8 @@ take_helper length at single_slice slice_ranges range:(Index_Sub_Range | Range | Index_Sub_Range.First count -> single_slice 0 (length.min count) Index_Sub_Range.Last count -> single_slice length-count length Index_Sub_Range.While predicate -> - end = 0.up_to length . find i-> (predicate (at i)).not - true_end = if end.is_nothing then length else end - single_slice 0 true_end + end = 0.up_to length . find (i-> (predicate (at i)).not) if_missing=length + single_slice 0 end Index_Sub_Range.By_Index one_or_many_descriptors -> Panic.recover [Index_Out_Of_Bounds, Illegal_Argument] <| indices = case one_or_many_descriptors of _ : Vector -> one_or_many_descriptors @@ -255,9 +254,8 @@ drop_helper length at single_slice slice_ranges range:(Index_Sub_Range | Range | Index_Sub_Range.First count -> single_slice count length Index_Sub_Range.Last count -> single_slice 0 length-count Index_Sub_Range.While predicate -> - end = 0.up_to length . find i-> (predicate (at i)).not - true_end = if end.is_nothing then length else end - single_slice true_end length + end = 0.up_to length . find (i-> (predicate (at i)).not) if_missing=length + single_slice end length Index_Sub_Range.By_Index one_or_many_descriptors -> Panic.recover [Index_Out_Of_Bounds, Illegal_Argument] <| indices = case one_or_many_descriptors of _ : Vector -> one_or_many_descriptors diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Range.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Range.enso index 8c588cb434cd..2064d7927fdb 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Range.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Range.enso @@ -7,6 +7,7 @@ import project.Data.Text.Text import project.Data.Vector.Vector import project.Error.Error import project.Errors.Common.Index_Out_Of_Bounds +import project.Errors.Common.Not_Found import project.Errors.Empty_Error.Empty_Error import project.Errors.Illegal_Argument.Illegal_Argument import project.Errors.Illegal_State.Illegal_State @@ -397,7 +398,7 @@ type Range @condition range_default_filter_condition_widget any : (Filter_Condition | (Integer -> Boolean)) -> Boolean any self (condition : Filter_Condition | (Integer -> Boolean)) = - self.find condition . is_nothing . not + self.find condition if_missing=Nothing . is_nothing . not ## GROUP Selections ICON find @@ -422,7 +423,7 @@ type Range 1.up_to 100 . find (..Greater than=10) @condition range_default_filter_condition_widget find : (Filter_Condition | (Integer -> Boolean)) -> Integer -> Any -> Any - find self (condition : Filter_Condition | (Integer -> Boolean)) (start : Integer = 0) ~if_missing=Nothing = + find self (condition : Filter_Condition | (Integer -> Boolean)) (start : Integer = 0) ~if_missing=(Error.throw Not_Found) = predicate = unify_condition_or_predicate condition check_start_valid start self used_start-> result = find_internal self used_start predicate diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Range.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Range.enso index e51d9f12062e..3f0d9077dfba 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Range.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Range.enso @@ -11,6 +11,7 @@ import project.Data.Time.Period.Period import project.Data.Vector.Vector import project.Error.Error import project.Errors.Common.Index_Out_Of_Bounds +import project.Errors.Common.Not_Found import project.Errors.Empty_Error.Empty_Error import project.Errors.Illegal_Argument.Illegal_Argument import project.Function.Function @@ -418,7 +419,7 @@ type Date_Range @condition date_range_default_filter_condition_widget any : (Filter_Condition | (Date -> Boolean)) -> Boolean any self (condition : Filter_Condition | (Date -> Boolean)) = - self.find condition . is_nothing . not + self.find condition if_missing=Nothing . is_nothing . not ## GROUP Selections ICON find @@ -438,7 +439,7 @@ type Date_Range (Date.new 2020 10 01).up_to (Date.new 2020 10 31) . find (d-> d.day_of_week == Day_Of_Week.Monday) @condition date_range_default_filter_condition_widget find : (Filter_Condition | (Date -> Boolean)) -> Integer -> Any -> Any - find self (condition : Filter_Condition | (Date -> Boolean)) (start : Integer = 0) ~if_missing=Nothing = + find self (condition : Filter_Condition | (Date -> Boolean)) (start : Integer = 0) ~if_missing=(Error.throw Not_Found) = predicate = unify_condition_or_predicate condition index = self.index_of predicate start case index of diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Array_Like_Helpers.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Array_Like_Helpers.enso index 131d1de9fd9f..b2351bcde3b7 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Array_Like_Helpers.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Array_Like_Helpers.enso @@ -216,7 +216,7 @@ find vector condition start ~if_missing = predicate = unify_condition_or_predicate condition self_len = vector.length check_start_valid start self_len used_start-> - found = used_start.up_to self_len . find (idx -> (predicate (vector.at idx))) + found = used_start.up_to self_len . find (idx -> (predicate (vector.at idx))) if_missing=Nothing if found.is_nothing then if_missing else vector.at found transpose vec_of_vecs = diff --git a/distribution/lib/Standard/Test/0.0.0-dev/src/Extensions.enso b/distribution/lib/Standard/Test/0.0.0-dev/src/Extensions.enso index 46741a56a5c0..ff852368a507 100644 --- a/distribution/lib/Standard/Test/0.0.0-dev/src/Extensions.enso +++ b/distribution/lib/Standard/Test/0.0.0-dev/src/Extensions.enso @@ -1,9 +1,11 @@ from Standard.Base import all +import Standard.Base.Errors.Common.Incomparable_Values import Standard.Base.Errors.Common.No_Such_Method import Standard.Base.Errors.Illegal_Argument.Illegal_Argument import project.Spec_Result.Spec_Result import project.Test.Test +from project.Extensions_Helpers import rhs_error_check ## Expect a function to fail with the provided dataflow error. @@ -70,23 +72,25 @@ Error.should_fail_with self matcher frames_to_skip=0 unwrap_errors=True = example_should_equal = Examples.add_1_to 1 . should_equal 2 Any.should_equal : Any -> Integer -> Spec_Result -Any.should_equal self that frames_to_skip=0 = case self == that of - True -> Spec_Result.Success - False -> - loc = Meta.get_source_location 2+frames_to_skip - additional_comment = case self of - _ : Vector -> case that of - _ : Vector -> - case self.length == that.length of - True -> - diff = self.zip that . index_of p-> - p.first != p.second - "; first difference at index " + diff.to_text + " " - False -> "; lengths differ (" + self.length.to_text + " != " + that.length.to_text + ") " +Any.should_equal self that frames_to_skip=0 = + rhs_error_check that + loc = Meta.get_source_location 1+frames_to_skip + case self == that of + True -> Spec_Result.Success + False -> + additional_comment = case self of + _ : Vector -> case that of + _ : Vector -> + case self.length == that.length of + True -> + diff = self.zip that . index_of p-> + p.first != p.second + "; first difference at index " + diff.to_text + " " + False -> "; lengths differ (" + self.length.to_text + " != " + that.length.to_text + ") " + _ -> "" _ -> "" - _ -> "" - msg = self.pretty + " did not equal " + that.pretty + additional_comment + " (at " + loc + ")." - Test.fail msg + msg = self.pretty + " did not equal " + that.pretty + additional_comment + " (at " + loc + ")." + Test.fail msg ## Asserts that `self` value is equal to the expected type value. @@ -130,12 +134,13 @@ Error.should_equal_type self that frames_to_skip=0 = example_should_not_equal = Examples.add_1_to 1 . should_not_equal 2 Any.should_not_equal : Any -> Integer -> Spec_Result -Any.should_not_equal self that frames_to_skip=0 = case self != that of - True -> Spec_Result.Success - False -> - loc = Meta.get_source_location 2+frames_to_skip - msg = self.to_text + " did equal " + that.to_text + " (at " + loc + ")." - Test.fail msg +Any.should_not_equal self that frames_to_skip=0 = if that.is_error then (Panic.throw (Illegal_Argument.Error "Expected value provided as `that` for `should_not_equal` cannot be an error, but got: "+that.to_display_text)) else + loc = Meta.get_source_location 2+frames_to_skip + case self != that of + True -> Spec_Result.Success + False -> + msg = self.to_text + " did equal " + that.to_text + " (at " + loc + ")." + Test.fail msg ## Added so that dataflow errors are not silently lost. Error.should_not_equal self that frames_to_skip=0 = @@ -183,15 +188,16 @@ Error.should_not_equal_type self that frames_to_skip=0 = example_should_start_with = "Hello World!" . should_start_with "Hello" Any.should_start_with : Text -> Integer -> Spec_Result -Any.should_start_with self that frames_to_skip=0 = case self of - _ : Text -> if self.starts_with that then Spec_Result.Success else - loc = Meta.get_source_location 3+frames_to_skip - msg = self.to_text + " does not start with " + that.to_text + " (at " + loc + ")." - Test.fail msg - _ -> - loc = Meta.get_source_location 2+frames_to_skip - msg = self.to_text + " is not a `Text` value (at " + loc + ")." - Test.fail msg +Any.should_start_with self that frames_to_skip=0 = + rhs_error_check that + loc = Meta.get_source_location 1+frames_to_skip + case self of + _ : Text -> if self.starts_with that then Spec_Result.Success else + msg = self.to_text + " does not start with " + that.to_text + " (at " + loc + ")." + Test.fail msg + _ -> + msg = self.to_text + " is not a `Text` value (at " + loc + ")." + Test.fail msg ## Asserts that `self` value is a Text value and ends with `that`. @@ -207,15 +213,16 @@ Any.should_start_with self that frames_to_skip=0 = case self of example_should_end_with = "Hello World!" . should_end_with "ld!" Any.should_end_with : Text -> Integer -> Spec_Result -Any.should_end_with self that frames_to_skip=0 = case self of - _ : Text -> if self.ends_with that then Spec_Result.Success else - loc = Meta.get_source_location 3+frames_to_skip - msg = self.to_text + " does not end with " + that.to_text + " (at " + loc + ")." - Test.fail msg - _ -> - loc = Meta.get_source_location 2+frames_to_skip - msg = self.to_text + " is not a `Text` value (at " + loc + ")." - Test.fail msg +Any.should_end_with self that frames_to_skip=0 = + rhs_error_check that + loc = Meta.get_source_location 1+frames_to_skip + case self of + _ : Text -> if self.ends_with that then Spec_Result.Success else + msg = self.to_text + " does not end with " + that.to_text + " (at " + loc + ")." + Test.fail msg + _ -> + msg = self.to_text + " is not a `Text` value (at " + loc + ")." + Test.fail msg ## Asserts that `self` value is a Text value and starts with `that`. @@ -267,7 +274,7 @@ Error.should_end_with self that frames_to_skip=0 = example_should_equal = Examples.add_1_to 1 . should_equal 2 Error.should_equal : Any -> Integer -> Spec_Result Error.should_equal self that frames_to_skip=0 = - _ = [that] + rhs_error_check that Test.fail_match_on_unexpected_error self 1+frames_to_skip ## Asserts that `self` is within `epsilon` from `that`. @@ -294,13 +301,18 @@ Error.should_equal self that frames_to_skip=0 = 1.00000001 . should_equal 1.00000002 epsilon=0.0001 Number.should_equal : Float -> Float -> Integer -> Spec_Result Number.should_equal self that epsilon=0 frames_to_skip=0 = + rhs_error_check that + loc = Meta.get_source_location 1+frames_to_skip matches = case that of - n : Number -> self.equals n epsilon + n : Number -> self.equals n epsilon . catch Incomparable_Values _-> + ## Incomparable_Values is thrown if one of the values is NaN. + We fallback to is_same_object_as, + because in tests we actually NaN.should_equal NaN to succeed. + self.is_same_object_as n _ -> self==that case matches of True -> Spec_Result.Success False -> - loc = Meta.get_source_location 2+frames_to_skip msg = self.to_text + " did not equal " + that.to_text + " (at " + loc + ")." Test.fail msg @@ -313,6 +325,7 @@ Number.should_equal self that epsilon=0 frames_to_skip=0 = displayed as the source of this error. Decimal.should_equal : Number -> Float-> Float -> Integer -> Spec_Result Decimal.should_equal self that epsilon=0 frames_to_skip=0 = + rhs_error_check that self.to_float . should_equal that.to_float epsilon frames_to_skip+1 ## Asserts that `self` value is not an error. @@ -423,6 +436,7 @@ Error.should_be_false self = Test.fail_match_on_unexpected_error self 1 example_should_be_a = 1.should_be_a Boolean Any.should_be_a : Any -> Spec_Result Any.should_be_a self typ = + rhs_error_check typ loc = Meta.get_source_location 1 fail_on_wrong_arg_type = Panic.throw <| @@ -490,6 +504,8 @@ Any.should_be_a self typ = Any.should_equal_ignoring_order : Any -> Integer -> Spec_Result Any.should_equal_ignoring_order self that frames_to_skip=0 = loc = Meta.get_source_location 1+frames_to_skip + if that.is_a Vector . not then + Panic.throw (Illegal_Argument.Error "Expected a Vector, but got a "+that.to_display_text+" (at "+loc+").") that.each element-> if self.contains element . not then msg = "The collection (" + self.to_text + ") did not contain " + element.to_text + " (at " + loc + ")." @@ -556,6 +572,7 @@ Error.should_equal_ignoring_order self that frames_to_skip=0 = example_should_equal = [1, 2] . should_only_contain_elements_in [1, 2, 3, 4] Any.should_only_contain_elements_in : Any -> Integer -> Spec_Result Any.should_only_contain_elements_in self that frames_to_skip=0 = + rhs_error_check that loc = Meta.get_source_location 1+frames_to_skip self.each element-> if that.contains element . not then @@ -609,6 +626,7 @@ Error.should_only_contain_elements_in self that frames_to_skip=0 = example_should_equal = "foobar".should_contain "foo" Any.should_contain : Any -> Integer -> Spec_Result Any.should_contain self element frames_to_skip=0 = + rhs_error_check element loc = Meta.get_source_location 1+frames_to_skip contains_result = Panic.catch No_Such_Method (self.contains element) caught_panic-> if caught_panic.payload.method_name != "contains" then Panic.throw caught_panic else @@ -652,6 +670,7 @@ Error.should_contain self element frames_to_skip=0 = implementing a method `contains : a -> Boolean`. Any.should_not_contain : Any -> Integer -> Spec_Result Any.should_not_contain self element frames_to_skip=0 = + rhs_error_check element loc = Meta.get_source_location 1+frames_to_skip contains_result = Panic.catch No_Such_Method (self.contains element) caught_panic-> if caught_panic.payload.method_name != "contains" then Panic.throw caught_panic else diff --git a/distribution/lib/Standard/Test/0.0.0-dev/src/Extensions_Helpers.enso b/distribution/lib/Standard/Test/0.0.0-dev/src/Extensions_Helpers.enso new file mode 100644 index 000000000000..73f4437ef8bf --- /dev/null +++ b/distribution/lib/Standard/Test/0.0.0-dev/src/Extensions_Helpers.enso @@ -0,0 +1,14 @@ +private + +from Standard.Base import all +import Standard.Base.Errors.Illegal_Argument.Illegal_Argument + +## PRIVATE + A helper that ensures that the expected value provided in some of the Test + operations is not an error. + The left-hand side may be an error and that will cause a test failure. + But the right-hand side being an error is bad test design and should be fixed. +rhs_error_check that = + if that.is_error then + msg = "Dataflow error ("+that.to_display_text+") provided as expected value. Use `should_fail_with` or change the test."+ ' Error stack trace was:\n'+that.get_stack_trace_text + Panic.throw (Illegal_Argument.Error msg) diff --git a/docs/types/errors.md b/docs/types/errors.md index 653645734301..8d0aeb4f3187 100644 --- a/docs/types/errors.md +++ b/docs/types/errors.md @@ -1,15 +1,17 @@ --- layout: developer-doc -title: Errors +title: Errors & Panics category: types tags: [types, errors] order: 12 --- -# Errors +# Errors & Panics -Enso supports two notions of errors. One is the standard exceptions model, while -the other is a theory of 'broken values' that propagate through computations. +Enso supports two notions of errors. One is the standard exceptions model (built +around `Panic.throw` and related methods), while the other is a theory of +_broken values_ that propagate through computations (represented by `Error` and +created by `Error.throw` method). > [!WARNING] The actionables for this section are: > @@ -19,65 +21,109 @@ the other is a theory of 'broken values' that propagate through computations. -- [Async Exceptions](#async-exceptions) +- [Exceptions/Panics](#errors--panics) - [Broken Values](#broken-values) -## Async Exceptions +## Exceptions/Panics > [!WARNING] The actionables for this section are: > -> - why is this called _"asynchronous"_ when the `Panic` is raised -> synchronously? -> - Formalise the model of async exceptions as implemented. +> - Formalise the model of `Panic.throw` as implemented. ## Broken Values -In Enso we have the notion of a 'broken' value: one which is in an invalid state -but not an error. While these may initially seem a touch useless, they are -actually key for the display of errors in the GUI. +In Enso we have the notion of a _broken value_: one which is in an invalid +state. Such values are very useful for displaying errors in the GUI. -Broken values can be thought of like checked monadic exceptions in Haskell, but -with an automatic propagation mechanism: +Broken values are fast to allocate and pass around the program. They record line +of their own creation - e.g. where `Error.throw` has happened. Shall that not be +enough, one can run with `-ea` flag, like: -- Broken values that aren't handled explicitly are automatically promoted - through the parent scope. This is trivial inference as no evidence discharge - will have occurred on the value. +```bash +enso$ JAVA_OPTS=-ea ./built-distribution/enso-engine-*/enso-*/bin/enso --run x.enso +``` - ```ruby - open : String -> String in IO ! IO.Exception - open = ... +to get full stack where the _broken value_ has been created. Collecting such +full stack trace however prevents the execution to run at _full speed_. - test = - print 'Opening the gates!' - txt = open 'gates.txt' - print 'Gates were opened!' - 7 - ``` +### Promotion of Broken Values - In the above example, the type of test is inferred to - `test : Int in IO ! IO.Exception`, because no evidence discharge has taken - place as the potential broken value hasn't been handled. +Broken values that aren't handled explicitly are automatically promoted through +the parent scope. Let's assume an `open` function that can yield a `Text` or +_broken value_ representing a `File_Error`: -- This allows for very natural error handling in the GUI. +```ruby +open file_name:Text -> Text ! File_Error = ... +``` -> [!WARNING] The actionables for this section are: -> -> - Determine what kinds of APIs we want to use async exceptions for, and which -> broken values are more suited for. -> - Ensure that we are okay with initially designing everything around async -> exceptions as broken values are very hard to support without a type checker. -> - Initially not supported for APIs. +Then imagine following `test` function trying to open a non-existing file +`gates.txt` + +```ruby +test = + IO.println 'Opening the gates!' + open 'gates.txt' + IO.println 'Gates were opened!' +``` + +Execution of such function will: + +- print `Opening the gates!` text +- finish with `File_Error` _broken value_ +- **not print** `Gates were opened!` + +E.g. the execution of a function body ends after first _uhandled broken value_. + +### Propagation of Broken Values -Broken values (implemented as `DataflowError` class in the interpreter) are fast -to allocate and pass around the program. They record line of their own -creation - e.g. where `Error.throw` has happened. Shall that not be enough, one -can run with `-ea` flag, like: +Let's modify the previous example a bit. Let's assign the read text (or _broken +value_) to a variable and return it from the `test` function: + +```ruby +test = + IO.println 'Opening the gates!' + content = open 'gates.txt' + IO.println 'Gates were opened!' + content +``` + +If the `gates.txt` file exists, its content is returned from the `test` +function. If a `File_Error` _broken value_ is returned from the `open` function, +then the variable `content` will contain such a _broken value_ and as `content` +is the return value from the `test` function, the `File_Error` will be returned +from the `test` function and propagated further as a _broken value_. + +In both situations (if the file exists or not) both `IO.println` statements are +executed and the execution of `test` function thus prints both +`Opening the gates!` as well as `Gates were opened!`. + +### Detection of Unused Broken Values + +Should the last statement (e.g. `content`) of the `test` function defined in +previous section be missing, then the _broken value_ assigned to `content` +variable might _"disappear"_ unnoticed. However in such a situation the Enso +compiler emits a _compile time warning_: ```bash -enso$ JAVA_OPTS=-ea ./built-distribution/enso-engine-*/enso-*/bin/enso --run x.enso +test.enso:3:3: warning: Unused variable content. + 3 | content = open 'gates.txt' + | ^~~~~~~ ``` -to get full stack where the _broken value_ has been created. Collecting such -full stack trace however prevents the execution to run at _full speed_. +The combination of _detection_, _propagation_ and _promotion_ of _broken values_ +ensures `File_Error` and other _broken values_ are **never lost** +(unintentionally). Should _loosing a broken value_ be a goal, one can change the +line in question to: + +```ruby + _ = open 'gates.txt' +``` + +e.g. assign it to anonymous variable. That signals to the system one doesn't +care about the result of the `open` function call. No _compiler warning_ is thus +reported and the _broken value_ gets lost during execution. + +To handle _broken values_ properly and recover from such an errorneous state, +use methods offered by `Standard.Base.Error` type like `catch`. diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/semantic/DataflowErrorPropagationTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/semantic/DataflowErrorPropagationTest.java new file mode 100644 index 000000000000..f5facc9d72d1 --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/semantic/DataflowErrorPropagationTest.java @@ -0,0 +1,86 @@ +package org.enso.interpreter.test.semantic; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.enso.common.MethodNames; +import org.enso.test.utils.ContextUtils; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.PolyglotException; +import org.graalvm.polyglot.Value; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +public class DataflowErrorPropagationTest { + private static Context ctx; + private static Value suppressError; + private static Value suppressErrorWithAssign; + + @BeforeClass + public static void prepareCtx() { + ctx = ContextUtils.createDefaultContext(); + var code = + """ + from Standard.Base import all + + private yield_error yes:Boolean -> Text = + if yes then Error.throw "Yielding an error" else + "OK" + + suppress_error yes:Boolean value = + yield_error yes + value + + suppress_error_with_assign yes:Boolean value = + _ = yield_error yes + value + """; + suppressError = + ctx.eval("enso", code).invokeMember(MethodNames.Module.EVAL_EXPRESSION, "suppress_error"); + suppressErrorWithAssign = + ctx.eval("enso", code) + .invokeMember(MethodNames.Module.EVAL_EXPRESSION, "suppress_error_with_assign"); + } + + @AfterClass + public static void disposeCtx() { + ctx.close(); + ctx = null; + } + + @Test + public void noErrorReturnValue() { + var value = suppressError.execute(false, 42); + assertTrue("It is a number", value.isNumber()); + assertEquals(42, value.asInt()); + } + + @Test + public void propagateErrorImmediatelly() { + var value = suppressError.execute(true, 42); + assertFalse("It is not a number", value.isNumber()); + assertTrue("It is an error", value.isException()); + try { + throw value.throwException(); + } catch (PolyglotException ex) { + assertEquals("Yielding an error", ex.getMessage()); + } + } + + @Test + public void noErrorReturnValueWithAssign() { + var value = suppressErrorWithAssign.execute(false, 42); + assertTrue("It is a number", value.isNumber()); + assertEquals(42, value.asInt()); + } + + @Test + public void errorIsAssignedAndThatIsEnoughReturnValue() { + var value = suppressErrorWithAssign.execute(true, 42); + assertTrue("It is a number", value.isNumber()); + assertFalse("Not an error", value.isException()); + assertEquals(42, value.asInt()); + } +} diff --git a/engine/runtime/src/main/java/org/enso/interpreter/node/callable/function/BlockNode.java b/engine/runtime/src/main/java/org/enso/interpreter/node/callable/function/BlockNode.java index 994b68b34149..41d51a54a9ee 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/node/callable/function/BlockNode.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/node/callable/function/BlockNode.java @@ -6,9 +6,12 @@ import com.oracle.truffle.api.instrumentation.Tag; import com.oracle.truffle.api.nodes.ExplodeLoop; import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.profiles.BranchProfile; import com.oracle.truffle.api.source.SourceSection; import java.util.Set; import org.enso.interpreter.node.ExpressionNode; +import org.enso.interpreter.runtime.EnsoContext; +import org.enso.interpreter.runtime.error.DataflowError; /** * This node defines the body of a function for execution, as well as the protocol for executing the @@ -16,11 +19,17 @@ */ @NodeInfo(shortName = "Block") public class BlockNode extends ExpressionNode { + private final BranchProfile unexpectedReturnValue; @Children private final ExpressionNode[] statements; @Child private ExpressionNode returnExpr; private BlockNode(ExpressionNode[] expressions, ExpressionNode returnExpr) { this.statements = expressions; + if (expressions.length > 0) { + this.unexpectedReturnValue = BranchProfile.create(); + } else { + this.unexpectedReturnValue = BranchProfile.getUncached(); + } this.returnExpr = returnExpr; } @@ -55,8 +64,16 @@ public static BlockNode buildSilent(ExpressionNode[] expressions, ExpressionNode @Override @ExplodeLoop public Object executeGeneric(VirtualFrame frame) { + var ctx = EnsoContext.get(this); + var nothing = ctx.getBuiltins().nothing(); for (ExpressionNode statement : statements) { - statement.executeGeneric(frame); + var result = statement.executeGeneric(frame); + if (result != nothing) { + unexpectedReturnValue.enter(); + if (result instanceof DataflowError err) { + return err; + } + } } return returnExpr.executeGeneric(frame); } diff --git a/test/Base_Tests/src/Data/Decimal_Spec.enso b/test/Base_Tests/src/Data/Decimal_Spec.enso index 3465ce9cd85a..98a9d096b215 100644 --- a/test/Base_Tests/src/Data/Decimal_Spec.enso +++ b/test/Base_Tests/src/Data/Decimal_Spec.enso @@ -484,10 +484,10 @@ add_specs suite_builder = (Decimal.from_integer -29388920982834 . subtract (Decimal.from_integer 842820) (Math_Context.new 7)) . should_equal (Decimal.from_integer -29388920000000) (Decimal.new "-8273762787.3535345" . subtract (Decimal.new "76287273.23434535") (Math_Context.new 10)) . should_equal (Decimal.new "-8350050061") - (Decimal.from_integer 7297927982888383 . multiply (Decimal.from_integer 828737) (Math_Context.new 6)) . should_equal (Decimal.from_integer 6048060000000000000000 ) + (Decimal.from_integer 7297927982888383 . multiply (Decimal.from_integer 828737) (Math_Context.new 6)) . should_equal (Decimal.from_integer 6048060000000000000000) (Decimal.new "893872388.3535345" . multiply (Decimal.new "72374727737.23434535") (Math_Context.new 14)) . should_equal (Decimal.new "64693770738918000000") - (Decimal.new "909678645268840" . divide (Decimal.new "28029830") (Math_Context.new 6)) . should_equal (Decimal.new "32453900 ") + (Decimal.new "909678645268840" . divide (Decimal.new "28029830") (Math_Context.new 6)) . should_equal (Decimal.new "32453900") (Decimal.new "384456406.7860325392609633764" . divide (Decimal.new "24556.125563546") (Math_Context.new 7)) . should_equal (Decimal.new "15656.23") (Decimal.from_integer 3948539458034580838458034803485 . add (Decimal.from_integer 237957498573948579387495837459837) (Math_Context.new 20)) . should_equal (Decimal.from_integer 241906038031983160230000000000000) diff --git a/test/Base_Tests/src/Data/Function_Spec.enso b/test/Base_Tests/src/Data/Function_Spec.enso index b0cd9099e11a..c808ce77d9c8 100644 --- a/test/Base_Tests/src/Data/Function_Spec.enso +++ b/test/Base_Tests/src/Data/Function_Spec.enso @@ -90,4 +90,3 @@ main filter=Nothing = suite = Test.build suite_builder-> add_specs suite_builder suite.run_with_filter filter - diff --git a/test/Base_Tests/src/Data/Range_Spec.enso b/test/Base_Tests/src/Data/Range_Spec.enso index a80a5cd52a0f..5bb3ecc8446f 100644 --- a/test/Base_Tests/src/Data/Range_Spec.enso +++ b/test/Base_Tests/src/Data/Range_Spec.enso @@ -3,6 +3,7 @@ import Standard.Base.Data.Vector.Builder import Standard.Base.Errors.Empty_Error.Empty_Error import Standard.Base.Errors.Common.Index_Out_Of_Bounds import Standard.Base.Errors.Common.No_Such_Method +import Standard.Base.Errors.Common.Not_Found import Standard.Base.Errors.Common.Type_Error import Standard.Base.Errors.Common.Unsupported_Argument_Types import Standard.Base.Errors.Illegal_Argument.Illegal_Argument @@ -250,9 +251,10 @@ add_specs suite_builder = suite_builder.group "Range" group_builder-> group_builder.specify "should find elements" <| 1.up_to 10 . find (> 5) . should_equal 6 1.up_to 10 . find (..Greater 5) . should_equal 6 - 1.up_to 10 . find (> 10) . should_be_a Nothing + 1.up_to 10 . find (> 10) . should_fail_with Not_Found 1.up_to 10 . find (v-> v%4 == 0) start=6 . should_equal 8 - 1.up_to 10 . find (< 5) start=6 . should_be_a Nothing + 1.up_to 10 . find (< 5) start=6 if_missing=Nothing . should_be_a Nothing + 1.up_to 10 . find (< 5) start=6 . should_fail_with Not_Found 1.up_to 10 . find (< 5) start=10 . should_fail_with Index_Out_Of_Bounds 1.up_to 10 . find (< 5) start=10 . catch . should_equal (Index_Out_Of_Bounds.Error 10 10) Test.expect_panic_with (1.up_to 10 . find "invalid arg") Type_Error @@ -343,7 +345,8 @@ add_specs suite_builder = suite_builder.group "Range" group_builder-> r.fold 0 (+) . should_equal 0 r.any _->True . should_equal False r.all _->False . should_equal True - r.find _->True . should_equal Nothing + r.find _->True if_missing=Nothing . should_equal Nothing + r.find _->True . should_fail_with Not_Found verify_contains r [] [-1, 0, 1, 2, 10] check_empty_range (0.up_to 0) @@ -370,7 +373,7 @@ add_specs suite_builder = suite_builder.group "Range" group_builder-> r1.all (_ == 10) . should_equal True r1.all (_ == 11) . should_equal False r1.find (x-> x*x == 100) . should_equal 10 - r1.find (x-> x*x == 25) . should_equal Nothing + r1.find (x-> x*x == 25) if_missing=Nothing . should_equal Nothing verify_contains r1 [10] [-1, 0, 1, 2, 9, 11, 12] group_builder.specify "should behave correctly with step greater than 1" <| @@ -387,7 +390,7 @@ add_specs suite_builder = suite_builder.group "Range" group_builder-> r1.all (x-> x % 2 == 0) . should_equal True r1.all (_ == 2) . should_equal False r1.find (x-> x*x == 16) . should_equal 4 - r1.find (x-> x*x == 25) . should_equal Nothing + r1.find (x-> x*x == 25) if_missing=Nothing . should_equal Nothing verify_contains r1 [0, 2, 4, 6, 8] [-3, -2, -1, 1, 3, 5, 7, 11, 12, 13, 14] r2 = Range.Between 0 3 2 @@ -402,7 +405,7 @@ add_specs suite_builder = suite_builder.group "Range" group_builder-> r2.any (_ == 3) . should_equal False r2.all (x-> x % 2 == 0) . should_equal True r2.all (_ == 2) . should_equal False - r2.find (x-> x*x == 16) . should_equal Nothing + r2.find (x-> x*x == 16) . should_fail_with Not_Found r2.find (x-> x*x == 4) . should_equal 2 verify_contains r2 [0, 2] [-3, -2, -1, 1, 3, 4, 5] @@ -418,7 +421,7 @@ add_specs suite_builder = suite_builder.group "Range" group_builder-> r3.any (_ == 3) . should_equal False r3.all (_ == 5) . should_equal True r3.all (_ == 3) . should_equal False - r3.find (x-> x*x == 16) . should_equal Nothing + r3.find (x-> x*x == 16) . should_fail_with Not_Found r3.find (x-> x*x == 25) . should_equal 5 verify_contains r3 [5] [0, 1, 4, 6, 7, 10] @@ -435,7 +438,7 @@ add_specs suite_builder = suite_builder.group "Range" group_builder-> r4.all (x-> x % 2 == 1) . should_equal True r4.all (_ == 5) . should_equal False r4.find (x-> x*x == 25) . should_equal 5 - r4.find (x-> x*x == 4) . should_equal Nothing + r4.find (x-> x*x == 4) if_missing=Nothing . should_equal Nothing verify_contains r4 [5, 7] [0, 1, 4, 6, 8, 10] r5 = Range.Between 5 7 2 @@ -451,7 +454,7 @@ add_specs suite_builder = suite_builder.group "Range" group_builder-> r5.all (x-> x % 2 == 1) . should_equal True r5.all (_ == 5) . should_equal True r5.find (x-> x*x == 25) . should_equal 5 - r5.find (x-> x*x == 4) . should_equal Nothing + r5.find (x-> x*x == 4) if_missing=Nothing . should_equal Nothing verify_contains r5 [5] [0, 1, 4, 6, 7, 10] r6 = Range.Between 0 10 3 @@ -467,7 +470,7 @@ add_specs suite_builder = suite_builder.group "Range" group_builder-> r6.all (x-> x % 2 == 0) . should_equal False r6.all (x-> x % 3 == 0) . should_equal True r6.find (x-> x*x == 9) . should_equal 3 - r6.find (x-> x*x == 25) . should_equal Nothing + r6.find (x-> x*x == 25) if_missing=Nothing . should_equal Nothing r6.filter (_ < 4) . should_equal [0, 3] verify_contains r6 [0, 3, 6, 9] [-3, -2, -1, 1, 2, 4, 5, 7, 8, 10, 11] @@ -485,7 +488,7 @@ add_specs suite_builder = suite_builder.group "Range" group_builder-> r1.all (x-> x % 2 == 0) . should_equal False r1.all (_ > 0) . should_equal True r1.find (x-> x*x == 16) . should_equal 4 - r1.find (x-> x*x == 0) . should_equal Nothing + r1.find (x-> x*x == 0) if_missing=Nothing . should_equal Nothing verify_contains r1 [4, 3, 2, 1] [-2, -1, 0, 5, 6, 7, 10] r2 = Range.Between 4 0 -2 @@ -501,7 +504,7 @@ add_specs suite_builder = suite_builder.group "Range" group_builder-> r2.all (x-> x % 2 == 0) . should_equal True r2.all (_ > 2) . should_equal False r2.find (x-> x*x == 16) . should_equal 4 - r2.find (x-> x*x == 0) . should_equal Nothing + r2.find (x-> x*x == 0) . should_fail_with Not_Found verify_contains r2 [4, 2] [-2, -1, 0, 1, 3, 5, 6, 7, 10] r3 = Range.Between 4 0 -10 @@ -517,7 +520,7 @@ add_specs suite_builder = suite_builder.group "Range" group_builder-> r3.all (x-> x % 2 == 0) . should_equal True r3.all (_ > 4) . should_equal False r3.find (x-> x*x == 16) . should_equal 4 - r3.find (x-> x*x == 0) . should_equal Nothing + r3.find (x-> x*x == 0) . should_fail_with Not_Found verify_contains r3 [4] [-2, -1, 0, 1, 2, 3, 5, 6, 7, 10] r4 = Range.Between 3 0 -3 @@ -533,7 +536,7 @@ add_specs suite_builder = suite_builder.group "Range" group_builder-> r4.all (x-> x % 2 == 0) . should_equal False r4.all (_ > 0) . should_equal True r4.find (x-> x*x == 9) . should_equal 3 - r4.find (x-> x*x == 0) . should_equal Nothing + r4.find (x-> x*x == 0) . should_fail_with Not_Found verify_contains r4 [3] [-3, -2, -1, 0, 1, 2, 4, 5, 6, 7, 10] group_builder.specify "should report errors if trying to set step to 0" <| @@ -582,4 +585,3 @@ main filter=Nothing = suite = Test.build suite_builder-> add_specs suite_builder suite.run_with_filter filter - diff --git a/test/Base_Tests/src/Data/Time/Date_Range_Spec.enso b/test/Base_Tests/src/Data/Time/Date_Range_Spec.enso index b2b469ae1c61..9d74f3604c7b 100644 --- a/test/Base_Tests/src/Data/Time/Date_Range_Spec.enso +++ b/test/Base_Tests/src/Data/Time/Date_Range_Spec.enso @@ -1,4 +1,5 @@ from Standard.Base import all +import Standard.Base.Errors.Common.Not_Found import Standard.Base.Errors.Common.Type_Error import Standard.Base.Errors.Empty_Error.Empty_Error import Standard.Base.Errors.Illegal_Argument.Illegal_Argument @@ -157,7 +158,7 @@ add_specs suite_builder = r.partition p . should_equal (r.to_vector.partition p) r.all p . should_equal (r.to_vector.all p) r.any p . should_equal (r.to_vector.any p) - r.find p . should_equal (r.to_vector.find p) + r.find p if_missing="not found" . should_equal (r.to_vector.find p if_missing="not found") r.index_of p . should_equal (r.to_vector.index_of p) r.last_index_of p . should_equal (r.to_vector.last_index_of p) count_mondays acc date = @@ -170,7 +171,7 @@ add_specs suite_builder = r.partition fc . should_equal (r.to_vector.partition fc) r.all fc . should_equal (r.to_vector.all fc) r.any fc . should_equal (r.to_vector.any fc) - r.find fc . should_equal (r.to_vector.find fc) + r.find fc if_missing="not found" . should_equal (r.to_vector.find fc if_missing="not found") r.index_of fc . should_equal (r.to_vector.index_of fc) r.last_index_of fc . should_equal (r.to_vector.last_index_of fc) @@ -182,6 +183,9 @@ add_specs suite_builder = Test.expect_panic_with (r.index_of invalid_arg) Type_Error Test.expect_panic_with (r.last_index_of invalid_arg) Type_Error + # If `if_missing` is not provided, it defaults to `Not_Found` dataflow error + r.find (== 123) . should_fail_with Not_Found + reducer x y = if x > y then x else y case r.length of diff --git a/test/Base_Tests/src/Random_Spec.enso b/test/Base_Tests/src/Random_Spec.enso index ecb2b6ac07cb..980dc33707a2 100644 --- a/test/Base_Tests/src/Random_Spec.enso +++ b/test/Base_Tests/src/Random_Spec.enso @@ -157,4 +157,3 @@ main filter=Nothing = suite = Test.build suite_builder-> add_specs suite_builder suite.run_with_filter filter - diff --git a/test/Base_Tests/src/System/File_Spec.enso b/test/Base_Tests/src/System/File_Spec.enso index 4f56688efa4f..826771ed3de6 100644 --- a/test/Base_Tests/src/System/File_Spec.enso +++ b/test/Base_Tests/src/System/File_Spec.enso @@ -391,7 +391,7 @@ add_specs suite_builder = subdir.should_succeed cleanup = Enso_User.flush_caches - subdir.delete + subdir.delete recursive=True Panic.with_finalizer cleanup <| Test_Environment.unsafe_with_environment_override "ENSO_CLOUD_PROJECT_DIRECTORY_PATH" subdir.path <| # Flush caches to ensure fresh dir is used diff --git a/test/Table_Tests/src/Common_Table_Operations/Cross_Tab_Spec.enso b/test/Table_Tests/src/Common_Table_Operations/Cross_Tab_Spec.enso index c0d17b70a245..ad79702d5c8b 100644 --- a/test/Table_Tests/src/Common_Table_Operations/Cross_Tab_Spec.enso +++ b/test/Table_Tests/src/Common_Table_Operations/Cross_Tab_Spec.enso @@ -252,7 +252,6 @@ add_specs suite_builder setup = r1.catch.to_display_text . should_contain "cannot contain the NUL character" r2 = data.table2.cross_tab [] "Key" values=[Aggregate_Column.Average "Value" as='x\0'] - r2.print r2.should_fail_with Invalid_Column_Names r2.catch.to_display_text . should_contain "cannot contain the NUL character" diff --git a/test/Table_Tests/src/Common_Table_Operations/Nothing_Spec.enso b/test/Table_Tests/src/Common_Table_Operations/Nothing_Spec.enso index 43596f79bbd8..e929623dc265 100644 --- a/test/Table_Tests/src/Common_Table_Operations/Nothing_Spec.enso +++ b/test/Table_Tests/src/Common_Table_Operations/Nothing_Spec.enso @@ -1,4 +1,5 @@ from Standard.Base import all +import Standard.Base.Errors.Common.Incomparable_Values from Standard.Table import all @@ -257,7 +258,7 @@ add_nothing_specs suite_builder setup = other_value = triple.at 1 value_type = triple.at 2 - is_comparable = case Ordering.compare value other_value of + is_comparable = case Ordering.compare value other_value . catch Incomparable_Values of _:Ordering -> True _ -> False diff --git a/test/Table_Tests/src/Database/SQLite_Spec.enso b/test/Table_Tests/src/Database/SQLite_Spec.enso index 1c10b7d3b1ea..5ea4e8ce960b 100644 --- a/test/Table_Tests/src/Database/SQLite_Spec.enso +++ b/test/Table_Tests/src/Database/SQLite_Spec.enso @@ -3,6 +3,7 @@ import Standard.Base.Runtime.Ref.Ref from Standard.Base.Runtime import assert import Standard.Base.Errors.File_Error.File_Error import Standard.Base.Errors.Illegal_Argument.Illegal_Argument +import Standard.Base.Runtime.Context from Standard.Table import Table, Value_Type, Bits from Standard.Table.Errors import Invalid_Column_Names, Duplicate_Output_Column_Names @@ -54,8 +55,8 @@ type Metadata_Data [connection, tinfo, t] teardown self = - self.connection.drop_table self.t.name - self.connection.drop_table self.tinfo + self.connection.drop_table self.t.name if_exists=True + self.connection.drop_table self.tinfo if_exists=True self.connection.close @@ -81,9 +82,9 @@ type Tables_And_Table_Types_Data [connection, tinfo, vinfo, temporary_table] teardown self = - self.connection.drop_table self.tinfo - self.connection.drop_table self.vinfo - self.connection.drop_table self.temporary_table + self.connection.drop_table self.tinfo if_exists=True + self.connection.execute_update "DROP VIEW IF EXISTS '"+self.vinfo+"'" + self.connection.drop_table self.temporary_table if_exists=True self.connection.close @@ -377,9 +378,7 @@ type Database_File Value ~file create = Database_File.Value <| - transient_dir = enso_project.data / "transient" - assert transient_dir.exists ("The directory " + transient_dir.path + " should exist (ensured by containing a `.gitignore` file).") - f = transient_dir / "sqlite_test.db" + f = File.create_temporary_file "sqlite-test" ".db" f.delete_if_exists f @@ -388,7 +387,10 @@ create_inmem_connection = create_file_connection file = connection = Database.connect (SQLite.From_File file) - connection.execute_update 'CREATE TABLE "Dummy" ("strs" VARCHAR, "ints" INTEGER, "bools" BOOLEAN, "reals" REAL)' + ## We need to re-enable the context because this initializer may be executed + lazily in a context where Output was disabled (e.g. Upload_Spec). + Context.Output.with_enabled <| + connection.execute_update 'CREATE TABLE IF NOT EXISTS "Dummy" ("strs" VARCHAR, "ints" INTEGER, "bools" BOOLEAN, "reals" REAL)' connection type File_Connection @@ -401,10 +403,6 @@ type File_Connection assert tmp_file.exists tmp_file - teardown self = - assert self.file.exists - self.file.delete - add_specs suite_builder = in_file_prefix = "[SQLite File] " @@ -425,9 +423,6 @@ add_specs suite_builder = suite_builder.group "SQLite_Format should allow connecting to SQLite files" group_builder-> data = File_Connection.setup database_file - group_builder.teardown <| - data.teardown - group_builder.specify "should recognise a SQLite database file" <| Auto_Detect.get_reading_format data.file . should_be_a SQLite_Format diff --git a/test/Table_Tests/src/Database/Upload_Spec.enso b/test/Table_Tests/src/Database/Upload_Spec.enso index ffaa385e9e3c..dce15a1d1fbb 100644 --- a/test/Table_Tests/src/Database/Upload_Spec.enso +++ b/test/Table_Tests/src/Database/Upload_Spec.enso @@ -498,7 +498,7 @@ add_specs suite_builder setup make_new_connection persistent_connector=True = copied_table = db_table.select_into_database_table tmp_connection (Name_Generator.random_name "copied-table") temporary=False copied_table.is_trivial_query . should_be_true name = copied_table.name - Panic.with_finalizer (data.connection.drop_table name) <| + Panic.with_finalizer (data.connection.drop_table name if_exists=True) <| setup.expect_integer_type <| copied_table.at "X" copied_table.at "Y" . value_type . is_text . should_be_true copied_table.at "Z" . value_type . is_floating_point . should_be_true diff --git a/test/Table_Tests/src/IO/Fetch_Spec.enso b/test/Table_Tests/src/IO/Fetch_Spec.enso index 0e27747890c4..c4b11c1a27c6 100644 --- a/test/Table_Tests/src/IO/Fetch_Spec.enso +++ b/test/Table_Tests/src/IO/Fetch_Spec.enso @@ -4,8 +4,10 @@ import Standard.Base.Errors.Common.Response_Too_Large import Standard.Base.Errors.File_Error.File_Error import Standard.Base.Errors.Illegal_Argument.Illegal_Argument import Standard.Base.Network.HTTP.Cache_Policy.Cache_Policy +import Standard.Base.Network.HTTP.HTTP_Error.HTTP_Error import Standard.Base.Network.HTTP.Request.Request import Standard.Base.Network.HTTP.Request_Body.Request_Body +import Standard.Base.Network.HTTP.Request_Error import Standard.Base.Network.HTTP.Response.Response import Standard.Base.Runtime.Context import Standard.Base.Runtime.Ref.Ref @@ -412,11 +414,11 @@ add_specs suite_builder = group_builder.specify "Should not cache if the request fails" pending=pending_has_url <| Test.with_retries <| with_default_cache <| - HTTP.fetch url0 . decode_as_text + HTTP.fetch url0 . decode_as_text . should_succeed get_num_response_cache_entries . should_equal 1 - HTTP.fetch base_url_with_slash+'crash' . decode_as_text + HTTP.fetch base_url_with_slash+'crash' . decode_as_text . should_fail_with Request_Error get_num_response_cache_entries . should_equal 1 - HTTP.fetch base_url_with_slash+'nonexistent_endpoint' . decode_as_text + HTTP.fetch base_url_with_slash+'nonexistent_endpoint' . decode_as_text . should_fail_with HTTP_Error get_num_response_cache_entries . should_equal 1 cloud_setup = Cloud_Tests_Setup.prepare @@ -437,9 +439,9 @@ add_specs suite_builder = . add_query_argument "arg1" secret2 . add_query_argument "arg2" "plain value" - HTTP.fetch url1 . decode_as_text + HTTP.fetch url1 . decode_as_text . should_succeed get_num_response_cache_entries . should_equal 1 - HTTP.fetch uri2 . decode_as_text + HTTP.fetch uri2 . decode_as_text . should_succeed get_num_response_cache_entries . should_equal 2 group_builder.specify "Should work with secrets in the headers" pending=pending_has_url <| Test.with_retries <| @@ -455,9 +457,9 @@ add_specs suite_builder = headers1 = [Header.new "A-Header" secret1] headers2 = [Header.new "A-Header" secret2] - HTTP.fetch headers=headers1 uri . decode_as_text + HTTP.fetch headers=headers1 uri . decode_as_text . should_succeed get_num_response_cache_entries . should_equal 1 - HTTP.fetch headers=headers2 uri . decode_as_text + HTTP.fetch headers=headers2 uri . decode_as_text . should_succeed get_num_response_cache_entries . should_equal 2 group_builder.specify "Does not attempt to make room for the maximum file size when that is larger than the total cache size" pending=pending_has_url <| Test.with_retries <| diff --git a/test/Table_Tests/src/In_Memory/Split_Tokenize_Spec.enso b/test/Table_Tests/src/In_Memory/Split_Tokenize_Spec.enso index 2f365d7ce65c..bfdb332f3199 100644 --- a/test/Table_Tests/src/In_Memory/Split_Tokenize_Spec.enso +++ b/test/Table_Tests/src/In_Memory/Split_Tokenize_Spec.enso @@ -2,7 +2,7 @@ from Standard.Base import all import Standard.Test.Extensions -from Standard.Table import Table +from Standard.Table import Table, Value_Type from Standard.Table.Columns_To_Add import Columns_To_Add from Standard.Table.Errors import Invalid_Value_Type, Column_Count_Exceeded, Duplicate_Output_Column_Names, No_Such_Column from Standard.Test import all @@ -204,7 +204,7 @@ add_specs suite_builder = cols = [["foo", [0, 1, 2]], ["bar", ["abc", "cbdbef", "ghbijbu"]]] t = Table.new cols expected_rows = [[0, "a", "c", Nothing, Nothing], [1, "c", "d", "ef", Nothing], [2, "gh", "ij", "u", Nothing]] - expected = Table.from_rows ["foo", "bar 1", "bar 2", "bar 3", "bar 3"] expected_rows + expected = Table.from_rows ["foo", "bar 1", "bar 2", "bar 3", "bar 4"] expected_rows . cast "bar 4" Value_Type.Char t2 = t.split_to_columns "bar" "b" column_count=(Columns_To_Add.First 4) t2.should_equal expected t2.at "bar 3" . value_type . is_text . should_be_true @@ -213,7 +213,7 @@ add_specs suite_builder = cols = [["foo", [0, 1, 2]], ["bar", ["abc", "cbdbef", "ghbijbu"]]] t = Table.new cols expected_rows = [[0, "a", "c", Nothing, Nothing], [1, "c", "d", "ef", Nothing], [2, "gh", "ij", "u", Nothing]] - expected = Table.from_rows ["foo", "bar 1", "bar 2", "bar 3", "bar 3"] expected_rows + expected = Table.from_rows ["foo", "bar 1", "bar 2", "bar 3", "bar 4"] expected_rows . cast "bar 4" Value_Type.Char t2 = t.split_to_columns "bar" "b" column_count=4 t2.should_equal expected t2.at "bar 3" . value_type . is_text . should_be_true @@ -262,7 +262,7 @@ add_specs suite_builder = cols = [["foo", [0, 1, 2]], ["bar", ["ghbijbu", "cbdbef", "abc"]]] t = Table.new cols expected_rows = [[0, "gh", "ij", "u", Nothing], [1, "c", "d", "ef", Nothing], [2, "a", "c", Nothing, Nothing]] - expected = Table.from_rows ["foo", "bar 1", "bar 2", "bar 3", "bar 3"] expected_rows + expected = Table.from_rows ["foo", "bar 1", "bar 2", "bar 3", "bar 4"] expected_rows . cast "bar 4" Value_Type.Char t2 = t.split_to_columns "bar" "b" column_count=(Columns_To_Add.First 4) t2.should_equal expected t2.at "bar 3" . value_type . is_text . should_be_true @@ -271,7 +271,7 @@ add_specs suite_builder = cols = [["foo", [0, 1, 2]], ["bar", ["ghbijbu", "cbdbef", "abc"]]] t = Table.new cols expected_rows = [[0, "gh", "ij", "u", Nothing], [1, "c", "d", "ef", Nothing], [2, "a", "c", Nothing, Nothing]] - expected = Table.from_rows ["foo", "bar 1", "bar 2", "bar 3", "bar 3"] expected_rows + expected = Table.from_rows ["foo", "bar 1", "bar 2", "bar 3", "bar 4"] expected_rows . cast "bar 4" Value_Type.Char t2 = t.split_to_columns "bar" "b" column_count=4 t2.should_equal expected t2.at "bar 3" . value_type . is_text . should_be_true @@ -433,4 +433,3 @@ main filter=Nothing = suite = Test.build suite_builder-> add_specs suite_builder suite.run_with_filter filter - diff --git a/test/Table_Tests/src/Util.enso b/test/Table_Tests/src/Util.enso index b956fe84dd66..2976f150d6f9 100644 --- a/test/Table_Tests/src/Util.enso +++ b/test/Table_Tests/src/Util.enso @@ -1,4 +1,5 @@ from Standard.Base import all +import Standard.Base.Errors.Illegal_Argument.Illegal_Argument import Standard.Database.DB_Table.DB_Table import Standard.Database.DB_Column.DB_Column @@ -11,23 +12,27 @@ polyglot java import org.enso.base_test_helpers.FileSystemHelper Table.should_equal : Any -> Integer -> Any Table.should_equal self expected frames_to_skip=0 = loc = Meta.get_source_location 1+frames_to_skip + rhs_error_check expected Panic.catch Test_Failure_Error (table_should_equal_impl self expected loc) error-> - Test.fail error.payload.message + Test.fail error.payload.message Column.should_equal : Any -> Integer -> Any Column.should_equal self expected frames_to_skip=0 = loc = Meta.get_source_location 1+frames_to_skip + rhs_error_check expected Panic.catch Test_Failure_Error (column_should_equal_impl self expected loc) error-> Test.fail error.payload.message DB_Table.should_equal : DB_Table -> Integer -> Any DB_Table.should_equal self expected frames_to_skip=0 = + rhs_error_check expected t0 = self.read t1 = expected.read t0 . should_equal t1 frames_to_skip+1 DB_Column.should_equal : DB_Column -> Integer -> Any DB_Column.should_equal self expected frames_to_skip=0 = + rhs_error_check expected t0 = self.read t1 = expected.read t0 . should_equal t1 frames_to_skip+1 @@ -142,3 +147,14 @@ Error.should_have_relative_ordering self example = loc = Meta.get_source_location 1 _ = example Test.fail "Expected a vector but got a dataflow error "+self.catch.to_display_text+" (at "+loc+")." + + +## PRIVATE + A helper that ensures that the expected value provided in some of the Test + operations is not an error. + The left-hand side may be an error and that will cause a test failure. + But the right-hand side being an error is bad test design and should be fixed. +rhs_error_check that = + if that.is_error then + msg = "Dataflow error ("+that.to_display_text+") provided as expected value. Use `should_fail_with` or change the test."+ ' Error stack trace was:\n'+that.get_stack_trace_text + Panic.throw (Illegal_Argument.Error msg) diff --git a/test/Table_Tests/src/Util_Spec.enso b/test/Table_Tests/src/Util_Spec.enso index f8f02507d2a0..d51f3a6c8d9c 100644 --- a/test/Table_Tests/src/Util_Spec.enso +++ b/test/Table_Tests/src/Util_Spec.enso @@ -1,10 +1,59 @@ from Standard.Base import all + from Standard.Table import Column, Table -from project.Util import all + +from Standard.Database import all + from Standard.Test import all +from enso_dev.Test_Tests.Helpers import expect_test_failure + +from project.Util import all + add_specs suite_builder = - suite_builder.group "Column should_equal" group_builder-> + suite_builder.group "Table/Column.should_equal helpers" group_builder-> + group_builder.specify "should report correct location for Table" <| + r1 = expect_test_failure <| + (Table.new [["X", [1]]]) . should_equal (Table.new [["X", [2]]]) + r1.message.should_contain "Util_Spec.enso:17" + + r2 = expect_test_failure <| + (Table.new [["X", [1]]]) . should_equal (Table.new [["A", [1]]]) + r2.message.should_contain "Util_Spec.enso:21" + + group_builder.specify "should report correct location for Column" <| + r1 = expect_test_failure <| + Column.from_vector "X" [1] . should_equal (Column.from_vector "X" [2]) + r1.message.should_contain "Util_Spec.enso:26" + + r2 = expect_test_failure <| + Column.from_vector "X" [1] . should_equal (Column.from_vector "A" [1]) + r2.message.should_contain "Util_Spec.enso:30" + + group_builder.specify "should report correct location for DB_Table" <| + tables = DB_Tables.make + r1 = expect_test_failure <| + tables.t1 . should_equal tables.t2 + r1.message.should_contain "Util_Spec.enso:36" + + r2 = expect_test_failure <| + tables.t1 . should_equal tables.tA + r2.message.should_contain "Util_Spec.enso:40" + + group_builder.specify "should report correct location for DB_Column" <| + tables = DB_Tables.make + c1 = tables.t1.at "X" + c2 = tables.t2.at "X" + cA = tables.tA.at "A" + + r1 = expect_test_failure <| + c1 . should_equal c2 + r1.message.should_contain "Util_Spec.enso:50" + + r2 = expect_test_failure <| + c1 . should_equal cA + r2.message.should_contain "Util_Spec.enso:54" + group_builder.specify "Two Columns Are Equal" <| expected_column = Column.from_vector "Col" ["Quis", "custodiet", "ipsos", "custodes?"] actual_column = Column.from_vector "Col" ["Quis", "custodiet", "ipsos", "custodes?"] @@ -44,7 +93,7 @@ add_specs suite_builder = expected_column = Column.from_vector "Col" [1.0, 2.0, Number.nan] actual_column = Column.from_vector "Col" [1.0, 2.0, Number.nan] actual_column.should_equal expected_column - suite_builder.group "Table should_equal" group_builder-> + group_builder.specify "Two Tables Are Equal" <| expected_table = Table.new [Column.from_vector "Col1" ["Quis", "custodiet", "ipsos", "custodes?"], Column.from_vector "Col2" ["Who", "guards", "the", "guards?"]] actual_table = Table.new [Column.from_vector "Col1" ["Quis", "custodiet", "ipsos", "custodes?"], Column.from_vector "Col2" ["Who", "guards", "the", "guards?"]] @@ -75,6 +124,17 @@ add_specs suite_builder = res = Panic.recover Test_Failure_Error (table_should_equal_impl actual_table expected_table "LOCATION_PATH") res.catch.message.should_equal "Got a Table, but expected a 42 (at LOCATION_PATH)." +type DB_Tables + Value t1 t2 tA + + make = + connection = Database.connect ..In_Memory + t1 = (Table.new [["X", [1]]]).select_into_database_table connection "t1" + t2 = (Table.new [["X", [2]]]).select_into_database_table connection "t2" + tA = (Table.new [["A", [1]]]).select_into_database_table connection "tA" + DB_Tables.Value t1 t2 tA + + main filter=Nothing = suite = Test.build suite_builder-> add_specs suite_builder diff --git a/test/Test_Tests/package.yaml b/test/Test_Tests/package.yaml new file mode 100644 index 000000000000..2d35ea3d05a9 --- /dev/null +++ b/test/Test_Tests/package.yaml @@ -0,0 +1,7 @@ +name: Test_Tests +namespace: enso_dev +version: 0.0.1 +license: MIT +author: enso-dev@enso.org +maintainer: enso-dev@enso.org +prefer-local-libraries: true diff --git a/test/Test_Tests/src/Extensions_Spec.enso b/test/Test_Tests/src/Extensions_Spec.enso new file mode 100644 index 000000000000..afee91d94597 --- /dev/null +++ b/test/Test_Tests/src/Extensions_Spec.enso @@ -0,0 +1,86 @@ +from Standard.Base import all +import Standard.Base.Errors.Illegal_Argument.Illegal_Argument + +from Standard.Test import all + +from project.Helpers import expect_test_failure + +main filter=Nothing = + suite = Test.build suite_builder-> + add_specs suite_builder + suite.run_with_filter filter + +add_specs suite_builder = + suite_builder.group "should_equal extension method" group_builder-> + group_builder.specify "should report correct location for Text" <| + r1 = expect_test_failure <| + "a".should_equal "b" + r1.message.should_contain "Extensions_Spec.enso:17" + + group_builder.specify "should report correct location for numbers" <| + r1 = expect_test_failure <| + 1.should_equal 2 + r1.message.should_contain "Extensions_Spec.enso:22" + + r2 = expect_test_failure <| + 1.0 . should_equal 2 + r2.message.should_contain "Extensions_Spec.enso:26" + + r3 = expect_test_failure <| + 1.to_decimal . should_equal 2 + r3.message.should_contain "Extensions_Spec.enso:30" + + r4 = expect_test_failure <| + Number.nan.should_equal 2 + r4.message.should_contain "Extensions_Spec.enso:34" + + group_builder.specify "should report correct location for errors" <| + error = Error.throw (Illegal_Argument.Error "foo") + r1 = expect_test_failure <| + error.should_equal 10 + r1.message.should_contain "Extensions_Spec.enso:40" + + group_builder.specify "should panic if error is expected" <| + error = Error.throw (Illegal_Argument.Error "foo") + Test.expect_panic Illegal_Argument <| + 10.should_equal error + + suite_builder.group "should_not_equal extension method" group_builder-> + group_builder.specify "should report correct location" <| + r1 = expect_test_failure <| + 1.should_not_equal 1 + r1.message.should_contain "Extensions_Spec.enso:51" + + group_builder.specify "should report correct location for errors" <| + error = Error.throw (Illegal_Argument.Error "foo") + r1 = expect_test_failure <| + error.should_not_equal 1 + r1.message.should_contain "Extensions_Spec.enso:57" + + suite_builder.group "should_contain extension method" group_builder-> + group_builder.specify "should report correct location" <| + r1 = expect_test_failure <| + [1, 2].should_contain 3 + r1.message.should_contain "Extensions_Spec.enso:63" + + r2 = expect_test_failure <| + "abc".should_contain "d" + r2.message.should_contain "Extensions_Spec.enso:67" + + suite_builder.group "should_not_contain extension method" group_builder-> + group_builder.specify "should report correct location" <| + r1 = expect_test_failure <| + [1, 2].should_not_contain 2 + r1.message.should_contain "Extensions_Spec.enso:73" + + suite_builder.group "should_start_with extension method" group_builder-> + group_builder.specify "should report correct location" <| + r1 = expect_test_failure <| + "abc".should_start_with "d" + r1.message.should_contain "Extensions_Spec.enso:79" + + suite_builder.group "should_end_with extension method" group_builder-> + group_builder.specify "should report correct location" <| + r1 = expect_test_failure <| + "abc".should_end_with "d" + r1.message.should_contain "Extensions_Spec.enso:85" diff --git a/test/Test_Tests/src/Helpers.enso b/test/Test_Tests/src/Helpers.enso new file mode 100644 index 000000000000..beaa75f34fcb --- /dev/null +++ b/test/Test_Tests/src/Helpers.enso @@ -0,0 +1,17 @@ +from Standard.Base import all + +import Standard.Test.Spec_Result.Spec_Result +from Standard.Test import Test + +## Expects the inner action to report a test failure exception and returns its payload. +expect_test_failure ~action -> Spec_Result = + loc = Meta.get_source_location 1 + handle_panic caught_panic = + result = caught_panic.payload + case result of + Spec_Result.Failure _ _ -> result + _ -> Test.fail "Expected test failure, but "+result.to_text+" was raised as error." + + Panic.catch Spec_Result handler=handle_panic <| + action + Test.fail "Expected the inner action to fail, but there was no failure (at "+loc+")." diff --git a/test/Test_Tests/src/Main.enso b/test/Test_Tests/src/Main.enso new file mode 100644 index 000000000000..a4542f7d522b --- /dev/null +++ b/test/Test_Tests/src/Main.enso @@ -0,0 +1,13 @@ +from Standard.Base import all + +from Standard.Test import Test + +import project.Extensions_Spec + +add_specs suite_builder = + Extensions_Spec.add_specs suite_builder + +main filter=Nothing = + suite = Test.build suite_builder-> + add_specs suite_builder + suite.run_with_filter filter