[PM-23496] Add Sourcery library to automatically generate mocks (#1724)

This commit is contained in:
Federico Maccaroni 2025-07-08 09:11:17 -03:00 committed by GitHub
parent fe7b68fa1c
commit bbb93bdef1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 257 additions and 0 deletions

View File

@ -122,6 +122,14 @@
<key>Type</key>
<string>PSChildPaneSpecifier</string>
</dict>
<dict>
<key>File</key>
<string>Acknowledgements/Sourcery</string>
<key>Title</key>
<string>Sourcery</string>
<key>Type</key>
<string>PSChildPaneSpecifier</string>
</dict>
<dict>
<key>File</key>
<string>Acknowledgements/swift-custom-dump</string>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreferenceSpecifiers</key>
<array>
<dict>
<key>FooterText</key>
<string>MIT License
Copyright (c) 2016-2021 Krzysztof Zabłocki
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
</string>
<key>License</key>
<string>MIT</string>
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
</array>
</dict>
</plist>

View File

@ -0,0 +1,2 @@
# Ignore SwiftGen generated files
*.swift

View File

@ -0,0 +1,18 @@
sources:
- ..
templates:
- ../../Sourcery/Templates/AutoMockable.stencil
output:
Generated
exclude:
- Generated
- Tests
- TestHelpers
- Fixtures
args:
autoMockableImports: ["BitwardenKit", "BitwardenSdk", "Combine"]
autoMockableTestableImports: ["AuthenticatorShared"]

View File

@ -0,0 +1,2 @@
# Ignore SwiftGen generated files
*.swift

View File

@ -0,0 +1,18 @@
sources:
- ..
templates:
- ../../Sourcery/Templates/AutoMockable.stencil
output:
Generated
exclude:
- Generated
- Tests
- TestHelpers
- Fixtures
args:
autoMockableImports: ["BitwardenKit", "BitwardenSdk", "Combine"]
autoMockableTestableImports: ["BitwardenShared"]

View File

@ -3,3 +3,4 @@ nicklockwood/SwiftFormat@0.56.4
SwiftGen/SwiftGen@6.6.3
realm/SwiftLint@0.59.1
yonaskolb/xcodegen@2.43.0
krzysztofzablocki/Sourcery@2.2.7

View File

@ -0,0 +1,136 @@
// swiftlint:disable line_length
// swiftlint:disable variable_name
import Foundation
{% for import in argument.autoMockableImports %}
import {{ import }}
{% endfor %}
{% for import in argument.autoMockableTestableImports %}
@testable import {{ import }}
{% endfor %}
{% macro swiftifyMethodName name %}{{ name | replace:"(","_" | replace:")","" | replace:":","_" | replace:"`","" | snakeToCamelCase | lowerFirstWord }}{% endmacro %}
{% macro methodThrowableErrorDeclaration method %}
var {% call swiftifyMethodName method.selectorName %}Error: Error?
{% endmacro %}
{% macro methodThrowableErrorUsage method %}
if let error = {% call swiftifyMethodName method.selectorName %}Error {
throw error
}
{% endmacro %}
{% macro methodReceivedParameters method %}
{%if method.parameters.count == 1 %}
{% set receivedVarName %}{% call swiftifyMethodName method.selectorName %}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }}{% endfor %}{% endset %}
{{ receivedVarName }}{% for param in method.parameters %} = {{ param.name }}{% endfor %}
{% call swiftifyMethodName method.selectorName %}ReceivedInvocations.append({{ receivedVarName }}!)
{% else %}
{% if not method.parameters.count == 0 %}
{% set receivedVarName %}{% call swiftifyMethodName method.selectorName %}ReceivedArguments{% endset %}
{{ receivedVarName }} = ({% for param in method.parameters %}{{ param.name }}: {{ param.name }}{% if not forloop.last%}, {% endif %}{% endfor %}){% if method.isGeneric %} as? ({% for param in method.parameters %}{{ param.name }}: {% call parameterType param %}{% if not forloop.last%}, {% endif %}{% endfor %}){% endif %}
{% call swiftifyMethodName method.selectorName %}ReceivedInvocations.append({{ receivedVarName }}!)
{% endif %}
{% endif %}
{% endmacro %}
{% macro methodReturnType method %}{% if method.returnTypeName.isVoid %}Void{% elif method.annotations["GenericReturn"] %}{{ method.annotations["GenericReturn"] }}{% else %}{{ method.returnTypeName }}{% endif %}{% endmacro %}
{% macro parameterType param %}{% if param.annotations["Generic"] %}{{ param.annotations["Generic"] }}{% else %}{{ param.unwrappedTypeName if param.typeAttributes.escaping else param.typeName }}{% endif %}{% endmacro %}
{% macro parameterArguments param %}{% if param.typeAttributes.escaping %}@escaping {% endif %}{% endmacro %}
{% macro callClosure method %}return {{ 'try ' if method.throws }}{% if method.isGeneric %}({% endif %}{% call methodClosureName method %}.map({ {{ 'try ' if method.throws }}$0({% call methodClosureCallParameters method %}) }) ?? {% call swiftifyMethodName method.selectorName %}ReturnValue{% if method.isGeneric %}) as! {{ method.returnTypeName }}{% endif %}{% endmacro %}
{% macro methodClosureName method %}{% call swiftifyMethodName method.selectorName %}Closure{% endmacro %}
{% macro methodClosureDeclaration method %}
var {% call methodClosureName method %}: (({% for param in method.parameters %}{% call parameterArguments param %}{% call parameterType param %}{% if not forloop.last %}, {% endif %}{% endfor %}) {% if method.throws %}throws {% endif %}-> {% if method.isInitializer %}Void{% else %}{% call methodReturnType method %}{% endif %})?
{% endmacro %}
{% macro methodClosureCallParameters method %}{% for param in method.parameters %}{{ param.name }}{% if not forloop.last %}, {% endif %}{% endfor %}{% endmacro %}
{% macro methodClosureCallParametersFromArguments method %}{% for param in method.parameters %}{% call receivedArgumentsVarName method %}!{% if method.parameters.count > 1 %}.{{ param.name }}{% if not forloop.last %}, {% endif %}{% endif %}{% endfor %}{% endmacro %}
{% macro receivedArgumentsVarName method %}{% if method.parameters.count == 1 %}{% call swiftifyMethodName method.selectorName %}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }}{% endfor %}{% else %}{% call swiftifyMethodName method.selectorName %}ReceivedArguments{% endif %}{% endmacro %}
{% macro mockMethod method %}
//MARK: - {{ method.shortName }}
{% if method.throws %}
{% call methodThrowableErrorDeclaration method %}
{% endif %}
{% if not method.isInitializer %}
var {% call swiftifyMethodName method.selectorName %}CalledCount = 0
var {% call swiftifyMethodName method.selectorName %}Called: Bool {
return {% call swiftifyMethodName method.selectorName %}CalledCount > 0
}
{% endif %}
{% if method.parameters.count == 1 %}
var {% call swiftifyMethodName method.selectorName %}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }}: {{ '(' if param.isClosure }}{{ param.typeName.unwrappedTypeName }}{{ ')' if param.isClosure }}?{% endfor %}
var {% call swiftifyMethodName method.selectorName %}ReceivedInvocations{% for param in method.parameters %}: [{{ '(' if param.isClosure }}{{ param.typeName.unwrappedTypeName }}{{ ')' if param.isClosure }}{%if param.typeName.isOptional%}?{%endif%}]{% endfor %} = []
{% elif not method.parameters.count == 0 %}
var {% call swiftifyMethodName method.selectorName %}ReceivedArguments: ({% for param in method.parameters %}{{ param.name }}: {% call parameterType param %}{{ ', ' if not forloop.last }}{% endfor %})?
var {% call swiftifyMethodName method.selectorName %}ReceivedInvocations: [({% for param in method.parameters %}{{ param.name }}: {% call parameterType param %}{{ ', ' if not forloop.last }}{% endfor %})] = []
{% endif %}
{% if not method.returnTypeName.isVoid and not method.isInitializer %}
var {% call swiftifyMethodName method.selectorName %}ReturnValue: {% call methodReturnType method %}{{ '!' if not method.isOptionalReturnType }}
{% endif %}
{% call methodClosureDeclaration method %}
{% if method.isInitializer %}
required {{ method.name }} {
{% call methodReceivedParameters method %}
{% call methodClosureName method %}?({% call methodClosureCallParametersFromArguments method %})
}
{% else %}
func {{ method.name }}{{ ' throws' if method.throws }}{% if not method.returnTypeName.isVoid %} -> {{ method.returnTypeName }}{% endif %} {
{% if method.throws %}
{% call methodThrowableErrorUsage method %}
{% endif %}
{% call swiftifyMethodName method.selectorName %}CalledCount += 1
{% call methodReceivedParameters method %}
{% if method.returnTypeName.isVoid %}
{% if method.throws %}try {% endif %}{% call methodClosureName method %}?({% call methodClosureCallParametersFromArguments method %})
{% else %}
{% call callClosure method %}
{% endif %}
}
{% endif %}
{% endmacro %}
{% macro mockOptionalVariable variable %}
var {% call mockedVariableName variable %}: {{ variable.typeName }}
{% endmacro %}
{% macro mockNonOptionalArrayOrDictionaryVariable variable %}
var {% call mockedVariableName variable %}: {{ variable.typeName }} = {% if variable.isArray %}[]{% elif variable.isDictionary %}[:]{% endif %}
{% endmacro %}
{% macro mockNonOptionalVariable variable %}
var {% call mockedVariableName variable %}: {{ variable.typeName }} {
get {
return {% call underlyingMockedVariableName variable %}
}
set(value) {
{% call underlyingMockedVariableName variable %} = value
}
}
var {% call underlyingMockedVariableName variable %}: {{ variable.typeName }}!
{% endmacro %}
{% macro underlyingMockedVariableName variable %}underlying{{ variable.name|upperFirstLetter }}{% endmacro %}
{% macro mockedVariableName variable %}{{ variable.name }}{% endmacro %}
{% for type in types.protocols where type.based.AutoMockable or type|annotated:"AutoMockable" %}{% if type.name != "AutoMockable" %}
class Mock{{ type.name }}: {{ type.name }} {
{% for variable in type.allVariables|!definedInExtension %}
{% if variable.isOptional %}{% call mockOptionalVariable variable %}{% elif variable.isArray or variable.isDictionary %}{% call mockNonOptionalArrayOrDictionaryVariable variable %}{% else %}{% call mockNonOptionalVariable variable %}{% endif %}
{% endfor %}
{% for method in type.allMethods|!definedInExtension %}
{% call mockMethod method %}
{% endfor %}
}
{% endif %}{% endfor %}

