2
0

RotatingFileHandlerTest.php 12 KB

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