ST-0009: Attachments

Hello Swift community,

The review of ST-0009 "Attachments" begins now and runs through Tuesday April 8, 2025. The proposal is available here:

swift-evolution/proposals/testing/0009-attachments.md at main · swiftlang/swift-evolution · GitHub

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager. When emailing the review manager directly, please keep the proposal link at the top of the message.

Trying it out

To try this feature out, add a dependency to the main branch of swift-testing to your project:

...
dependencies: [
   .package(url: "https://github.com/swiftlang/swift-testing.git", branch: "main"),
],
...

and to your test target:

.testTarget(...
    ...,
    dependencies: [
       ...,
       .product(name: "Testing", package: "swift-testing")
    ]

Finally, import Swift Testing using @_spi(Experimental)
So, instead of import Testing, use @_spi(Experimental) import Testing instead.

What goes into a review?

The goal of the review process is to improve the proposal under review
through constructive criticism and, eventually, determine the direction of
Swift. When writing your review, here are some questions you might want to
answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a
    change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar
    feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick
    reading, or an in-depth study?

More information about the Swift evolution process is available at

swift-evolution/process.md at main · swiftlang/swift-evolution · GitHub

Thank you for contributing to Swift!

Rachel Brindle

Review Manager

8 Likes

+1 like it very much.

I have nothing to add but my hope that this paves the way for UI Tests in Xcode to drop XCTest and use Swift Testing instead :wink:

2 Likes

Generally a +1 from me on the proposal. I have used various forms of testing in the past that would have benefited from attachments such as snapshot tests or unit tests that wanted to attach the logs.

We will also introduce a cross-import overlay with Foundation—that is, a tertiary module that is automatically imported when a test target imports both Foundation and Swift Testing—that includes additional conformances for Foundation types such as Data and URL and provides support for attaching values that also conform to Encodable or NSSecureCoding .

This section is the only thing that stood out to me. My understanding is that cross-import overlays are not an accepted proposal and currently just a pitch. While the functionality is present in the compiler itself they aren't exposed in any way through Swift PM. I'm personally a bit worried for a proposal to use an unreviewed feature. Additionally, what impact does this have when consuming Testing directly from the package or hypothetically moving Testing out of the toolchain in the future again and making it a package dependency?

Quotes are out-of-order:

This statement is partially incorrect. We worked with @beccadax to enable the feature when building test targets via Swift Package Manager (Xcode already implicitly supports cross-import overlays.) And when using Apple's fork of the Swift toolchain, cross-import overlays are always enabled. So this "just works" when you use Swift Testing from the toolchain. (modulo some incompatibilities with @_spi that will be resolved if/when the interface is approved and promoted to full API.)

If you add Swift Testing as a package dependency instead of using the toolchain's copy, you will need to manually import the cross-import overlay's module to use it. Resolving that constraint is beyond the scope of this proposal, as is a discussion about moving Swift Testing out of the toolchain. :slightly_smiling_face:

Edit: If we do move out of the toolchain in the future and are distributed as a package, we won't need a cross-import overlay anymore. We'll be able to just move the code directly into the main Testing module and guard any uses of Foundation with a package trait check.

That's understandable. In general, the cross-import overlay feature is not ready to be formally reviewed via the Swift Evolution process (although I'll have to defer to Becca on any specifics here.) However, because of Swift Testing's position in the overall stack, we can't publicly link to any modules other than the standard library (and underscored extensions thereto like _Concurrency). In particular, if we add Foundation to the public interface of the Testing module, Foundation won't be able to write unit tests using Swift Testing.

This problem does not affect the broader ecosystem (including third-party testing libraries) because, in general, components outside the toolchain can freely link to those in the toolchain (or to other packages).

2 Likes

Thanks for the detailed reply. I agree that this is not blocking the proposal and your suggestion to use package traits if the package is consumed via the package manager sounds great.

2 Likes

This feels possibly underspecified... Should an implementing type err on the side of providing an underestimate or overestimate? To borrow a word from a former president, what are the implications of "misunderestimating" or "misoverestimating"?

I'd suggest using a different term than "container" here—I don't believe it's been used anywhere in the Swift project as a synonym for a wrapper, and elsewhere, we've used it as a possible term for the "next-generation" collection hierarchy. Would be non-ideal if this ends up being a non-container "container."

