. * */ namespace Ampache\Module\System; use FilesystemIterator; use PDO; use PHPUnit\Framework\TestCase; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use ReflectionMethod; use SplFileInfo; /** * A server left with `autocommit = 0` puts every connection in one transaction that Ampache never commits, so * a cron run holds a read view from its first SELECT to its last write. Nothing fails loudly until MariaDB * 11.6.2, where snapshot isolation turns the next concurrent play into `1020 Record has changed since last read`. * * These are source guards: the state belongs to a live connection, which the suite has none of. */ class DbaConnectionTest extends TestCase { /** * The clean code is the string '00000', which is truthy, so a handle that has run anything at all would be * thrown away as broken. Forcing autocommit at connection time is what makes that reachable. */ public function testACleanErrorCodeIsNotAFailure(): void { $clean = $this->createMock(PDO::class); $clean->method('errorCode')->willReturn(PDO::ERR_NONE); $broken = $this->createMock(PDO::class); $broken->method('errorCode')->willReturn('HY000'); $hasError = new ReflectionMethod(Dba::class, '_has_error'); self::assertFalse($hasError->invoke(null, $clean), 'a connection that ran a statement cleanly is usable'); self::assertTrue($hasError->invoke(null, $broken)); } public function testEveryConnectionIsPutBackIntoAutocommit(): void { self::assertStringContainsString( "\$dbh->exec('SET SESSION autocommit = 1');", (string) file_get_contents(__DIR__ . '/../../../src/Module/System/Dba.php'), 'Dba no longer forces autocommit on, so a server configured without it breaks the cron' ); } public function testNothingElseOpensItsOwnConnection(): void { $sourcePath = realpath(__DIR__ . '/../../../src'); self::assertIsString($sourcePath); $offenders = []; $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($sourcePath, FilesystemIterator::SKIP_DOTS) ); /** @var SplFileInfo $file */ foreach ($iterator as $file) { $path = (string) $file->getRealPath(); if ( $file->getExtension() === 'php' && $file->getFilename() !== 'Dba.php' && str_contains((string) file_get_contents($path), 'new PDO(') ) { // getRealPath() carries the platform separator, and a report worth reading has to look the // same whether the suite ran on Windows or not $offenders[] = str_replace('\\', '/', str_replace($sourcePath, '', $path)); } } self::assertSame([], $offenders, 'a connection built outside Dba never gets the autocommit statement'); } public function testTheTwoTransactionsThatDoCommitAreStillTheOnlyOnes(): void { $sourcePath = realpath(__DIR__ . '/../../../src'); self::assertIsString($sourcePath); $callers = []; $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($sourcePath, FilesystemIterator::SKIP_DOTS) ); /** @var SplFileInfo $file */ foreach ($iterator as $file) { $path = (string) $file->getRealPath(); if ($file->getExtension() === 'php' && str_contains((string) file_get_contents($path), '->beginTransaction()')) { // getRealPath() carries the platform separator, and the assertion below is written portably $callers[] = str_replace('\\', '/', str_replace($sourcePath, '', $path)); } } self::assertSame(['/Module/Statistics/Stats.php'], $callers, 'a new transaction must commit, or autocommit is a lie'); } }