Brightify / Cuckoo

Boilerplate-free mocking framework for Swift!
MIT License
1.67k stars 174 forks source link

Why does Swift complain that the class for which Cuckoo has provided a mock doesn't conform to Mock? #377

Closed jwb closed 3 years ago

jwb commented 3 years ago

I asked a question on StackOverflow because this seems to be a Swift behavior that I don't understand.

Using Cuckoo 1.4.1, I get the error

Global function 'stub(_:block:)' requires that 'DeepFileManager' conform to 'Mock'

I don't know how to proceed, because the instance passed to the stub function was created from the class in GeneratedMocks.swift. The reference to DeepFileManager is the class that is mocked.

So it's a mystery to me why Swift 4 would complain about the conformance of the superclass of the instance I've passed to it. Can you lead me out of this conundrum?

Here's the code for the test:

class RecordingTests: QuickSpec {
    override func spec() {
        let FAKE_ID = "String";
        let mockFileManager: MockDeepFileManager = MockDeepFileManager()
        let cut = Recording(id: FAKE_ID, date: nil, status: nil, device: nil, deviceId: nil, deviceName: nil, fileManager: mockFileManager)
        describe("A recording") {
            context("when it's not on disk") {
                it("responds that filePresent is false") {
                    stub(mockFileManager) {stub in
                        when(stub.fileExists(audioId: FAKE_ID)).then()
                    }
                    expect(cut.filePresent()).to(equal(false))
                    verify(mockFileManager).fileExists(audioId: "Matchable")
                }
            }
        }
    }

And here's the declaration of MockDeepFileManager from GeneratedMocks.swift:

 class MockDeepFileManager: DeepFileManager, Cuckoo.ClassMock {
MatyasKriz commented 3 years ago

Hey, @jwb. Although this might not be the problem, make sure to include DeepFileManager's dependencies in the Cuckoo generator call as well. They are needed for correct inheritance detection.

As for this situation, I can't infer the cause based on the limited information provided. Can you share DeepFileManager structure as well as the full command you're using to generate mocks? If not, would you be able to replicate this problem in a minimal working example?

jwb commented 3 years ago

Here's the DeepFileManager:

import UIKit

class DeepFileManager {
    static let shared = DeepFileManager()

    func getDocumentsDirectory() -> URL {
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        let documentsDirectory = paths[0]
        return documentsDirectory
    }

    func getFileUrl(audioId: String) -> URL {
        let fileName = audioId + ".wav"
        let fileURL = getDocumentsDirectory().appendingPathComponent(fileName)

        return fileURL
    }

    func fileExists(audioId: String) -> Bool {
        let fileURL = getFileUrl(audioId: audioId)

        return FileManager.default.fileExists(atPath: fileURL.path)
    }

    func findOldRecordings() -> [URL] {
        do {
            let directoryContents = try FileManager.default.contentsOfDirectory(at: getDocumentsDirectory(),
                                                                                includingPropertiesForKeys: nil,
                                                                                options: [])
            var filesPath = directoryContents.filter{ $0.pathExtension == "wav" }

            let calendar = Calendar(identifier: .gregorian)
            var dateComponents = DateComponents()
            dateComponents.year = 2020
            dateComponents.month = 12
            dateComponents.day = 13
            let lastRecordingDate = calendar.date(from: dateComponents)!

            filesPath = filesPath.filter {
                do {
                    let fileAttrs = try FileManager.default.attributesOfItem(atPath: "\($0.path)")

                    if let fileDate = fileAttrs[.creationDate] as? Date {
                        if fileDate < lastRecordingDate {
                            return true
                        }
                    }
                } catch { }
                return false
            }
            return filesPath
        } catch {
            print("getAllRecordingFiles error")
        }

        return [URL]()
    }

    // DeepFileManager methods for iOS only.
    #if os(iOS)
    func getAudioRecordingURL(audioId: String) -> URL {
        let fileName = UserDefaults.standard.getAudioRecordingId(audioId: audioId) + ".wav"
        let fileURL = getDocumentsDirectory().appendingPathComponent(fileName)

        return fileURL
    }

