2
0

StreamHandlerTest.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  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\Handler\StreamHandler;
  12. use Monolog\Test\TestCase;
  13. use Monolog\Logger;
  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(Logger::WARNING, 'test'));
  31. $handler->handle($this->getRecord(Logger::WARNING, 'test2'));
  32. $handler->handle($this->getRecord(Logger::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(Logger::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(Logger::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(Logger::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, Logger::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 function invalidArgumentProvider()
  111. {
  112. return [
  113. [1],
  114. [[]],
  115. [['bogus://url']],
  116. ];
  117. }
  118. /**
  119. * @dataProvider invalidArgumentProvider
  120. * @covers Monolog\Handler\StreamHandler::__construct
  121. */
  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. $record['context'] = ['foo' => 'bar'];
  153. $record['extra'] = [1, 2, 3];
  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. * @dataProvider provideNonExistingAndNotCreatablePath
  239. */
  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 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 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. /**
  284. * @dataProvider provideMemoryValues
  285. * @return void
  286. */
  287. public function testPreventOOMError($phpMemory, $expectedChunkSize)
  288. {
  289. $previousValue = ini_set('memory_limit', $phpMemory);
  290. if ($previousValue === false) {
  291. $this->markTestSkipped('We could not set a memory limit that would trigger the error.');
  292. }
  293. try {
  294. $stream = tmpfile();
  295. if ($stream === false) {
  296. $this->markTestSkipped('We could not create a temp file to be use as a stream.');
  297. }
  298. $handler = new StreamHandler($stream);
  299. stream_get_contents($stream, 1024);
  300. $this->assertEquals($expectedChunkSize, $handler->getStreamChunkSize());
  301. } finally {
  302. ini_set('memory_limit', $previousValue);
  303. }
  304. }
  305. /**
  306. * @return void
  307. */
  308. public function testSimpleOOMPrevention()
  309. {
  310. $previousValue = ini_set('memory_limit', '2048M');
  311. if ($previousValue === false) {
  312. $this->markTestSkipped('We could not set a memory limit that would trigger the error.');
  313. }
  314. try {
  315. $stream = tmpfile();
  316. new StreamHandler($stream);
  317. stream_get_contents($stream);
  318. $this->assertTrue(true);
  319. } finally {
  320. ini_set('memory_limit', $previousValue);
  321. }
  322. }
  323. }