From 7c15d42c66e220df26227ffa55591d8d39aeae2c Mon Sep 17 00:00:00 2001 From: Lewis Larsen Date: Tue, 30 Jul 2024 00:39:35 +0100 Subject: [PATCH] refactor: Moved statistics queries to model --- app/Livewire/StatisticsPage.php | 134 +++++---------------- app/Models/BackupTask.php | 141 ++++++++++++++++++++++ tests/Unit/Models/BackupTaskTest.php | 173 +++++++++++++++++++++++++++ 3 files changed, 341 insertions(+), 107 deletions(-) diff --git a/app/Livewire/StatisticsPage.php b/app/Livewire/StatisticsPage.php index bfabe59a..b468662f 100644 --- a/app/Livewire/StatisticsPage.php +++ b/app/Livewire/StatisticsPage.php @@ -7,11 +7,7 @@ use App\Models\BackupDestination; use App\Models\BackupTask; use App\Models\BackupTaskData; -use App\Models\BackupTaskLog; use App\Models\RemoteServer; -use Carbon\Carbon; -use Carbon\CarbonInterface; -use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Number; use Illuminate\View\View; @@ -25,31 +21,31 @@ */ class StatisticsPage extends Component { - /** @var array Dates for the backup tasks chart */ + /** @var array Dates for the backup tasks chart */ public array $backupDates = []; - /** @var array Counts of file backups for each date */ + /** @var array Counts of file backups for each date */ public array $fileBackupCounts = []; - /** @var array Counts of database backups for each date */ + /** @var array Counts of database backups for each date */ public array $databaseBackupCounts = []; - /** @var array Labels for the backup success rate chart */ + /** @var array Labels for the backup success rate chart */ public array $backupSuccessRateLabels = []; - /** @var array Data for the backup success rate chart */ + /** @var array Data for the backup success rate chart */ public array $backupSuccessRateData = []; - /** @var array Labels for the average backup size chart */ + /** @var array Labels for the average backup size chart */ public array $averageBackupSizeLabels = []; - /** @var array Data for the average backup size chart */ + /** @var array Data for the average backup size chart */ public array $averageBackupSizeData = []; - /** @var array Labels for the completion time chart */ + /** @var array Labels for the completion time chart */ public array $completionTimeLabels = []; - /** @var array Data for the completion time chart */ + /** @var array Data for the completion time chart */ public array $completionTimeData = []; /** @var string Total data backed up in the past seven days */ @@ -98,33 +94,10 @@ public function render(): View */ private function loadBackupTasksData(): void { - $startDate = now()->subDays(89); - $endDate = now(); - - $backupTasks = BackupTask::selectRaw('DATE(created_at) as date, type, COUNT(*) as count') - ->whereBetween('created_at', [$startDate, $endDate]) - ->groupBy('date', 'type') - ->orderBy('date') - ->get(); - - $dates = Collection::make($startDate->daysUntil($endDate)->toArray()) - ->map(fn (CarbonInterface $date, $key): string => $date->format('Y-m-d')); - - $fileBackups = $databaseBackups = array_fill_keys($dates->toArray(), 0); - - foreach ($backupTasks as $backupTask) { - $date = $backupTask['date']; - $count = (int) $backupTask['count']; - if ($backupTask['type'] === 'Files') { - $fileBackups[$date] = $count; - } else { - $databaseBackups[$date] = $count; - } - } - - $this->backupDates = $dates->values()->toArray(); - $this->fileBackupCounts = array_values($fileBackups); - $this->databaseBackupCounts = array_values($databaseBackups); + $data = BackupTask::getBackupTasksData(); + $this->backupDates = $data['backupDates']; + $this->fileBackupCounts = $data['fileBackupCounts']; + $this->databaseBackupCounts = $data['databaseBackupCounts']; } /** @@ -132,15 +105,15 @@ private function loadBackupTasksData(): void */ private function loadStatistics(): void { - $this->dataBackedUpInThePastSevenDays = $this->formatFileSize( + $this->dataBackedUpInThePastSevenDays = Number::fileSize( (int) BackupTaskData::where('created_at', '>=', now()->subDays(7))->sum('size') ); - $this->dataBackedUpInThePastMonth = $this->formatFileSize( + $this->dataBackedUpInThePastMonth = Number::fileSize( (int) BackupTaskData::where('created_at', '>=', now()->subMonth())->sum('size') ); - $this->dataBackedUpInTotal = $this->formatFileSize( + $this->dataBackedUpInTotal = Number::fileSize( (int) BackupTaskData::sum('size') ); @@ -155,24 +128,9 @@ private function loadStatistics(): void */ private function loadBackupSuccessRateData(): void { - $startDate = now()->startOfMonth()->subMonths(5); - $endDate = now()->endOfMonth(); - - $backupLogs = BackupTaskLog::selectRaw("DATE_TRUNC('month', created_at)::date as month") - ->selectRaw('COUNT(*) as total') - ->selectRaw('SUM(CASE WHEN successful_at IS NOT NULL THEN 1 ELSE 0 END) as successful') - ->whereBetween('created_at', [$startDate, $endDate]) - ->groupBy('month') - ->orderBy('month') - ->get(); - - $this->backupSuccessRateLabels = $backupLogs->pluck('month')->map(fn ($date): string => Carbon::parse($date)->format('Y-m'))->toArray(); - $this->backupSuccessRateData = $backupLogs->map(function ($log): float|int { - $total = (int) ($log['total'] ?? 0); - $successful = (int) ($log['successful'] ?? 0); - - return $total > 0 ? round(($successful / $total) * 100, 2) : 0; - })->toArray(); + $data = BackupTask::getBackupSuccessRateData(); + $this->backupSuccessRateLabels = array_map('strval', $data['labels']); + $this->backupSuccessRateData = array_map('floatval', $data['data']); } /** @@ -180,56 +138,18 @@ private function loadBackupSuccessRateData(): void */ private function loadAverageBackupSizeData(): void { - $backupSizes = BackupTask::join('backup_task_data', 'backup_tasks.id', '=', 'backup_task_data.backup_task_id') - ->join('backup_task_logs', 'backup_tasks.id', '=', 'backup_task_logs.backup_task_id') - ->select('backup_tasks.type') - ->selectRaw('AVG(backup_task_data.size) as average_size') - ->whereNotNull('backup_task_logs.successful_at') - ->groupBy('backup_tasks.type') - ->get(); - - $this->averageBackupSizeLabels = $backupSizes->pluck('type')->toArray(); - $this->averageBackupSizeData = $backupSizes->pluck('average_size') - ->map(fn ($size): string => $this->formatFileSize((int) $size)) - ->toArray(); - } - - private function loadCompletionTimeData(): void - { - $startDate = now()->subMonths(3); - $endDate = now(); - - $completionTimes = BackupTaskData::join('backup_task_logs', 'backup_task_data.backup_task_id', '=', 'backup_task_logs.backup_task_id') - ->selectRaw('DATE(backup_task_logs.created_at) as date') - ->selectRaw(" - AVG( - CASE - WHEN backup_task_data.duration ~ '^\\d+$' THEN backup_task_data.duration::integer - WHEN backup_task_data.duration ~ '^(\\d+):(\\d+):(\\d+)$' THEN - (SUBSTRING(backup_task_data.duration FROM '^(\\d+)'))::integer * 3600 + - (SUBSTRING(backup_task_data.duration FROM '^\\d+:(\\d+)'))::integer * 60 + - (SUBSTRING(backup_task_data.duration FROM ':(\\d+)$'))::integer - ELSE 0 - END - ) as avg_duration - ") - ->whereBetween('backup_task_logs.created_at', [$startDate, $endDate]) - ->whereNotNull('backup_task_logs.successful_at') - ->groupBy('date') - ->orderBy('date') - ->get(); - - $this->completionTimeLabels = $completionTimes->pluck('date')->toArray(); - $this->completionTimeData = $completionTimes->pluck('avg_duration') - ->map(fn ($duration): float => round($duration / 60, 2)) - ->toArray(); + $data = BackupTask::getAverageBackupSizeData(); + $this->averageBackupSizeLabels = array_map('strval', $data['labels']); + $this->averageBackupSizeData = array_map('floatval', $data['data']); } /** - * Format file size using the Number facade + * Load completion time data */ - private function formatFileSize(int $bytes): string + private function loadCompletionTimeData(): void { - return Number::fileSize($bytes); + $data = BackupTask::getCompletionTimeData(); + $this->completionTimeLabels = array_map('strval', $data['labels']); + $this->completionTimeData = array_map('floatval', $data['data']); } } diff --git a/app/Models/BackupTask.php b/app/Models/BackupTask.php index c943767d..041eaf3d 100644 --- a/app/Models/BackupTask.php +++ b/app/Models/BackupTask.php @@ -11,6 +11,7 @@ use App\Jobs\RunFileBackupTaskJob; use App\Mail\BackupTasks\OutputMail; use App\Traits\HasTags; +use Carbon\CarbonInterface; use Cron\CronExpression; use Database\Factories\BackupTaskFactory; use Illuminate\Database\Eloquent\Builder; @@ -27,6 +28,7 @@ use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Mail; use InvalidArgumentException; +use Number; use RuntimeException; class BackupTask extends Model @@ -110,6 +112,145 @@ public static function backupTasksCountByType(int $userId): array ->toArray(); } + /** + * Get backup tasks data for the past 90 days + * + * @return array + */ + public static function getBackupTasksData(): array + { + $startDate = now()->subDays(89); + $endDate = now(); + + $backupTasks = self::selectRaw('DATE(created_at) as date, type, COUNT(*) as count') + ->whereBetween('created_at', [$startDate, $endDate]) + ->groupBy('date', 'type') + ->orderBy('date') + ->get(); + + $dates = Collection::make($startDate->daysUntil($endDate)->toArray()) + ->map(fn (CarbonInterface $date): string => $date->format('Y-m-d')); + + $fileBackups = $databaseBackups = array_fill_keys($dates->toArray(), 0); + + foreach ($backupTasks as $backupTask) { + $date = $backupTask['date']; + $count = (int) $backupTask['count']; + if ($backupTask['type'] === 'Files') { + $fileBackups[$date] = $count; + } else { + $databaseBackups[$date] = $count; + } + } + + return [ + 'backupDates' => $dates->values()->toArray(), + 'fileBackupCounts' => array_values($fileBackups), + 'databaseBackupCounts' => array_values($databaseBackups), + ]; + } + + /** + * Get backup success rate data for the past 6 months + * + * @return array> + */ + public static function getBackupSuccessRateData(): array + { + $startDate = now()->startOfMonth()->subMonths(5); + $endDate = now()->endOfMonth(); + + $backupLogs = BackupTaskLog::selectRaw("DATE_TRUNC('month', created_at)::date as month") + ->selectRaw('COUNT(*) as total') + ->selectRaw('SUM(CASE WHEN successful_at IS NOT NULL THEN 1 ELSE 0 END) as successful') + ->whereBetween('created_at', [$startDate, $endDate]) + ->groupBy('month') + ->orderBy('month') + ->get(); + + $labels = $backupLogs->pluck('month')->map(fn ($date): string => Carbon::parse($date)->format('Y-m'))->toArray(); + $data = $backupLogs->map(function ($log): float { + $total = (int) ($log['total'] ?? 0); + $successful = (int) ($log['successful'] ?? 0); + + return $total > 0 ? round(($successful / $total) * 100, 2) : 0; + })->toArray(); + + return [ + 'labels' => $labels, + 'data' => $data, + ]; + } + + /** + * Get average backup size data + * + * @return array> + */ + public static function getAverageBackupSizeData(): array + { + $backupSizes = self::join('backup_task_data', 'backup_tasks.id', '=', 'backup_task_data.backup_task_id') + ->join('backup_task_logs', 'backup_tasks.id', '=', 'backup_task_logs.backup_task_id') + ->select('backup_tasks.type') + ->selectRaw('AVG(backup_task_data.size) as average_size') + ->whereNotNull('backup_task_logs.successful_at') + ->groupBy('backup_tasks.type') + ->get(); + + return [ + 'labels' => $backupSizes->pluck('type')->toArray(), + 'data' => $backupSizes->pluck('average_size') + ->map(fn ($size): string => self::formatFileSize((int) $size)) + ->toArray(), + ]; + } + + /** + * Get completion time data for the past 3 months + * + * @return array> + */ + public static function getCompletionTimeData(): array + { + $startDate = now()->subMonths(3); + $endDate = now(); + + $completionTimes = BackupTaskData::join('backup_task_logs', 'backup_task_data.backup_task_id', '=', 'backup_task_logs.backup_task_id') + ->selectRaw('DATE(backup_task_logs.created_at) as date') + ->selectRaw(" + AVG( + CASE + WHEN backup_task_data.duration ~ '^\\d+$' THEN backup_task_data.duration::integer + WHEN backup_task_data.duration ~ '^(\\d+):(\\d+):(\\d+)$' THEN + (SUBSTRING(backup_task_data.duration FROM '^(\\d+)'))::integer * 3600 + + (SUBSTRING(backup_task_data.duration FROM '^\\d+:(\\d+)'))::integer * 60 + + (SUBSTRING(backup_task_data.duration FROM ':(\\d+)$'))::integer + ELSE 0 + END + ) as avg_duration + ") + ->whereBetween('backup_task_logs.created_at', [$startDate, $endDate]) + ->whereNotNull('backup_task_logs.successful_at') + ->groupBy('date') + ->orderBy('date') + ->get(); + + return [ + 'labels' => $completionTimes->pluck('date')->toArray(), + 'data' => $completionTimes->pluck('avg_duration') + ->map(fn ($duration): float => round($duration / 60, 2)) + ->toArray(), + ]; + } + + /** + * Format file size using the Number facade + */ + private static function formatFileSize(int $bytes): string + { + return Number::fileSize($bytes); + } + /** * @param Builder $builder * @return Builder diff --git a/tests/Unit/Models/BackupTaskTest.php b/tests/Unit/Models/BackupTaskTest.php index ff955822..0a31b96b 100644 --- a/tests/Unit/Models/BackupTaskTest.php +++ b/tests/Unit/Models/BackupTaskTest.php @@ -1339,3 +1339,176 @@ expect($result)->toBe('13:00') ->and($user2->timezone)->toBe('Europe/London'); }); + +it('retrieves backup tasks data for the past 90 days', function (): void { + Carbon::setTestNow('2023-07-30'); + + // Create some test data + BackupTask::factory()->count(5)->create([ + 'type' => 'Files', + 'created_at' => now()->subDays(10), + ]); + BackupTask::factory()->count(3)->create([ + 'type' => 'Database', + 'created_at' => now()->subDays(5), + ]); + + $result = BackupTask::getBackupTasksData(); + + expect($result)->toHaveKeys(['backupDates', 'fileBackupCounts', 'databaseBackupCounts']) + ->and($result['backupDates'])->toHaveCount(90) + ->and($result['fileBackupCounts'])->toHaveCount(90) + ->and($result['databaseBackupCounts'])->toHaveCount(90) + ->and($result['fileBackupCounts'][79])->toBe(5) // 10 days ago + ->and($result['databaseBackupCounts'][84])->toBe(3); // 5 days ago + + Carbon::setTestNow(); // Reset the mock +}); + +it('retrieves backup success rate data only for months with data', function (): void { + Carbon::setTestNow('2023-07-30'); + + $testMonths = [ + now()->subMonths(2)->startOfMonth(), + now()->subMonths(3)->startOfMonth(), + now()->subMonths(4)->startOfMonth(), + ]; + + foreach ($testMonths as $date) { + BackupTaskLog::factory()->count(8)->create([ + 'created_at' => $date, + 'successful_at' => $date, + ]); + BackupTaskLog::factory()->count(2)->create([ + 'created_at' => $date, + 'successful_at' => null, + ]); + } + + $result = BackupTask::getBackupSuccessRateData(); + + expect($result)->toHaveKeys(['labels', 'data']) + ->and($result['labels'])->toHaveCount(3) + ->and($result['data'])->toHaveCount(3); + + $expectedLabels = array_map(fn ($date): string => $date->format('Y-m'), array_reverse($testMonths)); + expect($result['labels'])->toBe($expectedLabels) + ->and($result['data'])->each(fn ($rate) => $rate->toBe(80.0)); + + Carbon::setTestNow(); +}); + +it('retrieves average backup size data', function (): void { + $fileTasks = BackupTask::factory()->count(3)->create(['type' => 'Files']); + $dbTasks = BackupTask::factory()->count(2)->create(['type' => 'Database']); + + foreach ($fileTasks as $fileTask) { + BackupTaskData::create([ + 'backup_task_id' => $fileTask->getAttribute('id'), + 'size' => 1500000, // 1.5 MB + 'duration' => 100000, + ]); + BackupTaskLog::factory()->create([ + 'backup_task_id' => $fileTask->getAttribute('id'), + 'successful_at' => now(), + ]); + } + + foreach ($dbTasks as $dbTask) { + BackupTaskData::create([ + 'backup_task_id' => $dbTask->getAttribute('id'), + 'size' => 750000, // 750 KB + 'duration' => 100000, + ]); + BackupTaskLog::factory()->create([ + 'backup_task_id' => $dbTask->getAttribute('id'), + 'successful_at' => now(), + ]); + } + + $result = BackupTask::getAverageBackupSizeData(); + + expect($result)->toHaveKeys(['labels', 'data']) + ->and($result['labels'])->toBe(['Files', 'Database']); + + $expectedFileSize = Number::fileSize(1500000); + $expectedDbSize = Number::fileSize(750000); + + expect($result['data'][0])->toBe($expectedFileSize) + ->and($result['data'][1])->toBe($expectedDbSize) + ->and($result['data'][0])->toMatch('/^\d+(\.\d+)?\s(B|KB|MB|GB|TB)$/') + ->and($result['data'][1])->toMatch('/^\d+(\.\d+)?\s(B|KB|MB|GB|TB)$/'); +}); + +it('retrieves completion time data for the past 3 months', function (): void { + Carbon::setTestNow('2023-07-30'); + + // Create some test data + $tasks = BackupTask::factory()->count(5)->create(); + + foreach ($tasks as $index => $task) { + BackupTaskData::create([ + 'backup_task_id' => $task->getAttribute('id'), + 'duration' => ($index + 1) * 60, // 1-5 minutes + ]); + BackupTaskLog::factory()->create([ + 'backup_task_id' => $task->getAttribute('id'), + 'created_at' => now()->subDays($index * 15), // Spread over 2 months + 'successful_at' => now()->subDays($index * 15), + ]); + } + + $result = BackupTask::getCompletionTimeData(); + + expect($result)->toHaveKeys(['labels', 'data']) + ->and($result['labels'])->toHaveCount(5) + ->and($result['data'])->toHaveCount(5) + ->and($result['data'][0])->toBe(5.0) // 5 minutes + ->and($result['data'][4])->toBe(1.0); // 1 minute + + Carbon::setTestNow(); +}); + +it('correctly formats file sizes in getAverageBackupSizeData method', function (): void { + $sizesAndExpectedFormats = [ + 500 => '500 B', + 1024 => '1 KB', + 1500 => '1 KB', + 1048576 => '1 MB', + 1500000 => '1 MB', + 1073741824 => '1 GB', + 1500000000 => '1 GB', + ]; + + foreach ($sizesAndExpectedFormats as $size => $expectedFormat) { + $task = BackupTask::factory()->create(['type' => 'Test']); + + BackupTaskData::create([ + 'backup_task_id' => $task->id, + 'size' => $size, + 'duration' => 100000, + ]); + BackupTaskLog::factory()->create([ + 'backup_task_id' => $task->id, + 'successful_at' => now(), + ]); + + $result = BackupTask::getAverageBackupSizeData(); + + $actualFormat = $result['data'][0]; + + // Check if the unit (B, KB, MB, GB) is correct + expect($actualFormat)->toContain(explode(' ', $expectedFormat)[1]); + + // Check if the numeric part is close to what we expect + $actualNumber = (float) explode(' ', $actualFormat)[0]; + $expectedNumber = (float) explode(' ', $expectedFormat)[0]; + expect($actualNumber)->toBeGreaterThanOrEqual($expectedNumber * 0.9) + ->toBeLessThanOrEqual($expectedNumber * 1.1); + + // Clean up for the next iteration + BackupTask::query()->delete(); + BackupTaskData::query()->delete(); + BackupTaskLog::query()->delete(); + } +});