Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make test failure source locations lazy #12260

Open
wants to merge 69 commits into
base: develop
Choose a base branch
from

Conversation

GregoryTravis
Copy link
Contributor

@GregoryTravis GregoryTravis commented Feb 11, 2025

Replaces eager calls to Test.get_source_location to lazy calls to Frame_Hider.get_top_stack_frame, resulting in a 2x speedup for Base_Tests.

Test utilities wrap their contents in Frame_Hider.hide, and wrap callbacks in Frame_Hider.unhide. These marker frames are used to skip test utility frames to find the real user-level location of the test failure.

The frames_to_skip parameter is no longer needed, since the number of frames to skip is computed dynamically on test failure.

Checklist

Please ensure that the following checklist has been satisfied before submitting the PR:

  • The documentation has been updated, if necessary.
  • Screenshots/screencasts have been attached, if there are any visual changes. For interactive or animated visual changes, a screencast is preferred.
  • All code follows the
    Scala,
    Java,
    TypeScript,
    and
    Rust
    style guides. In case you are using a language not listed above, follow the Rust style guide.
  • Unit tests have been written where possible.
  • If meaningful changes were made to logic or tests affecting Enso Cloud integration in the libraries,
    or the Snowflake database integration, a run of the Extra Tests has been scheduled.
    • If applicable, it is suggested to paste a link to a successful run of the Extra Tests.

from Standard.Base import all
import Standard.Base.Runtime.Ref.Ref

## PRIVATE
Copy link
Member

@JaroslavTulach JaroslavTulach Feb 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this module be private? Probably it couldn't be.

@@ -27,6 +27,8 @@ export project.Extensions.should_succeed

export project.Faker.Faker

export project.Frame_Hider.Frame_Hider
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ech, probably the Frame_Hider couldn't be private.

test_utility test_predicate x =
...
Frame_Hider.unhide_1 test_predicate x
unhide_1 f x =
Copy link
Member

@JaroslavTulach JaroslavTulach Feb 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, this is so non-FPish!

I mean:

  • Enso does support currying
  • this kind constructs (Function1, Function2, etc.) are more likely to find in language that don't support currying (Java, Scala, Kotlin)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course I wanted to curry it, but it seems not to work; perhaps there is something I'm doing wrong.

Demonstrated below: for a two-argument function, only the version with two explicit arguments has wrap in its call stack:

foo a b =
    st = Runtime.get_stack_trace
    st.map IO.println
    IO.println "===="
    a + b

wrap_0 f =
    f

wrap_1 f x =
    f x

wrap_2 f x y =
    f x y

main =
    sp <| foo 1 2
    sp <| wrap_0 foo 1 2
    sp <| wrap_1 foo 1 2
    sp <| wrap_2 foo 1 2

Results:

M:dee wip/gmt/12215-slow-stack-frame$ g
(Stack_Trace_Element.Value 'dbbd.foo' (Source_Location dbbd.enso:126:10-32))
(Stack_Trace_Element.Value 'dbbd.main<arg-1>' (Source_Location dbbd.enso:141:11-17))
(Stack_Trace_Element.Value 'dbbd.main' (Source_Location dbbd.enso:141:5-17))
====
3
(Stack_Trace_Element.Value 'dbbd.foo' (Source_Location dbbd.enso:126:10-32))
(Stack_Trace_Element.Value 'dbbd.main<arg-1>' (Source_Location dbbd.enso:142:11-24))
(Stack_Trace_Element.Value 'dbbd.main' (Source_Location dbbd.enso:142:5-24))
====
3
(Stack_Trace_Element.Value 'dbbd.foo' (Source_Location dbbd.enso:126:10-32))
(Stack_Trace_Element.Value 'dbbd.main<arg-1>' (Source_Location dbbd.enso:143:11-24))
(Stack_Trace_Element.Value 'dbbd.main' (Source_Location dbbd.enso:143:5-24))
====
3
(Stack_Trace_Element.Value 'dbbd.foo' (Source_Location dbbd.enso:126:10-32))
(Stack_Trace_Element.Value 'dbbd.wrap_2' (Source_Location dbbd.enso:138:5-9))
(Stack_Trace_Element.Value 'dbbd.main<arg-1>' (Source_Location dbbd.enso:144:11-24))
(Stack_Trace_Element.Value 'dbbd.main' (Source_Location dbbd.enso:144:5-24))
====
3

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to this unfortunate code, I have had to do this in some tests that use callbacks:

-            tester = expect_column_names ["A", "1980-01-01", "Column 1", "5.3", "True"]
+            tester t = expect_column_names ["A", "1980-01-01", "Column 1", "5.3", "True"] t
             problems = [Duplicate_Output_Column_Names.Error ["A", "A", "A"]]
             Problems.test_problem_handling action problems tester

(tester is invoked within an unhide call in test_problem_handling.)

It makes me sad, but it was necessary.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following program:

from Standard.Base import all

sp = IO.println

foo a b =
    st = Runtime.get_stack_trace
    st.map IO.println
    IO.println "===="
    a + b

wrap_l ~a =
    a

main =
    sp <| foo 1 2
    sp <| wrap_l <| foo 1 2
    sp <| wrap_l <| foo 1 2
    sp <| wrap_l <| foo 1 2

does include wrap_l in all the invocations:

(Stack_Trace_Element.Value 'g.foo' (Source_Location g.enso:7:10-32))
(Stack_Trace_Element.Value 'g.main<arg-1>' (Source_Location g.enso:16:11-17))
(Stack_Trace_Element.Value 'g.main' (Source_Location g.enso:16:5-17))
====
3
(Stack_Trace_Element.Value 'g.foo' (Source_Location g.enso:7:10-32))
(Stack_Trace_Element.Value 'g.main<arg-1>' (Source_Location g.enso:17:21-27))
(Stack_Trace_Element.Value 'g.wrap_l' (Source_Location g.enso:13:5-5))
(Stack_Trace_Element.Value 'g.main<arg-1>' (Source_Location g.enso:17:11-27))
(Stack_Trace_Element.Value 'g.main' (Source_Location g.enso:17:5-27))
====
3
(Stack_Trace_Element.Value 'g.foo' (Source_Location g.enso:7:10-32))
(Stack_Trace_Element.Value 'g.main<arg-1>' (Source_Location g.enso:18:21-27))
(Stack_Trace_Element.Value 'g.wrap_l' (Source_Location g.enso:13:5-5))
(Stack_Trace_Element.Value 'g.main<arg-1>' (Source_Location g.enso:18:11-27))
(Stack_Trace_Element.Value 'g.main' (Source_Location g.enso:18:5-27))
====
3
(Stack_Trace_Element.Value 'g.foo' (Source_Location g.enso:7:10-32))
(Stack_Trace_Element.Value 'g.main<arg-1>' (Source_Location g.enso:19:21-27))
(Stack_Trace_Element.Value 'g.wrap_l' (Source_Location g.enso:13:5-5))
(Stack_Trace_Element.Value 'g.main<arg-1>' (Source_Location g.enso:19:11-27))
(Stack_Trace_Element.Value 'g.main' (Source_Location g.enso:19:5-27))
====
3

The only difference is that one has to use wrap_l <| instead of just wrap_l.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But for a callback, wrap_l is not included, as seen below. This is why I added unhide_1 and unhide_2 for -- the case when user-supplied callbacks are used to implement the value testers.

    sp <| (wrap_l <| foo 1) 2
(Stack_Trace_Element.Value 'jt.foo' (Source_Location jt.enso:6:10-32))
(Stack_Trace_Element.Value 'jt.main<arg-1>' (Source_Location jt.enso:18:11-29))
(Stack_Trace_Element.Value 'jt.main' (Source_Location jt.enso:18:5-29))
====
3