Additional nit: Seems unfortunate that the associated type here, which is meant to be a type that doesn't and can't conform to Attachable (unless I'm misunderstanding?), is named AttachableValue. I get that the point is that it makes the value of a non-Attachable type attachable-by-wrapping, but the effect of naming the associated type this way is that now your non-Attachable type is aliased as AttachableValue.

The documentation for this property covers this question:

The testing library uses this property to determine if an attachment should be held in memory or should be immediately persisted to storage. Larger attachments are more likely to be persisted, but the algorithm the testing library uses is an implementation detail and is subject to change.

The value of this property is approximately equal to the number of bytes that will actually be needed, or nil if the value cannot be computed efficiently. The default implementation of this property returns nil.

So if the implementation can't get a reasonably accurate estimate (either over- or under-), it should just return nil.

I'm open to suggestions!

I'm constrained here by needing to name the associated type such that it does not conflict with other associated types from other (common) protocols. Again, open to suggestions.

But what is "reasonable accuracy"? Besides absolute exactitude, how are users not privy to implementation detail to know what is "close enough" here?

The name with precedent for a wrapper would be...Wrapper (cf. property wrapper, type wrapper (pitched), etc.), and I'd suggest that here or something like it. The key is that it should be a term which has precedent for denoting one thing inside another thing, and that we're sure won't someday end up having usages (or we'd wish had usages) to mean many things inside another thing.

I'm not sure I understand this constraint: associated types should have meaningful names, certainly, and ideally not vacuous ones, but they needn't be prefixed so that they're distinguishable from the associated types of unrelated protocols not in the hierarchy.

