Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions core/Command/SetupChecks.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@

namespace OC\Core\Command;

use OC\Migration\ConsoleOutput;
use OCP\RichObjectStrings\IRichTextFormatter;
use OCP\SetupCheck\ISetupCheckManager;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class SetupChecks extends Base {
Expand Down Expand Up @@ -60,12 +62,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return self::FAILURE;
}

$progressOutput = new ConsoleOutput($output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);

if ($filterByCategory !== '') {
$results = $this->setupCheckManager->runByCategory($filterByCategory);
$results = $this->setupCheckManager->runByCategory($filterByCategory, $progressOutput);
} elseif ($filterByClass !== '') {
$results = $this->setupCheckManager->runByClass($filterByClass);
$results = $this->setupCheckManager->runByClass($filterByClass, $progressOutput);
} else {
$results = $this->setupCheckManager->runAll();
$results = $this->setupCheckManager->runAll($progressOutput);
}

switch ($input->getOption('output')) {
Expand Down
31 changes: 23 additions & 8 deletions lib/private/SetupCheck/SetupCheckManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
namespace OC\SetupCheck;

use OC\AppFramework\Bootstrap\Coordinator;
use OCP\Migration\IOutput;
use OCP\Server;
use OCP\SetupCheck\ISetupCheck;
use OCP\SetupCheck\ISetupCheckManager;
use OCP\SetupCheck\SetupResult;
use OCP\Util;
use Psr\Log\LoggerInterface;

class SetupCheckManager implements ISetupCheckManager {
Expand All @@ -24,24 +26,24 @@ public function __construct(
}

#[\Override]
public function runByClass(string $filterByClass): array {
public function runByClass(string $filterByClass, ?IOutput $output = null): array {
if (str_starts_with($filterByClass, '\\')) {
$filterByClass = substr($filterByClass, 1);
}
return $this->run(filterByClass: $filterByClass);
return $this->run(filterByClass: $filterByClass, output: $output);
}

#[\Override]
public function runByCategory(string $filterByCategory): array {
return $this->run(filterByCategory: $filterByCategory);
public function runByCategory(string $filterByCategory, ?IOutput $output = null): array {
return $this->run(filterByCategory: $filterByCategory, output: $output);
}

#[\Override]
public function runAll(): array {
return $this->run();
public function runAll(?IOutput $output = null): array {
return $this->run(output: $output);
}

private function run(?string $filterByCategory = null, ?string $filterByClass = null): array {
private function run(?string $filterByCategory = null, ?string $filterByClass = null, ?IOutput $output = null): array {
$results = [];
$setupChecks = $this->coordinator->getRegistrationContext()->getSetupChecks();
foreach ($setupChecks as $setupCheck) {
Expand All @@ -55,13 +57,26 @@ private function run(?string $filterByCategory = null, ?string $filterByClass =
continue;
}

$this->logger->debug('Running check ' . get_class($setupCheckObject));
$checkDetails = $setupCheckObject->getName() . ' (' . get_class($setupCheckObject) . ')';
$message = 'Starting check ' . $checkDetails;
$output?->debug($message);
$this->logger->debug($message);

memory_reset_peak_usage();
$startTime = microtime(true);
try {
$setupResult = $setupCheckObject->run();
} catch (\Throwable $t) {
$setupResult = SetupResult::error("An exception occurred while running the setup check:\n$t");
$this->logger->error('Exception running check ' . get_class($setupCheckObject) . ': ' . $t->getMessage(), ['exception' => $t]);
}
$timeSpent = microtime(true) - $startTime;
$memoryPeak = memory_get_peak_usage();

$message = 'Check ' . $checkDetails . ' done in ' . number_format($timeSpent, 2) . ' seconds, peak memory usage: ' . Util::humanFileSize($memoryPeak);
$output?->debug($message);
$this->logger->debug($message);

$setupResult->setName($setupCheckObject->getName());
$category = $setupCheckObject->getCategory();
$results[$category] ??= [];
Expand Down
14 changes: 11 additions & 3 deletions lib/public/SetupCheck/ISetupCheckManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,41 @@

namespace OCP\SetupCheck;

use OCP\Migration\IOutput;

/**
* @since 28.0.0
*/
interface ISetupCheckManager {
/**
* Run all setup checks and return the results.
*
* @param ?IOutput $output - Reports the check that is about to run as debug output, so a check that crashes or runs out of memory can be identified.
* @since 28.0.0
* @since 36.0.0 - parameter $output was added
* @return array<string,array<string,SetupResult>> Result of each check, first level key is category, second level key is title
*/
public function runAll(): array;
public function runAll(?IOutput $output = null): array;

/**
* Run all tests from one specific category and return the results.
*
* @param string $filterByCategory - The id of the category to run.
* @param ?IOutput $output - Reports the check that is about to run as debug output, so a check that crashes or runs out of memory can be identified.
* @return array<string,array<string,SetupResult>> Result of each check, first level key is category, second level key is title
* @since 35.0.0
* @since 36.0.0 - parameter $output was added
*/
public function runByCategory(string $filterByCategory): array;
public function runByCategory(string $filterByCategory, ?IOutput $output = null): array;

/**
* Run all tests from one specific class and return the results.
*
* @param string $filterByClass - The class to run.
* @param ?IOutput $output - Reports the check that is about to run as debug output, so a check that crashes or runs out of memory can be identified.
* @return array<string,array<string,SetupResult>> Result of each check, first level key is category, second level key is title
* @since 35.0.0
* @since 36.0.0 - parameter $output was added
*/
public function runByClass(string $filterByClass): array;
public function runByClass(string $filterByClass, ?IOutput $output = null): array;
}
146 changes: 146 additions & 0 deletions tests/Core/Command/SetupChecksTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace Tests\Core\Command;

use OC\Core\Command\SetupChecks;
use OCP\Migration\IOutput;
use OCP\RichObjectStrings\IRichTextFormatter;
use OCP\SetupCheck\ISetupCheck;
use OCP\SetupCheck\ISetupCheckManager;
use OCP\SetupCheck\SetupResult;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Tester\CommandTester;
use Test\TestCase;

class SetupChecksTestCheck implements ISetupCheck {
#[\Override]
public function getCategory(): string {
return 'system';
}

#[\Override]
public function getName(): string {
return 'Test check';
}

#[\Override]
public function run(): SetupResult {
return SetupResult::success();
}
}

class SetupChecksTest extends TestCase {
private ISetupCheckManager&MockObject $setupCheckManager;
private IRichTextFormatter&MockObject $richTextFormatter;
private CommandTester $commandTester;

#[\Override]
protected function setUp(): void {
parent::setUp();

$this->setupCheckManager = $this->createMock(ISetupCheckManager::class);
$this->richTextFormatter = $this->createMock(IRichTextFormatter::class);

$this->commandTester = new CommandTester(
new SetupChecks($this->setupCheckManager, $this->richTextFormatter)
);
}

/**
* Report one check through the output the command passes to the manager, and return its result.
*/
private function runOneCheck(?IOutput $output): array {
$this->assertInstanceOf(IOutput::class, $output);
$check = new SetupChecksTestCheck();
$output->debug('Starting check ' . $check->getName() . ' (' . $check::class . ')');
$output->debug('Check ' . $check->getName() . ' (' . $check::class . ') done in 0.01 seconds, peak memory usage: 8 MB');
$result = SetupResult::success('Everything is fine');
$result->setName($check->getName());
return ['system' => [$check::class => $result]];
}

private function expectRunAll(): void {
$this->setupCheckManager->expects($this->once())
->method('runAll')
->willReturnCallback($this->runOneCheck(...));
}

public function testProgressIsReportedOnVerboseOutput(): void {
$this->expectRunAll();

$this->assertSame(Command::SUCCESS, $this->commandTester->execute([], [
'verbosity' => OutputInterface::VERBOSITY_VERBOSE,
'capture_stderr_separately' => true,
]));

$this->assertStringContainsString('Starting check Test check (' . SetupChecksTestCheck::class . ')', $this->commandTester->getErrorOutput());
$this->assertStringContainsString('done in 0.01 seconds, peak memory usage: 8 MB', $this->commandTester->getErrorOutput());
$this->assertStringContainsString('Everything is fine', $this->commandTester->getDisplay());
$this->assertStringNotContainsString('Starting check Test check', $this->commandTester->getDisplay());
}

public function testProgressIsNotReportedOnNormalOutput(): void {
$this->expectRunAll();

$this->assertSame(Command::SUCCESS, $this->commandTester->execute([], [
'capture_stderr_separately' => true,
]));

$this->assertStringNotContainsString('Starting check Test check', $this->commandTester->getErrorOutput());
$this->assertStringNotContainsString('Starting check Test check', $this->commandTester->getDisplay());
}

public function testProgressKeepsJsonOutputParsable(): void {
$this->expectRunAll();

$this->assertSame(Command::SUCCESS, $this->commandTester->execute(['--output' => 'json'], [
'verbosity' => OutputInterface::VERBOSITY_VERBOSE,
'capture_stderr_separately' => true,
]));

$this->assertStringContainsString('Starting check Test check', $this->commandTester->getErrorOutput());
$this->assertIsArray(json_decode($this->commandTester->getDisplay(), true, flags: JSON_THROW_ON_ERROR));
}

public function testFilterByCategory(): void {
$this->setupCheckManager->expects($this->once())
->method('runByCategory')
->willReturnCallback(function (string $category, ?IOutput $output): array {
$this->assertSame('system', $category);
return $this->runOneCheck($output);
});

$this->assertSame(Command::SUCCESS, $this->commandTester->execute(['category' => 'system']));
}

public function testFilterByClass(): void {
$this->setupCheckManager->expects($this->once())
->method('runByClass')
->willReturnCallback(function (string $class, ?IOutput $output): array {
$this->assertSame(SetupChecksTestCheck::class, $class);
return $this->runOneCheck($output);
});

$this->assertSame(Command::SUCCESS, $this->commandTester->execute(['class' => SetupChecksTestCheck::class]));
}

public function testFilterByCategoryAndClassIsRejected(): void {
$this->setupCheckManager->expects($this->never())
->method($this->anything());

$this->assertSame(Command::FAILURE, $this->commandTester->execute([
'category' => 'system',
'class' => SetupChecksTestCheck::class,
]));
$this->assertStringContainsString('Please specify only one of category or class', $this->commandTester->getDisplay());
}
}
Loading
Loading