88namespace OCA \Files_Versions \Tests ;
99
1010use OCA \Files_Versions \Storage ;
11- use ReflectionClass ;
12- use ReflectionException ;
1311
14- class GetAutoExpireListTest extends \ Test \ TestCase {
12+ class GetAutoExpireListTest extends TestCase {
1513
1614 /**
17- * @throws ReflectionException
15+ * Frozen reference time for all tests
1816 */
19- protected static function callGetAutoExpireList (int $ time , array $ versions ): array {
20- $ ref = new ReflectionClass (Storage::class);
17+ private const NOW = 1600000000 ;
18+
19+ /**
20+ * Helper to call the private retention logic
21+ *
22+ * @param int $now
23+ * @param array $versions
24+ * @return array{array<int,array>, int}
25+ */
26+ private static function callGetAutoExpireList (int $ now , array $ versions ): array {
27+ $ ref = new \ReflectionClass (Storage::class);
2128 $ method = $ ref ->getMethod ('getAutoExpireList ' );
22- $ method ->setAccessible (true );
2329
24- return $ method ->invokeArgs (null , [$ time , $ versions ]);
30+ /** @var array{array<int,array>, int} */
31+ return $ method ->invoke (null , $ now , $ versions );
2532 }
2633
2734 /**
2835 * @dataProvider provideBucketKeepsLatest
2936 */
30- public function testBucketKeepsLatest (int $ offset1 , int $ offset2 , int $ size1 , int $ size2 ) {
37+ public function testBucketKeepsLatest (int $ age1 , int $ age2 , int $ size1 , int $ size2 ): void {
3138 $ now = time ();
3239
33- $ first = $ now - $ offset1 ;
34- $ second = $ first - $ offset2 ;
40+ $ first = $ now - $ age1 ;
41+ $ second = $ now - $ age2 ;
42+
43+ // Ensure first is newer than second
44+ if ($ first < $ second ) {
45+ [$ first , $ second ] = [$ second , $ first ];
46+ [$ size1 , $ size2 ] = [$ size2 , $ size1 ];
47+ }
3548
3649 $ versions = [
37- $ first => ['version ' => $ first , 'size ' => $ size1 , 'path ' => 'f ' ],
50+ $ first => ['version ' => $ first , 'size ' => $ size1 , 'path ' => 'f ' ],
3851 $ second => ['version ' => $ second , 'size ' => $ size2 , 'path ' => 'f ' ],
3952 ];
4053
@@ -46,166 +59,183 @@ public function testBucketKeepsLatest(int $offset1, int $offset2, int $size1, in
4659 $ this ->assertEquals ($ versions [$ second ]['size ' ], $ size , 'Deleted size mismatch ' );
4760 }
4861
49- /**
50- * Provides test cases for different bucket intervals.
51- * Each case is [offset1 (age of first), offset2 (extra gap for second), size1, size2].
52- * @return array<string, array{int,int,int,int}>
53- */
5462 public static function provideBucketKeepsLatest (): array {
5563 $ DAY = 24 * 60 * 60 ;
56- $ WEEK = 7 * $ DAY ;
5764
5865 return [
5966 'minute ' => [
60- 8 , // 8s old
61- 1 , // 9s old → both in same 2s slot
67+ 8 , // 8s old
68+ 9 , // 9s old
6269 5 ,
6370 6 ,
6471 ],
6572 'hour ' => [
66- 2 * 60 , // 2 minutes old
67- 30 , // 2m30s old → both in same 1m slot
73+ 120 , // 2 minutes old
74+ 150 , // 2m30s old
6875 10 ,
6976 11 ,
7077 ],
7178 'day ' => [
72- 5 * 3600 , // 5 hours old
73- 1800 , // 5.5h old → both in same 1h slot
79+ 5 * 3600 , // 5 hours old
80+ 5 * 3600 + 1800 , // 5.5h old
7481 20 ,
7582 21 ,
7683 ],
7784 'week ' => [
78- 2 * $ DAY , // 2 days old
79- 6 * 3600 , // 2.25 days old → both in same 1d slot
85+ 2 * $ DAY , // 2 days old
86+ 2 * $ DAY + 6 * 3600 , // 2.25 days old
8087 40 ,
8188 41 ,
8289 ],
8390 'month ' => [
84- 5 * $ DAY , // 5 days old
85- 12 * 60 * 60 , // 5.5 days old → both in same 1d slot
91+ 5 * $ DAY , // 5 days old
92+ 5 * $ DAY + 12 * 3600 , // 5.5 days old
8693 30 ,
8794 31 ,
8895 ],
8996 'year ' => [
9097 35 * $ DAY , // 35 days old
91- 2 * $ DAY , // 37 days old → both in same 1w slot
98+ 37 * $ DAY , // 37 days old
9299 42 ,
93100 43 ,
94101 ],
95102 'beyond-year ' => [
96- 400 * $ DAY , // ~13.3 months old
97- 5 * $ DAY , // 405 days old → same 30d slot
103+ 400 * $ DAY , // ~13.3 months old
104+ 405 * $ DAY , // ~13.5 months old
98105 50 ,
99106 51 ,
100107 ],
101108 ];
102109 }
103110
104- public function testFiveDaysOfVersionsEveryTenMinutes () {
111+ /**
112+ * @dataProvider provideVersionRetentionRanges
113+ */
114+ public function testRetentionOverTimeEveryTenMinutes (
115+ int $ days ,
116+ int $ expectedMin ,
117+ int $ expectedMax
118+ ): void {
105119 $ now = time ();
106120 $ versions = [];
107121
108- // Create one version every 10 minutes for 5 days
109- for ($ i = 0 ; $ i < (5 * 24 * 6 ); $ i ++) {
110- $ ts = $ now - ($ i * 600 );
111- $ versions [$ ts ] = ['version ' => $ ts , 'size ' => 1 , 'path ' => 'f ' ];
122+ // One version every 10 minutes
123+ $ interval = 600 ; // 10 minutes
124+ $ total = $ days * 24 * 6 ;
125+
126+ for ($ i = 0 ; $ i < $ total ; $ i ++) {
127+ $ ts = $ now - ($ i * $ interval );
128+ $ versions [$ ts ] = [
129+ 'version ' => $ ts ,
130+ 'size ' => 1 ,
131+ 'path ' => 'f ' ,
132+ ];
112133 }
113134
114135 [$ toDelete , $ size ] = self ::callGetAutoExpireList ($ now , $ versions );
115- $ retained = array_diff (array_keys ($ versions ), array_keys ($ toDelete ));
116-
117- // Expect ~28-33 retained due to bucket rules
118- $ this ->assertGreaterThanOrEqual (28 , count ($ retained ));
119- $ this ->assertLessThanOrEqual (33 , count ($ retained ));
120- }
121-
122- public function testThirtyDaysOfVersionsEveryTenMinutes () {
123- $ now = time ();
124- $ versions = [];
125136
126- // Create one version every 10 minutes for 30 days
127- for ($ i = 0 ; $ i < (30 * 24 * 6 ); $ i ++) {
128- $ ts = $ now - ($ i * 600 );
129- $ versions [$ ts ] = ['version ' => $ ts , 'size ' => 1 , 'path ' => 'f ' ];
130- }
131-
132- [$ toDelete , $ size ] = self ::callGetAutoExpireList ($ now , $ versions );
133137 $ retained = array_diff (array_keys ($ versions ), array_keys ($ toDelete ));
138+ $ retainedCount = count ($ retained );
139+
140+ $ this ->assertGreaterThanOrEqual (
141+ $ expectedMin ,
142+ $ retainedCount ,
143+ "Too few versions retained for {$ days } days "
144+ );
145+
146+ $ this ->assertLessThanOrEqual (
147+ $ expectedMax ,
148+ $ retainedCount ,
149+ "Too many versions retained for {$ days } days "
150+ );
151+ }
134152
135- // Expect ~54-60 retained (24 hours hourly + 29 daily + bucket overlap)
136- $ this ->assertGreaterThanOrEqual (54 , count ($ retained ));
137- $ this ->assertLessThanOrEqual (60 , count ($ retained ));
153+ public static function provideVersionRetentionRanges (): array {
154+ return [
155+ '5 days ' => [
156+ 5 ,
157+ 28 ,
158+ 33 ,
159+ ],
160+ '30 days ' => [
161+ 30 ,
162+ 54 ,
163+ 60 ,
164+ ],
165+ '1 year ' => [
166+ 365 ,
167+ 100 ,
168+ 140 ,
169+ ],
170+ ];
138171 }
139172
140- public function testYearOfVersionsEveryTenMinutes () {
141- $ now = time ();
173+ /**
174+ * Exact deterministic retention count for evenly spaced versions.
175+ *
176+ * One version per hour, offset away from bucket edges.
177+ *
178+ * @dataProvider provideExactRetentionCounts
179+ */
180+ public function testExactRetentionCounts (
181+ int $ days ,
182+ int $ expectedRetained
183+ ): void {
184+ $ now = self ::NOW ;
142185 $ versions = [];
143186
144- // Create one version every 10 minutes for 365 days
145- for ($ i = 0 ; $ i < ( 365 * 24 * 6 ) ; $ i ++) {
146- $ ts = $ now - ($ i * 600 ) ;
187+ // One version per hour, safely inside bucket slots
188+ for ($ i = 0 ; $ i < $ days * 24 ; $ i ++) {
189+ $ ts = $ now - ($ i * 3600 ) - 1 ;
147190 $ versions [$ ts ] = ['version ' => $ ts , 'size ' => 1 , 'path ' => 'f ' ];
148191 }
149192
150- [$ toDelete, $ size ] = self ::callGetAutoExpireList ($ now , $ versions );
151- $ retained = array_diff ( array_keys ( $ versions), array_keys ( $ toDelete) );
193+ [$ toDelete ] = self ::callGetAutoExpireList ($ now , $ versions );
194+ $ retained = array_diff_key ( $ versions, $ toDelete );
152195
153- // Expect ~100-140 retained due to buckets (minute, hour, day, week, month)
154- $ this ->assertGreaterThanOrEqual (100 , count ($ retained ));
155- $ this ->assertLessThanOrEqual (140 , count ($ retained ));
196+ $ this ->assertSame (
197+ $ expectedRetained ,
198+ count ($ retained ),
199+ "Exact retention count mismatch for {$ days } days "
200+ );
156201 }
157202
158- public function testMoreThanAYearOfVersionsEveryTenMinutesWithDeletion () {
159- $ now = time ();
160- $ versions = [];
161-
162- // Define bucket steps (same as retention logic)
163- $ buckets = [
164- 1 => ['intervalEndsAfter ' => 10 , 'step ' => 2 ],
165- 2 => ['intervalEndsAfter ' => 60 , 'step ' => 10 ],
166- 3 => ['intervalEndsAfter ' => 3600 , 'step ' => 60 ],
167- 4 => ['intervalEndsAfter ' => 86400 , 'step ' => 3600 ],
168- 5 => ['intervalEndsAfter ' => 2592000 , 'step ' => 86400 ],
169- 6 => ['intervalEndsAfter ' => -1 , 'step ' => 604800 ],
203+ /**
204+ * @return array<string, array{int,int}>
205+ */
206+ public static function provideExactRetentionCounts (): array {
207+ return [
208+ 'five-days ' => [
209+ 5 ,
210+ self ::expectedHourlyRetention (5 ),
211+ ],
212+ 'thirty-days ' => [
213+ 30 ,
214+ self ::expectedHourlyRetention (30 ),
215+ ],
216+ 'one-year ' => [
217+ 365 ,
218+ self ::expectedHourlyRetention (365 ),
219+ ],
220+ 'one-year-plus ' => [
221+ 500 ,
222+ self ::expectedHourlyRetention (500 ),
223+ ],
170224 ];
225+ }
171226
172- $ lastBoundary = 0 ;
173- foreach ($ buckets as $ bucket ) {
174- $ intervalEnd = $ bucket ['intervalEndsAfter ' ] > 0 ? $ bucket ['intervalEndsAfter ' ] : 500 * 86400 ;
175- $ step = $ bucket ['step ' ];
176-
177- for ($ age = $ lastBoundary ; $ age <= $ intervalEnd ; $ age += $ step ) {
178- // Add multiple versions per step (3 versions spaced evenly within step)
179- for ($ i = 0 ; $ i < 3 ; $ i ++) {
180- $ ts = $ now - ($ age + $ i * floor ($ step / 3 ));
181- $ versions [$ ts ] = ['version ' => $ ts , 'size ' => 1 , 'path ' => 'f ' ];
182- }
183- }
184-
185- $ lastBoundary = $ intervalEnd ;
186- }
187-
188- [$ toDelete , $ size ] = self ::callGetAutoExpireList ($ now , $ versions );
189- $ retained = array_diff (array_keys ($ versions ), array_keys ($ toDelete ));
190-
191- $ lastBoundary = 0 ;
192- foreach ($ buckets as $ bucket ) {
193- $ intervalEnd = $ bucket ['intervalEndsAfter ' ] > 0 ? $ bucket ['intervalEndsAfter ' ] : PHP_INT_MAX ;
194-
195- $ bucketRetained = array_filter ($ retained , function ($ ts ) use ($ now , $ lastBoundary , $ intervalEnd ) {
196- $ age = $ now - $ ts ;
197- return $ age >= $ lastBoundary && $ age <= $ intervalEnd ;
198- });
227+ private static function expectedHourlyRetention (int $ days ): int {
228+ // Hourly for first day
229+ $ hourly = min (24 , $ days * 24 );
199230
200- $ this ->assertGreaterThanOrEqual (
201- 1 ,
202- count ($ bucketRetained ),
203- "Bucket ending at $ intervalEnd seconds has " . count ($ bucketRetained ) . ' retained, expected at least 1 '
204- );
231+ // Daily from day 1 to day 30
232+ $ dailyDays = max (0 , min ($ days , 30 ) - 1 );
233+ $ daily = $ dailyDays ;
205234
206- $ lastBoundary = $ intervalEnd ;
207- }
235+ // Weekly beyond 30 days
236+ $ weeklyDays = max (0 , $ days - 30 );
237+ $ weekly = intdiv ($ weeklyDays , 7 ) + ($ weeklyDays > 0 ? 1 : 0 );
208238
239+ return $ hourly + $ daily + $ weekly ;
209240 }
210-
211241}
0 commit comments