-
Notifications
You must be signed in to change notification settings - Fork 590
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add the basic mechanics for plugin management
- Loading branch information
Showing
6 changed files
with
237 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
19 changes: 19 additions & 0 deletions
19
Sources/SnapshotTesting/Documentation.docc/Articles/Plugins.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Plugins | ||
|
||
SnapshotTesting offers a wide range of built-in snapshot strategies, and over the years, third-party developers have introduced new ones. However, when there’s a need for functionality that spans multiple strategies, plugins become essential. | ||
|
||
## Overview | ||
|
||
Plugins provide greater flexibility and extensibility by enabling shared behavior across different strategies without the need to duplicate code or modify each strategy individually. They can be dynamically discovered, registered, and executed at runtime, making them ideal for adding new functionality without altering the core system. This architecture promotes modularity and decoupling, allowing features to be easily added or swapped out without impacting existing functionality. | ||
|
||
### Plugin architecture | ||
|
||
The plugin architecture is designed around the concept of **dynamic discovery and registration**. Plugins conform to specific protocols, such as `SnapshotTestingPlugin`, and are registered automatically by the `PluginRegistry`. This registry manages plugin instances, allowing them to be retrieved by identifier or filtered by the protocols they conform to. | ||
|
||
The primary components of the plugin system include: | ||
|
||
- **Plugin Protocols**: Define the behavior that plugins must implement. | ||
- **PluginRegistry**: Manages plugin discovery, registration, and retrieval. | ||
- **Objective-C Runtime Integration**: Allows automatic discovery of plugins that conform to specific protocols. | ||
|
||
The `PluginRegistry` is a singleton that registers plugins during its initialization. Plugins can be retrieved by their identifier or cast to specific types, allowing flexible interaction. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
#if canImport(SwiftUI) && canImport(ObjectiveC) | ||
import Foundation | ||
import ObjectiveC.runtime | ||
import SnapshotTestingPlugin | ||
|
||
/// A singleton class responsible for managing and registering plugins conforming to the `SnapshotTestingPlugin` protocol. | ||
/// | ||
/// The `PluginRegistry` automatically discovers and registers classes conforming to the `SnapshotTestingPlugin` protocol | ||
/// within the Objective-C runtime. It allows retrieval of specific plugins by identifier, access to all registered plugins, | ||
/// and filtering of plugins that conform to the `ImageSerialization` protocol. | ||
public class PluginRegistry { | ||
|
||
/// Shared singleton instance of `PluginRegistry`. | ||
private static let shared = PluginRegistry() | ||
|
||
/// Dictionary holding registered plugins, keyed by their identifier. | ||
private var plugins: [String: AnyObject] = [:] | ||
|
||
/// Private initializer enforcing the singleton pattern. | ||
/// | ||
/// Automatically triggers `automaticPluginRegistration()` to discover and register plugins. | ||
private init() { | ||
defer { automaticPluginRegistration() } | ||
} | ||
|
||
// MARK: - Public Methods | ||
|
||
/// Registers a plugin. | ||
/// | ||
/// - Parameter plugin: An instance conforming to `SnapshotTestingPlugin`. | ||
public static func registerPlugin(_ plugin: SnapshotTestingPlugin) { | ||
PluginRegistry.shared.registerPlugin(plugin) | ||
} | ||
|
||
/// Retrieves a plugin by its identifier, casting it to the specified type. | ||
/// | ||
/// - Parameter identifier: The unique identifier for the plugin. | ||
/// - Returns: The plugin instance cast to `Output` if found and castable, otherwise `nil`. | ||
public static func plugin<Output>(for identifier: String) -> Output? { | ||
PluginRegistry.shared.plugin(for: identifier) | ||
} | ||
|
||
/// Returns all registered plugins cast to the specified type. | ||
/// | ||
/// - Returns: An array of all registered plugins that can be cast to `Output`. | ||
public static func allPlugins<Output>() -> [Output] { | ||
PluginRegistry.shared.allPlugins() | ||
} | ||
|
||
// MARK: - Internal Methods | ||
|
||
/// Registers a plugin. | ||
/// | ||
/// - Parameter plugin: An instance conforming to `SnapshotTestingPlugin`. | ||
private func registerPlugin(_ plugin: SnapshotTestingPlugin) { | ||
plugins[type(of: plugin).identifier] = plugin | ||
} | ||
|
||
/// Retrieves a plugin by its identifier, casting it to the specified type. | ||
/// | ||
/// - Parameter identifier: The unique identifier for the plugin. | ||
/// - Returns: The plugin instance cast to `Output` if found and castable, otherwise `nil`. | ||
private func plugin<Output>(for identifier: String) -> Output? { | ||
return plugins[identifier] as? Output | ||
} | ||
|
||
/// Returns all registered plugins cast to the specified type. | ||
/// | ||
/// - Returns: An array of all registered plugins that can be cast to `Output`. | ||
private func allPlugins<Output>() -> [Output] { | ||
return Array(plugins.values.compactMap { $0 as? Output }) | ||
} | ||
|
||
/// Discovers and registers all classes conforming to the `SnapshotTestingPlugin` protocol. | ||
/// | ||
/// This method iterates over all Objective-C runtime classes, identifying those that conform to `SnapshotTestingPlugin`, | ||
/// instantiating them, and registering them as plugins. | ||
private func automaticPluginRegistration() { | ||
let classCount = objc_getClassList(nil, 0) | ||
guard classCount > 0 else { return } | ||
|
||
let classes = UnsafeMutablePointer<AnyClass?>.allocate(capacity: Int(classCount)) | ||
defer { classes.deallocate() } | ||
|
||
let autoreleasingClasses = AutoreleasingUnsafeMutablePointer<AnyClass>(classes) | ||
objc_getClassList(autoreleasingClasses, classCount) | ||
|
||
for i in 0..<Int(classCount) { | ||
guard | ||
let someClass = classes[i], | ||
class_conformsToProtocol(someClass, SnapshotTestingPlugin.self), | ||
let pluginType = someClass as? SnapshotTestingPlugin.Type | ||
else { continue } | ||
self.registerPlugin(pluginType.init()) | ||
} | ||
} | ||
|
||
// TEST-ONLY Reset Method | ||
#if DEBUG | ||
internal static func reset() { | ||
shared.plugins.removeAll() | ||
} | ||
|
||
internal static func automaticPluginRegistration() { | ||
shared.automaticPluginRegistration() | ||
} | ||
#endif | ||
} | ||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
#if canImport(Foundation) && canImport(ObjectiveC) | ||
import Foundation | ||
|
||
/// A protocol that defines a plugin for snapshot testing, designed to be used in environments that support Objective-C. | ||
/// | ||
/// The `SnapshotTestingPlugin` protocol is intended to be adopted by classes that provide specific functionality for snapshot testing. | ||
/// It requires each conforming class to have a unique identifier and a parameterless initializer. This protocol is designed to be used in | ||
/// environments where both Foundation and Objective-C are available, making it compatible with Objective-C runtime features. | ||
/// | ||
/// Conforming classes must be marked with `@objc` to ensure compatibility with Objective-C runtime mechanisms. | ||
@objc public protocol SnapshotTestingPlugin { | ||
|
||
/// A unique string identifier for the plugin. | ||
/// | ||
/// Each plugin must provide a static identifier that uniquely distinguishes it from other plugins. This identifier is used | ||
/// to register and retrieve plugins within a registry, ensuring that each plugin can be easily identified and utilized. | ||
static var identifier: String { get } | ||
|
||
/// Initializes a new instance of the plugin. | ||
/// | ||
/// This initializer is required to allow the Objective-C runtime to create instances of the plugin class when registering | ||
/// and utilizing plugins. The initializer must not take any parameters. | ||
init() | ||
} | ||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
#if canImport(SwiftUI) && canImport(ObjectiveC) | ||
import XCTest | ||
import ObjectiveC | ||
@testable import SnapshotTesting | ||
import SnapshotTestingPlugin | ||
|
||
class MockPlugin: NSObject, SnapshotTestingPlugin { | ||
static var identifier: String = "MockPlugin" | ||
|
||
required override init() { | ||
super.init() | ||
} | ||
} | ||
|
||
class AnotherMockPlugin: NSObject, SnapshotTestingPlugin { | ||
static var identifier: String = "AnotherMockPlugin" | ||
|
||
required override init() { | ||
super.init() | ||
} | ||
} | ||
|
||
final class PluginRegistryTests: XCTestCase { | ||
|
||
override func setUp() { | ||
super.setUp() | ||
PluginRegistry.reset() // Reset state before each test | ||
} | ||
|
||
override func tearDown() { | ||
PluginRegistry.reset() // Reset state after each test | ||
super.tearDown() | ||
} | ||
|
||
func testRegisterPlugin() { | ||
// Register a mock plugin | ||
PluginRegistry.registerPlugin(MockPlugin()) | ||
|
||
// Retrieve the plugin by identifier | ||
let retrievedPlugin: MockPlugin? = PluginRegistry.plugin(for: MockPlugin.identifier) | ||
XCTAssertNotNil(retrievedPlugin) | ||
} | ||
|
||
func testRetrieveNonExistentPlugin() { | ||
// Try to retrieve a non-existent plugin | ||
let nonExistentPlugin: MockPlugin? = PluginRegistry.plugin(for: "NonExistentPlugin") | ||
XCTAssertNil(nonExistentPlugin) | ||
} | ||
|
||
func testAllPlugins() { | ||
// Register two mock plugins | ||
PluginRegistry.registerPlugin(MockPlugin()) | ||
PluginRegistry.registerPlugin(AnotherMockPlugin()) | ||
|
||
// Retrieve all plugins | ||
let allPlugins: [SnapshotTestingPlugin] = PluginRegistry.allPlugins() | ||
|
||
XCTAssertEqual(allPlugins.count, 2) | ||
XCTAssertTrue(allPlugins.contains { $0 is MockPlugin }) | ||
XCTAssertTrue(allPlugins.contains { $0 is AnotherMockPlugin }) | ||
} | ||
|
||
func testAutomaticPluginRegistration() { | ||
// Automatically register plugins using the Objective-C runtime | ||
PluginRegistry.automaticPluginRegistration() // Reset state before each test | ||
|
||
// Verify if the mock plugin was automatically registered | ||
let registeredPlugin: MockPlugin? = PluginRegistry.plugin(for: MockPlugin.identifier) | ||
XCTAssertNotNil(registeredPlugin) | ||
} | ||
} | ||
|
||
#endif |