RotatingFileHandlerTest.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. <?php declare(strict_types=1);
  2. /*
  3. * This file is part of the Monolog package.
  4. *
  5. * (c) Jordi Boggiano <j.boggiano@seld.be>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Monolog\Handler;
  11. use InvalidArgumentException;
  12. use Monolog\Test\TestCase;
  13. use PHPUnit\Framework\Attributes\DataProvider;
  14. /**
  15. * @covers Monolog\Handler\RotatingFileHandler
  16. */
  17. class RotatingFileHandlerTest extends TestCase
  18. {
  19. private array|null $lastError = null;
  20. public function setUp(): void
  21. {
  22. $dir = __DIR__.'/Fixtures';
  23. chmod($dir, 0777);
  24. if (!is_writable($dir)) {
  25. $this->markTestSkipped($dir.' must be writable to test the RotatingFileHandler.');
  26. }
  27. $this->lastError = null;
  28. set_error_handler(function ($code, $message) {
  29. $this->lastError = [
  30. 'code' => $code,
  31. 'message' => $message,
  32. ];
  33. return true;
  34. });
  35. }
  36. public function tearDown(): void
  37. {
  38. parent::tearDown();
  39. foreach (glob(__DIR__.'/Fixtures/*.rot') as $file) {
  40. unlink($file);
  41. }
  42. if ('testRotationWithFolderByDate' === $this->name()) {
  43. foreach (glob(__DIR__.'/Fixtures/[0-9]*') as $folder) {
  44. $this->rrmdir($folder);
  45. }
  46. }
  47. restore_error_handler();
  48. unset($this->lastError);
  49. }
  50. private function rrmdir($directory) {
  51. if (! is_dir($directory)) {
  52. throw new InvalidArgumentException("$directory must be a directory");
  53. }
  54. if (substr($directory, strlen($directory) - 1, 1) !== '/') {
  55. $directory .= '/';
  56. }
  57. foreach (glob($directory . '*', GLOB_MARK) as $path) {
  58. if (is_dir($path)) {
  59. $this->rrmdir($path);
  60. } else {
  61. unlink($path);
  62. }
  63. }
  64. return rmdir($directory);
  65. }
  66. private function assertErrorWasTriggered($code, $message)
  67. {
  68. if (empty($this->lastError)) {
  69. $this->fail(
  70. sprintf(
  71. 'Failed asserting that error with code `%d` and message `%s` was triggered',
  72. $code,
  73. $message
  74. )
  75. );
  76. }
  77. $this->assertEquals($code, $this->lastError['code'], sprintf('Expected an error with code %d to be triggered, got `%s` instead', $code, $this->lastError['code']));
  78. $this->assertEquals($message, $this->lastError['message'], sprintf('Expected an error with message `%d` to be triggered, got `%s` instead', $message, $this->lastError['message']));
  79. }
  80. public function testRotationCreatesNewFile()
  81. {
  82. touch(__DIR__.'/Fixtures/foo-'.date('Y-m-d', time() - 86400).'.rot');
  83. $handler = new RotatingFileHandler(__DIR__.'/Fixtures/foo.rot');
  84. $handler->setFormatter($this->getIdentityFormatter());
  85. $handler->handle($this->getRecord());
  86. $log = __DIR__.'/Fixtures/foo-'.date('Y-m-d').'.rot';
  87. $this->assertTrue(file_exists($log));
  88. $this->assertEquals('test', file_get_contents($log));
  89. }
  90. #[DataProvider('rotationTests')]
  91. public function testRotation($createFile, $dateFormat, $timeCallback)
  92. {
  93. touch($old1 = __DIR__.'/Fixtures/foo-'.date($dateFormat, $timeCallback(-1)).'.rot');
  94. touch($old2 = __DIR__.'/Fixtures/foo-'.date($dateFormat, $timeCallback(-2)).'.rot');
  95. touch($old3 = __DIR__.'/Fixtures/foo-'.date($dateFormat, $timeCallback(-3)).'.rot');
  96. touch($old4 = __DIR__.'/Fixtures/foo-'.date($dateFormat, $timeCallback(-4)).'.rot');
  97. $log = __DIR__.'/Fixtures/foo-'.date($dateFormat).'.rot';
  98. if ($createFile) {
  99. touch($log);
  100. }
  101. $handler = new RotatingFileHandler(__DIR__.'/Fixtures/foo.rot', 2);
  102. $handler->setFormatter($this->getIdentityFormatter());
  103. $handler->setFilenameFormat('{filename}-{date}', $dateFormat);
  104. $handler->handle($this->getRecord());
  105. $handler->close();
  106. $this->assertTrue(file_exists($log));
  107. $this->assertTrue(file_exists($old1));
  108. $this->assertEquals($createFile, file_exists($old2));
  109. $this->assertEquals($createFile, file_exists($old3));
  110. $this->assertEquals($createFile, file_exists($old4));
  111. $this->assertEquals('test', file_get_contents($log));
  112. }
  113. public static function rotationTests()
  114. {
  115. $now = time();
  116. $dayCallback = function ($ago) use ($now) {
  117. return $now + 86400 * $ago;
  118. };
  119. $monthCallback = function ($ago) {
  120. return gmmktime(0, 0, 0, (int) (date('n') + $ago), 1, (int) date('Y'));
  121. };
  122. $yearCallback = function ($ago) {
  123. return gmmktime(0, 0, 0, 1, 1, (int) (date('Y') + $ago));
  124. };
  125. return [
  126. 'Rotation is triggered when the file of the current day is not present'
  127. => [true, RotatingFileHandler::FILE_PER_DAY, $dayCallback],
  128. 'Rotation is not triggered when the file of the current day is already present'
  129. => [false, RotatingFileHandler::FILE_PER_DAY, $dayCallback],
  130. 'Rotation is triggered when the file of the current month is not present'
  131. => [true, RotatingFileHandler::FILE_PER_MONTH, $monthCallback],
  132. 'Rotation is not triggered when the file of the current month is already present'
  133. => [false, RotatingFileHandler::FILE_PER_MONTH, $monthCallback],
  134. 'Rotation is triggered when the file of the current year is not present'
  135. => [true, RotatingFileHandler::FILE_PER_YEAR, $yearCallback],
  136. 'Rotation is not triggered when the file of the current year is already present'
  137. => [false, RotatingFileHandler::FILE_PER_YEAR, $yearCallback],
  138. ];
  139. }
  140. private function createDeep($file)
  141. {
  142. mkdir(dirname($file), 0777, true);
  143. touch($file);
  144. return $file;
  145. }
  146. #[DataProvider('rotationWithFolderByDateTests')]
  147. public function testRotationWithFolderByDate($createFile, $dateFormat, $timeCallback)
  148. {
  149. $old1 = $this->createDeep(__DIR__.'/Fixtures/'.date($dateFormat, $timeCallback(-1)).'/foo.rot');
  150. $old2 = $this->createDeep(__DIR__.'/Fixtures/'.date($dateFormat, $timeCallback(-2)).'/foo.rot');
  151. $old3 = $this->createDeep(__DIR__.'/Fixtures/'.date($dateFormat, $timeCallback(-3)).'/foo.rot');
  152. $old4 = $this->createDeep(__DIR__.'/Fixtures/'.date($dateFormat, $timeCallback(-4)).'/foo.rot');
  153. $log = __DIR__.'/Fixtures/'.date($dateFormat).'/foo.rot';
  154. if ($createFile) {
  155. $this->createDeep($log);
  156. }
  157. $handler = new RotatingFileHandler(__DIR__.'/Fixtures/foo.rot', 2);
  158. $handler->setFormatter($this->getIdentityFormatter());
  159. $handler->setFilenameFormat('{date}/{filename}', $dateFormat);
  160. $handler->handle($this->getRecord());
  161. $handler->close();
  162. $this->assertTrue(file_exists($log));
  163. $this->assertTrue(file_exists($old1));
  164. $this->assertEquals($createFile, file_exists($old2));
  165. $this->assertEquals($createFile, file_exists($old3));
  166. $this->assertEquals($createFile, file_exists($old4));
  167. $this->assertEquals('test', file_get_contents($log));
  168. }
  169. public static function rotationWithFolderByDateTests()
  170. {
  171. $now = time();
  172. $dayCallback = function ($ago) use ($now) {
  173. return $now + 86400 * $ago;
  174. };
  175. $monthCallback = function ($ago) {
  176. return gmmktime(0, 0, 0, (int) (date('n') + $ago), 1, (int) date('Y'));
  177. };
  178. $yearCallback = function ($ago) {
  179. return gmmktime(0, 0, 0, 1, 1, (int) (date('Y') + $ago));
  180. };
  181. return [
  182. 'Rotation is triggered when the file of the current day is not present'
  183. => [true, 'Y/m/d', $dayCallback],
  184. 'Rotation is not triggered when the file of the current day is already present'
  185. => [false, 'Y/m/d', $dayCallback],
  186. 'Rotation is triggered when the file of the current month is not present'
  187. => [true, 'Y/m', $monthCallback],
  188. 'Rotation is not triggered when the file of the current month is already present'
  189. => [false, 'Y/m', $monthCallback],
  190. 'Rotation is triggered when the file of the current year is not present'
  191. => [true, 'Y', $yearCallback],
  192. 'Rotation is not triggered when the file of the current year is already present'
  193. => [false, 'Y', $yearCallback],
  194. ];
  195. }
  196. #[DataProvider('dateFormatProvider')]
  197. public function testAllowOnlyFixedDefinedDateFormats($dateFormat, $valid)
  198. {
  199. $handler = new RotatingFileHandler(__DIR__.'/Fixtures/foo.rot', 2);
  200. if (!$valid) {
  201. $this->expectException(InvalidArgumentException::class);
  202. $this->expectExceptionMessageMatches('~^Invalid date format~');
  203. }
  204. $handler->setFilenameFormat('{filename}-{date}', $dateFormat);
  205. $this->assertTrue(true);
  206. }
  207. public static function dateFormatProvider()
  208. {
  209. return [
  210. [RotatingFileHandler::FILE_PER_DAY, true],
  211. [RotatingFileHandler::FILE_PER_MONTH, true],
  212. [RotatingFileHandler::FILE_PER_YEAR, true],
  213. ['Y/m/d', true],
  214. ['Y.m.d', true],
  215. ['Y_m_d', true],
  216. ['Ymd', true],
  217. ['Ym/d', true],
  218. ['Y/m', true],
  219. ['Ym', true],
  220. ['Y.m', true],
  221. ['Y_m', true],
  222. ['Y/md', true],
  223. ['', false],
  224. ['m-d-Y', false],
  225. ['Y-m-d-h-i', false],
  226. ['Y-', false],
  227. ['Y-m-', false],
  228. ['Y--', false],
  229. ['m-d', false],
  230. ['Y-d', false],
  231. ];
  232. }
  233. #[DataProvider('filenameFormatProvider')]
  234. public function testDisallowFilenameFormatsWithoutDate($filenameFormat, $valid)
  235. {
  236. $handler = new RotatingFileHandler(__DIR__.'/Fixtures/foo.rot', 2);
  237. if (!$valid) {
  238. $this->expectException(InvalidArgumentException::class);
  239. $this->expectExceptionMessageMatches('~^Invalid filename format~');
  240. }
  241. $handler->setFilenameFormat($filenameFormat, RotatingFileHandler::FILE_PER_DAY);
  242. }
  243. public static function filenameFormatProvider()
  244. {
  245. return [
  246. ['{filename}', false],
  247. ['{filename}-{date}', true],
  248. ['{date}', true],
  249. ['foobar-{date}', true],
  250. ['foo-{date}-bar', true],
  251. ['{date}-foobar', true],
  252. ['{date}/{filename}', true],
  253. ['foobar', false],
  254. ];
  255. }
  256. #[DataProvider('rotationWhenSimilarFilesExistTests')]
  257. public function testRotationWhenSimilarFileNamesExist($dateFormat)
  258. {
  259. touch($old1 = __DIR__.'/Fixtures/foo-foo-'.date($dateFormat).'.rot');
  260. touch($old2 = __DIR__.'/Fixtures/foo-bar-'.date($dateFormat).'.rot');
  261. $log = __DIR__.'/Fixtures/foo-'.date($dateFormat).'.rot';
  262. $handler = new RotatingFileHandler(__DIR__.'/Fixtures/foo.rot', 2);
  263. $handler->setFormatter($this->getIdentityFormatter());
  264. $handler->setFilenameFormat('{filename}-{date}', $dateFormat);
  265. $handler->handle($this->getRecord());
  266. $handler->close();
  267. $this->assertTrue(file_exists($log));
  268. }
  269. public static function rotationWhenSimilarFilesExistTests()
  270. {
  271. return [
  272. 'Rotation is triggered when the file of the current day is not present but similar exists'
  273. => [RotatingFileHandler::FILE_PER_DAY],
  274. 'Rotation is triggered when the file of the current month is not present but similar exists'
  275. => [RotatingFileHandler::FILE_PER_MONTH],
  276. 'Rotation is triggered when the file of the current year is not present but similar exists'
  277. => [RotatingFileHandler::FILE_PER_YEAR],
  278. ];
  279. }
  280. public function testReuseCurrentFile()
  281. {
  282. $log = __DIR__.'/Fixtures/foo-'.date('Y-m-d').'.rot';
  283. file_put_contents($log, "foo");
  284. $handler = new RotatingFileHandler(__DIR__.'/Fixtures/foo.rot');
  285. $handler->setFormatter($this->getIdentityFormatter());
  286. $handler->handle($this->getRecord());
  287. $this->assertEquals('footest', file_get_contents($log));
  288. }
  289. }