Passed
Branch master (52a373)
by Josh
02:18
created

Test::testEncode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 3
rs 10
1
<?php declare(strict_types=1);
2
3
namespace s9e\Bencode\Tests;
4
5
use ArrayObject;
6
use PHPUnit\Framework\TestCase;
7
use TypeError;
8
use s9e\Bencode\Bencode;
9
use s9e\Bencode\Exceptions\ComplianceError;
10
use s9e\Bencode\Exceptions\DecodingException;
11
use s9e\Bencode\Exceptions\EncodingException;
12
use stdClass;
13
14
class Test extends TestCase
15
{
16
	public static function setUpBeforeClass(): void
17
	{
18
		// Preload the library so the memory-related tests don't count it as overhead
19
		Bencode::decode('i1e');
20
	}
21
22
	/**
23
	* @group memory
24
	*/
25
	public function testMemoryList()
26
	{
27
		$len = 10000;
28
		$str = str_repeat('i0e', $len + 2);
29
30
		$str[0]  = $str[1]  = $str[2]  = 'l';
31
		$str[-1] = $str[-2] = $str[-3] = 'e';
32
33
		$reference = memory_get_peak_usage();
34
35
		// Create a copy of the expected result so we get a feel for how much memory it will use
36
		$expected = array_fill(0, $len, 0);
37
		unset($expected);
38
39
		$before = memory_get_peak_usage();
40
		if ($before === $reference)
41
		{
42
			$this->markTestSkipped('Cannot measure peak memory because the reference value is too high');
43
		}
44
45
		$decoded = Bencode::decode($str);
0 ignored issues
show
Unused Code introduced by
The assignment to $decoded is dead and can be removed.
Loading history...
46
		$after   = memory_get_peak_usage();
47
		$delta   = $after - $before;
48
49
		// Test that the delta is less than 4 KB
50
		$this->assertLessThan(4096, $delta);
51
	}
52
53
	/**
54
	* @group memory
55
	*/
56
	public function testMemoryString()
57
	{
58
		$reference = memory_get_peak_usage();
59
60
		// Create a bencoded value that will be decoded into a string that is 2e6 characters long.
61
		// The overhead from bencoding is 8 for "2000000:" and we avoid creating copies of the
62
		// string by modifying it in place
63
		$len    = 2000000;
64
		$str    = str_repeat('0', $len + 8);
65
		$str[0] = '2';
66
		$str[7] = ':';
67
68
		$before = memory_get_peak_usage();
69
		if ($before === $reference)
70
		{
71
			$this->markTestSkipped('Cannot measure peak memory because the reference value is too high');
72
		}
73
74
		$decoded  = Bencode::decode($str);
75
		$after    = memory_get_peak_usage();
76
		$delta    = $after - $before;
77
		$overhead = $delta - $len;
78
79
		// Test that the overhead was less than 4 KB
80
		$this->assertLessThan(4096, $overhead);
81
		$this->assertEquals($len, strlen($decoded));
82
	}
83
84
	/**
85
	* @dataProvider getEncodeTests
86
	*/
87
	public function testEncode($bencoded, $value)
88
	{
89
		$this->assertSame($bencoded, Bencode::encode($value));
90
	}
91
92
	public function getEncodeTests()
93
	{
94
		return [
95
			[
96
				'i22e',
97
				22
98
			],
99
			[
100
				'i22e',
101
				(double) 22.0
102
			],
103
			[
104
				'i1e',
105
				true
106
			],
107
			[
108
				'i0e',
109
				false
110
			],
111
			[
112
				'i-1e',
113
				-1
114
			],
115
			[
116
				'le',
117
				[]
118
			],
119
			[
120
				'de',
121
				new stdClass
122
			],
123
			[
124
				'de',
125
				new foo
126
			],
127
			[
128
				'd3:fooi1ee',
129
				['foo' => 1]
130
			],
131
			[
132
				'd3:fooi1ee',
133
				(object) ['foo' => 1]
134
			],
135
			[
136
				'd3:bari2e3:fooi1ee',
137
				['foo' => 1, 'bar' => 2]
138
			],
139
			[
140
				'd3:fool1:a1:b1:cee',
141
				['foo' => ['a', 'b', 'c']]
142
			],
143
			[
144
				'd0:l1:a1:b1:cee',
145
				['' => ['a', 'b', 'c']]
146
			],
147
			[
148
				'd3:food3:bari1ee1:xd1:yi1eee',
149
				new ArrayObject([
150
					'foo' => new ArrayObject(['bar' => 1]),
151
					'x'   => new ArrayObject(['y' => 1])
152
				])
153
			],
154
			[
155
				'd1:0i0e1:1i1ee',
156
				[1 => 1, 0 => 0]
157
			],
158
			[
159
				'd2:11i11e1:5i5ee',
160
				[5 => 5, 11 => 11]
161
			],
162
			[
163
				'i1000000000e',
164
				1000000000
165
			],
166
		];
167
	}
168
169
	/**
170
	* @dataProvider getEncodeInvalidTests
171
	*/
172
	public function testEncodeInvalid($input)
173
	{
174
		$this->expectException(EncodingException::class);
175
		$this->expectExceptionMessage('Unsupported value');
176
177
		try
178
		{
179
			$this->assertNull(Bencode::encode($input));
180
		}
181
		catch (EncodingException $e)
182
		{
183
			$this->assertSame($input, $e->getValue());
184
185
			throw $e;
186
		}
187
	}
188
189
	public function getEncodeInvalidTests()
190
	{
191
		$fp = fopen('php://stdin', 'rb');
192
		fclose($fp);
193
194
		return [
195
			[function(){}],
196
			[1.2],
197
			[$fp],
198
		];
199
	}
200
201
	/**
202
	* @dataProvider getDecodeTests
203
	*/
204
	public function testDecode($bencoded, $value)
205
	{
206
		$this->assertEquals($value, Bencode::decode($bencoded));
207
	}
208
209
	public function getDecodeTests()
210
	{
211
		return [
212
			[
213
				'i1234567890e',
214
				1234567890
215
			],
216
			[
217
				'i-1e',
218
				-1
219
			],
220
			[
221
				'i0e',
222
				0
223
			],
224
			[
225
				'le',
226
				[]
227
			],
228
			[
229
				'de',
230
				new ArrayObject
231
			],
232
			[
233
				'd3:fooi1ee',
234
				new ArrayObject(['foo' => 1])
235
			],
236
			[
237
				'd3:bari2e3:fooi1ee',
238
				new ArrayObject(['foo' => 1, 'bar' => 2])
239
			],
240
			[
241
				'd3:fool1:a1:b1:cee',
242
				new ArrayObject(['foo' => ['a', 'b', 'c']])
243
			],
244
			[
245
				'd3:food3:bari1ee1:xd1:yi1eee',
246
				new ArrayObject([
247
					'foo' => new ArrayObject(['bar' => 1]),
248
					'x'   => new ArrayObject(['y' => 1])
249
				])
250
			],
251
			[
252
				'3:abc',
253
				'abc'
254
			],
255
			[
256
				'd0:l1:a1:b1:cee',
257
				new ArrayObject(['' => ['a', 'b', 'c']])
258
			],
259
			[
260
				'0:',
261
				''
262
			],
263
			[
264
				'1:i',
265
				'i'
266
			],
267
			[
268
				'2:i-',
269
				'i-'
270
			],
271
			[
272
				'3:i-1',
273
				'i-1'
274
			],
275
			[
276
				'4:i-1e',
277
				'i-1e'
278
			],
279
			[
280
				'lli0ei1eeli2ei3eee',
281
				[[0, 1], [2, 3]]
282
			],
283
		];
284
	}
285
286
	/**
287
	* @dataProvider getDecodeInvalidTests
288
	*/
289
	public function testDecodeInvalid($input, $expected)
290
	{
291
		$this->expectException(get_class($expected));
292
		$this->expectExceptionMessage($expected->getMessage());
293
294
		try
295
		{
296
			$this->assertNull(Bencode::decode($input));
297
		}
298
		catch (DecodingException $e)
299
		{
300
			$this->assertEquals($expected->getOffset(), $e->getOffset());
301
			throw $e;
302
		}
303
	}
304
305
	public function getDecodeInvalidTests()
306
	{
307
		return [
308
			[
309
				null,
310
				new TypeError(Bencode::class . '::decode(): Argument #1 ($bencoded) must be of type string, null given')
311
			],
312
			[
313
				'',
314
				new DecodingException('Premature end of data', 0)
315
			],
316
			[
317
				'lxe',
318
				new DecodingException('Illegal character', 1)
319
			],
320
			[
321
				'l',
322
				new DecodingException('Premature end of data', 0)
323
			],
324
			[
325
				'lle',
326
				new DecodingException('Premature end of data', 2)
327
			],
328
			[
329
				'lee',
330
				new ComplianceError('Superfluous content', 2)
331
			],
332
			[
333
				'le0',
334
				new ComplianceError('Superfluous content', 2)
335
			],
336
			[
337
				'ddee',
338
				new DecodingException('Illegal character', 1)
339
			],
340
			[
341
				'd1:xe',
342
				new DecodingException('Illegal character', 4)
343
			],
344
			[
345
				'd1:xl',
346
				new DecodingException('Premature end of data', 4)
347
			],
348
			[
349
				'd1:xx',
350
				new DecodingException('Illegal character', 4)
351
			],
352
			[
353
				'ie',
354
				new DecodingException('Illegal character', 1)
355
			],
356
			[
357
				'i1x',
358
				new DecodingException('Illegal character', 2)
359
			],
360
			[
361
				'lxe',
362
				new DecodingException('Illegal character', 1)
363
			],
364
			[
365
				'3:abcd',
366
				new ComplianceError('Superfluous content', 5)
367
			],
368
			[
369
				'li',
370
				new DecodingException('Premature end of data', 1)
371
			],
372
			[
373
				'l3',
374
				new DecodingException('Premature end of data', 1)
375
			],
376
			[
377
				'i-1-e',
378
				new DecodingException('Illegal character', 3)
379
			],
380
			[
381
				'i',
382
				new DecodingException('Premature end of data', 0)
383
			],
384
			[
385
				'i-',
386
				new DecodingException('Premature end of data', 1)
387
			],
388
			[
389
				'd1:xi-',
390
				new DecodingException('Premature end of data', 5)
391
			],
392
			[
393
				'i1',
394
				new DecodingException('Premature end of data', 1)
395
			],
396
			[
397
				'i-1',
398
				new DecodingException('Premature end of data', 2)
399
			],
400
			[
401
				'lli123',
402
				new DecodingException('Premature end of data', 5)
403
			],
404
			[
405
				'3 abc',
406
				new DecodingException('Illegal character', 1)
407
			],
408
			[
409
				'3a3:abc',
410
				new DecodingException('Illegal character', 1)
411
			],
412
			[
413
				'3a',
414
				new DecodingException('Illegal character', 1)
415
			],
416
			[
417
				':a',
418
				new DecodingException('Illegal character', 0)
419
			],
420
			[
421
				'3:abc3:abc',
422
				new ComplianceError('Superfluous content', 5)
423
			],
424
			[
425
				'3:abci',
426
				new ComplianceError('Superfluous content', 5)
427
			],
428
			[
429
				'3:',
430
				new DecodingException('Premature end of data', 1)
431
			],
432
			[
433
				'3:a',
434
				new DecodingException('Premature end of data', 2)
435
			],
436
			[
437
				'2:a',
438
				new DecodingException('Premature end of data', 2)
439
			],
440
			[
441
				'l11:ae',
442
				new DecodingException('Premature end of data', 5)
443
			],
444
			[
445
				'i0123e',
446
				new ComplianceError('Illegal character', 2)
447
			],
448
			[
449
				'i00e',
450
				new ComplianceError('Illegal character', 2)
451
			],
452
			[
453
				'i-0e',
454
				new ComplianceError('Illegal character', 2)
455
			],
456
			[
457
				'01:a',
458
				new ComplianceError('Illegal character', 1)
459
			],
460
			[
461
				'1',
462
				new DecodingException('Premature end of data', 0)
463
			],
464
			[
465
				'e',
466
				new DecodingException('Illegal character', 0)
467
			],
468
			[
469
				'-1',
470
				new DecodingException('Illegal character', 0)
471
			],
472
			[
473
				'd3:fooi0e3:foo3:abce',
474
				new ComplianceError("Duplicate dictionary entry 'foo'", 9)
475
			],
476
			[
477
				'd4:abcdi0e4:abcdli0eee',
478
				new ComplianceError("Duplicate dictionary entry 'abcd'", 10)
479
			],
480
			[
481
				'd3:fooi0e3:bar3:abce',
482
				new ComplianceError("Out of order dictionary entry 'bar'", 9)
483
			],
484
			[
485
				'd1:5i0e2:11i0ee',
486
				new ComplianceError("Out of order dictionary entry '11'", 7)
487
			],
488
		];
489
	}
490
491
	public function testDecodeDictionaryAccess()
492
	{
493
		$dict = Bencode::decode('d3:bar4:spam3:fooi42ee');
494
495
		$this->assertSame('spam', $dict->bar);
0 ignored issues
show
Bug introduced by
The property bar does not seem to exist on ArrayObject.
Loading history...
496
		$this->assertSame(42,     $dict['foo']);
497
498
		$actual = [];
499
		foreach ($dict as $k => $v)
500
		{
501
			$actual[$k] = $v;
502
		}
503
		$this->assertSame(['bar' => 'spam', 'foo' => 42], $actual);
504
	}
505
}
506
507
class foo extends stdClass
508
{
509
}