Without system cron: warning and a backup that moves on while the page is open (#13); progress of the database phase; Bin tab

- The admin page and backup:status warn when background jobs do not run on a
  system cron, or the cron stopped, with a link to how to set one up.
- Without a working system cron, the open admin page moves a running backup
  forward through /api/tick, a short step per poll.
- The database phase shows the tables dumped ("db: 47/183 tables"); a backup
  that has not moved for 15 minutes says it is waiting for background jobs.
- The trash of restore points is on a tab of its own, Bin.
- New screenshots; changelog and plan for 1.0.2.
This commit is contained in:
2026-11-04 04:08:20 +01:00
parent d7c7ca093b
commit 36a30732ad
20 changed files with 529 additions and 75 deletions
+21
View File
@@ -24,4 +24,25 @@ class BackupProgressTest extends TestCase {
$this->assertNull(BackupRun::progress(['phase' => 'files', 'estimate' => ['bytes' => 0, 'approximate' => true], 'stats' => ['bytes' => 1]]));
$this->assertNull(BackupRun::progress(['phase' => 'commit', 'estimate' => ['bytes' => 10, 'approximate' => false], 'stats' => ['bytes' => 1]]));
}
private static function runIn(string $phase, array $state, string $status = \OCA\NgBackup\Db\Run::RUNNING): \OCA\NgBackup\Db\Run {
$run = new \OCA\NgBackup\Db\Run();
$run->setStatus($status);
$run->setPhase($phase);
$run->setState(json_encode($state, JSON_THROW_ON_ERROR));
return $run;
}
public function testDatabasePhaseCountsTheTablesDumped(): void {
$run = self::runIn('db', ['dump' => ['tables' => array_fill(0, 183, 'oc_t'), 'i' => 47]]);
$this->assertSame(['fraction' => 47 / 183, 'approximate' => false, 'tables' => 47, 'totalTables' => 183], BackupRun::progressOf($run));
}
public function testDatabasePhaseHasNoFigureBeforeItsFirstStep(): void {
$this->assertNull(BackupRun::progressOf(self::runIn('db', ['files' => []])));
}
public function testNoProgressForAFinishedRun(): void {
$this->assertNull(BackupRun::progressOf(self::runIn('db', ['dump' => ['tables' => ['oc_a'], 'i' => 0]], \OCA\NgBackup\Db\Run::DONE)));
}
}
+88
View File
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace OCA\NgBackup\Tests\Unit\Service;
use OCA\NgBackup\Db\Run;
use OCA\NgBackup\Service\CronHealth;
use OCP\Exceptions\AppConfigTypeConflictException;
use OCP\IAppConfig;
use OCP\IURLGenerator;
use PHPUnit\Framework\TestCase;
class CronHealthTest extends TestCase {
private const NOW = 1_800_000_000;
private const DOCS = 'https://docs.example.com/background-jobs';
private function health(string $mode, ?int $lastCron, bool $lastCronIsInt = false): CronHealth {
$config = $this->createMock(IAppConfig::class);
$config->method('getValueString')->willReturnCallback(function (string $app, string $key, string $default) use ($mode, $lastCron, $lastCronIsInt): string {
if ($key === 'backgroundjobs_mode') {
return $mode;
}
if ($lastCronIsInt) {
throw new AppConfigTypeConflictException('stored as int');
}
return $lastCron === null ? $default : (string)$lastCron;
});
$config->method('getValueInt')->willReturn($lastCron ?? 0);
$urls = $this->createMock(IURLGenerator::class);
$urls->method('linkToDocs')->with('admin-background-jobs')->willReturn(self::DOCS);
return new CronHealth($config, $urls);
}
public function testSystemCronThatRanRecentlyIsFine(): void {
$check = $this->health('cron', self::NOW - 120)->check(self::NOW);
$this->assertTrue($check['ok']);
$this->assertFalse($check['stopped']);
$this->assertNull(CronHealth::describe($check, self::NOW));
}
public function testAjaxAndWebcronAreNotEnough(): void {
foreach (['ajax' => 'AJAX', 'webcron' => 'Webcron'] as $mode => $name) {
$check = $this->health($mode, self::NOW - 60)->check(self::NOW);
$this->assertFalse($check['ok'], $mode);
$this->assertFalse($check['stopped'], $mode);
$this->assertStringContainsString("they are set to \"$name\" and last ran 1 min ago.", (string)CronHealth::describe($check, self::NOW));
$this->assertStringContainsString(self::DOCS, (string)CronHealth::describe($check, self::NOW));
}
}
public function testSystemCronThatStoppedRunning(): void {
$check = $this->health('cron', self::NOW - 2 * 3600)->check(self::NOW);
$this->assertFalse($check['ok']);
$this->assertTrue($check['stopped']);
$this->assertStringContainsString('has not run for over an hour (last ran 2 h ago)', (string)CronHealth::describe($check, self::NOW));
}
public function testDefaultsToAjaxThatNeverRan(): void {
$check = $this->health('ajax', null)->check(self::NOW);
$this->assertSame(0, $check['lastRun']);
$this->assertStringContainsString('and never ran.', (string)CronHealth::describe($check, self::NOW));
}
public function testReadsALastRunStoredAsAnInteger(): void {
$check = $this->health('cron', self::NOW - 60, true)->check(self::NOW);
$this->assertSame(self::NOW - 60, $check['lastRun']);
$this->assertTrue($check['ok']);
}
public function testARunningBackupWaitsForBackgroundJobsWhenItDidNotMoveForAWhile(): void {
$run = new Run();
$run->setStatus(Run::RUNNING);
$run->setUpdatedAt(self::NOW - 12 * 60);
$this->assertNull(CronHealth::waitingFor($run, self::NOW));
$run->setUpdatedAt(self::NOW - CronHealth::WAITING_AFTER);
$this->assertSame(CronHealth::WAITING_AFTER, CronHealth::waitingFor($run, self::NOW));
$run->setStatus(Run::DONE);
$this->assertNull(CronHealth::waitingFor($run, self::NOW));
}
public function testAgo(): void {
$this->assertSame('1 min ago', CronHealth::ago(10));
$this->assertSame('12 min ago', CronHealth::ago(12 * 60));
$this->assertSame('3 h ago', CronHealth::ago(3 * 3600 + 5));
$this->assertSame('4 days ago', CronHealth::ago(4 * 86400));
}
}