From 447cc4cd267a277b120b66de87b0d567aa453d54 Mon Sep 17 00:00:00 2001 From: Nick Randall Date: Tue, 14 Apr 2026 13:41:39 +0200 Subject: [PATCH] Replace path string literals with pre-computed hashes --- ...ndencyProviderRegistrationSerializer.swift | 2 +- .../Serializers/OutputSerializer.swift | 4 +- .../Models/DependencyProvider.swift | 13 ++++ .../DependencyGraphExporterTests.swift | 12 ++-- ...ependencyProviderSerializerTaskTests.swift | 12 ++-- ...uginizedDependencyGraphExporterTests.swift | 16 ++--- NeedleFoundation.xcodeproj/project.pbxproj | 8 +++ .../Internal/DependencyProviderRegistry.swift | 38 +++++++++--- .../Internal/StableFNVHasher.swift | 59 +++++++++++++++++++ .../DependencyProviderRegistryTests.swift | 16 +++++ .../StableFNVHasherTests.swift | 57 ++++++++++++++++++ 11 files changed, 205 insertions(+), 32 deletions(-) create mode 100644 Sources/NeedleFoundation/Internal/StableFNVHasher.swift create mode 100644 Tests/NeedleFoundationTests/StableFNVHasherTests.swift diff --git a/Generator/Sources/NeedleFramework/Generating/Serializers/DependencyProviderRegistrationSerializer.swift b/Generator/Sources/NeedleFramework/Generating/Serializers/DependencyProviderRegistrationSerializer.swift index ba99edc2..2cba12b9 100644 --- a/Generator/Sources/NeedleFramework/Generating/Serializers/DependencyProviderRegistrationSerializer.swift +++ b/Generator/Sources/NeedleFramework/Generating/Serializers/DependencyProviderRegistrationSerializer.swift @@ -36,7 +36,7 @@ class DependencyProviderRegistrationSerializer: Serializer { func serialize() -> String { let factoryName = provider.isEmptyDependency ? "factoryEmptyDependencyProvider" : factoryFuncNameSerializer.serialize() return """ - registerProviderFactory("\(provider.unprocessed.pathString)", \(factoryName))\n + registerProviderFactory(\(provider.unprocessed.pathHash), \(factoryName))\n """ } diff --git a/Generator/Sources/NeedleFramework/Generating/Serializers/OutputSerializer.swift b/Generator/Sources/NeedleFramework/Generating/Serializers/OutputSerializer.swift index 023b16b7..41bcade1 100644 --- a/Generator/Sources/NeedleFramework/Generating/Serializers/OutputSerializer.swift +++ b/Generator/Sources/NeedleFramework/Generating/Serializers/OutputSerializer.swift @@ -137,8 +137,8 @@ class OutputSerializer: Serializer { } // MARK: - Registration - private func registerProviderFactory(_ componentPath: String, _ factory: @escaping (NeedleFoundation.Scope) -> AnyObject) { - __DependencyProviderRegistry.instance.registerDependencyProviderFactory(for: componentPath, factory) + private func registerProviderFactory(_ pathHash: Int, _ factory: @escaping (NeedleFoundation.Scope) -> AnyObject) { + __DependencyProviderRegistry.instance.registerDependencyProviderFactory(forPathHash: pathHash, factory) } #if !NEEDLE_DYNAMIC diff --git a/Generator/Sources/NeedleFramework/Models/DependencyProvider.swift b/Generator/Sources/NeedleFramework/Models/DependencyProvider.swift index cc6e3a75..83fca996 100644 --- a/Generator/Sources/NeedleFramework/Models/DependencyProvider.swift +++ b/Generator/Sources/NeedleFramework/Models/DependencyProvider.swift @@ -32,6 +32,19 @@ struct DependencyProvider { } .joined(separator: "->") } + + /// Stable FNV-1a hash of the path string. This must match the hash + /// computed by `StableFNVHasher` at runtime. + var pathHash: Int { + let offsetBasis: UInt64 = 0xcbf29ce484222325 + let prime: UInt64 = 0x100000001b3 + var hash = offsetBasis + for byte in pathString.utf8 { + hash ^= UInt64(byte) + hash &*= prime + } + return Int(bitPattern: UInt(hash)) + } } /// The data model representing a dependency provider to be generated for a diff --git a/Generator/Tests/NeedleFrameworkTests/Generating/DependencyGraphExporterTests.swift b/Generator/Tests/NeedleFrameworkTests/Generating/DependencyGraphExporterTests.swift index d882170c..b96eb592 100644 --- a/Generator/Tests/NeedleFrameworkTests/Generating/DependencyGraphExporterTests.swift +++ b/Generator/Tests/NeedleFrameworkTests/Generating/DependencyGraphExporterTests.swift @@ -50,22 +50,22 @@ class DependencyGraphExporterTests: AbstractGeneratorTests { XCTAssertTrue(generated.contains("import UIKit")) XCTAssertTrue(generated.contains("// MARK: - Registration")) XCTAssertTrue(generated.contains(""" - registerProviderFactory(\"^->RootComponent->LoggedInComponent->GameComponent\", factorycf9c02c4def4e3d508816cd03d3cf415b70dfb0e) + registerProviderFactory(-322842588535013842, factorycf9c02c4def4e3d508816cd03d3cf415b70dfb0e) """)) XCTAssertTrue(generated.contains(""" - registerProviderFactory(\"^->RootComponent->LoggedInComponent->GameComponent->ScoreSheetComponent\", factory3f7d60e2119708f293bac0d8c882e1e0d9b5eda1) + registerProviderFactory(3176341359983622439, factory3f7d60e2119708f293bac0d8c882e1e0d9b5eda1) """)) XCTAssertTrue(generated.contains(""" - registerProviderFactory(\"^->RootComponent->LoggedInComponent->ScoreSheetComponent\", factory3f7d60e2119708f293ba0b20504d5a9e5588d7b3) + registerProviderFactory(-8216646763020093181, factory3f7d60e2119708f293ba0b20504d5a9e5588d7b3) """)) XCTAssertTrue(generated.contains(""" - registerProviderFactory(\"^->RootComponent->LoggedOutComponent\", factory1434ff4463106e5c4f1bb3a8f24c1d289f2c0f2e) + registerProviderFactory(6062161635131552075, factory1434ff4463106e5c4f1bb3a8f24c1d289f2c0f2e) """)) XCTAssertTrue(generated.contains(""" - registerProviderFactory(\"^->RootComponent->LoggedInComponent\", factory2d08e87342cecea575b3e3b0c44298fc1c149afb) + registerProviderFactory(-860435685645118366, factory2d08e87342cecea575b3e3b0c44298fc1c149afb) """)) XCTAssertTrue(generated.contains(""" - registerProviderFactory(\"^->RootComponent\", factoryEmptyDependencyProvider) + registerProviderFactory(-3171806194175578407, factoryEmptyDependencyProvider) """)) XCTAssertTrue(generated.contains("// MARK: - Traversal Helpers")) XCTAssertTrue(generated.contains(""" diff --git a/Generator/Tests/NeedleFrameworkTests/Generating/DependencyProviderSerializerTaskTests.swift b/Generator/Tests/NeedleFrameworkTests/Generating/DependencyProviderSerializerTaskTests.swift index 83160124..57526815 100644 --- a/Generator/Tests/NeedleFrameworkTests/Generating/DependencyProviderSerializerTaskTests.swift +++ b/Generator/Tests/NeedleFrameworkTests/Generating/DependencyProviderSerializerTaskTests.swift @@ -40,7 +40,7 @@ class DependencyProviderSerializerTaskTests: AbstractGeneratorTests { switch provider.unprocessed.pathString { case "^->RootComponent->LoggedInComponent->GameComponent": XCTAssertEqual(serializedProviders[1].registration, """ -registerProviderFactory("^->RootComponent->LoggedInComponent->GameComponent", factorycf9c02c4def4e3d508816cd03d3cf415b70dfb0e) +registerProviderFactory(-322842588535013842, factorycf9c02c4def4e3d508816cd03d3cf415b70dfb0e) """) XCTAssertEqual(serializedProviders[1].content, """ @@ -52,7 +52,7 @@ private func factorycf9c02c4def4e3d508816cd03d3cf415b70dfb0e(_ component: Needle """) case "^->RootComponent->LoggedInComponent->GameComponent->ScoreSheetComponent": XCTAssertEqual(serializedProviders[1].registration, """ -registerProviderFactory("^->RootComponent->LoggedInComponent->GameComponent->ScoreSheetComponent", factory3f7d60e2119708f293bac0d8c882e1e0d9b5eda1) +registerProviderFactory(3176341359983622439, factory3f7d60e2119708f293bac0d8c882e1e0d9b5eda1) """) XCTAssertEqual(serializedProviders[1].content, """ @@ -64,7 +64,7 @@ private func factory3f7d60e2119708f293bac0d8c882e1e0d9b5eda1(_ component: Needle """) case "^->RootComponent->LoggedInComponent->ScoreSheetComponent": XCTAssertEqual(serializedProviders[1].registration, """ -registerProviderFactory("^->RootComponent->LoggedInComponent->ScoreSheetComponent", factory62cd15b035cb1b1ab3e00b20504d5a9e5588d7b3) +registerProviderFactory(-8216646763020093181, factory62cd15b035cb1b1ab3e00b20504d5a9e5588d7b3) """) XCTAssertEqual(serializedProviders[1].content, """ @@ -76,7 +76,7 @@ private func factory62cd15b035cb1b1ab3e00b20504d5a9e5588d7b3(_ component: Needle """) case "^->RootComponent->LoggedOutComponent": XCTAssertEqual(serializedProviders[1].registration, """ -registerProviderFactory("^->RootComponent->LoggedOutComponent", factory1434ff4463106e5c4f1bb3a8f24c1d289f2c0f2e) +registerProviderFactory(6062161635131552075, factory1434ff4463106e5c4f1bb3a8f24c1d289f2c0f2e) """) XCTAssertEqual(serializedProviders[1].content, """ @@ -88,12 +88,12 @@ private func factory1434ff4463106e5c4f1bb3a8f24c1d289f2c0f2e(_ component: Needle """) case "^->RootComponent->LoggedInComponent": XCTAssertEqual(serializedProviders[1].registration, """ -registerProviderFactory("^->RootComponent->LoggedInComponent", factory2d08e87342cecea575b3e3b0c44298fc1c149afb) +registerProviderFactory(-860435685645118366, factory2d08e87342cecea575b3e3b0c44298fc1c149afb) """) case "^->RootComponent": XCTAssertEqual(serializedProviders[0].registration, """ -registerProviderFactory("^->RootComponent", factoryEmptyDependencyProvider) +registerProviderFactory(-3171806194175578407, factoryEmptyDependencyProvider) """) default: diff --git a/Generator/Tests/NeedleFrameworkTests/Generating/Pluginized/PluginizedDependencyGraphExporterTests.swift b/Generator/Tests/NeedleFrameworkTests/Generating/Pluginized/PluginizedDependencyGraphExporterTests.swift index c741b6b6..7cfad0e6 100644 --- a/Generator/Tests/NeedleFrameworkTests/Generating/Pluginized/PluginizedDependencyGraphExporterTests.swift +++ b/Generator/Tests/NeedleFrameworkTests/Generating/Pluginized/PluginizedDependencyGraphExporterTests.swift @@ -52,28 +52,28 @@ class PluginizedDependencyGraphExporterTests: AbstractPluginizedGeneratorTests { XCTAssertTrue(generated.contains("private let needleDependenciesHash : String? = \"86deb40d0ec1c9fc9fd5e5e8fc17a167\"")) XCTAssertTrue(generated.contains("// MARK: - Registration")) XCTAssertTrue(generated.contains(""" - registerProviderFactory(\"^->RootComponent->LoggedOutComponent\", factory1434ff4463106e5c4f1bb3a8f24c1d289f2c0f2e) + registerProviderFactory(6062161635131552075, factory1434ff4463106e5c4f1bb3a8f24c1d289f2c0f2e) """)) XCTAssertTrue(generated.contains(""" - registerProviderFactory("^->RootComponent", factoryEmptyDependencyProvider) + registerProviderFactory(-3171806194175578407, factoryEmptyDependencyProvider) """)) XCTAssertTrue(generated.contains(""" - registerProviderFactory(\"^->RootComponent->LoggedInComponent->GameComponent->GameNonCoreComponent->ScoreSheetComponent\", factoryb11b7d1dec7e3c9b3dca49b41e44e0ed6a6f8eaf) + registerProviderFactory(7090139472367530245, factoryb11b7d1dec7e3c9b3dca49b41e44e0ed6a6f8eaf) """)) XCTAssertTrue(generated.contains(""" - registerProviderFactory(\"^->RootComponent->LoggedInComponent->LoggedInNonCoreComponent->ScoreSheetComponent\", factory3306c50e89e2421d0b0c65d055996113f3c13de1) + registerProviderFactory(-6719425751305065620, factory3306c50e89e2421d0b0c65d055996113f3c13de1) """)) XCTAssertTrue(generated.contains(""" - registerProviderFactory(\"^->RootComponent->LoggedInComponent->GameComponent->GameNonCoreComponent\", factoryEmptyDependencyProvider) + registerProviderFactory(5898069836772964084, factoryEmptyDependencyProvider) """)) XCTAssertTrue(generated.contains(""" - registerProviderFactory(\"^->RootComponent->LoggedInComponent->LoggedInNonCoreComponent\", factoryEmptyDependencyProvider) + registerProviderFactory(-7553811199512117245, factoryEmptyDependencyProvider) """)) XCTAssertTrue(generated.contains(""" - registerProviderFactory(\"^->RootComponent->LoggedInComponent->GameComponent\", factorycf9c02c4def4e3d508816cd03d3cf415b70dfb0e) + registerProviderFactory(-322842588535013842, factorycf9c02c4def4e3d508816cd03d3cf415b70dfb0e) """)) XCTAssertTrue(generated.contains(""" - registerProviderFactory(\"^->RootComponent->LoggedInComponent\", factoryEmptyDependencyProvider) + registerProviderFactory(-860435685645118366, factoryEmptyDependencyProvider) """)) XCTAssertTrue(generated.contains(""" __PluginExtensionProviderRegistry.instance.registerPluginExtensionProviderFactory(for: \"GameComponent\") { component in diff --git a/NeedleFoundation.xcodeproj/project.pbxproj b/NeedleFoundation.xcodeproj/project.pbxproj index e9510a8e..48daba23 100644 --- a/NeedleFoundation.xcodeproj/project.pbxproj +++ b/NeedleFoundation.xcodeproj/project.pbxproj @@ -22,6 +22,8 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + OBJ_89 /* StableFNVHasher.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_90 /* StableFNVHasher.swift */; }; + OBJ_91 /* StableFNVHasherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_92 /* StableFNVHasherTests.swift */; }; OBJ_42 /* Bootstrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* Bootstrap.swift */; }; OBJ_43 /* Component.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* Component.swift */; }; OBJ_44 /* DependencyProviderRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* DependencyProviderRegistry.swift */; }; @@ -78,6 +80,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + OBJ_90 /* StableFNVHasher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StableFNVHasher.swift; sourceTree = ""; }; + OBJ_92 /* StableFNVHasherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StableFNVHasherTests.swift; sourceTree = ""; }; "NeedleFoundation::NeedleFoundation::Product" /* NeedleFoundation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = NeedleFoundation.framework; sourceTree = BUILT_PRODUCTS_DIR; }; "NeedleFoundation::NeedleFoundationTest::Product" /* NeedleFoundationTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = NeedleFoundationTest.framework; sourceTree = BUILT_PRODUCTS_DIR; }; "NeedleFoundation::NeedleFoundationTestTests::Product" /* NeedleFoundationTestTests.xctest */ = {isa = PBXFileReference; lastKnownFileType = file; path = NeedleFoundationTestTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -138,6 +142,7 @@ isa = PBXGroup; children = ( OBJ_12 /* DependencyProviderRegistry.swift */, + OBJ_90 /* StableFNVHasher.swift */, ); path = Internal; sourceTree = ""; @@ -185,6 +190,7 @@ OBJ_23 /* ComponentTests.swift */, OBJ_24 /* DependencyProviderRegistryTests.swift */, OBJ_25 /* Pluginized */, + OBJ_92 /* StableFNVHasherTests.swift */, ); name = NeedleFoundationTests; path = Tests/NeedleFoundationTests; @@ -372,6 +378,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 0; files = ( + OBJ_89 /* StableFNVHasher.swift in Sources */, OBJ_42 /* Bootstrap.swift in Sources */, OBJ_43 /* Component.swift in Sources */, OBJ_44 /* DependencyProviderRegistry.swift in Sources */, @@ -413,6 +420,7 @@ OBJ_83 /* ComponentTests.swift in Sources */, OBJ_84 /* DependencyProviderRegistryTests.swift in Sources */, OBJ_85 /* PluginizedComponentTests.swift in Sources */, + OBJ_91 /* StableFNVHasherTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/NeedleFoundation/Internal/DependencyProviderRegistry.swift b/Sources/NeedleFoundation/Internal/DependencyProviderRegistry.swift index e1c039d1..ea9c7b5b 100644 --- a/Sources/NeedleFoundation/Internal/DependencyProviderRegistry.swift +++ b/Sources/NeedleFoundation/Internal/DependencyProviderRegistry.swift @@ -31,6 +31,23 @@ public class __DependencyProviderRegistry { /// The singleton instance. public static let instance = __DependencyProviderRegistry() + /// Register the given factory closure with a pre-computed path hash. + /// + /// - note: This method is thread-safe. + /// - parameter pathHash: The stable hash of the component path, + /// computed by the Needle code generator. + /// - parameter dependencyProviderFactory: The closure that takes in a + /// component to be injected and returns a provider instance that conforms + /// to the component's dependency protocol. + public func registerDependencyProviderFactory(forPathHash pathHash: Int, _ dependencyProviderFactory: @escaping (Scope) -> AnyObject) { + providerFactoryLock.lock() + defer { + providerFactoryLock.unlock() + } + + providerFactories[pathHash] = dependencyProviderFactory + } + /// Register the given factory closure with given key. /// /// - note: This method is thread-safe. @@ -45,7 +62,8 @@ public class __DependencyProviderRegistry { providerFactoryLock.unlock() } - providerFactories[componentPath.hashValue] = dependencyProviderFactory + let key = StableFNVHasher.hash(componentPath) + providerFactories[key] = dependencyProviderFactory } /// Unregister the given factory closure with given key. @@ -58,9 +76,10 @@ public class __DependencyProviderRegistry { defer { providerFactoryLock.unlock() } - providerFactories.removeValue(forKey: componentPath.hashValue) + let key = StableFNVHasher.hash(componentPath) + providerFactories.removeValue(forKey: key) } - + /// Retrieve the dependency provider for the given componentpath. /// /// - parameter componentpath: The component path that uses the returned dependency provider. @@ -70,12 +89,13 @@ public class __DependencyProviderRegistry { defer { providerFactoryLock.unlock() } - - return providerFactories[componentPath.hashValue] + + let key = StableFNVHasher.hash(componentPath) + return providerFactories[key] } - + // MARK: - Internal - + /// Retrieve the dependency provider for the given component and its parent. /// /// - parameter component: The component that uses the returned dependency provider. @@ -86,8 +106,8 @@ public class __DependencyProviderRegistry { providerFactoryLock.unlock() } - let pathString = component.path.joined(separator: "->") - if let factory = providerFactories[pathString.hashValue] { + let key = StableFNVHasher.hash(component.path, separator: "->") + if let factory = providerFactories[key] { return factory(component) } else { // This case should never occur with properly generated Needle code. diff --git a/Sources/NeedleFoundation/Internal/StableFNVHasher.swift b/Sources/NeedleFoundation/Internal/StableFNVHasher.swift new file mode 100644 index 00000000..c4893b8c --- /dev/null +++ b/Sources/NeedleFoundation/Internal/StableFNVHasher.swift @@ -0,0 +1,59 @@ +// +// Copyright (c) 2026. Uber Technologies +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +/// A deterministic hasher using FNV-1a (64-bit). Unlike Swift's `Hasher`, +/// this produces the same output across processes and platforms, so hashes +/// can be pre-computed at build time by the Needle code generator. +struct StableFNVHasher { + private static let offsetBasis: UInt64 = 0xcbf29ce484222325 + private static let prime: UInt64 = 0x100000001b3 + + private var hash: UInt64 = offsetBasis + + /// Hash a single string. + static func hash(_ string: String) -> Int { + var hasher = StableFNVHasher() + hasher.combine(string) + return hasher.finalize() + } + + /// Hash an array of strings as if they were joined by `separator`. + /// Produces the same result as `hash(segments.joined(separator: separator))` + /// without allocating the joined string. + static func hash(_ segments: [String], separator: String) -> Int { + var hasher = StableFNVHasher() + var first = true + for segment in segments { + if !first { + hasher.combine(separator) + } + first = false + hasher.combine(segment) + } + return hasher.finalize() + } + + private mutating func combine(_ string: String) { + for byte in string.utf8 { + hash ^= UInt64(byte) + hash &*= Self.prime + } + } + + private func finalize() -> Int { + Int(bitPattern: UInt(hash)) + } +} diff --git a/Tests/NeedleFoundationTests/DependencyProviderRegistryTests.swift b/Tests/NeedleFoundationTests/DependencyProviderRegistryTests.swift index 88fee3c1..9d11afec 100644 --- a/Tests/NeedleFoundationTests/DependencyProviderRegistryTests.swift +++ b/Tests/NeedleFoundationTests/DependencyProviderRegistryTests.swift @@ -42,6 +42,22 @@ class DependencyProviderRegistryTests: XCTestCase { XCTAssertTrue(expectedProvider === actualProvider) XCTAssertTrue(appComponent.rootComponent.dependency === expectedProvider) } + + func test_registerProviderFactoryForPathHash_verifyRetrievingProvider() { + let expectedProvider = MockRootDependencyProvider() + + let path = ["^", "MockAppComponent", "MockRootComponent"] + let pathHash = StableFNVHasher.hash(path, separator: "->") + __DependencyProviderRegistry.instance.registerDependencyProviderFactory(forPathHash: pathHash) { (component: Scope) -> AnyObject in + return expectedProvider + } + + let appComponent = MockAppComponent() + let actualProvider = __DependencyProviderRegistry.instance.dependencyProvider(for: appComponent.rootComponent) + + XCTAssertTrue(expectedProvider === actualProvider) + XCTAssertTrue(appComponent.rootComponent.dependency === expectedProvider) + } } class MockAppComponent: BootstrapComponent { diff --git a/Tests/NeedleFoundationTests/StableFNVHasherTests.swift b/Tests/NeedleFoundationTests/StableFNVHasherTests.swift new file mode 100644 index 00000000..fb6c6f69 --- /dev/null +++ b/Tests/NeedleFoundationTests/StableFNVHasherTests.swift @@ -0,0 +1,57 @@ +// +// Copyright (c) 2026. Uber Technologies +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import NeedleFoundation + +class StableFNVHasherTests: XCTestCase { + + // These expected values must match the hashes produced by the Needle code + // generator (DependencyProvider.pathHash). If this test fails, the runtime + // and codegen FNV implementations have diverged. + + func test_stablePathHash_matchesCodegenHashes() { + let cases: [(path: String, expected: Int)] = [ + ("^->RootComponent", -3171806194175578407), + ("^->RootComponent->LoggedInComponent", -860435685645118366), + ("^->RootComponent->LoggedOutComponent", 6062161635131552075), + ("^->RootComponent->LoggedInComponent->GameComponent", -322842588535013842), + ("^->RootComponent->LoggedInComponent->ScoreSheetComponent", -8216646763020093181), + ("^->RootComponent->LoggedInComponent->GameComponent->ScoreSheetComponent", 3176341359983622439), + ] + + for (path, expected) in cases { + let segments = path.components(separatedBy: "->") + let hash = StableFNVHasher.hash(segments, separator: "->") + XCTAssertEqual(hash, expected, "Hash mismatch for path: \(path)") + } + } + + func test_stablePathHash_matchesSingleStringHash() { + // Hashing segments with separator should produce the same result + // as hashing the joined string directly. + let paths = [ + ["^", "AppComponent", "RootComponent"], + ["^", "AppComponent", "RootComponent", "LoggedInComponent", "GameComponent"], + ] + + for segments in paths { + let joinedHash = StableFNVHasher.hash(segments.joined(separator: "->")) + let segmentHash = StableFNVHasher.hash(segments, separator: "->") + XCTAssertEqual(joinedHash, segmentHash, "Segment hash != joined hash for: \(segments)") + } + } +}