Using Property Wrappers enclosing self in an experiment

I had an idea of using a Property Wrapper to help setup Publishers and wait for their outputs in unit tests. I’m using the reference enclosing self unofficial feature to do it. (I’m aware that it might change, but I’m trying to warm up to this use case anyway)

Here’s the code and the errors I’m getting when I try to initialize the PW variable:

@propertyWrapper
struct Await<Value> {
    
    public static subscript<EnclosingSelf: XCTestCase>(
        _enclosingInstance instance: EnclosingSelf,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value?>,
        storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Self>
    ) -> Value? {
        
        // Setup expectation, fulfilled when the publisher emits a value.
        let expectation = instance.expectation(description: "Waiting for publisher")
        
        // Asserting no failure for now, ideally this wouldn't matter as we can change the `value` to be of type Result<Value, Error>? and handle failures.
        let cancellable = instance[keyPath: storageKeyPath].publisher
            .assertNoFailure()
            .sink(receiveValue: { value in
                instance[keyPath: storageKeyPath].value = value
                expectation.fulfill()
            })
        
        instance.waitForExpectations(timeout: 3)
        cancellable.cancel()
        
        // Ideally I would use XCTUnwrap and throw here so the caller doesn't have to.
        return instance[keyPath: storageKeyPath].value
    }
    
    @available(*, unavailable)
    var wrappedValue: Value? {
        get { fatalError() }
        set { fatalError() }
    }
    
    private var publisher: AnyPublisher<Value, Never>
    private var value: Value?
    
    public init(_ publisher: AnyPublisher<Value, Never>) {
      self.publisher = publisher
    }
}

And using it:

class SamplePWTests: XCTestCase {

// Error: Extraneous argument label 'wrappedValue:' in call
// suggested fix: Insert '' (does nothing)
    @Await var publisher = Just(false).eraseToAnyPublisher()
}

or

    func test() throws {
        @Await(Just(false).eraseToAnyPublisher()) var publisher // 'wrappedValue' is unavailable
        let unwrapped = try XCTUnwrap(publisher)
        XCTAssertFalse(unwrapped)
}

or

class SamplePWTests: XCTestCase {
    // Illegal instruction 4
    @Await(Just(false).eraseToAnyPublisher()) var publisher    
}

Am I missing something on why I can't initialize it with one of these options?

3 Likes

I think there are 3 different things going on here and 2.5 of them are compiler bugs:

This one is correct behavior, with a horrible error message that should be improved. Property wrappers only support initialization via = if they declare an init(wrappedValue:) initializer. That's why the error message talks about the wrappedValue label.

I'm guessing this is a bug with local property wrappers where it only checks its direct enclosing scope to see if it's a class. Since it doesn't find a class, it attempts to use var wrappedValue from the property wrapper, which is unavailable.

I can't spot a reason why this shouldn't work. In any case, the compiler should never crash!

3 Likes