user-supplied callback, depending on the number of arguments. If one test
utility forwards a callback to another test utility, the callback should be
wrapped at the forward site as well.
type Frame_Hider
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is Frame_Hider a type, when it doesn't have a constructor? Putting its static methods as module methods would work as well.

Copy link
Contributor Author

@GregoryTravis GregoryTravis Feb 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strangely, I am getting this error when I try this, but only in some tests -- other tests run correctly.

    - [FAILED] should work as shown in the doc examples [472ms]
        Reason: An unexpected panic was thrown: (Syntax_Error.Error 'Type for ResolvedType(Concrete(CompilerContext.Module{module=Module[Standard.Test.Frame_Hider]}),Type(Frame_Hider,Seq(),Seq(),false)) is null')
        at <enso> Util.expect_column_names<arg-0>(/Users/gmt/dev/enso/enso/test/Table_Tests/src/Common_Table_Operations/Util.enso:10:54-64)
        at <enso> Util.expect_column_names(/Users/gmt/dev/enso/enso/test/Table_Tests/src/Common_Table_Operations/Util.enso:10-13)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected panic is an suspicious outcome - consider reporting it as a separate issue. If there is supposed to be an error, it shouldn't be unexpected panic. But yeah, such situations happen when we tease the compiler with unusual coding approaches.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After this PR, I'll create a task to remove the type and if it is still failing I'll report the bug.


## PRIVATE

Frame_Hider is used in test utilities to hide uninteresting stack frames from
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name Frame_Hider feels "too strong" to me. The verb "to hide" feel negative. But we want users of our APIs to feel good!

Do you have some prior art for selecting such a name?

When thinking about an alternative a phase shadow stack come to my mind:

The shadow stack itself is a second, separate stack that "shadows" the program call stack.

Maybe "shadow stack" would be misleading as well, but "to shadow" certainly feels more positive than "to hide" ;-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if this is shadowing -- that implies a duplicate / parallel stack. What we are really doing is removing some frames from the stack before picking the top frame.

I originally called this Omitter, maybe that's less hostile? Frame_Omitter perhaps?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Frame_Tidy? Frame_Grooming? Frame_Refining? Frame_Cleansing?

Copy link
Member

@radeusgd radeusgd Feb 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest looking at all the possible names, Frame_Hider still seems most clear (IMHO) about what it is doing, I don't really share the feeling that it is 'negative' in any way...

It is an internal API anyway - mostly for library developers so the naming can probably be a bit less stringent.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Internal API would be private. E.g. no, this is not an internal API.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it is less public than GUI-facing things.

It is marked as PRIVATE and meant to be used by library developers (advanced users) not by every user. There is some difference.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is an API?

Everything somebody else can depend on!

The PRIVATE comment is mostly irrelevant in deciding whether someone can depend on the Frame_Hider name or not. It is just a comment.

There is some difference.

Even users of the IDE can depend and use the Frame_Hider. The PRIVATE comment just makes it harder for them to find it. But we have no way to know whether or not they found it!

The only way to make sure someone cannot (easily) depend on an element in a Standard.Test library is to use private keyword.

Once a check for Standard.Test API stability is in, the "difference" will be clear. PRIVATE comment is unimportant from API perspective.

@@ -703,8 +703,8 @@ add_specs suite_builder =
actual.column_names . should_equal ["col0", "col_between", "col1"]

suite_builder.group "Use First Row As Names" group_builder->
expect_column_names names table =
table.columns . map .name . should_equal names frames_to_skip=2
expect_column_names names table = Frame_Hider.hide <|
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, removing frames_to_skip=2 is certainly an improvement.

Copy link
Member

@JaroslavTulach JaroslavTulach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opinions inline...


## PRIVATE

Frame_Hider is used in test utilities to hide uninteresting stack frames from
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Frame_Tidy? Frame_Grooming? Frame_Refining? Frame_Cleansing?

