2
0

StreamHandlerTest.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  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 Monolog\Level;
  12. use PHPUnit\Framework\Attributes\DataProvider;
  13. class StreamHandlerTest extends \Monolog\Test\MonologTestCase
  14. {
  15. public function tearDown(): void
  16. {
  17. parent::tearDown();
  18. @unlink(__DIR__.'/test.log');
  19. }
  20. /**
  21. * @covers Monolog\Handler\StreamHandler::__construct
  22. * @covers Monolog\Handler\StreamHandler::write
  23. */
  24. public function testWrite()
  25. {
  26. $handle = fopen('php://memory', 'a+');
  27. $handler = new StreamHandler($handle);
  28. $handler->setFormatter($this->getIdentityFormatter());
  29. $handler->handle($this->getRecord(Level::Warning, 'test'));
  30. $handler->handle($this->getRecord(Level::Warning, 'test2'));
  31. $handler->handle($this->getRecord(Level::Warning, 'test3'));
  32. fseek($handle, 0);
  33. $this->assertEquals('testtest2test3', fread($handle, 100));
  34. }
  35. /**
  36. * @covers Monolog\Handler\StreamHandler::close
  37. */
  38. public function testCloseKeepsExternalHandlersOpen()
  39. {
  40. $handle = fopen('php://memory', 'a+');
  41. $handler = new StreamHandler($handle);
  42. $this->assertTrue(\is_resource($handle));
  43. $handler->close();
  44. $this->assertTrue(\is_resource($handle));
  45. }
  46. /**
  47. * @covers Monolog\Handler\StreamHandler::close
  48. */
  49. public function testClose()
  50. {
  51. $handler = new StreamHandler('php://memory');
  52. $handler->handle($this->getRecord(Level::Warning, 'test'));
  53. $stream = $handler->getStream();
  54. $this->assertTrue(\is_resource($stream));
  55. $handler->close();
  56. $this->assertFalse(\is_resource($stream));
  57. }
  58. /**
  59. * @covers Monolog\Handler\StreamHandler::close
  60. * @covers Monolog\Handler\Handler::__serialize
  61. */
  62. public function testSerialization()
  63. {
  64. $handler = new StreamHandler('php://memory');
  65. $handler->handle($this->getRecord(Level::Warning, 'testfoo'));
  66. $stream = $handler->getStream();
  67. $this->assertTrue(\is_resource($stream));
  68. fseek($stream, 0);
  69. $this->assertStringContainsString('testfoo', stream_get_contents($stream));
  70. $serialized = serialize($handler);
  71. $this->assertFalse(\is_resource($stream));
  72. $handler = unserialize($serialized);
  73. $handler->handle($this->getRecord(Level::Warning, 'testbar'));
  74. $stream = $handler->getStream();
  75. $this->assertTrue(\is_resource($stream));
  76. fseek($stream, 0);
  77. $contents = stream_get_contents($stream);
  78. $this->assertStringNotContainsString('testfoo', $contents);
  79. $this->assertStringContainsString('testbar', $contents);
  80. }
  81. /**
  82. * @covers Monolog\Handler\StreamHandler::write
  83. */
  84. public function testWriteCreatesTheStreamResource()
  85. {
  86. $handler = new StreamHandler('php://memory');
  87. $handler->handle($this->getRecord());
  88. }
  89. /**
  90. * @covers Monolog\Handler\StreamHandler::__construct
  91. * @covers Monolog\Handler\StreamHandler::write
  92. */
  93. public function testWriteLocking()
  94. {
  95. $temp = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'monolog_locked_log';
  96. $handler = new StreamHandler($temp, Level::Debug, true, null, true);
  97. $handler->handle($this->getRecord());
  98. }
  99. /**
  100. * @covers Monolog\Handler\StreamHandler::__construct
  101. * @covers Monolog\Handler\StreamHandler::write
  102. */
  103. public function testWriteMissingResource()
  104. {
  105. $this->expectException(\LogicException::class);
  106. $handler = new StreamHandler(null);
  107. $handler->handle($this->getRecord());
  108. }
  109. public static function invalidArgumentProvider()
  110. {
  111. return [
  112. [1],
  113. [[]],
  114. [['bogus://url']],
  115. ];
  116. }
  117. /**
  118. * @covers Monolog\Handler\StreamHandler::__construct
  119. */
  120. #[DataProvider('invalidArgumentProvider')]
  121. public function testWriteInvalidArgument($invalidArgument)
  122. {
  123. $this->expectException(\InvalidArgumentException::class);
  124. $handler = new StreamHandler($invalidArgument);
  125. }
  126. /**
  127. * @covers Monolog\Handler\StreamHandler::__construct
  128. * @covers Monolog\Handler\StreamHandler::write
  129. */
  130. public function testWriteInvalidResource()
  131. {
  132. $this->expectException(\UnexpectedValueException::class);
  133. $php7xMessage = <<<STRING
  134. The stream or file "bogus://url" could not be opened in append mode: failed to open stream: No such file or directory
  135. The exception occurred while attempting to log: test
  136. Context: {"foo":"bar"}
  137. Extra: [1,2,3]
  138. STRING;
  139. $php8xMessage = <<<STRING
  140. The stream or file "bogus://url" could not be opened in append mode: Failed to open stream: No such file or directory
  141. The exception occurred while attempting to log: test
  142. Context: {"foo":"bar"}
  143. Extra: [1,2,3]
  144. STRING;
  145. $phpVersionString = phpversion();
  146. $phpVersionComponents = explode('.', $phpVersionString);
  147. $majorVersion = (int) $phpVersionComponents[0];
  148. $this->expectExceptionMessage(($majorVersion >= 8) ? $php8xMessage : $php7xMessage);
  149. $handler = new StreamHandler('bogus://url');
  150. $record = $this->getRecord(
  151. context: ['foo' => 'bar'],
  152. extra: [1, 2, 3],
  153. );
  154. $handler->handle($record);
  155. }
  156. /**
  157. * @covers Monolog\Handler\StreamHandler::__construct
  158. * @covers Monolog\Handler\StreamHandler::write
  159. */
  160. public function testWriteNonExistingResource()
  161. {
  162. $this->expectException(\UnexpectedValueException::class);
  163. $handler = new StreamHandler('ftp://foo/bar/baz/'.rand(0, 10000));
  164. $handler->handle($this->getRecord());
  165. }
  166. /**
  167. * @covers Monolog\Handler\StreamHandler::__construct
  168. * @covers Monolog\Handler\StreamHandler::write
  169. */
  170. public function testWriteNonExistingPath()
  171. {
  172. $handler = new StreamHandler(sys_get_temp_dir().'/bar/'.rand(0, 10000).DIRECTORY_SEPARATOR.rand(0, 10000));
  173. $handler->handle($this->getRecord());
  174. }
  175. /**
  176. * @covers Monolog\Handler\StreamHandler::__construct
  177. * @covers Monolog\Handler\StreamHandler::write
  178. */
  179. public function testWriteNonExistingFileResource()
  180. {
  181. $handler = new StreamHandler('file://'.sys_get_temp_dir().'/bar/'.rand(0, 10000).DIRECTORY_SEPARATOR.rand(0, 10000));
  182. $handler->handle($this->getRecord());
  183. }
  184. /**
  185. * @covers Monolog\Handler\StreamHandler::write
  186. */
  187. public function testWriteErrorDuringWriteRetriesWithClose()
  188. {
  189. $handler = $this->getMockBuilder(StreamHandler::class)
  190. ->onlyMethods(['streamWrite'])
  191. ->setConstructorArgs(['file://'.sys_get_temp_dir().'/bar/'.rand(0, 10000).DIRECTORY_SEPARATOR.rand(0, 10000)])
  192. ->getMock();
  193. $refs = [];
  194. $handler->expects($this->exactly(2))
  195. ->method('streamWrite')
  196. ->willReturnCallback(function ($stream) use (&$refs) {
  197. $refs[] = $stream;
  198. if (\count($refs) === 2) {
  199. self::assertNotSame($stream, $refs[0]);
  200. }
  201. if (\count($refs) === 1) {
  202. trigger_error('fwrite(): Write of 378 bytes failed with errno=32 Broken pipe', E_USER_ERROR);
  203. }
  204. });
  205. $handler->handle($this->getRecord());
  206. if (method_exists($this, 'assertIsClosedResource')) {
  207. self::assertIsClosedResource($refs[0]);
  208. self::assertIsResource($refs[1]);
  209. }
  210. }
  211. /**
  212. * @covers Monolog\Handler\StreamHandler::write
  213. */
  214. public function testWriteErrorDuringWriteRetriesButThrowsIfStillFails()
  215. {
  216. $handler = $this->getMockBuilder(StreamHandler::class)
  217. ->onlyMethods(['streamWrite'])
  218. ->setConstructorArgs(['file://'.sys_get_temp_dir().'/bar/'.rand(0, 10000).DIRECTORY_SEPARATOR.rand(0, 10000)])
  219. ->getMock();
  220. $refs = [];
  221. $handler->expects($this->exactly(2))
  222. ->method('streamWrite')
  223. ->willReturnCallback(function ($stream) use (&$refs) {
  224. $refs[] = $stream;
  225. if (\count($refs) === 2) {
  226. self::assertNotSame($stream, $refs[0]);
  227. }
  228. trigger_error('fwrite(): Write of 378 bytes failed with errno=32 Broken pipe', E_USER_ERROR);
  229. });
  230. self::expectException(\UnexpectedValueException::class);
  231. self::expectExceptionMessage('Writing to the log file failed: Write of 378 bytes failed with errno=32 Broken pipe
  232. The exception occurred while attempting to log: test');
  233. $handler->handle($this->getRecord());
  234. }
  235. /**
  236. * @covers Monolog\Handler\StreamHandler::__construct
  237. * @covers Monolog\Handler\StreamHandler::write
  238. */
  239. #[DataProvider('provideNonExistingAndNotCreatablePath')]
  240. public function testWriteNonExistingAndNotCreatablePath($nonExistingAndNotCreatablePath)
  241. {
  242. if (\defined('PHP_WINDOWS_VERSION_BUILD')) {
  243. $this->markTestSkipped('Permissions checks can not run on windows');
  244. }
  245. $handler = null;
  246. try {
  247. $handler = new StreamHandler($nonExistingAndNotCreatablePath);
  248. } catch (\Exception $fail) {
  249. $this->fail(
  250. 'A non-existing and not creatable path should throw an Exception earliest on first write.
  251. Not during instantiation.'
  252. );
  253. }
  254. $this->expectException(\UnexpectedValueException::class);
  255. $this->expectExceptionMessage('There is no existing directory at');
  256. $handler->handle($this->getRecord());
  257. }
  258. public static function provideNonExistingAndNotCreatablePath()
  259. {
  260. return [
  261. '/foo/bar/…' => [
  262. '/foo/bar/'.rand(0, 10000).DIRECTORY_SEPARATOR.rand(0, 10000),
  263. ],
  264. 'file:///foo/bar/…' => [
  265. 'file:///foo/bar/'.rand(0, 10000).DIRECTORY_SEPARATOR.rand(0, 10000),
  266. ],
  267. ];
  268. }
  269. public static function provideMemoryValues()
  270. {
  271. return [
  272. ['1M', (int) (1024*1024/10)],
  273. ['10M', (int) (1024*1024)],
  274. ['1024M', (int) (1024*1024*1024/10)],
  275. ['1G', (int) (1024*1024*1024/10)],
  276. ['2000M', (int) (2000*1024*1024/10)],
  277. ['2050M', (int) (2050*1024*1024/10)],
  278. ['2048M', (int) (2048*1024*1024/10)],
  279. ['3G', (int) (3*1024*1024*1024/10)],
  280. ['2560M', (int) (2560*1024*1024/10)],
  281. ];
  282. }
  283. #[DataProvider('provideMemoryValues')]
  284. public function testPreventOOMError($phpMemory, $expectedChunkSize): void
  285. {
  286. $previousValue = @ini_set('memory_limit', $phpMemory);
  287. if ($previousValue === false) {
  288. $this->markTestSkipped('We could not set a memory limit that would trigger the error.');
  289. }
  290. try {
  291. $stream = tmpfile();
  292. if ($stream === false) {
  293. $this->markTestSkipped('We could not create a temp file to be use as a stream.');
  294. }
  295. $handler = new StreamHandler($stream);
  296. stream_get_contents($stream, 1024);
  297. $this->assertEquals($expectedChunkSize, $handler->getStreamChunkSize());
  298. } finally {
  299. ini_set('memory_limit', $previousValue);
  300. }
  301. }
  302. public function testSimpleOOMPrevention(): void
  303. {
  304. $previousValue = ini_set('memory_limit', '2048M');
  305. if ($previousValue === false) {
  306. $this->markTestSkipped('We could not set a memory limit that would trigger the error.');
  307. }
  308. try {
  309. $stream = tmpfile();
  310. new StreamHandler($stream);
  311. stream_get_contents($stream);
  312. $this->assertTrue(true);
  313. } finally {
  314. ini_set('memory_limit', $previousValue);
  315. }
  316. }
  317. public function testReopensFileIfInodeChanges()
  318. {
  319. $filename = __DIR__ . '/test.log';
  320. $handler = new StreamHandler($filename);
  321. $handler->setFormatter($this->getIdentityFormatter());
  322. $handler->handle($this->getRecord(Level::Warning, 'test1'));
  323. @unlink($filename);
  324. $handler->handle($this->getRecord(Level::Warning, 'test2'));
  325. $data = @file_get_contents($filename);
  326. $this->assertEquals('test2', $data);
  327. }
  328. }