Skip to content

Commit 44efda2

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 44efda2

File tree

1 file changed

+225
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)