user-supplied callback, depending on the number of arguments. If one test
utility forwards a callback to another test utility, the callback should be
wrapped at the forward site as well.
type Frame_Hider
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected panic is an suspicious outcome - consider reporting it as a separate issue. If there is supposed to be an error, it shouldn't be unexpected panic. But yeah, such situations happen when we tease the compiler with unusual coding approaches.

test_utility test_predicate x =
...
Frame_Hider.unhide_1 test_predicate x
unhide_1 f x =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following program:

from Standard.Base import all

sp = IO.println

foo a b =
    st = Runtime.get_stack_trace
    st.map IO.println
    IO.println "===="
    a + b

wrap_l ~a =
    a

main =
    sp <| foo 1 2
    sp <| wrap_l <| foo 1 2
    sp <| wrap_l <| foo 1 2
    sp <| wrap_l <| foo 1 2

does include wrap_l in all the invocations:

(Stack_Trace_Element.Value 'g.foo' (Source_Location g.enso:7:10-32))
(Stack_Trace_Element.Value 'g.main<arg-1>' (Source_Location g.enso:16:11-17))
(Stack_Trace_Element.Value 'g.main' (Source_Location g.enso:16:5-17))
====
3
(Stack_Trace_Element.Value 'g.foo' (Source_Location g.enso:7:10-32))
(Stack_Trace_Element.Value 'g.main<arg-1>' (Source_Location g.enso:17:21-27))
(Stack_Trace_Element.Value 'g.wrap_l' (Source_Location g.enso:13:5-5))
(Stack_Trace_Element.Value 'g.main<arg-1>' (Source_Location g.enso:17:11-27))
(Stack_Trace_Element.Value 'g.main' (Source_Location g.enso:17:5-27))
====
3
(Stack_Trace_Element.Value 'g.foo' (Source_Location g.enso:7:10-32))
(Stack_Trace_Element.Value 'g.main<arg-1>' (Source_Location g.enso:18:21-27))
(Stack_Trace_Element.Value 'g.wrap_l' (Source_Location g.enso:13:5-5))
(Stack_Trace_Element.Value 'g.main<arg-1>' (Source_Location g.enso:18:11-27))
(Stack_Trace_Element.Value 'g.main' (Source_Location g.enso:18:5-27))
====
3
(Stack_Trace_Element.Value 'g.foo' (Source_Location g.enso:7:10-32))
(Stack_Trace_Element.Value 'g.main<arg-1>' (Source_Location g.enso:19:21-27))
(Stack_Trace_Element.Value 'g.wrap_l' (Source_Location g.enso:13:5-5))
(Stack_Trace_Element.Value 'g.main<arg-1>' (Source_Location g.enso:19:11-27))
(Stack_Trace_Element.Value 'g.main' (Source_Location g.enso:19:5-27))
====
3

The only difference is that one has to use wrap_l <| instead of just wrap_l.

@JaroslavTulach
Copy link
Member

Every time I am executing runEngineDistribution --run test/Base_Tests and waiting for more than five minutes, I am praying for this PR to be finally integrated. Is there any chance we will get the execution of test/Base_Tests below three minutes this week? What's blocking the integration?

@GregoryTravis
Copy link
Contributor Author

Every time I am executing runEngineDistribution --run test/Base_Tests and waiting for more than five minutes, I am praying for this PR to be finally integrated. Is there any chance we will get the execution of test/Base_Tests below three minutes this week? What's blocking the integration?

Just waiting on an engine approval.

Copy link
Member

@JaroslavTulach JaroslavTulach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • There may be things I don't fully adore present in this PR
  • I have expressed my opinion in a previous review
  • However none of my opinions is critical enough to prevent the integration
  • If you are OK maintaining the API, go on and integrate
  • Approving from engine point of view

@GregoryTravis GregoryTravis added CI: Ready to merge This PR is eligible for automatic merge CI: Clean build required CI runners will be cleaned before and after this PR is built. labels Feb 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CI: Clean build required CI runners will be cleaned before and after this PR is built. CI: No changelog needed Do not require a changelog entry for this PR. CI: Ready to merge This PR is eligible for automatic merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants