typealiased / mockingbird

A Swifty mocking framework for Swift and Objective-C.
https://mockingbirdswift.com
MIT License
657 stars 81 forks source link

Broken mocks when using protocols with associated types #198

Open s-hocking opened 3 years ago

s-hocking commented 3 years ago

New Issue Checklist

Description

With my particular class and protocol setup, using associated types, Mockingbird either generates a mocks that doesn't compile or a mock that crashes the Swift compiler, depending on my function's return type.

Generator Bugs

If the generator produces code that is malformed or does not compile, please provide:

  1. A minimal example of the original source

This source can produce either non-compiling mocks or a Swift compiler seg fault, depending which set of functions you comment out:

public class MySuperType {}

public class MySubtype: MySuperType {}

protocol MyProtocol {
    associatedtype U: MySuperType
    associatedtype T

//    func producesBadCode(input: U) -> [T] // produces code that doesn't compile
    func segFaults(input: U) -> T // produces a compiler seg fault
}

protocol MyOtherProtocol: MyProtocol {
//    func producesBadCode(input: MySubtype) -> [String] // produces code that doesn't compile
    func segFaults(input: MySubtype) -> String // produces a compiler seg fault
}
  1. The actual mocking code generated
public final class MyOtherProtocolMock<U: MockingbirdGeneratorBug.MySuperType, T>: MockingbirdGeneratorBug.MyOtherProtocol, Mockingbird.Mock {
  static var staticMock: Mockingbird.StaticMock { fatalError("See 'Thunk Pruning' in the README") }
  public let mockingContext = Mockingbird.MockingContext()
  public let stubbingContext = Mockingbird.StubbingContext()
  public let mockMetadata = Mockingbird.MockMetadata(["generator_version": "0.16.0", "module_name": "MockingbirdGeneratorBug"])
  public var sourceLocation: Mockingbird.SourceLocation? { get { fatalError("See 'Thunk Pruning' in the README") } set { fatalError("See 'Thunk Pruning' in the README") } }

  fileprivate init(sourceLocation: Mockingbird.SourceLocation) {
    Mockingbird.checkVersion(for: self)
    self.sourceLocation = sourceLocation
  }

  // MARK: Mocked `producesBadCode`(`input`: MockingbirdGeneratorBug.MySubtype)

  public func `producesBadCode`(`input`: MockingbirdGeneratorBug.MySubtype) -> [String] { fatalError("See 'Thunk Pruning' in the README") }

  public func `producesBadCode`(`input`: @escaping @autoclosure () -> MockingbirdGeneratorBug.MySubtype) -> Mockingbird.Mockable<Mockingbird.FunctionDeclaration, (MockingbirdGeneratorBug.MySubtype) -> [String], [String]> { fatalError("See 'Thunk Pruning' in the README") }

  // MARK: Mocked `producesBadCode`(`input`: U)

  public func `producesBadCode`(`input`: U) -> [T] { fatalError("See 'Thunk Pruning' in the README") }

  public func `producesBadCode`(`input`: @escaping @autoclosure () -> U) -> Mockingbird.Mockable<Mockingbird.FunctionDeclaration, (U) -> [T], [T]> { fatalError("See 'Thunk Pruning' in the README") }
}

public enum MyOtherProtocol<U: MockingbirdGeneratorBug.MySuperType, T> {}
/// Returns a concrete mock of `MyOtherProtocol`.
public func mock<U: MockingbirdGeneratorBug.MySuperType, T>(_ type: MyOtherProtocol<U, T>.Type, file: StaticString = #file, line: UInt = #line) -> MyOtherProtocolMock<U, T> {
  return MyOtherProtocolMock<U, T>(sourceLocation: Mockingbird.SourceLocation(file, line))
}

// MARK: - Mocked MyProtocol

public final class MyProtocolMock<U: MockingbirdGeneratorBug.MySuperType, T>: MockingbirdGeneratorBug.MyProtocol, Mockingbird.Mock {
  static var staticMock: Mockingbird.StaticMock { fatalError("See 'Thunk Pruning' in the README") }
  public let mockingContext = Mockingbird.MockingContext()
  public let stubbingContext = Mockingbird.StubbingContext()
  public let mockMetadata = Mockingbird.MockMetadata(["generator_version": "0.16.0", "module_name": "MockingbirdGeneratorBug"])
  public var sourceLocation: Mockingbird.SourceLocation? { get { fatalError("See 'Thunk Pruning' in the README") } set { fatalError("See 'Thunk Pruning' in the README") } }

  fileprivate init(sourceLocation: Mockingbird.SourceLocation) {
    Mockingbird.checkVersion(for: self)
    self.sourceLocation = sourceLocation
  }

  // MARK: Mocked `producesBadCode`(`input`: U)

  public func `producesBadCode`(`input`: U) -> [T] { fatalError("See 'Thunk Pruning' in the README") }

  public func `producesBadCode`(`input`: @escaping @autoclosure () -> U) -> Mockingbird.Mockable<Mockingbird.FunctionDeclaration, (U) -> [T], [T]> { fatalError("See 'Thunk Pruning' in the README") }
}

public enum MyProtocol<U: MockingbirdGeneratorBug.MySuperType, T> {}
/// Returns a concrete mock of `MyProtocol`.
public func mock<U: MockingbirdGeneratorBug.MySuperType, T>(_ type: MyProtocol<U, T>.Type, file: StaticString = #file, line: UInt = #line) -> MyProtocolMock<U, T> {
  return MyProtocolMock<U, T>(sourceLocation: Mockingbird.SourceLocation(file, line))
}

// MARK: - Mocked MySubtype

public final class MySubtypeMock: MockingbirdGeneratorBug.MySubtype, Mockingbird.Mock {
  static let staticMock = Mockingbird.StaticMock()
  public let mockingContext = Mockingbird.MockingContext()
  public let stubbingContext = Mockingbird.StubbingContext()
  public let mockMetadata = Mockingbird.MockMetadata(["generator_version": "0.16.0", "module_name": "MockingbirdGeneratorBug"])
  public var sourceLocation: Mockingbird.SourceLocation? { get { fatalError("See 'Thunk Pruning' in the README") } set { fatalError("See 'Thunk Pruning' in the README") } }

  fileprivate init(sourceLocation: Mockingbird.SourceLocation) {
    super.init()
    Mockingbird.checkVersion(for: self)
    self.sourceLocation = sourceLocation
  }
}
  1. The expected mocking code that should be generated (or a description)

I think the issue is the duplicate function definitions in class MyOtherProtocolMock. Maybe this is related to #183 ? When removing the dupes the broken mock compiles:

public final class MyOtherProtocolMock<U: MockingbirdGeneratorBug.MySuperType, T>: MockingbirdGeneratorBug.MyOtherProtocol, Mockingbird.Mock {
  static var staticMock: Mockingbird.StaticMock { fatalError("See 'Thunk Pruning' in the README") }
  public let mockingContext = Mockingbird.MockingContext()
  public let stubbingContext = Mockingbird.StubbingContext()
  public let mockMetadata = Mockingbird.MockMetadata(["generator_version": "0.16.0", "module_name": "MockingbirdGeneratorBug"])
  public var sourceLocation: Mockingbird.SourceLocation? { get { fatalError("See 'Thunk Pruning' in the README") } set { fatalError("See 'Thunk Pruning' in the README") } }

  fileprivate init(sourceLocation: Mockingbird.SourceLocation) {
    Mockingbird.checkVersion(for: self)
    self.sourceLocation = sourceLocation
  }

  // MARK: Mocked `producesBadCode`(`input`: MockingbirdGeneratorBug.MySubtype)

  public func `producesBadCode`(`input`: MockingbirdGeneratorBug.MySubtype) -> [String] { fatalError("See 'Thunk Pruning' in the README") }

  public func `producesBadCode`(`input`: @escaping @autoclosure () -> MockingbirdGeneratorBug.MySubtype) -> Mockingbird.Mockable<Mockingbird.FunctionDeclaration, (MockingbirdGeneratorBug.MySubtype) -> [String], [String]> { fatalError("See 'Thunk Pruning' in the README") }
}

Environment

andrewchang-bird commented 3 years ago

A workaround for the producesBadCode issue (missing inferred generic type specialization) is to keep the member declarations the same (referencing the inherited generic types) and use generic type constraints instead. So for the example above, MyOtherProtocol would look like the following:

protocol MyOtherProtocol: MyProtocol where U == MySubtype, T == String {
  func producesBadCode(input: U) -> [T]
}
mecoFarid commented 1 year ago

To be honest, it it better to write verbose code than this library which is unpredictable in many scenarios and there is basically no support at all. Let alone support there is not even a single example for protocols with generic types. So dump this and write your own mocks as simple as that