Skip to content

Commit 4914b4a

Browse files
committed
fix: added test to assert auto expire list preserve's version as per max_versions_per_interval
Signed-off-by: yemkareems <[email protected]>
1 parent 706c78f commit 4914b4a

File tree

1 file changed

+226
-0
lines changed

1 file changed

+226
-0
lines changed
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
6+
* SPDX-FileCopyrightText: 2025 ownCloud, Inc.
7+
* SPDX-License-Identifier: AGPL-3.0-only
8+
*/
9+
namespace OCA\Files_Versions\Tests;
10+
11+
use OCA\Files_Versions\Storage;
12+
use ReflectionClass;
13+
use ReflectionException;
14+
15+
class GetAutoExpireListTest extends \Test\TestCase {
16+
17+
/**
18+
* @throws ReflectionException
19+
* @since 32.0.0
20+
*/
21+
protected static function callGetAutoExpireList(int $time, array $versions): array {
22+
$ref = new ReflectionClass(Storage::class);
23+
$method = $ref->getMethod('getAutoExpireList');
24+
$method->setAccessible(true);
25+
26+
return $method->invokeArgs(null, [$time, $versions]);
27+
}
28+
29+
/**
30+
* @since 32.0.0
31+
* @dataProvider provideBucketKeepsLatest
32+
*/
33+
public function testBucketKeepsLatest(int $offset1, int $offset2, int $size1, int $size2) {
34+
$now = time();
35+
36+
$first = $now - $offset1;
37+
$second = $first - $offset2;
38+
39+
$versions = [
40+
$first => ['version' => $first, 'size' => $size1, 'path' => 'f'],
41+
$second => ['version' => $second, 'size' => $size2, 'path' => 'f'],
42+
];
43+
44+
[$toDelete, $size] = self::callGetAutoExpireList($now, $versions);
45+
46+
$deletedKeys = array_map('intval', array_keys($toDelete));
47+
48+
$this->assertEquals([$second], $deletedKeys, 'Older version was not deleted');
49+
$this->assertEquals($versions[$second]['size'], $size, 'Deleted size mismatch');
50+
}
51+
52+
/**
53+
* Provides test cases for different bucket intervals.
54+
* Each case is [offset1 (age of first), offset2 (extra gap for second), size1, size2].
55+
* @return array<string, array{int,int,int,int}>
56+
*/
57+
public static function provideBucketKeepsLatest(): array {
58+
$DAY = 24 * 60 * 60;
59+
$WEEK = 7 * $DAY;
60+
61+
return [
62+
'minute' => [
63+
8, // 8s old
64+
1, // 9s old → both in same 2s slot
65+
5,
66+
6,
67+
],
68+
'hour' => [
69+
2 * 60, // 2 minutes old
70+
30, // 2m30s old → both in same 1m slot
71+
10,
72+
11,
73+
],
74+
'day' => [
75+
5 * 3600, // 5 hours old
76+
1800, // 5.5h old → both in same 1h slot
77+
20,
78+
21,
79+
],
80+
'week' => [
81+
2 * $DAY, // 2 days old
82+
6 * 3600, // 2.25 days old → both in same 1d slot
83+
40,
84+
41,
85+
],
86+
'month' => [
87+
5 * $DAY, // 5 days old
88+
12 * 60 * 60, // 5.5 days old → both in same 1d slot
89+
30,
90+
31,
91+
],
92+
'year' => [
93+
35 * $DAY, // 35 days old
94+
2 * $DAY, // 37 days old → both in same 1w slot
95+
42,
96+
43,
97+
],
98+
'beyond-year' => [
99+
400 * $DAY, // ~13.3 months old
100+
5 * $DAY, // 405 days old → same 30d slot
101+
50,
102+
51,
103+
],
104+
];
105+
}
106+
107+
/**
108+
* @since 32.0.0
109+
*/
110+
public function testFiveDaysOfVersionsEveryTenMinutes() {
111+
$now = time();
112+
$versions = [];
113+
114+
// Create one version every 10 minutes for 5 days
115+
for ($i = 0; $i < (5 * 24 * 6); $i++) {
116+
$ts = $now - ($i * 600);
117+
$versions[$ts] = ['version' => $ts, 'size' => 1, 'path' => 'f'];
118+
}
119+
120+
[$toDelete, $size] = self::callGetAutoExpireList($now, $versions);
121+
$retained = array_diff(array_keys($versions), array_keys($toDelete));
122+
123+
// Expect ~28-33 retained due to bucket rules
124+
$this->assertGreaterThanOrEqual(28, count($retained));
125+
$this->assertLessThanOrEqual(33, count($retained));
126+
}
127+
128+
/**
129+
* @since 32.0.0
130+
*/
131+
public function testThirtyDaysOfVersionsEveryTenMinutes() {
132+
$now = time();
133+
$versions = [];
134+
135+
// Create one version every 10 minutes for 30 days
136+
for ($i = 0; $i < (30 * 24 * 6); $i++) {
137+
$ts = $now - ($i * 600);
138+
$versions[$ts] = ['version' => $ts, 'size' => 1, 'path' => 'f'];
139+
}
140+
141+
[$toDelete, $size] = self::callGetAutoExpireList($now, $versions);
142+
$retained = array_diff(array_keys($versions), array_keys($toDelete));
143+
144+
// Expect ~54-60 retained (24 hours hourly + 29 daily + bucket overlap)
145+
$this->assertGreaterThanOrEqual(54, count($retained));
146+
$this->assertLessThanOrEqual(60, count($retained));
147+
}
148+
149+
/**
150+
* @since 32.0.0
151+
*/
152+
public function testYearOfVersionsEveryTenMinutes() {
153+
$now = time();
154+
$versions = [];
155+
156+
// Create one version every 10 minutes for 365 days
157+
for ($i = 0; $i < (365 * 24 * 6); $i++) {
158+
$ts = $now - ($i * 600);
159+
$versions[$ts] = ['version' => $ts, 'size' => 1, 'path' => 'f'];
160+
}
161+
162+
[$toDelete, $size] = self::callGetAutoExpireList($now, $versions);
163+
$retained = array_diff(array_keys($versions), array_keys($toDelete));
164+
165+
// Expect ~100-140 retained due to buckets (minute, hour, day, week, month)
166+
$this->assertGreaterThanOrEqual(100, count($retained));
167+
$this->assertLessThanOrEqual(140, count($retained));
168+
}
169+
170+
/**
171+
* @since 32.0.0
172+
*/
173+
public function testMoreThanAYearOfVersionsEveryTenMinutesWithDeletion() {
174+
$now = time();
175+
$versions = [];
176+
177+
// Define bucket steps (same as retention logic)
178+
$buckets = [
179+
1 => ['intervalEndsAfter' => 10, 'step' => 2],
180+
2 => ['intervalEndsAfter' => 60, 'step' => 10],
181+
3 => ['intervalEndsAfter' => 3600, 'step' => 60],
182+
4 => ['intervalEndsAfter' => 86400, 'step' => 3600],
183+
5 => ['intervalEndsAfter' => 2592000, 'step' => 86400],
184+
6 => ['intervalEndsAfter' => -1, 'step' => 604800],
185+
];
186+
187+
$lastBoundary = 0;
188+
foreach ($buckets as $bucket) {
189+
$intervalEnd = $bucket['intervalEndsAfter'] > 0 ? $bucket['intervalEndsAfter'] : 500 * 86400;
190+
$step = $bucket['step'];
191+
192+
for ($age = $lastBoundary; $age <= $intervalEnd; $age += $step) {
193+
// Add multiple versions per step (3 versions spaced evenly within step)
194+
for ($i = 0; $i < 3; $i++) {
195+
$ts = $now - ($age + $i * floor($step / 3));
196+
$versions[$ts] = ['version' => $ts, 'size' => 1, 'path' => 'f'];
197+
}
198+
}
199+
200+
$lastBoundary = $intervalEnd;
201+
}
202+
203+
[$toDelete, $size] = self::callGetAutoExpireList($now, $versions);
204+
$retained = array_diff(array_keys($versions), array_keys($toDelete));
205+
206+
$lastBoundary = 0;
207+
foreach ($buckets as $bucket) {
208+
$intervalEnd = $bucket['intervalEndsAfter'] > 0 ? $bucket['intervalEndsAfter'] : PHP_INT_MAX;
209+
210+
$bucketRetained = array_filter($retained, function ($ts) use ($now, $lastBoundary, $intervalEnd) {
211+
$age = $now - $ts;
212+
return $age >= $lastBoundary && $age <= $intervalEnd;
213+
});
214+
215+
$this->assertGreaterThanOrEqual(
216+
1,
217+
count($bucketRetained),
218+
"Bucket ending at $intervalEnd seconds has " . count($bucketRetained) . ' retained, expected at least 1'
219+
);
220+
221+
$lastBoundary = $intervalEnd;
222+
}
223+
224+
}
225+
226+
}

0 commit comments

Comments
 (0)