1+ import Foundation
2+ import Testing
3+ @testable @_spi ( Internals) import ScipioKit
4+ import ScipioStorage
5+ import Logging
6+
7+ struct FrameworkProducerTests {
8+
9+ init ( ) {
10+ LoggingSystem . bootstrap { _ in SwiftLogNoOpLogHandler ( ) }
11+ }
12+
13+ @Test func cacheSharing( ) async throws {
14+ let tempDir = FileManager . default. temporaryDirectory
15+ let outputDir = tempDir. appendingPathComponent ( " test-output- \( UUID ( ) . uuidString) " )
16+
17+ try FileManager . default. createDirectory ( at: outputDir, withIntermediateDirectories: true )
18+
19+ defer {
20+ try ? FileManager . default. removeItem ( at: outputDir)
21+ }
22+
23+ // Create mock cache storages
24+ let consumerStorage = MockCacheStorage ( name: " consumer " )
25+ let producer1Storage = MockCacheStorage ( name: " producer1 " )
26+ let producer2Storage = MockCacheStorage ( name: " producer2 " )
27+
28+ // Create cache policies
29+ let consumerPolicy = Runner . Options. CachePolicy (
30+ storage: consumerStorage,
31+ actors: [ . consumer]
32+ )
33+ let producerPolicy1 = Runner . Options. CachePolicy (
34+ storage: producer1Storage,
35+ actors: [ . producer]
36+ )
37+ let producerPolicy2 = Runner . Options. CachePolicy (
38+ storage: producer2Storage,
39+ actors: [ . producer]
40+ )
41+
42+ let cachePolicies = [ consumerPolicy, producerPolicy1, producerPolicy2]
43+
44+ // Use CacheKeyTests/AsRemotePackage fixture to avoid revision detection issues
45+ let testPackagePath = URL ( fileURLWithPath: #filePath)
46+ . deletingLastPathComponent ( )
47+ . appendingPathComponent ( " Resources " )
48+ . appendingPathComponent ( " Fixtures " )
49+ . appendingPathComponent ( " CacheKeyTests " )
50+ . appendingPathComponent ( " AsRemotePackage " )
51+
52+ let descriptionPackage = try await DescriptionPackage (
53+ packageDirectory: testPackagePath,
54+ mode: . prepareDependencies,
55+ onlyUseVersionsFromResolvedFile: false
56+ )
57+
58+ let frameworkProducer = FrameworkProducer (
59+ descriptionPackage: descriptionPackage,
60+ buildOptions: BuildOptions (
61+ buildConfiguration: . debug,
62+ isDebugSymbolsEmbedded: false ,
63+ frameworkType: . dynamic,
64+ sdks: [ . iOS] ,
65+ extraFlags: nil ,
66+ extraBuildParameters: nil ,
67+ enableLibraryEvolution: false ,
68+ keepPublicHeadersStructure: false ,
69+ customFrameworkModuleMapContents: nil ,
70+ stripStaticDWARFSymbols: false
71+ ) ,
72+ buildOptionsMatrix: [ : ] ,
73+ cachePolicies: cachePolicies,
74+ overwrite: false ,
75+ outputDir: outputDir
76+ )
77+
78+ let cacheSystem = CacheSystem ( outputDirectory: outputDir)
79+
80+ // Create a mock cache target using the real package info
81+ let package = try #require(
82+ descriptionPackage
83+ . graph
84+ . allPackages
85+ . values
86+ . first { $0. name == " scipio-testing " }
87+ )
88+ let target = try #require( package . targets. first { $0. name == " ScipioTesting " } )
89+ let buildProduct = BuildProduct ( package : package , target: target)
90+ let mockTarget = CacheSystem . CacheTarget (
91+ buildProduct: buildProduct,
92+ buildOptions: BuildOptions (
93+ buildConfiguration: . debug,
94+ isDebugSymbolsEmbedded: false ,
95+ frameworkType: . dynamic,
96+ sdks: [ . iOS] ,
97+ extraFlags: nil ,
98+ extraBuildParameters: nil ,
99+ enableLibraryEvolution: false ,
100+ keepPublicHeadersStructure: false ,
101+ customFrameworkModuleMapContents: nil ,
102+ stripStaticDWARFSymbols: false
103+ )
104+ )
105+
106+ let restoredTargets : Set < CacheSystem . CacheTarget > = [ mockTarget]
107+ let mockCacheKey = try await cacheSystem. calculateCacheKey ( of: mockTarget)
108+
109+ // Setup initial state: producer1 has cache, producer2 doesn't
110+ try await producer1Storage. setHasCache ( for: mockCacheKey, value: true )
111+ try await producer2Storage. setHasCache ( for: mockCacheKey, value: false )
112+
113+ // Test FrameworkProducer's cache sharing functionality
114+ await frameworkProducer. shareRestoredCachesToProducers ( restoredTargets, cacheSystem: cacheSystem)
115+
116+ let producer1CacheCalls = await producer1Storage. getCacheFrameworkCalls ( )
117+ let producer2CacheCalls = await producer2Storage. getCacheFrameworkCalls ( )
118+
119+ // Verify behavior: producer1 already has cache so no call, producer2 doesn't have cache so gets a call
120+ #expect( producer1CacheCalls. count == 0 , " Producer1 already has cache, so cacheFramework should not be called " )
121+ try #require( producer2CacheCalls. count == 1 , " Producer2 doesn't have cache, so cacheFramework should be called once " )
122+
123+ // Verify the cache call was made with correct parameters
124+ let actualFrameworkPath = producer2CacheCalls [ 0 ] . frameworkPath
125+ let expectedFrameworkPath = outputDir. appendingPathComponent ( buildProduct. frameworkName)
126+ #expect( actualFrameworkPath == expectedFrameworkPath, " Framework path should match " )
127+
128+ // Verify the cache call was made with the correct cache key
129+ let expectedCacheKey = try mockCacheKey. calculateChecksum ( )
130+ #expect( producer2CacheCalls [ 0 ] . cacheKey == expectedCacheKey, " Cache key should match the actual cache key used " )
131+ }
132+ }
133+
134+ // MARK: - Mock Classes
135+
136+ private struct MockCacheKey : CacheKey {
137+ let targetName : String
138+
139+ func calculateChecksum( ) throws -> String {
140+ return " mock-checksum- \( targetName) "
141+ }
142+ }
143+
144+ // MARK: - Mock Cache Storage
145+
146+ private actor MockCacheStorage : CacheStorage {
147+ let displayName : String
148+ let parallelNumber : Int ? = 1
149+
150+ private var hasCacheMap : [ String : Bool ] = [ : ]
151+ private var cacheFrameworkCalls : [ ( frameworkPath: URL , cacheKey: String ) ] = [ ]
152+
153+ init ( name: String ) {
154+ self . displayName = name
155+ }
156+
157+ func existsValidCache( for cacheKey: some CacheKey ) async throws -> Bool {
158+ let keyString = try cacheKey. calculateChecksum ( )
159+ return hasCacheMap [ keyString] ?? false
160+ }
161+
162+ func fetchArtifacts( for cacheKey: some CacheKey , to destinationDir: URL ) async throws {
163+ // Mock implementation - no-op
164+ }
165+
166+ func cacheFramework( _ frameworkPath: URL , for cacheKey: some CacheKey ) async throws {
167+ let keyString = try cacheKey. calculateChecksum ( )
168+ let call = ( frameworkPath: frameworkPath, cacheKey: keyString)
169+ cacheFrameworkCalls. append ( call)
170+ }
171+
172+ // Test helper methods
173+ func setHasCache( for cacheKey: some CacheKey , value: Bool ) async throws {
174+ let keyString = try cacheKey. calculateChecksum ( )
175+ hasCacheMap [ keyString] = value
176+ }
177+
178+ func getCacheFrameworkCalls( ) -> [ ( frameworkPath: URL , cacheKey: String ) ] {
179+ return cacheFrameworkCalls
180+ }
181+ }
0 commit comments