A schedule per location, and locations backed up one after the other
Off, once a day or every N hours from a time, each in its own time zone. Existing locations keep the time that held for all of them. A backup that has to wait shows as queued, and its restore point has the time it really began.
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\NgBackup\Tests\Unit\Service;
|
||||
|
||||
use OCA\NgBackup\Service\LocationSchedule as S;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class LocationScheduleTest extends TestCase {
|
||||
private const BERLIN = 'Europe/Berlin';
|
||||
|
||||
private static function at(string $time): int {
|
||||
return (int)strtotime($time);
|
||||
}
|
||||
|
||||
public function testWhatCanBeSaved(): void {
|
||||
$this->assertSame('', S::normalize('off'));
|
||||
$this->assertSame('', S::normalize(' '));
|
||||
$this->assertSame('02:30', S::normalize('02:30'));
|
||||
$this->assertSame('02:30', S::normalize('02:30/24'));
|
||||
$this->assertSame('00:00/6', S::normalize('00:00/6'));
|
||||
foreach (['2:30', '24:00', '02:30/0', '02:30/25', 'daily', '02:30/x'] as $bad) {
|
||||
try {
|
||||
S::normalize($bad);
|
||||
$this->fail("$bad accepted");
|
||||
} catch (\InvalidArgumentException) {
|
||||
$this->addToAssertionCount(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testOffHasNoMoment(): void {
|
||||
$this->assertNull(S::lastMoment('', self::BERLIN, self::at('2026-11-10 12:00 UTC')));
|
||||
}
|
||||
|
||||
public function testDailyIsInTheZoneOfTheLocation(): void {
|
||||
$this->assertSame(self::at('2026-11-10 02:00 UTC'), S::lastMoment('03:00', self::BERLIN, self::at('2026-11-10 12:00 UTC')));
|
||||
$this->assertSame(self::at('2026-11-09 02:00 UTC'), S::lastMoment('03:00', self::BERLIN, self::at('2026-11-10 01:00 UTC')), 'not yet today');
|
||||
}
|
||||
|
||||
public function testEveryNHoursStartsOverTheNextDay(): void {
|
||||
$zone = 'UTC';
|
||||
$this->assertSame(self::at('2026-11-10 12:00 UTC'), S::lastMoment('02:00/5', $zone, self::at('2026-11-10 13:00 UTC')));
|
||||
$this->assertSame(self::at('2026-11-09 22:00 UTC'), S::lastMoment('02:00/5', $zone, self::at('2026-11-10 01:59 UTC')), '02, 07, 12, 17, 22, then 02 again');
|
||||
$this->assertSame(self::at('2026-11-10 02:00 UTC'), S::lastMoment('02:00/5', $zone, self::at('2026-11-10 02:00 UTC')), 'at the moment itself');
|
||||
}
|
||||
|
||||
public function testAClockChangeInTheNightStillGivesOneMoment(): void {
|
||||
$this->assertSame(self::at('2026-03-29 01:30 UTC'), S::lastMoment('02:30', self::BERLIN, self::at('2026-03-29 12:00 UTC')), '02:30 does not exist that night');
|
||||
$this->assertNotNull(S::lastMoment('02:30', self::BERLIN, self::at('2026-10-25 12:00 UTC')));
|
||||
}
|
||||
|
||||
public function testChoosingATimeDoesNotStartABackup(): void {
|
||||
$moment = self::at('2026-11-10 02:00 UTC');
|
||||
$this->assertFalse(S::isDue($moment, $moment + 1, null, 0, null, $moment + 3600));
|
||||
$this->assertTrue(S::isDue($moment, $moment - 1, null, 0, null, $moment + 3600));
|
||||
$this->assertTrue(S::isDue($moment, 0, null, 0, null, $moment), 'a location from before schedules per location');
|
||||
}
|
||||
|
||||
public function testABackupSinceTheMomentIsEnough(): void {
|
||||
$moment = 1000000;
|
||||
$this->assertFalse(S::isDue($moment, 0, $moment, 1, $moment + 10, $moment + 20));
|
||||
$this->assertTrue(S::isDue($moment, 0, $moment - 1, 0, null, $moment + 20), 'it started before this moment: one more');
|
||||
}
|
||||
|
||||
public function testAfterAFailureOnlyAFewAttemptsAnHourApart(): void {
|
||||
$moment = 1000000;
|
||||
$finished = $moment + 60;
|
||||
$this->assertFalse(S::isDue($moment, 0, null, 1, $finished, $finished + S::RETRY_AFTER - 1));
|
||||
$this->assertTrue(S::isDue($moment, 0, null, 1, $finished, $finished + S::RETRY_AFTER));
|
||||
$this->assertFalse(S::isDue($moment, 0, null, S::MAX_ATTEMPTS, $finished, $finished + 86400));
|
||||
}
|
||||
}
|
||||
@@ -57,15 +57,4 @@ class ScheduleTest extends TestCase {
|
||||
$this->assertSame(['time' => '02:30', 'timezone' => 'UTC'], $this->schedule()->get());
|
||||
}
|
||||
|
||||
public function testTodayAtIsTheMomentInTheSchedulesZone(): void {
|
||||
$this->values = [Schedule::TIME_KEY => '03:00', Schedule::ZONE_KEY => 'Europe/Berlin'];
|
||||
$at = $this->schedule()->todayAt(strtotime('2026-10-22 12:00 UTC'));
|
||||
$this->assertSame(strtotime('2026-10-22 01:00 UTC'), $at, '03:00 in Berlin (summer time) is 01:00 UTC');
|
||||
}
|
||||
|
||||
public function testAnUnknownZoneOrABadTimeIsRefused(): void {
|
||||
$s = $this->schedule();
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$s->set('03:00', 'Mars/Olympus');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +101,24 @@ ok "without system cron: warning in backup:status and the API, backup finished b
|
||||
# The Bin reads in parts, as the page does (what is in the parts: CatalogTest)
|
||||
api "$API/targets/$TID/trash" | php -r '$p = json_decode(stream_get_contents(STDIN), true); exit(is_int($p["total"] ?? null) && is_array($p["items"] ?? null) && count($p["items"]) <= $p["total"] ? 0 : 1);' || fail "the Bin API does not give a total and items"
|
||||
ok "the Bin API reads in parts, with a total"
|
||||
# Locations are backed up one after the other: the second one started is queued until the first is done.
|
||||
for L in 2 3; do
|
||||
$X sh -c "mkdir -p /tmp/ngb-rt-target$L"
|
||||
occ backup:target:add ci$L local -a null::null -o datadir=/tmp/ngb-rt-target$L --path=repo >/dev/null || fail "target add ci$L"
|
||||
done
|
||||
IDS=$(api "$API/status" | php -r 'foreach (json_decode(stream_get_contents(STDIN), true)["targets"] as $t) { if ($t["name"] === "ci2" || $t["name"] === "ci3") { echo $t["id"], " "; } }')
|
||||
for ID in $IDS; do api -X POST -d "{\"targetId\": $ID, \"label\": \"turn\"}" "$API/backups" >/dev/null || fail "start a backup to location $ID"; done
|
||||
occ backup:status | grep "queued" >/dev/null || fail "the second backup is not queued"
|
||||
QUEUED=$(api "$API/status" | php -r 'echo count(array_filter(json_decode(stream_get_contents(STDIN), true)["runs"], fn ($r) => $r["queued"]));')
|
||||
[ "$QUEUED" = 1 ] || fail "$QUEUED backups queued instead of 1"
|
||||
for i in $(seq 1 60); do
|
||||
api -X POST "$API/tick" >/dev/null || fail "tick"
|
||||
RUNNING=$(api "$API/status" | php -r 'echo count(array_filter(json_decode(stream_get_contents(STDIN), true)["runs"], fn ($r) => $r["status"] === "running"));')
|
||||
[ "$RUNNING" != 0 ] || break
|
||||
done
|
||||
[ "$RUNNING" = 0 ] || fail "the queued backups did not finish"
|
||||
occ backup:list ci3 | grep " turn " >/dev/null || fail "the queued backup made no snapshot"
|
||||
ok "locations in turn: the second backup queued, and done after the first"
|
||||
occ background:cron >/dev/null
|
||||
$X php -f /var/www/html/cron.php >/dev/null 2>&1 || true
|
||||
occ backup:status | grep "system cron" >/dev/null && fail "backup:status still warns after cron ran"
|
||||
@@ -109,6 +127,26 @@ api "$API/status" | php -r 'exit(json_decode(stream_get_contents(STDIN), true)["
|
||||
ok "with a system cron that ran: no warning in backup:status or the API"
|
||||
occ user:delete "$ADM" >/dev/null
|
||||
|
||||
# A schedule per location. Choosing a time starts nothing; the moment itself does, and only there.
|
||||
JOB=$(occ background-job:list --class='OCA\NgBackup\BackgroundJob\BackupJob' --output=json | php -r 'echo json_decode(stream_get_contents(STDIN), true)[0]["id"];')
|
||||
job() { occ background-job:execute "$JOB" --force-execute >/dev/null; }
|
||||
occ backup:target:schedule ci off >/dev/null
|
||||
occ backup:target:schedule ci3 off >/dev/null
|
||||
occ backup:target:schedule ci2 00:00 --timezone=UTC >/dev/null || fail "set a schedule"
|
||||
occ backup:target:schedule ci2 | grep "00:00 (UTC)" >/dev/null || fail "schedule not shown"
|
||||
job
|
||||
[ "$(occ backup:list ci2 | grep -c ' scheduled ')" = 0 ] || fail "choosing a time started a backup"
|
||||
occ backup:target:schedule ci2 "$($X date -u -d '+1 minute' +%H:%M)/6" --timezone=UTC >/dev/null || fail "set every 6 hours"
|
||||
sleep 65
|
||||
job
|
||||
occ backup:list ci2 | grep " scheduled " >/dev/null || fail "the scheduled moment started no backup"
|
||||
[ "$(occ backup:list ci3 | grep -c ' scheduled ')" = 0 ] || fail "a location that is off was backed up"
|
||||
job
|
||||
[ "$(occ backup:list ci2 | grep -c ' scheduled ')" = 1 ] || fail "one moment started more than one backup"
|
||||
occ backup:target:schedule ci2 nonsense >/dev/null 2>&1 && fail "a bad schedule was accepted"
|
||||
for L in 2 3; do occ backup:target:remove ci$L >/dev/null || fail "target remove ci$L"; done
|
||||
ok "schedule per location: nothing on choosing a time, one backup at the moment, none when off"
|
||||
|
||||
# Checksum audit: shallow (index presence) and deep (download, decrypt, verify every blob).
|
||||
occ backup:verify ci "$SNAP" | grep "^OK$" >/dev/null || fail "shallow verify"
|
||||
occ backup:verify ci "$SNAP" --deep | grep "^OK$" >/dev/null || fail "deep verify"
|
||||
|
||||
Reference in New Issue
Block a user