11use assert_fs:: prelude:: * ;
22use predicates:: prelude:: * ;
3+ use soroban_ledger_snapshot:: LedgerSnapshot ;
34use soroban_test:: { AssertExt , TestEnv } ;
45
56#[ test]
@@ -67,7 +68,6 @@ fn snapshot() {
6768 sandbox
6869 . new_assert_cmd ( "snapshot" )
6970 . arg ( "create" )
70- . arg ( "--output=json" )
7171 . arg ( "--address" )
7272 . arg ( & account_a)
7373 . arg ( "--address" )
@@ -84,3 +84,296 @@ fn snapshot() {
8484 . assert ( predicates:: str:: contains ( & contract_b) )
8585 . assert ( predicates:: str:: contains ( & contract_a) . not ( ) ) ;
8686}
87+
88+ #[ test]
89+ #[ allow( clippy:: too_many_lines) ]
90+ fn snapshot_merge ( ) {
91+ let sandbox = & TestEnv :: new ( ) ;
92+
93+ // Create accounts and contracts for two separate snapshots
94+ sandbox
95+ . new_assert_cmd ( "keys" )
96+ . arg ( "generate" )
97+ . arg ( "--fund" )
98+ . arg ( "a" )
99+ . assert ( )
100+ . success ( ) ;
101+ let account_a = sandbox
102+ . new_assert_cmd ( "keys" )
103+ . arg ( "address" )
104+ . arg ( "a" )
105+ . assert ( )
106+ . success ( )
107+ . stdout_as_str ( ) ;
108+
109+ sandbox
110+ . new_assert_cmd ( "keys" )
111+ . arg ( "generate" )
112+ . arg ( "--fund" )
113+ . arg ( "b" )
114+ . assert ( )
115+ . success ( ) ;
116+ let account_b = sandbox
117+ . new_assert_cmd ( "keys" )
118+ . arg ( "address" )
119+ . arg ( "b" )
120+ . assert ( )
121+ . success ( )
122+ . stdout_as_str ( ) ;
123+
124+ let contract_a = sandbox
125+ . new_assert_cmd ( "contract" )
126+ . arg ( "asset" )
127+ . arg ( "deploy" )
128+ . arg ( format ! ( "--asset=A1:{account_a}" ) )
129+ . assert ( )
130+ . success ( )
131+ . stdout_as_str ( ) ;
132+
133+ let contract_b = sandbox
134+ . new_assert_cmd ( "contract" )
135+ . arg ( "asset" )
136+ . arg ( "deploy" )
137+ . arg ( format ! ( "--asset=A2:{account_b}" ) )
138+ . assert ( )
139+ . success ( )
140+ . stdout_as_str ( ) ;
141+
142+ // Wait 8 ledgers for a checkpoint
143+ for i in 1 ..=8 {
144+ sandbox
145+ . new_assert_cmd ( "keys" )
146+ . arg ( "generate" )
147+ . arg ( "--fund" )
148+ . arg ( format ! ( "k{i}" ) )
149+ . assert ( )
150+ . success ( ) ;
151+ }
152+
153+ // Create first snapshot with account_a and contract_a
154+ sandbox
155+ . new_assert_cmd ( "snapshot" )
156+ . arg ( "create" )
157+ . arg ( "--address" )
158+ . arg ( & account_a)
159+ . arg ( "--address" )
160+ . arg ( & contract_a)
161+ . arg ( "--out=snapshot_a.json" )
162+ . assert ( )
163+ . success ( ) ;
164+
165+ // Create second snapshot with account_b and contract_b
166+ sandbox
167+ . new_assert_cmd ( "snapshot" )
168+ . arg ( "create" )
169+ . arg ( "--address" )
170+ . arg ( & account_b)
171+ . arg ( "--address" )
172+ . arg ( & contract_b)
173+ . arg ( "--out=snapshot_b.json" )
174+ . assert ( )
175+ . success ( ) ;
176+
177+ // Merge the two snapshots
178+ sandbox
179+ . new_assert_cmd ( "snapshot" )
180+ . arg ( "merge" )
181+ . arg ( "snapshot_a.json" )
182+ . arg ( "snapshot_b.json" )
183+ . arg ( "--out=merged.json" )
184+ . assert ( )
185+ . success ( ) ;
186+
187+ // Verify the merged snapshot contains all accounts and contracts
188+ sandbox
189+ . dir ( )
190+ . child ( "merged.json" )
191+ . assert ( predicates:: str:: contains ( & account_a) )
192+ . assert ( predicates:: str:: contains ( & account_b) )
193+ . assert ( predicates:: str:: contains ( & contract_a) )
194+ . assert ( predicates:: str:: contains ( & contract_b) ) ;
195+
196+ let snapshot_a_path = sandbox. dir ( ) . join ( "snapshot_a.json" ) ;
197+ let snapshot_b_path = sandbox. dir ( ) . join ( "snapshot_b.json" ) ;
198+ let merged_path = sandbox. dir ( ) . join ( "merged.json" ) ;
199+
200+ let snapshot_a = LedgerSnapshot :: read_file ( snapshot_a_path) . unwrap ( ) ;
201+ let snapshot_b = LedgerSnapshot :: read_file ( snapshot_b_path) . unwrap ( ) ;
202+ let merged = LedgerSnapshot :: read_file ( merged_path) . unwrap ( ) ;
203+
204+ assert_eq ! ( merged. protocol_version, snapshot_b. protocol_version) ;
205+ assert_eq ! ( merged. sequence_number, snapshot_b. sequence_number) ;
206+ assert_eq ! ( merged. timestamp, snapshot_b. timestamp) ;
207+ assert_eq ! ( merged. network_id, snapshot_b. network_id) ;
208+
209+ // Verify that we have more entries in merged than in either individual snapshot
210+ assert ! ( merged. ledger_entries. len( ) > snapshot_a. ledger_entries. len( ) ) ;
211+ assert ! ( merged. ledger_entries. len( ) > snapshot_b. ledger_entries. len( ) ) ;
212+ }
213+
214+ #[ test]
215+ fn snapshot_merge_conflict_resolution ( ) {
216+ let sandbox = & TestEnv :: new ( ) ;
217+ let identity = "ineffable-serval-3633" ;
218+
219+ // Create an account
220+ sandbox
221+ . new_assert_cmd ( "keys" )
222+ . arg ( "generate" )
223+ . arg ( "--fund" )
224+ . arg ( identity)
225+ . assert ( )
226+ . success ( ) ;
227+ let account = sandbox
228+ . new_assert_cmd ( "keys" )
229+ . arg ( "address" )
230+ . arg ( identity)
231+ . assert ( )
232+ . success ( )
233+ . stdout_as_str ( ) ;
234+
235+ // Wait 8 ledgers for a checkpoint
236+ for i in 1 ..=8 {
237+ sandbox
238+ . new_assert_cmd ( "keys" )
239+ . arg ( "generate" )
240+ . arg ( "--fund" )
241+ . arg ( format ! ( "k{i}" ) )
242+ . assert ( )
243+ . success ( ) ;
244+ }
245+
246+ // Create first snapshot with the account
247+ sandbox
248+ . new_assert_cmd ( "snapshot" )
249+ . arg ( "create" )
250+ . arg ( "--address" )
251+ . arg ( & account)
252+ . arg ( "--out=snapshot_1.json" )
253+ . assert ( )
254+ . success ( ) ;
255+
256+ // Wait for another checkpoint to get a different ledger sequence
257+ for i in 9 ..=16 {
258+ sandbox
259+ . new_assert_cmd ( "keys" )
260+ . arg ( "generate" )
261+ . arg ( "--fund" )
262+ . arg ( format ! ( "k{i}" ) )
263+ . assert ( )
264+ . success ( ) ;
265+ }
266+
267+ // Create second snapshot with the same account at a later ledger sequence
268+ sandbox
269+ . new_assert_cmd ( "snapshot" )
270+ . arg ( "create" )
271+ . arg ( "--address" )
272+ . arg ( & account)
273+ . arg ( "--out=snapshot_2.json" )
274+ . assert ( )
275+ . success ( ) ;
276+
277+ // Merge the snapshots - snapshot_2 should win
278+ sandbox
279+ . new_assert_cmd ( "snapshot" )
280+ . arg ( "merge" )
281+ . arg ( "snapshot_1.json" )
282+ . arg ( "snapshot_2.json" )
283+ . arg ( "--out=merged_conflict.json" )
284+ . assert ( )
285+ . success ( ) ;
286+
287+ // Read snapshots and verify the merged one has the same sequence as snapshot_2
288+ let snapshot_2_path = sandbox. dir ( ) . join ( "snapshot_2.json" ) ;
289+ let merged_path = sandbox. dir ( ) . join ( "merged_conflict.json" ) ;
290+
291+ let snapshot_2 = LedgerSnapshot :: read_file ( snapshot_2_path) . unwrap ( ) ;
292+ let merged = LedgerSnapshot :: read_file ( merged_path) . unwrap ( ) ;
293+
294+ // The merged snapshot should have metadata from snapshot_2 (last wins)
295+ assert_eq ! ( merged. sequence_number, snapshot_2. sequence_number) ;
296+ assert ! ( merged. sequence_number > 0 ) ;
297+ }
298+
299+ #[ test]
300+ fn snapshot_merge_multiple ( ) {
301+ let sandbox = & TestEnv :: new ( ) ;
302+
303+ // Create three accounts
304+ let mut accounts = Vec :: new ( ) ;
305+ for name in [ "x" , "y" , "z" ] {
306+ sandbox
307+ . new_assert_cmd ( "keys" )
308+ . arg ( "generate" )
309+ . arg ( "--fund" )
310+ . arg ( name)
311+ . assert ( )
312+ . success ( ) ;
313+ let account = sandbox
314+ . new_assert_cmd ( "keys" )
315+ . arg ( "address" )
316+ . arg ( name)
317+ . assert ( )
318+ . success ( )
319+ . stdout_as_str ( ) ;
320+ accounts. push ( account. trim ( ) . to_string ( ) ) ;
321+ }
322+
323+ // Wait 8 ledgers for a checkpoint
324+ for i in 1 ..=8 {
325+ sandbox
326+ . new_assert_cmd ( "keys" )
327+ . arg ( "generate" )
328+ . arg ( "--fund" )
329+ . arg ( format ! ( "k{i}" ) )
330+ . assert ( )
331+ . success ( ) ;
332+ }
333+
334+ // Create three snapshots, one for each account
335+ for ( i, account) in accounts. iter ( ) . enumerate ( ) {
336+ sandbox
337+ . new_assert_cmd ( "snapshot" )
338+ . arg ( "create" )
339+ . arg ( "--address" )
340+ . arg ( account)
341+ . arg ( format ! ( "--out=snapshot_{}.json" , i) )
342+ . assert ( )
343+ . success ( ) ;
344+ }
345+
346+ // Merge all three snapshots at once
347+ sandbox
348+ . new_assert_cmd ( "snapshot" )
349+ . arg ( "merge" )
350+ . arg ( "snapshot_0.json" )
351+ . arg ( "snapshot_1.json" )
352+ . arg ( "snapshot_2.json" )
353+ . arg ( "--out=merged_multiple.json" )
354+ . assert ( )
355+ . success ( ) ;
356+
357+ // Read the individual snapshots and merged snapshot to verify
358+ let snapshot_0_path = sandbox. dir ( ) . join ( "snapshot_0.json" ) ;
359+ let snapshot_1_path = sandbox. dir ( ) . join ( "snapshot_1.json" ) ;
360+ let snapshot_2_path = sandbox. dir ( ) . join ( "snapshot_2.json" ) ;
361+ let merged_path = sandbox. dir ( ) . join ( "merged_multiple.json" ) ;
362+
363+ let snapshot_0 = LedgerSnapshot :: read_file ( snapshot_0_path) . unwrap ( ) ;
364+ let snapshot_1 = LedgerSnapshot :: read_file ( snapshot_1_path) . unwrap ( ) ;
365+ let snapshot_2 = LedgerSnapshot :: read_file ( snapshot_2_path) . unwrap ( ) ;
366+ let merged = LedgerSnapshot :: read_file ( merged_path) . unwrap ( ) ;
367+
368+ // Verify that metadata comes from the last snapshot (snapshot_2)
369+ assert_eq ! ( merged. sequence_number, snapshot_2. sequence_number) ;
370+ assert_eq ! ( merged. network_id, snapshot_2. network_id) ;
371+
372+ // Verify that merged has at least as many entries as the largest individual snapshot
373+ let max_individual = snapshot_0
374+ . ledger_entries
375+ . len ( )
376+ . max ( snapshot_1. ledger_entries . len ( ) )
377+ . max ( snapshot_2. ledger_entries . len ( ) ) ;
378+ assert ! ( merged. ledger_entries. len( ) >= max_individual) ;
379+ }
0 commit comments