View File

@ -174,6 +174,8 @@ targets:
- "**/TestHelpers/*"
- "**/Fixtures/*"
- "**/__Snapshots__/*"
- "**/Sourcery/Generated/*"
- "**/sourcery.yml"
- path: AuthenticatorShared
includes:
- "**/__Snapshots__/*"
@ -184,6 +186,8 @@ targets:
optional: true
- path: AuthenticatorShared/Core/Vault/Services/Importers/Support/Generated/GoogleAuth.pb.swift
optional: true
- path: AuthenticatorShared/Sourcery/sourcery.yml
buildPhase: none
dependencies:
- target: BitwardenKit/AuthenticatorBridgeKit
- package: BitwardenSdk
@ -210,6 +214,15 @@ targets:
$SRCROOT/AuthenticatorShared/Core/Vault/Services/Importers/Support/GoogleAuth.proto
outputFiles:
- $(SRCROOT)/AuthenticatorShared/Core/Vault/Services/Importers/Support/Generated/GoogleAuth.pb.swift
- name: Sourcery
script: |
if [[ ! "$PATH" =~ "/opt/homebrew/bin" ]]; then
PATH="/opt/homebrew/bin:$PATH"
fi
mint run sourcery --config AuthenticatorShared/Sourcery/sourcery.yml
basedOnDependencyAnalysis: false
outputFiles:
- $(SRCROOT)/AuthenticatorShared/Sourcery/Generated/AutoMockable.generated.swift
AuthenticatorSharedTests:
type: bundle.unit-test
platform: iOS
@ -225,6 +238,10 @@ targets:
- "**/TestHelpers/*"
- "**/Fixtures/*"
- path: GlobalTestHelpers-bwa
- path: AuthenticatorShared/Sourcery/Generated
optional: true
- path: AuthenticatorShared/Sourcery/Generated/AutoMockable.generated.swift
optional: true
dependencies:
- target: Authenticator
- target: AuthenticatorShared