So, if we go with the Wrapper terminology, then AttachableWrapper<Wrapped> or AttachableWrapper<WrappedValue> would work here. (While we're at it, I'd suggest Attachment<AttachedValue> would be more meaningful than Attachment<AttachableValue>: because here it's not just attach-able, it's the type of what's actually attached.)

Adopting such changes would immediately make the following extension more readable:

// before:
extension Attachment where AttachableValue: AttachableContainer & ~Copyable {
  public var attachableValue: AttachableValue.AttachableValue { get }
}

// after:
extension Attachment where AttachedValue: AttachableWrapper & ~Copyable {
  public var attachedValue: AttachedValue.WrappedValue { get }
}

Although: now that I am re-reading the documentation for this particular API, is it overloading the attach[able]Value property on type? If so, I think that would be contrary to the core team's commitment never to overload on return type within the Swift project.

1 Like

I would compare this property, and how it would be used, to Sequence.underestimatedCount.

I'm not sure what you mean. Associated types exist in a flat namespace (within a given type). We couldn't use RawValue, as an example, without potentially conflicting with the associated type in RawRepresentable.

Although the conforming types we're including in Swift Testing don't also conform to RawRepresentable, it would not be very nice of us to prevent conformance to one protocol or the other due to mismatched requirements.

In general, the developer is not expected to ever have to type this associated type's name as it can be inferred from context anyway:

struct IntContainer: AttachableContainer {
  var attachableValue: Int // type is inferred, never to be spoken of again

  // ...
}

The name AttachableWrapper is probably fine, but the associated type would likely still need to be AttachableWrappedValue or similar to avoid conflicts. I'll raise this question with the workgroup.

I will point out that the Swift project already reuses terminology to refer to different concepts. For example, Swift Testing introduced traits in Swift 6.0, and now Swift Package Manager has traits in Swift 6.1, but the features are completely unrelated.

Counterpoint: it's not attached to anything yet at the point you're dealing with an instance of Attachment, and once you attach it, your instance of Attachment is consumed.

The type of the property depends on the type of the attachable value. If it conforms to AttachableContainer, the property's type is that of the underlying value; otherwise it's that of the attachable value itself. Our expectation here is that a caller that needs to access this property is looking for the logical attachable value, not necessarily the physical attachable value.

Compare the effects LazySequence has on the transformative functions of a sequence when you add it to the mix. [1, 2, 3].map { ... } produces an array, but [1, 2, 3].lazy.map { ... } produces a LazyMapSequence (or an array, since the function is overloaded by return type. :grimacing:) This isn't exactly the same scenario of course, but I see it as conceptually similar.

Ah, but underestimatedCount doesn't share your proposed semantics: it is not required to be close to the actual count, but it is absolutely required to be underestimated—indeed, for many types where the count is not easy to estimate, one should estimate either 0 or (if known non-empty) 1, even if one can be quite sure there are many, many values.

Hence why I ask: with your proposed API, should one err towards overestimating or underestimating, and how is a user to know when they're potentially "close enough" versus too off-the-mark and instead should return nil?

Put another way: should instead your API actually be designed more like underestimatedCount, such that it doesn't rely on a notion of being "close enough"?

The scenario you articulate applies equally to all protocol requirements, not just associated types: for example, the Collection protocol has count, but that might interfere with what a non-collection protocol would reasonably want to call count—yet we don't prefix everything in Collection (for example, collectionCount).

In general, we have said that users 'discover' conformances by finding that their concrete type happens to align with protocol requirements. It would be unusual indeed for a type to be discovered to have the correct semantics for conformance to two distinct protocols but be frustrated because both have semantically different WrappedValue associated types, assuming each is aptly named: that would mean that a single type serves as a sort of wrapper for two different values of two different types. This doesn't sound to me like a scenario that would require special accommodation as an exception to our usual approach of not pessimizing protocol requirements with prefixes.

(Your argument would, on the other hand, come to the fore in the scenario of Attachable itself, which seems like a protocol to which all or nearly all fundamental types are expected to conform as table stakes; but Attachable[Container|Wrapper] isn't such a protocol.)

Not—I hope you would agree—a positive direction for the ecosystem to have the same term used for unrelated features. Where it can be reasonably foreseen, such as with "container," it would be wise for us not to dig ourselves into that hole again.

That makes sense in context. However, in that case, there's another convention which I think we ought to observe here: where we name a property with a lowercase version of a type, it should be of that type (see, for example, span being of type Span). If not in alignment, then the property should be otherwise named: that is, it should not be called attachableValue if there's an associated type AttachableValue and the property does not (always) give you an instance of the type.

3 Likes

This value is made available so that tools can look at an attachable value in an event stream and decide whether or not they should be persisted to disk, kept in memory, transmitted over the network, compressed, etc. The estimated byte count is, as the name implies, an estimate only, and more constrained guidelines do not need to exist for it to be useful.

I suspect this is a problem of insufficient documentation. Since documentation can be updated/improved independently of a Swift Evolution proposal, I'll make sure to take the time to do so when we go to clean up documentation for this feature as a whole.

Perhaps such protocols don't use a prefix for everything specifically, but care has been taken in their design to avoid overlapping names. Wrapper types in the standard library—sequences/collections, RawRepresentable conformees, and Optional, as examples—use different type names to refer to the things they wrap. RawRepresentable could have used Wrapped, but it didn't.

On the other hand, this is a pretty good point—if we can reasonably expect types conforming to AttachableShoppingBag to be bespoke types meant for this purpose, then we can probably assume that any RawRepresentable or Sequence conformances involve the same underlying/wrapped type. So I'll run AttachableWrapper and Wrapped by the workgroup before a final decision is made here.

Fair enough!

This is a good general rule, but it has some nuances. The attachable value for an attachment in this scenario is the underlying/wrapped/contained value, and the proxy/wrapper/container is just a technical constraint required by the language.

Compare perhaps var stringValue: String? rather than var optionalStringValue: String?, where there's a wrapper type required by the language or some technical constraint, but we don't call it out in the name because it's not the important bit.

Putting on my reviewer hat here:

On estimatedAttachmentByteCount: I find myself agreeing with @grynspan here. I think that the estimated here gets across the idea that it just needs to be an estimate, and the most we need to do is mention in the documentation what it's used for, as well as to give an idea of the level of accuracy needed.

On Attachable[Container|Wrapper]: I think that we should go with AttachableWrapper. Primarily because Wrapper is much less general of a concept than Container is. Considering that AttachableWrapper exists to wrap another type for purposes of getting the wrapped type to be attachable, that makes sense to me.

On AttachableValue vs. AttachedValue: While I understand the desire to continue to use past-tense forms as they are inline with the rest of swift (i.e. enums can have associated values, protocols use the associatedtype keyword), I think it's better to lean into & have the API explicitly acknowledge that the value being attached isn't attached until after you call Attachment.record(...) - i.e. AttachedValue implies the value is attached at the time of declaration/initialization, but AttachableValue implies that the value will be attached at runtime either in the future or during execution.

2 Likes

With my Review Manager hat on:

Thanks for the feedback everyone! The testing workgroup has decided to Accept this Proposal with Modifications! Please see the announcement thread for more!

2 Likes