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

[SWT-NNNN] Make the Issues API public & provide a public Issue.record instance method. #481

Closed
wants to merge 8 commits into from

Conversation

younata
Copy link

@younata younata commented Jun 13, 2024

See #474 for the related bug.

Read the full proposal here!

Motivation

To better facilitate integrating Swift Testing with third party assertion tools, Swift Testing should allow public access to creating and recording Issues directly.

Modifications:

At this stage, the PR is just the pitch doc, and I'm more than happy to contribute the small change as we discuss it. I figured it would be better to agree on the pitch first?

Result:

Once merged, this will open up third-party assertion tools to better integrate with Swift Testing.

Checklist:

  • Code and documentation should follow the style of the Style Guide.

@grynspan
Copy link
Contributor

One minor thing that worries me is that this allows creating issues of type .system which is meant to be reserved by the testing library. We can relax that definition, but I'm not sure we want to?

As well, should we consider deprecating the static record() functions? They exist to allow recording arbitrary issues without exposing a full initializer, so they're redundant with this change.

@grynspan grynspan added enhancement New feature or request public-api Affects public API api-proposal API proposal PRs (documentation only) labels Jun 13, 2024
Comment on lines 80 to 82
public func record() -> Self {
record(configuration: nil)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

For the purposes of a proposal, you can omit the body of the method

## Integration with supporting tools

Third-party assertion tools would be able to directly create an `Issue`
and then report it using the `Issue.report()` instance method. `Issue.report()`
Copy link
Contributor

Choose a reason for hiding this comment

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

These say report() but the actual proposed method is named record(), can you update those to be consistent throughout the document?

@stmontgomery
Copy link
Contributor

One thing which is likely to pose a challenge with using this API as proposed is that other related types don't yet expose a public initializer, either. Perhaps most important is the Expectation type. It's used as an associated value of the Issue.Kind.expectationFailed(_ expectation: Expectation) enum case.

In order to correctly represent an expectation failure, a client would need to first programmatically construct an Expectation, which implies that more APIs may need to be exposed than just the ones mentioned here. One approach could be to start conservatively and identify only a small subset of the Expectation API which is likely needed to express basic expectation failures, and not attempt to include as much detail as the built-in #expect does.

I'd recommend looking through the Issue.Kind enum and evaluating how each one might be used, or whether it should be used at all (as @grynspan mentioned, .system doesn't seem relevant).

younata and others added 2 commits June 13, 2024 11:30
Co-authored-by: Stuart Montgomery <smontgomery@apple.com>
Co-authored-by: Stuart Montgomery <smontgomery@apple.com>
@younata
Copy link
Author

younata commented Jun 13, 2024

One thing which is likely to pose a challenge with using this API as proposed is that other related types don't yet expose a public initializer, either. Perhaps most important is the Expectation type. It's used as an associated value of the Issue.Kind.expectationFailed(_ expectation: Expectation) enum case.

Thanks for the review!

I think a decent next-steps for me would be to go and implement this in my branch, and check out what needs to be modified to actually do this.

younata added 4 commits June 13, 2024 14:37
- Makes the Issue initializer public, with a slight cleanup.
- Creates the Issue.record() instance method, which just calls out to Issue.record(configuration:)
- Marks the Issue.Kind.system case as only available for tools integrations.
- Issue.record(configuration:) no longer has a default value of nil, to avoid confusion with the new Issue.record() methods.
- Creates a public initializer for Expectation, which creates an empty Expression
- Adds an explicit internal initializer for Expectation, where we can pass in Expressions
- Allow third parties to read & write to the Expectation.mismatchedErrorDescription and Expectation.differenceDescription properties
@younata
Copy link
Author

younata commented Jun 14, 2024

Update: I added an implementation, which I used with Nimble to actually create an integration.

You can check out the basic-swift-testing-support branch on Nimble if you'd like to try it out. Both the xcodeproj and the Package.swift use this branch of swift-testing. Run the SwiftTestingSupportSuite to verify that the error is properly recorded in Swift Testing.