    func getTotalStorageCapacity() -> Int64? {
        let fileURL = URL(fileURLWithPath: NSHomeDirectory() as String)

        do {
            let values = try fileURL.resourceValues(forKeys: [.volumeTotalCapacityKey])

            if let capacity = values.volumeTotalCapacity {
                return Int64(capacity)
            } else {
                print("DeepFileManager getStorageCapacity error: Capacity is unavailable")
            }
        } catch {
            print("DeepFileManager getStorageCapacity error: \(error.localizedDescription)")
        }

        return nil
    }

    func getAvailableStorageCapacity() -> Int64? {
        let fileURL = URL(fileURLWithPath: NSHomeDirectory() as String)

        do {
            let values = try fileURL.resourceValues(forKeys: [.volumeAvailableCapacityForImportantUsageKey])

            if let capacity = values.volumeAvailableCapacityForImportantUsage {
                return capacity
            } else {
                print("DeepFileManager getStorageCapacity error: Capacity is unavailable")
            }
        } catch {
            print("DeepFileManager getStorageCapacity error: \(error.localizedDescription)")
        }

        return nil
    }

    func getRemainingMinutesForRecording() -> Int? {
        if let storageCapacity = getAvailableStorageCapacity() {
            // 6 MB per minute
            let recordingPerMinute = Int64(6000000)

            let remainingMinutes = storageCapacity/recordingPerMinute

            return Int(remainingMinutes)
        }

        return nil
    }
    #endif
}

Here's the script in the Xcode build

# Define output file.
OUTPUT_FILE="${PROJECT_DIR}/${PROJECT_NAME}Tests/GeneratedMocks.swift"
echo "Generated Mocks File = ${OUTPUT_FILE}"

for ((i = 0; i < ${SCRIPT_INPUT_FILE_COUNT}; i ++ ))
  do var=SCRIPT_INPUT_FILE_$i
  list="$list ${(P)var}"
done
# Generate mock files, include as many input files as you'd like to create mocks for.
"${PODS_ROOT}/Cuckoo/run" generate --testable "${PROJECT_NAME}" \
    --output "${OUTPUT_FILE}" $list

The only input for the script is DeepFileManager.swift.

TadeasKriz commented 3 years ago

Which platform are you running the tests for? I'm thinking it might be the conditional #if. Do we have support for that in Cuckoo @MatyasKriz ?

MatyasKriz commented 3 years ago

I'd be surprised if we did, at least I don't remember adding it. Though it may not be too hard to add when we find the time if SourceKit parses this.

jwb commented 3 years ago

I removed everything from the #if, to the #endif (inclusive), and it still complains:

RecordingTests.swift:26:21: Global function 'stub(_:block:)' requires that 'DeepFileManager' conform to 'Mock'

Why would Swift want the superclass of the MockDeepFileManager to conform to 'Mock'?

jwb commented 3 years ago

After that excision, DeepFileManager uses FileManager, URL, Calendar, and DateComponents. Do I need to point the generator at source for those? I dunno what the path for the Foundation files is.

TadeasKriz commented 3 years ago

Could you share the generated mock class source?

jwb commented 3 years ago
// MARK: - Mocks generated from file: Shared/DeepFileManager.swift at 2021-01-21 23:22:05 +0000

//
//  DeepFileManager.swift

import Cuckoo
@testable import project

import UIKit

 class MockDeepFileManager: DeepFileManager, Cuckoo.ClassMock {

     typealias MocksType = DeepFileManager

     typealias Stubbing = __StubbingProxy_DeepFileManager
     typealias Verification = __VerificationProxy_DeepFileManager

     let cuckoo_manager = Cuckoo.MockManager.preconfiguredManager ?? Cuckoo.MockManager(hasParent: true)

    private var __defaultImplStub: DeepFileManager?

     func enableDefaultImplementation(_ stub: DeepFileManager) {
        __defaultImplStub = stub
        cuckoo_manager.enableDefaultStubImplementation()
    }

     override func getDocumentsDirectory() -> URL {

    return cuckoo_manager.call("getDocumentsDirectory() -> URL",
            parameters: (),
            escapingParameters: (),
            superclassCall:

                super.getDocumentsDirectory()
                ,
            defaultCall: __defaultImplStub!.getDocumentsDirectory())

    }

     override func getFileUrl(audioId: String) -> URL {

    return cuckoo_manager.call("getFileUrl(audioId: String) -> URL",
            parameters: (audioId),
            escapingParameters: (audioId),
            superclassCall:

                super.getFileUrl(audioId: audioId)
                ,
            defaultCall: __defaultImplStub!.getFileUrl(audioId: audioId))

    }

     override func fileExists(audioId: String) -> Bool {

    return cuckoo_manager.call("fileExists(audioId: String) -> Bool",
            parameters: (audioId),
            escapingParameters: (audioId),
            superclassCall:

                super.fileExists(audioId: audioId)
                ,
            defaultCall: __defaultImplStub!.fileExists(audioId: audioId))

    }

     override func getAllStuckRecordingFiles() -> [URL] {

    return cuckoo_manager.call("getAllStuckRecordingFiles() -> [URL]",
            parameters: (),
            escapingParameters: (),
            superclassCall:

                super.getAllStuckRecordingFiles()
                ,
            defaultCall: __defaultImplStub!.getAllStuckRecordingFiles())

    }

     override func getAudioRecordingURL(audioId: String) -> URL {

    return cuckoo_manager.call("getAudioRecordingURL(audioId: String) -> URL",
            parameters: (audioId),
            escapingParameters: (audioId),
            superclassCall:

                super.getAudioRecordingURL(audioId: audioId)
                ,
            defaultCall: __defaultImplStub!.getAudioRecordingURL(audioId: audioId))

    }

     override func getTotalStorageCapacity() -> Int64? {

    return cuckoo_manager.call("getTotalStorageCapacity() -> Int64?",
            parameters: (),
            escapingParameters: (),
            superclassCall:

                super.getTotalStorageCapacity()
                ,
            defaultCall: __defaultImplStub!.getTotalStorageCapacity())

    }

     override func getAvailableStorageCapacity() -> Int64? {

    return cuckoo_manager.call("getAvailableStorageCapacity() -> Int64?",
            parameters: (),
            escapingParameters: (),
            superclassCall:

                super.getAvailableStorageCapacity()
                ,
            defaultCall: __defaultImplStub!.getAvailableStorageCapacity())

    }

     override func getRemainingMinutesForRecording() -> Int? {

    return cuckoo_manager.call("getRemainingMinutesForRecording() -> Int?",
            parameters: (),
            escapingParameters: (),
            superclassCall:

                super.getRemainingMinutesForRecording()
                ,
            defaultCall: __defaultImplStub!.getRemainingMinutesForRecording())

    }

     struct __StubbingProxy_DeepFileManager: Cuckoo.StubbingProxy {
        private let cuckoo_manager: Cuckoo.MockManager

         init(manager: Cuckoo.MockManager) {
            self.cuckoo_manager = manager
        }

        func getDocumentsDirectory() -> Cuckoo.ClassStubFunction<(), URL> {
            let matchers: [Cuckoo.ParameterMatcher<Void>] = []
            return .init(stub: cuckoo_manager.createStub(for: MockDeepFileManager.self, method: "getDocumentsDirectory() -> URL", parameterMatchers: matchers))
        }

        func getFileUrl<M1: Cuckoo.Matchable>(audioId: M1) -> Cuckoo.ClassStubFunction<(String), URL> where M1.MatchedType == String {
            let matchers: [Cuckoo.ParameterMatcher<(String)>] = [wrap(matchable: audioId) { $0 }]
            return .init(stub: cuckoo_manager.createStub(for: MockDeepFileManager.self, method: "getFileUrl(audioId: String) -> URL", parameterMatchers: matchers))
        }

