StreamHandlerTest.php 12 KB

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