@@ -18,7 +18,6 @@ public struct Expectation: Sendable {
///
/// If this expectation passed, the value of this property is `nil` because no
/// error mismatch occurred.
@_spi(ForToolsIntegrationOnly)
Copy link
Contributor

Choose a reason for hiding this comment

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

This property should probably stay SPI for now as it's a bit hacky and probably not the final design for this data.

@@ -27,7 +26,6 @@ public struct Expectation: Sendable {
/// If this expectation passed, the value of this property is `nil` because
/// the difference is only computed when necessary to assist with diagnosing
/// test failures.
@_spi(ForToolsIntegrationOnly)
Copy link
Contributor

Choose a reason for hiding this comment

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

This property should probably stay SPI for now as it's a bit hacky and probably not the final design for this data.

@@ -54,6 +54,15 @@ extension Issue {
return issue.record(configuration: configuration)
}

/// Record this issue by wrapping it in an ``Event`` and passing it to the
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we really discuss events or event handlers in our API documentation because they're not otherwise part of our API surface. How about simply:

/// Record this issue.

/// Record this issue by wrapping it in an ``Event`` and passing it to the
/// current event handler.
///
/// - Returns: The issue that was recorded (`self` or a modified copy of it.)
Copy link
Contributor

Choose a reason for hiding this comment

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

We should make sure the API documentation explains what might be modified (the SPI documentation is, and this is a technical term here, "lazy".)

@@ -65,6 +65,7 @@ public struct Issue: Sendable {

/// An issue due to a failure in the underlying system, not due to a failure
/// within the tests being run.
@_spi(ForToolsIntegrationOnly)
Copy link
Contributor

Choose a reason for hiding this comment

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

We should leave .system as SPI for now since we don't generally want external components to generate system issues. These represent problems with Swift Testing itself. (We could maybe add an additional case e.g. case integratedToolFailed(description: String), what do you think @stmontgomery ?)

Copy link
Author

Choose a reason for hiding this comment

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

Just so I understand, you're saying this line is correct? Or is there a more-correct @_spi annotation than ForToolsIntegrationOnly? The SPI doc only lists Experimental and ForToolsIntegrationOnly.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, adding the SPI marker here is probably the right thing to do, but I'd like @stmontgomery to chime in too.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's OK to leave .system without an @_spi attribute. Although it's true that users shouldn't generally record system issues themselves, I do think it can be useful for them to match against issues of that kind using other APIs like withKnownIssue. So personally, I don't think we need to add it as part of this proposal/PR.

@@ -93,9 +94,9 @@ public struct Issue: Sendable {
/// occurred. This defaults to a default source context returned by
/// calling ``SourceContext/init(backtrace:sourceLocation:)`` with zero
/// arguments.
init(
public init(
Copy link
Contributor

@grynspan grynspan Jun 14, 2024

Choose a reason for hiding this comment

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

This is fine but will produce a source context for the issue that points into Swift Testing rather than the call site, which is an oversight of the existing code to be fair. As well, we're contemplating lowering Backtrace and SourceContext to SPI since the Swift standard library has its own backtrace type now that we'll eventually want to adopt and we don't want a naming conflict.

How about we expose a different initializer with the signature:

init(
  _ kind: Kind,
  comments: [Comment] = [],
  sourceLocation: SourceLocation = #_sourceLocation
)

(Whether or not kind deserves a label is something we can bikeshed.)

kind: Kind,
comments: [Comment] = [],
sourceContext: SourceContext = .init()
) {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Proposals don't need to include the bodies of methods/initializers/etc. You can leave the squiggly brackets out here.

}
```

Furthermore, to support passing in any `Issue.Kind` to `Issue`, `Expectation`
Copy link
Contributor

Choose a reason for hiding this comment

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

It is intentional that Expectation does not have a public initializer: it is always supposed to be the product of #expect or #require. If you record an issue that does not come from one of those macros, it should not be producing an instance of this type.

Copy link
Contributor

Choose a reason for hiding this comment

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


## Acknowledgments

I'd like to thank [Jonathon Grynspan](https://github.com/grynspan) and
Copy link
Contributor

Choose a reason for hiding this comment

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

Jonathan. :)

self.isPassing = isPassing
self.isRequired = isRequired
self.sourceLocation = sourceLocation
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd like to explore ideas for ways to structure this that avoid needing to publicly expose an init on Expectation. I know I brought that idea up in an earlier comment on this PR, but after reviewing this type again, and taking @grynspan 's comments about the two properties above into consideration, I'm wary to open this type up for general API use right now until it's a bit better designed.

As one alternative, I wonder if we could expose a static func on Issue.Kind which takes a generic message, as well as one or more values of the evaluated expression, and internally construct an Expectation with the relevant Expression and Expression.Value values representing all of the passed-in values? Something like

extension Issue.Kind {
  public static func expectationFailed<each T>(
    _ description: String,
    values: repeat each T,
    isRequired: Bool,
    sourceLocation: SourceLocation
  ) -> Self
}

This would require some internal change to __Expression but that seems doable, and it would allow capturing rich details of the left- and right-hand side values of binary expressions.

@grynspan
Copy link
Contributor

If we were to proceed with #490, would we still need this API change?

@younata
Copy link
Author

younata commented Jul 1, 2024

If we were to proceed with #490, would we still need this API change?

No, this PR can be closed if we proceed with #490, which solves this with less code required of third-party tools.

@grynspan
Copy link
Contributor

grynspan commented Jul 8, 2024

No, this PR can be closed if we proceed with #490, which solves this with less code required of third-party tools.

With that in mind, I'd like to suggest we close out this PR as well as #474, and focus on that PR. (If you agree that it's an approach that works for you.) We'll want to do an API PR and forum post for that one too, if you're interested in writing it?

@younata
Copy link
Author

younata commented Jul 9, 2024

With that in mind, I'd like to suggest we close out this PR as well as #474, and focus on that PR. (If you agree that it's an approach that works for you.) We'll want to do an API PR and forum post for that one too, if you're interested in writing it?

Ok, sure. I'll take that on.

@younata younata closed this Jul 9, 2024
@younata younata deleted the public-issues-pitch branch July 9, 2024 15:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-proposal API proposal PRs (documentation only) enhancement New feature or request public-api Affects public API
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants