Skip to content

Conversation

@Keno
Copy link
Member

@Keno Keno commented Aug 24, 2025

Test has Test.record_passes() which gets OncePerProcess initialized to the value of ENV["JULIA_TEST_RECORD_PASSES"]. However, in another palce the test suite once to override this value for its scope, and does this by setting the environment variable, relying on OncePerProcess picking this up. However, this is not at all semantically guaranteed and of course fails if anybody happened to have a testset before (which is possible, because this is used by Base.runtests). It would be much better if we could use a ScopedValue for this like the other Test setting state (after #53462). However, there currently isn't any way to once-initialize the ScopedValue default. This PR aims to fix that by letting the scoped value default be an evaluated callback. The original ScopedValue struct is renamed LazyScopedValue, but the ScopedValue name is retained with the same API as an alias to a LazyScopedValue that just uses Returns(default) as its initializer. It might be a bit weird that the default could return a different value at every time, but I think a reasonable way to think about this is that the default chains to some dynamic state which is expressed in this callback.

@Keno Keno requested a review from vchuravy August 24, 2025 01:34
@Keno Keno force-pushed the kf/lazyscopedvalue branch 2 times, most recently from 7d9026b to b879797 Compare August 24, 2025 01:45
@Keno
Copy link
Member Author

Keno commented Aug 24, 2025

Note to self, the reason this likely passed where #53462 did not is that we're not propagating JULIA_TEST_RECORD_PASSES down into the exec anymore. We should either restore the withenv or preferably explicitly pass it down in the exec.

ScopedValue{T}(val) where T = new{T}(true, val)
ScopedValue(val::T) where T = new{T}(true, val)
end
const ScopedValue{T} = Union{LazyScopedValue{T, Returns{T}}, LazyScopedValue{T, typeof(no_default)}}
Copy link
Contributor

@Seelengrab Seelengrab Aug 24, 2025

Choose a reason for hiding this comment

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

Isn't this considered breaking, since ScopedValue{Foo} is no longer concrete? Why not create a new abstract type and make ScopedValue subtype that, thereby making handling of LazyScopedValue opt-in?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't know that is technically breaking - the interface is the same, but yes it is a bit overly clever. I somewhat liked the uniformity of only having one object and providing the existing behavior as an API, but we can switch to separate struts.

Copy link
Member

@vchuravy vchuravy left a comment

Choose a reason for hiding this comment

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

One of the invariants in my head for scoped values is that they are "constant within the same context", with the current lazy version it is possible to observe multiple separate states within the same context, by virtue of the user doing something like ()->rand() as the get_default.

So I think we should do the struct split and make the lazy version use OncePerProcess explicity instead of just recommending it's use. I don't see a use for OncePerThread, and OncePerTask screams like we should have a TaskLocalValue.

@Keno
Copy link
Member Author

Keno commented Aug 25, 2025

Alright, worth the old college try. I'm ok with forcing OncePerProcess here, although I still want the constructor to take that object explicitly, so we can decide to expand in the future if we want to.

@Keno Keno force-pushed the kf/lazyscopedvalue branch from b879797 to 86c2b15 Compare August 25, 2025 12:02
@Keno
Copy link
Member Author

Keno commented Aug 25, 2025

Updated as discussed (Claude helped).

@Keno
Copy link
Member Author

Keno commented Aug 25, 2025

Note to self, the reason this likely passed where #53462 did not is that we're not propagating JULIA_TEST_RECORD_PASSES down into the exec anymore. We should either restore the withenv or preferably explicitly pass it down in the exec.

Actually, there's no point in doing this, because we don't store those in results.json anyway. We could consider serializing the results from the _exec and storing them in the parent test set for this purpose, but that is a separate change (cc @IanButterworth on whether this is desirable).

@IanButterworth
Copy link
Member

I don't recall if I was aware that the child processes weren't recording. I like the idea of optionally passing testsets up from the child processes, but yeah I think that can be a separate issue.

Copy link
Member

@vchuravy vchuravy left a comment

Choose a reason for hiding this comment

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

Looks good to me! Could you also add a few tests to https://github.com/JuliaLang/julia/blob/master/test/scopedvalues.jl

Base automatically changed from sb/test-scope to master August 26, 2025 15:06
LazyScopedValue requires Julia 1.13+.
"""
mutable struct LazyScopedValue{T} <: AbstractScopedValue{T}
const getdefault::OncePerProcess{T}
Copy link
Member

@vtjnash vtjnash Aug 29, 2025

Choose a reason for hiding this comment

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

It it worth abstracting over what OncePer is used here (just use F <: Function)? We could potentially consider also using Returns{T}(default) and throw_key_error() = throw(KeyError(self)) to re-combine LazyScopedValue/ScopedValue, though I don't know if that that is important. There is not (and cannot be) a convert method for OncePerProcess{S} to OncePerProcess{T}, so it could be a bit tedious sometimes if users are trying to write the type parameters explicitly while also using OncePerProcess's auto-interferred parameter ability (though users really shouldn't be writing the LazyScopedValue apply_type explicitly)

Copy link
Member Author

Choose a reason for hiding this comment

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

That's what this PR did originally, but @vchuravy didn't like it. Regardless, it is always possible to relax the implementation in that direction if it becomes apparent that it is useful, so this is the conservative version.

Copy link
Member

Choose a reason for hiding this comment

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

Sure, makes sense. I think flexibility would make more sense with alternatives like OncePerDepot than the ones Valentin mentioned or OncePerPeriod (aka debounce or throttle). But as you say, this is conservatively what is actually needed now

Copy link
Member

@vtjnash vtjnash left a comment

Choose a reason for hiding this comment

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

SGTM

Test has `Test.record_passes()` which gets `OncePerProcess` initialized
to the value of `ENV["JULIA_TEST_RECORD_PASSES"]`. However, in another
palce the test suite once to override this value for its scope, and does
this by setting the environment variable, relying on OncePerProcess
picking this up. However, this is not at all semantically guaranteed
and of course fails if anybody happened to have a testset before
(which is possible, because this is used by Base.runtests). It would
be much better if we could use a ScopedValue for this like the other
`Test` setting state (after #53462). However, there currently isn't
any way to once-initialize the ScopedValue default. This PR aims to
fix that by letting the scoped value default be an evaluated callback.
@Keno Keno force-pushed the kf/lazyscopedvalue branch from e56ac8a to 04cf53f Compare August 30, 2025 08:39
@Keno Keno added the merge me PR is reviewed. Merge when all tests are passing label Aug 30, 2025
implementation is available from the package ScopedValues.jl.
"""
mutable struct ScopedValue{T}
# NOTE this struct must be defined as mutable one since it's used as a key of
Copy link
Contributor

@Seelengrab Seelengrab Aug 30, 2025

Choose a reason for hiding this comment

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

Should these commente be preserved? They seem like important invariants.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't care much either way - and I probably would have kept them if not for the detour that deleted this, but it's also just how julia works, so I don't think they're really all that valuable.

Copy link
Contributor

Choose a reason for hiding this comment

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

Well they were intentionally added by @aviatesk in #52372, so that's why I'm asking :)

Copy link
Member

@IanButterworth IanButterworth Aug 30, 2025

Choose a reason for hiding this comment

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

Adding the comment back seems reasonable because a mutable struct with all const fields seems like something that should be immutable apart from this inobvious reason.

Copy link
Member

Choose a reason for hiding this comment

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

Added back (since you said you didn't mind either way @Keno)

@IanButterworth IanButterworth removed the merge me PR is reviewed. Merge when all tests are passing label Aug 30, 2025
@IanButterworth IanButterworth added the merge me PR is reviewed. Merge when all tests are passing label Aug 31, 2025
@IanButterworth IanButterworth merged commit 5fb39c6 into master Sep 1, 2025
7 checks passed
@IanButterworth IanButterworth deleted the kf/lazyscopedvalue branch September 1, 2025 10:45
@PallHaraldsson
Copy link
Contributor

Should packages be using AbstractScopedValue (and/or LazyScopedValue)? I.e. should it be backported to 1.12 (even 1.10?), even just the abstract type.

I'm not too up-to-speed on ScopedValues, but just looked into, and it seems at least one package should use AbstractScopedValue here:
https://github.com/vchuravy/ScopedValues.jl/blob/v1.5.0/src/ScopedValues.jl#L121

https://juliahub.com/ui/Search?type=code&q=::ScopedValue

@nsajko nsajko removed the merge me PR is reviewed. Merge when all tests are passing label Sep 1, 2025
@Seelengrab
Copy link
Contributor

New feature are not backported.

@PallHaraldsson
Copy link
Contributor

AbstractScopedValue is not a feature (or very borderline). I was thinking should most abstract types be in Base (at least for subtypes of Base)? Is it helpful to have it, even if LazyScopedValue isn't yet defined by a certain Julia version, to enable package system to use the abstract type, and help for future compatibility. Would it otherwise need to be in Compat.jl?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants