StreamTest.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. <?php
  2. declare(strict_types=1);
  3. namespace GuzzleHttp\Tests\Psr7;
  4. use GuzzleHttp\Psr7\FnStream;
  5. use GuzzleHttp\Psr7\Stream;
  6. use GuzzleHttp\Psr7\StreamWrapper;
  7. use PHPUnit\Framework\TestCase;
  8. /**
  9. * @covers \GuzzleHttp\Psr7\Stream
  10. */
  11. class StreamTest extends TestCase
  12. {
  13. public static $isFReadError = false;
  14. public function testConstructorThrowsExceptionOnInvalidArgument(): void
  15. {
  16. $this->expectException(\InvalidArgumentException::class);
  17. new Stream(true);
  18. }
  19. public function testConstructorInitializesProperties(): void
  20. {
  21. $handle = fopen('php://temp', 'r+');
  22. fwrite($handle, 'data');
  23. $stream = new Stream($handle);
  24. self::assertTrue($stream->isReadable());
  25. self::assertTrue($stream->isWritable());
  26. self::assertTrue($stream->isSeekable());
  27. self::assertSame('php://temp', $stream->getMetadata('uri'));
  28. self::assertIsArray($stream->getMetadata());
  29. self::assertSame(4, $stream->getSize());
  30. self::assertFalse($stream->eof());
  31. $stream->close();
  32. }
  33. public function testConstructorInitializesPropertiesWithRbPlus(): void
  34. {
  35. $handle = fopen('php://temp', 'rb+');
  36. fwrite($handle, 'data');
  37. $stream = new Stream($handle);
  38. self::assertTrue($stream->isReadable());
  39. self::assertTrue($stream->isWritable());
  40. self::assertTrue($stream->isSeekable());
  41. self::assertSame('php://temp', $stream->getMetadata('uri'));
  42. self::assertIsArray($stream->getMetadata());
  43. self::assertSame(4, $stream->getSize());
  44. self::assertFalse($stream->eof());
  45. $stream->close();
  46. }
  47. public function testStreamClosesHandleOnDestruct(): void
  48. {
  49. $handle = fopen('php://temp', 'r');
  50. $stream = new Stream($handle);
  51. unset($stream);
  52. self::assertFalse(is_resource($handle));
  53. }
  54. public function testConvertsToString(): void
  55. {
  56. $handle = fopen('php://temp', 'w+');
  57. fwrite($handle, 'data');
  58. $stream = new Stream($handle);
  59. self::assertSame('data', (string) $stream);
  60. self::assertSame('data', (string) $stream);
  61. $stream->close();
  62. }
  63. public function testConvertsToStringNonSeekableStream(): void
  64. {
  65. $handle = popen('echo foo', 'r');
  66. $stream = new Stream($handle);
  67. self::assertFalse($stream->isSeekable());
  68. self::assertSame('foo', trim((string) $stream));
  69. }
  70. public function testConvertsToStringNonSeekablePartiallyReadStream(): void
  71. {
  72. $handle = popen('echo bar', 'r');
  73. $stream = new Stream($handle);
  74. $firstLetter = $stream->read(1);
  75. self::assertFalse($stream->isSeekable());
  76. self::assertSame('b', $firstLetter);
  77. self::assertSame('ar', trim((string) $stream));
  78. }
  79. public function testGetsContents(): void
  80. {
  81. $handle = fopen('php://temp', 'w+');
  82. fwrite($handle, 'data');
  83. $stream = new Stream($handle);
  84. self::assertSame('', $stream->getContents());
  85. $stream->seek(0);
  86. self::assertSame('data', $stream->getContents());
  87. self::assertSame('', $stream->getContents());
  88. $stream->close();
  89. }
  90. public function testChecksEof(): void
  91. {
  92. $handle = fopen('php://temp', 'w+');
  93. fwrite($handle, 'data');
  94. $stream = new Stream($handle);
  95. self::assertSame(4, $stream->tell(), 'Stream cursor already at the end');
  96. self::assertFalse($stream->eof(), 'Stream still not eof');
  97. self::assertSame('', $stream->read(1), 'Need to read one more byte to reach eof');
  98. self::assertTrue($stream->eof());
  99. $stream->close();
  100. }
  101. public function testGetSize(): void
  102. {
  103. $size = filesize(__FILE__);
  104. $handle = fopen(__FILE__, 'r');
  105. $stream = new Stream($handle);
  106. self::assertSame($size, $stream->getSize());
  107. // Load from cache
  108. self::assertSame($size, $stream->getSize());
  109. $stream->close();
  110. }
  111. public function testEnsuresSizeIsConsistent(): void
  112. {
  113. $h = fopen('php://temp', 'w+');
  114. self::assertSame(3, fwrite($h, 'foo'));
  115. $stream = new Stream($h);
  116. self::assertSame(3, $stream->getSize());
  117. self::assertSame(4, $stream->write('test'));
  118. self::assertSame(7, $stream->getSize());
  119. self::assertSame(7, $stream->getSize());
  120. $stream->close();
  121. }
  122. public function testProvidesStreamPosition(): void
  123. {
  124. $handle = fopen('php://temp', 'w+');
  125. $stream = new Stream($handle);
  126. self::assertSame(0, $stream->tell());
  127. $stream->write('foo');
  128. self::assertSame(3, $stream->tell());
  129. $stream->seek(1);
  130. self::assertSame(1, $stream->tell());
  131. self::assertSame(ftell($handle), $stream->tell());
  132. $stream->close();
  133. }
  134. public function testDetachStreamAndClearProperties(): void
  135. {
  136. $handle = fopen('php://temp', 'r');
  137. $stream = new Stream($handle);
  138. self::assertSame($handle, $stream->detach());
  139. self::assertIsResource($handle, 'Stream is not closed');
  140. self::assertNull($stream->detach());
  141. $this->assertStreamStateAfterClosedOrDetached($stream);
  142. $stream->close();
  143. }
  144. public function testCloseResourceAndClearProperties(): void
  145. {
  146. $handle = fopen('php://temp', 'r');
  147. $stream = new Stream($handle);
  148. $stream->close();
  149. self::assertFalse(is_resource($handle));
  150. $this->assertStreamStateAfterClosedOrDetached($stream);
  151. }
  152. private function assertStreamStateAfterClosedOrDetached(Stream $stream): void
  153. {
  154. self::assertFalse($stream->isReadable());
  155. self::assertFalse($stream->isWritable());
  156. self::assertFalse($stream->isSeekable());
  157. self::assertNull($stream->getSize());
  158. self::assertSame([], $stream->getMetadata());
  159. self::assertNull($stream->getMetadata('foo'));
  160. $throws = function (callable $fn): void {
  161. try {
  162. $fn();
  163. } catch (\Exception $e) {
  164. $this->assertStringContainsString('Stream is detached', $e->getMessage());
  165. return;
  166. }
  167. $this->fail('Exception should be thrown after the stream is detached.');
  168. };
  169. $throws(function () use ($stream): void {
  170. $stream->read(10);
  171. });
  172. $throws(function () use ($stream): void {
  173. $stream->write('bar');
  174. });
  175. $throws(function () use ($stream): void {
  176. $stream->seek(10);
  177. });
  178. $throws(function () use ($stream): void {
  179. $stream->tell();
  180. });
  181. $throws(function () use ($stream): void {
  182. $stream->eof();
  183. });
  184. $throws(function () use ($stream): void {
  185. $stream->getContents();
  186. });
  187. if (\PHP_VERSION_ID >= 70400) {
  188. $throws(function () use ($stream): void {
  189. (string) $stream;
  190. });
  191. } else {
  192. $errors = [];
  193. set_error_handler(function (int $errorNumber, string $errorMessage) use (&$errors): void {
  194. $errors[] = ['message' => $errorMessage, 'number' => $errorNumber];
  195. });
  196. self::assertSame('', (string) $stream);
  197. restore_error_handler();
  198. self::assertCount(1, $errors);
  199. self::assertStringStartsWith('GuzzleHttp\Psr7\Stream::__toString exception', $errors[0]['message']);
  200. self::assertSame(E_USER_ERROR, $errors[0]['number']);
  201. }
  202. }
  203. public function testStreamReadingWithZeroLength(): void
  204. {
  205. $r = fopen('php://temp', 'r');
  206. $stream = new Stream($r);
  207. self::assertSame('', $stream->read(0));
  208. $stream->close();
  209. }
  210. public function testStreamReadingWithNegativeLength(): void
  211. {
  212. $r = fopen('php://temp', 'r');
  213. $stream = new Stream($r);
  214. $this->expectException(\RuntimeException::class);
  215. $this->expectExceptionMessage('Length parameter cannot be negative');
  216. try {
  217. $stream->read(-1);
  218. } catch (\Exception $e) {
  219. $stream->close();
  220. throw $e;
  221. }
  222. $stream->close();
  223. }
  224. public function testStreamReadingFreadFalse(): void
  225. {
  226. self::$isFReadError = true;
  227. $r = fopen('php://temp', 'r');
  228. $stream = new Stream($r);
  229. $this->expectException(\RuntimeException::class);
  230. $this->expectExceptionMessage('Unable to read from stream');
  231. try {
  232. $stream->read(1);
  233. } catch (\Exception $e) {
  234. self::$isFReadError = false;
  235. $stream->close();
  236. throw $e;
  237. }
  238. self::$isFReadError = false;
  239. $stream->close();
  240. }
  241. public function testStreamReadingFreadException(): void
  242. {
  243. $this->expectException(\RuntimeException::class);
  244. $this->expectExceptionMessage('Unable to read from stream');
  245. $r = StreamWrapper::getResource(new FnStream([
  246. 'read' => function ($len): string {
  247. throw new \ErrorException('Some error');
  248. },
  249. 'isReadable' => function (): bool {
  250. return true;
  251. },
  252. 'isWritable' => function (): bool {
  253. return false;
  254. },
  255. 'eof' => function (): bool {
  256. return false;
  257. },
  258. ]));
  259. $stream = new Stream($r);
  260. $stream->read(1);
  261. }
  262. /**
  263. * @requires extension zlib
  264. *
  265. * @dataProvider gzipModeProvider
  266. */
  267. public function testGzipStreamModes(string $mode, bool $readable, bool $writable): void
  268. {
  269. $r = gzopen('php://temp', $mode);
  270. $stream = new Stream($r);
  271. self::assertSame($readable, $stream->isReadable());
  272. self::assertSame($writable, $stream->isWritable());
  273. $stream->close();
  274. }
  275. public function gzipModeProvider(): iterable
  276. {
  277. return [
  278. ['mode' => 'rb9', 'readable' => true, 'writable' => false],
  279. ['mode' => 'wb2', 'readable' => false, 'writable' => true],
  280. ];
  281. }
  282. /**
  283. * @dataProvider readableModeProvider
  284. */
  285. public function testReadableStream(string $mode): void
  286. {
  287. $r = fopen('php://temp', $mode);
  288. $stream = new Stream($r);
  289. self::assertTrue($stream->isReadable());
  290. $stream->close();
  291. }
  292. public function readableModeProvider(): iterable
  293. {
  294. return [
  295. ['r'],
  296. ['w+'],
  297. ['r+'],
  298. ['x+'],
  299. ['c+'],
  300. ['rb'],
  301. ['w+b'],
  302. ['r+b'],
  303. ['x+b'],
  304. ['c+b'],
  305. ['rt'],
  306. ['w+t'],
  307. ['r+t'],
  308. ['x+t'],
  309. ['c+t'],
  310. ['a+'],
  311. ['rb+'],
  312. ];
  313. }
  314. public function testWriteOnlyStreamIsNotReadable(): void
  315. {
  316. $r = fopen('php://output', 'w');
  317. $stream = new Stream($r);
  318. self::assertFalse($stream->isReadable());
  319. $stream->close();
  320. }
  321. /**
  322. * @dataProvider writableModeProvider
  323. */
  324. public function testWritableStream(string $mode): void
  325. {
  326. $r = fopen('php://temp', $mode);
  327. $stream = new Stream($r);
  328. self::assertTrue($stream->isWritable());
  329. $stream->close();
  330. }
  331. public function writableModeProvider(): iterable
  332. {
  333. return [
  334. ['w'],
  335. ['w+'],
  336. ['rw'],
  337. ['r+'],
  338. ['x+'],
  339. ['c+'],
  340. ['wb'],
  341. ['w+b'],
  342. ['r+b'],
  343. ['rb+'],
  344. ['x+b'],
  345. ['c+b'],
  346. ['w+t'],
  347. ['r+t'],
  348. ['x+t'],
  349. ['c+t'],
  350. ['a'],
  351. ['a+'],
  352. ];
  353. }
  354. public function testReadOnlyStreamIsNotWritable(): void
  355. {
  356. $r = fopen('php://input', 'r');
  357. $stream = new Stream($r);
  358. self::assertFalse($stream->isWritable());
  359. $stream->close();
  360. }
  361. public function testCannotReadUnreadableStream(): void
  362. {
  363. $r = fopen(tempnam(sys_get_temp_dir(), 'guzzle-psr7-'), 'w');
  364. $stream = new Stream($r);
  365. $stream->write('Hello world!!');
  366. $stream->seek(0);
  367. $this->expectException(\RuntimeException::class);
  368. try {
  369. $stream->getContents();
  370. } finally {
  371. $stream->close();
  372. }
  373. }
  374. }
  375. namespace GuzzleHttp\Psr7;
  376. use GuzzleHttp\Tests\Psr7\StreamTest;
  377. function fread($handle, $length)
  378. {
  379. return StreamTest::$isFReadError ? false : \fread($handle, $length);
  380. }