        func fileExists<M1: Cuckoo.Matchable>(audioId: M1) -> Cuckoo.ClassStubFunction<(String), Bool> where M1.MatchedType == String {
            let matchers: [Cuckoo.ParameterMatcher<(String)>] = [wrap(matchable: audioId) { $0 }]
            return .init(stub: cuckoo_manager.createStub(for: MockDeepFileManager.self, method: "fileExists(audioId: String) -> Bool", parameterMatchers: matchers))
        }

        func getAllStuckRecordingFiles() -> Cuckoo.ClassStubFunction<(), [URL]> {
            let matchers: [Cuckoo.ParameterMatcher<Void>] = []
            return .init(stub: cuckoo_manager.createStub(for: MockDeepFileManager.self, method: "getAllStuckRecordingFiles() -> [URL]", parameterMatchers: matchers))
        }

        func getAudioRecordingURL<M1: Cuckoo.Matchable>(audioId: M1) -> Cuckoo.ClassStubFunction<(String), URL> where M1.MatchedType == String {
            let matchers: [Cuckoo.ParameterMatcher<(String)>] = [wrap(matchable: audioId) { $0 }]
            return .init(stub: cuckoo_manager.createStub(for: MockDeepFileManager.self, method: "getAudioRecordingURL(audioId: String) -> URL", parameterMatchers: matchers))
        }

        func getTotalStorageCapacity() -> Cuckoo.ClassStubFunction<(), Int64?> {
            let matchers: [Cuckoo.ParameterMatcher<Void>] = []
            return .init(stub: cuckoo_manager.createStub(for: MockDeepFileManager.self, method: "getTotalStorageCapacity() -> Int64?", parameterMatchers: matchers))
        }

        func getAvailableStorageCapacity() -> Cuckoo.ClassStubFunction<(), Int64?> {
            let matchers: [Cuckoo.ParameterMatcher<Void>] = []
            return .init(stub: cuckoo_manager.createStub(for: MockDeepFileManager.self, method: "getAvailableStorageCapacity() -> Int64?", parameterMatchers: matchers))
        }

        func getRemainingMinutesForRecording() -> Cuckoo.ClassStubFunction<(), Int?> {
            let matchers: [Cuckoo.ParameterMatcher<Void>] = []
            return .init(stub: cuckoo_manager.createStub(for: MockDeepFileManager.self, method: "getRemainingMinutesForRecording() -> Int?", parameterMatchers: matchers))
        }

    }

     struct __VerificationProxy_DeepFileManager: Cuckoo.VerificationProxy {
        private let cuckoo_manager: Cuckoo.MockManager
        private let callMatcher: Cuckoo.CallMatcher
        private let sourceLocation: Cuckoo.SourceLocation

         init(manager: Cuckoo.MockManager, callMatcher: Cuckoo.CallMatcher, sourceLocation: Cuckoo.SourceLocation) {
            self.cuckoo_manager = manager
            self.callMatcher = callMatcher
            self.sourceLocation = sourceLocation
        }

        @discardableResult
        func getDocumentsDirectory() -> Cuckoo.__DoNotUse<(), URL> {
            let matchers: [Cuckoo.ParameterMatcher<Void>] = []
            return cuckoo_manager.verify("getDocumentsDirectory() -> URL", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation)
        }

        @discardableResult
        func getFileUrl<M1: Cuckoo.Matchable>(audioId: M1) -> Cuckoo.__DoNotUse<(String), URL> where M1.MatchedType == String {
            let matchers: [Cuckoo.ParameterMatcher<(String)>] = [wrap(matchable: audioId) { $0 }]
            return cuckoo_manager.verify("getFileUrl(audioId: String) -> URL", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation)
        }

        @discardableResult
        func fileExists<M1: Cuckoo.Matchable>(audioId: M1) -> Cuckoo.__DoNotUse<(String), Bool> where M1.MatchedType == String {
            let matchers: [Cuckoo.ParameterMatcher<(String)>] = [wrap(matchable: audioId) { $0 }]
            return cuckoo_manager.verify("fileExists(audioId: String) -> Bool", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation)
        }

