2
0

RotatingFileHandlerTest.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  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. /**
  14. * @covers Monolog\Handler\RotatingFileHandler
  15. */
  16. class RotatingFileHandlerTest extends TestCase
  17. {
  18. private array|null $lastError = null;
  19. public function setUp(): void
  20. {
  21. $dir = __DIR__.'/Fixtures';
  22. chmod($dir, 0777);
  23. if (!is_writable($dir)) {
  24. $this->markTestSkipped($dir.' must be writable to test the RotatingFileHandler.');
  25. }
  26. $this->lastError = null;
  27. set_error_handler(function ($code, $message) {
  28. $this->lastError = [
  29. 'code' => $code,
  30. 'message' => $message,
  31. ];
  32. return true;
  33. });
  34. }
  35. public function tearDown(): void
  36. {
  37. parent::tearDown();
  38. foreach (glob(__DIR__.'/Fixtures/*.rot') as $file) {
  39. unlink($file);
  40. }
  41. if ('testRotationWithFolderByDate' === $this->getName(false)) {
  42. foreach (glob(__DIR__.'/Fixtures/[0-9]*') as $folder) {
  43. $this->rrmdir($folder);
  44. }
  45. }
  46. restore_error_handler();
  47. unset($this->lastError);
  48. }
  49. private function rrmdir($directory) {
  50. if (! is_dir($directory)) {
  51. throw new InvalidArgumentException("$directory must be a directory");
  52. }
  53. if (substr($directory, strlen($directory) - 1, 1) !== '/') {
  54. $directory .= '/';
  55. }
  56. foreach (glob($directory . '*', GLOB_MARK) as $path) {
  57. if (is_dir($path)) {
  58. $this->rrmdir($path);
  59. } else {
  60. unlink($path);
  61. }
  62. }
  63. return rmdir($directory);
  64. }
  65. private function assertErrorWasTriggered($code, $message)
  66. {
  67. if (empty($this->lastError)) {
  68. $this->fail(
  69. sprintf(
  70. 'Failed asserting that error with code `%d` and message `%s` was triggered',
  71. $code,
  72. $message
  73. )
  74. );
  75. }
  76. $this->assertEquals($code, $this->lastError['code'], sprintf('Expected an error with code %d to be triggered, got `%s` instead', $code, $this->lastError['code']));
  77. $this->assertEquals($message, $this->lastError['message'], sprintf('Expected an error with message `%d` to be triggered, got `%s` instead', $message, $this->lastError['message']));
  78. }
  79. public function testRotationCreatesNewFile()
  80. {
  81. touch(__DIR__.'/Fixtures/foo-'.date('Y-m-d', time() - 86400).'.rot');
  82. $handler = new RotatingFileHandler(__DIR__.'/Fixtures/foo.rot');
  83. $handler->setFormatter($this->getIdentityFormatter());
  84. $handler->handle($this->getRecord());
  85. $log = __DIR__.'/Fixtures/foo-'.date('Y-m-d').'.rot';
  86. $this->assertTrue(file_exists($log));
  87. $this->assertEquals('test', file_get_contents($log));
  88. }
  89. /**
  90. * @dataProvider rotationTests
  91. */
  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 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. /**
  148. * @dataProvider rotationWithFolderByDateTests
  149. */
  150. public function testRotationWithFolderByDate($createFile, $dateFormat, $timeCallback)
  151. {
  152. $old1 = $this->createDeep(__DIR__.'/Fixtures/'.date($dateFormat, $timeCallback(-1)).'/foo.rot');
  153. $old2 = $this->createDeep(__DIR__.'/Fixtures/'.date($dateFormat, $timeCallback(-2)).'/foo.rot');
  154. $old3 = $this->createDeep(__DIR__.'/Fixtures/'.date($dateFormat, $timeCallback(-3)).'/foo.rot');
  155. $old4 = $this->createDeep(__DIR__.'/Fixtures/'.date($dateFormat, $timeCallback(-4)).'/foo.rot');
  156. $log = __DIR__.'/Fixtures/'.date($dateFormat).'/foo.rot';
  157. if ($createFile) {
  158. $this->createDeep($log);
  159. }
  160. $handler = new RotatingFileHandler(__DIR__.'/Fixtures/foo.rot', 2);
  161. $handler->setFormatter($this->getIdentityFormatter());
  162. $handler->setFilenameFormat('{date}/{filename}', $dateFormat);
  163. $handler->handle($this->getRecord());
  164. $handler->close();
  165. $this->assertTrue(file_exists($log));
  166. $this->assertTrue(file_exists($old1));
  167. $this->assertEquals($createFile, file_exists($old2));
  168. $this->assertEquals($createFile, file_exists($old3));
  169. $this->assertEquals($createFile, file_exists($old4));
  170. $this->assertEquals('test', file_get_contents($log));
  171. }
  172. public function rotationWithFolderByDateTests()
  173. {
  174. $now = time();
  175. $dayCallback = function ($ago) use ($now) {
  176. return $now + 86400 * $ago;
  177. };
  178. $monthCallback = function ($ago) {
  179. return gmmktime(0, 0, 0, (int) (date('n') + $ago), 1, (int) date('Y'));
  180. };
  181. $yearCallback = function ($ago) {
  182. return gmmktime(0, 0, 0, 1, 1, (int) (date('Y') + $ago));
  183. };
  184. return [
  185. 'Rotation is triggered when the file of the current day is not present'
  186. => [true, 'Y/m/d', $dayCallback],
  187. 'Rotation is not triggered when the file of the current day is already present'
  188. => [false, 'Y/m/d', $dayCallback],
  189. 'Rotation is triggered when the file of the current month is not present'
  190. => [true, 'Y/m', $monthCallback],
  191. 'Rotation is not triggered when the file of the current month is already present'
  192. => [false, 'Y/m', $monthCallback],
  193. 'Rotation is triggered when the file of the current year is not present'
  194. => [true, 'Y', $yearCallback],
  195. 'Rotation is not triggered when the file of the current year is already present'
  196. => [false, 'Y', $yearCallback],
  197. ];
  198. }
  199. /**
  200. * @dataProvider dateFormatProvider
  201. */
  202. public function testAllowOnlyFixedDefinedDateFormats($dateFormat, $valid)
  203. {
  204. $handler = new RotatingFileHandler(__DIR__.'/Fixtures/foo.rot', 2);
  205. if (!$valid) {
  206. $this->expectException(InvalidArgumentException::class);
  207. $this->expectExceptionMessageMatches('~^Invalid date format~');
  208. }
  209. $handler->setFilenameFormat('{filename}-{date}', $dateFormat);
  210. $this->assertTrue(true);
  211. }
  212. public function dateFormatProvider()
  213. {
  214. return [
  215. [RotatingFileHandler::FILE_PER_DAY, true],
  216. [RotatingFileHandler::FILE_PER_MONTH, true],
  217. [RotatingFileHandler::FILE_PER_YEAR, true],
  218. ['Y/m/d', true],
  219. ['Y.m.d', true],
  220. ['Y_m_d', true],
  221. ['Ymd', true],
  222. ['Ym/d', true],
  223. ['Y/m', true],
  224. ['Ym', true],
  225. ['Y.m', true],
  226. ['Y_m', true],
  227. ['Y/md', true],
  228. ['', false],
  229. ['m-d-Y', false],
  230. ['Y-m-d-h-i', false],
  231. ['Y-', false],
  232. ['Y-m-', false],
  233. ['Y--', false],
  234. ['m-d', false],
  235. ['Y-d', false],
  236. ];
  237. }
  238. /**
  239. * @dataProvider filenameFormatProvider
  240. */
  241. public function testDisallowFilenameFormatsWithoutDate($filenameFormat, $valid)
  242. {
  243. $handler = new RotatingFileHandler(__DIR__.'/Fixtures/foo.rot', 2);
  244. if (!$valid) {
  245. $this->expectException(InvalidArgumentException::class);
  246. $this->expectExceptionMessageMatches('~^Invalid filename format~');
  247. }
  248. $handler->setFilenameFormat($filenameFormat, RotatingFileHandler::FILE_PER_DAY);
  249. }
  250. public function filenameFormatProvider()
  251. {
  252. return [
  253. ['{filename}', false],
  254. ['{filename}-{date}', true],
  255. ['{date}', true],
  256. ['foobar-{date}', true],
  257. ['foo-{date}-bar', true],
  258. ['{date}-foobar', true],
  259. ['{date}/{filename}', true],
  260. ['foobar', false],
  261. ];
  262. }
  263. /**
  264. * @dataProvider rotationWhenSimilarFilesExistTests
  265. */
  266. public function testRotationWhenSimilarFileNamesExist($dateFormat)
  267. {
  268. touch($old1 = __DIR__.'/Fixtures/foo-foo-'.date($dateFormat).'.rot');
  269. touch($old2 = __DIR__.'/Fixtures/foo-bar-'.date($dateFormat).'.rot');
  270. $log = __DIR__.'/Fixtures/foo-'.date($dateFormat).'.rot';
  271. $handler = new RotatingFileHandler(__DIR__.'/Fixtures/foo.rot', 2);
  272. $handler->setFormatter($this->getIdentityFormatter());
  273. $handler->setFilenameFormat('{filename}-{date}', $dateFormat);
  274. $handler->handle($this->getRecord());
  275. $handler->close();
  276. $this->assertTrue(file_exists($log));
  277. }
  278. public function rotationWhenSimilarFilesExistTests()
  279. {
  280. return [
  281. 'Rotation is triggered when the file of the current day is not present but similar exists'
  282. => [RotatingFileHandler::FILE_PER_DAY],
  283. 'Rotation is triggered when the file of the current month is not present but similar exists'
  284. => [RotatingFileHandler::FILE_PER_MONTH],
  285. 'Rotation is triggered when the file of the current year is not present but similar exists'
  286. => [RotatingFileHandler::FILE_PER_YEAR],
  287. ];
  288. }
  289. public function testReuseCurrentFile()
  290. {
  291. $log = __DIR__.'/Fixtures/foo-'.date('Y-m-d').'.rot';
  292. file_put_contents($log, "foo");
  293. $handler = new RotatingFileHandler(__DIR__.'/Fixtures/foo.rot');
  294. $handler->setFormatter($this->getIdentityFormatter());
  295. $handler->handle($this->getRecord());
  296. $this->assertEquals('footest', file_get_contents($log));
  297. }
  298. }