View File

@ -362,6 +362,8 @@ targets:
- "**/TestHelpers/*"
- "**/Fixtures/*"
- "**/__Snapshots__/*"
- "**/Sourcery/Generated/*"
- "**/sourcery.yml"
- path: BitwardenShared
includes:
- "**/__Snapshots__/*"
@ -373,6 +375,8 @@ targets:
- path: BitwardenShared/UI/Platform/Application/Support/Generated/Localizations.swift
optional: true
- path: BitwardenWatchShared
- path: BitwardenShared/Sourcery/sourcery.yml
buildPhase: none
dependencies:
- package: BitwardenSdk
- package: SwiftUIIntrospect
@ -391,6 +395,15 @@ targets:
- $(SRCROOT)/BitwardenShared/UI/Platform/Application/Support/Generated/Assets.swift
- $(SRCROOT)/BitwardenShared/UI/Platform/Application/Support/Generated/Fonts.swift
- $(SRCROOT)/BitwardenShared/UI/Platform/Application/Support/Generated/Localizations.swift
- name: Sourcery
script: |
if [[ ! "$PATH" =~ "/opt/homebrew/bin" ]]; then
PATH="/opt/homebrew/bin:$PATH"
fi
mint run sourcery --config BitwardenShared/Sourcery/sourcery.yml
basedOnDependencyAnalysis: false
outputFiles:
- $(SRCROOT)/BitwardenShared/Sourcery/Generated/AutoMockable.generated.swift
BitwardenSharedTests:
type: bundle.unit-test
platform: iOS
@ -409,6 +422,10 @@ targets:
- "**/TestHelpers/*"
- "**/Fixtures/*"
- path: GlobalTestHelpers
- path: BitwardenShared/Sourcery/Generated
optional: true
- path: BitwardenShared/Sourcery/Generated/AutoMockable.generated.swift
optional: true
dependencies:
- target: Bitwarden
- target: BitwardenShared