        @discardableResult
        func getAllStuckRecordingFiles() -> Cuckoo.__DoNotUse<(), [URL]> {
            let matchers: [Cuckoo.ParameterMatcher<Void>] = []
            return cuckoo_manager.verify("getAllStuckRecordingFiles() -> [URL]", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation)
        }

        @discardableResult
        func getAudioRecordingURL<M1: Cuckoo.Matchable>(audioId: M1) -> Cuckoo.__DoNotUse<(String), URL> where M1.MatchedType == String {
            let matchers: [Cuckoo.ParameterMatcher<(String)>] = [wrap(matchable: audioId) { $0 }]
            return cuckoo_manager.verify("getAudioRecordingURL(audioId: String) -> URL", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation)
        }

        @discardableResult
        func getTotalStorageCapacity() -> Cuckoo.__DoNotUse<(), Int64?> {
            let matchers: [Cuckoo.ParameterMatcher<Void>] = []
            return cuckoo_manager.verify("getTotalStorageCapacity() -> Int64?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation)
        }

        @discardableResult
        func getAvailableStorageCapacity() -> Cuckoo.__DoNotUse<(), Int64?> {
            let matchers: [Cuckoo.ParameterMatcher<Void>] = []
            return cuckoo_manager.verify("getAvailableStorageCapacity() -> Int64?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation)
        }

        @discardableResult
        func getRemainingMinutesForRecording() -> Cuckoo.__DoNotUse<(), Int?> {
            let matchers: [Cuckoo.ParameterMatcher<Void>] = []
            return cuckoo_manager.verify("getRemainingMinutesForRecording() -> Int?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation)
        }

    }
}

 class DeepFileManagerStub: DeepFileManager {

     override func getDocumentsDirectory() -> URL  {
        return DefaultValueRegistry.defaultValue(for: (URL).self)
    }

     override func getFileUrl(audioId: String) -> URL  {
        return DefaultValueRegistry.defaultValue(for: (URL).self)
    }

     override func fileExists(audioId: String) -> Bool  {
        return DefaultValueRegistry.defaultValue(for: (Bool).self)
    }

     override func getAllStuckRecordingFiles() -> [URL]  {
        return DefaultValueRegistry.defaultValue(for: ([URL]).self)
    }

     override func getAudioRecordingURL(audioId: String) -> URL  {
        return DefaultValueRegistry.defaultValue(for: (URL).self)
    }

     override func getTotalStorageCapacity() -> Int64?  {
        return DefaultValueRegistry.defaultValue(for: (Int64?).self)
    }

     override func getAvailableStorageCapacity() -> Int64?  {
        return DefaultValueRegistry.defaultValue(for: (Int64?).self)
    }

     override func getRemainingMinutesForRecording() -> Int?  {
        return DefaultValueRegistry.defaultValue(for: (Int?).self)
    }

}
iadcialim commented 3 years ago

guys any update? or @jwb have u resolved it on ur side? I just posted a problem like this one; I added the src codes as well

jwb commented 3 years ago

I've abandoned cuckoo for the time being. The Swift Mock Generator for XCode is at least a little helpful.

TadeasKriz commented 3 years ago

@jwb Sorry! I've had so much on my plate I completely forgot about your issue. I'll try to reproduce it.

TadeasKriz commented 3 years ago

@jwb Found it. when(stub.fileExists(audioId: FAKE_ID)).then() on this line you're calling then() which is not valid. You can either call thenReturn(x) where x is the value you want it to return, or use the then, but you have to provide it with a closure representing the method implementation. So in this case, it'd be a (String) -> Bool closure.

It's unfortunate the Swift compiler gives this unhelpful error, but I'm not sure if there's anything we can do on Cuckoo's end to improve it.

hlung commented 1 year ago

In my case I forgot the .get part in when(mock.id.get) 😂 .