Completed
Push — develop ( 043012...3d7377 )
by Vladimir
12s queued 10s
created

PHPUnit_Stakx_TestCase::getTestRoot()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
/**
4
 * @copyright 2018 Vladimir Jimenez
5
 * @license   https://github.com/stakx-io/stakx/blob/master/LICENSE.md MIT
6
 */
7
8
namespace allejo\stakx\Test;
9
10
use allejo\stakx\AssetEngine\AssetEngineManager;
11
use allejo\stakx\Configuration;
12
use allejo\stakx\Document\FrontMatterDocument;
13
use allejo\stakx\Filesystem\File;
14
use allejo\stakx\Filesystem\Filesystem;
15
use allejo\stakx\Filesystem\FilesystemLoader as fs;
16
use allejo\stakx\Filesystem\WritableFolder;
17
use allejo\stakx\Logger;
18
use allejo\stakx\Manager\AssetManager;
19
use allejo\stakx\Manager\CollectionManager;
20
use allejo\stakx\Manager\DataManager;
21
use allejo\stakx\Manager\MenuManager;
22
use allejo\stakx\Manager\PageManager;
23
use allejo\stakx\MarkupEngine\MarkdownEngine;
24
use allejo\stakx\MarkupEngine\MarkupEngineManager;
25
use allejo\stakx\MarkupEngine\PlainTextEngine;
26
use allejo\stakx\MarkupEngine\RstEngine;
27
use allejo\stakx\RedirectMapper;
28
use allejo\stakx\RuntimeStatus;
29
use allejo\stakx\Service;
30
use allejo\stakx\Templating\Twig\TwigExtension;
31
use allejo\stakx\Templating\Twig\TwigStakxBridge;
32
use allejo\stakx\Templating\Twig\TwigStakxBridgeFactory;
33
use org\bovigo\vfs\vfsStream;
34
use org\bovigo\vfs\vfsStreamDirectory;
35
use org\bovigo\vfs\vfsStreamFile;
36
use Psr\Log\LoggerInterface;
37
use Symfony\Component\Console\Output\ConsoleOutput;
38
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
39
use Symfony\Component\Yaml\Yaml;
40
41
abstract class PHPUnit_Stakx_TestCase extends \PHPUnit_Framework_TestCase
42
{
43
    const FM_OBJ_TEMPLATE = "---\n%s\n---\n\n%s";
44
45
    /** @var string */
46
    protected $assetFolder;
47
    /** @var vfsStreamFile */
48
    protected $dummyFile;
49
    /** @var vfsStreamDirectory */
50
    protected $rootDir;
51
52
    public function setUp()
53
    {
54
        $this->dummyFile = vfsStream::newFile('stakx.html.twig');
55
        $this->rootDir = vfsStream::setup();
56
57
        Service::resetRuntimeFlags();
58
59
        Service::setWorkingDirectory(null);
60
        Service::setRuntimeFlag(RuntimeStatus::USING_HIGHLIGHTER);
61
62
        // Inspect the VFS as an array
63
        // vfsStream::inspect(new vfsStreamStructureVisitor())->getStructure();
64
    }
65
66
    public function tearDown()
67
    {
68
        if ($this->assetFolder !== null)
69
        {
70
            fs::remove($this->assetFolder);
0 ignored issues
show
Bug introduced by
The method remove() does not exist on allejo\stakx\Filesystem\FilesystemLoader. Did you maybe mean removeExtension()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
71
        }
72
    }
73
74
    ///
75
    // Assertion Functions
76
    ///
77
78
    /**
79
     * @param string $needle
80
     * @param string $haystack
81
     * @param string $message
82
     */
83
    protected function assertStringContains($needle, $haystack, $message = '')
84
    {
85
        $this->assertNotFalse(strpos($haystack, $needle), $message);
86
    }
87
88
    /**
89
     * @param string $fileContent
90
     * @param string $filePath
91
     * @param string $message
92
     */
93
    protected function assertFileContains($fileContent, $filePath, $message = '')
94
    {
95
        (substr($filePath, -1, 1) == '/') && $filePath .= 'index.html';
96
97
        $contents = file_get_contents($filePath);
98
99
        $this->assertStringContains($fileContent, $contents, $message);
100
    }
101
102
    ///
103
    // Filesystem Functions
104
    ///
105
106
    /**
107
     * Create a temporary folder where temporary file writes will be made to.
108
     *
109
     * @param string $folderName
110
     */
111
    protected function createAssetFolder($folderName)
112
    {
113
        $this->assetFolder = fs::getRelativePath(fs::appendPath(__DIR__, $folderName));
0 ignored issues
show
Documentation Bug introduced by
It seems like \allejo\stakx\Filesystem...(__DIR__, $folderName)) of type object<string> is incompatible with the declared type string of property $assetFolder.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
114
115
        fs::mkdir($this->assetFolder);
116
    }
117
118
    /**
119
     * Write a file to the asset folder.
120
     *
121
     * This file will be written to the actual filesystem and not the virtual filesystem. This file will be deleted at
122
     * each tearDown().
123
     *
124
     * @param string $fileName
125
     * @param string $content
126
     *
127
     * @return string Path to the temporary file; relative to the project's root
128
     */
129
    protected function createPhysicalFile($fileName, $content)
130
    {
131
        $folder = new WritableFolder($this->assetFolder);
132
        $folder->writeFile($fileName, $content);
133
134
        return fs::appendPath($this->assetFolder, $fileName);
135
    }
136
137
    /**
138
     * Write a file to the virtual filesystem.
139
     *
140
     * This file will be deleted at each tearDown().
141
     *
142
     * @param string $filename
143
     * @param string $content
144
     *
145
     * @return string the URL of the file on the virtual filesystem
146
     */
147
    protected function createVirtualFile($filename, $content)
148
    {
149
        $file = vfsStream::newFile($filename);
150
        $file
151
            ->setContent($content)
152
            ->at($this->rootDir)
153
        ;
154
155
        return $file->url();
156
    }
157
158
    /**
159
     * Create an object of a given type.
160
     *
161
     * This will create a virtual file and then create an object of the specified type for the created file.
162
     *
163
     * @param string $classType
164
     * @param string $filename
165
     * @param string $content
166
     *
167
     * @return object An instance of $classType
168
     */
169
    protected function createDocumentOfType($classType, $filename, $content)
170
    {
171
        $file = $this->createVirtualFile($filename, $content);
172
173
        return new $classType(new File($file));
174
    }
175
176
    /**
177
     * Create an object of a given type following the Front Matter format.
178
     *
179
     * @param string $classType
180
     * @param string $filename
0 ignored issues
show
Documentation introduced by
Should the type for parameter $filename not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
181
     * @param array  $frontMatter
182
     * @param string $content
183
     *
184
     * @return object An instance of $classType
185
     */
186
    protected function createFrontMatterDocumentOfType($classType, $filename = null, $frontMatter = [], $content = 'Body Text')
187
    {
188
        $body = $this->buildFrontMatterTemplate($frontMatter, $content);
189
190
        if (!$filename)
0 ignored issues
show
Bug Best Practice introduced by
The expression $filename of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
191
        {
192
            $filename = hash('sha256', uniqid(mt_rand(), true), false);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $filename. This often makes code more readable.
Loading history...
193
        }
194
195
        return $this->createDocumentOfType($classType, $filename, $body);
196
    }
197
198
    /**
199
     * Create multiple virtual files from a given array of information.
200
     *
201
     * ```php
202
     * $elements = [
203
     *     [
204
     *         'filename' => '<string>',
205
     *         'frontmatter' => [],
206
     *         'body' => '<string>',
207
     *     ],
208
     * ];
209
     * ```
210
     *
211
     * @param string $classType
212
     * @param array  $elements
213
     *
214
     * @return array
215
     */
216
    protected function createMultipleFrontMatterDocumentsOfType($classType, $elements)
217
    {
218
        $results = [];
219
220
        foreach ($elements as $element)
221
        {
222
            $filename = (isset($element['filename'])) ? $element['filename'] : null;
223
            $frontMatter = (!isset($element['frontmatter']) || empty($element['frontmatter'])) ? [] : $element['frontmatter'];
224
            $body = (isset($element['body'])) ? $element['body'] : 'Body Text';
225
226
            /** @var FrontMatterDocument $item */
227
            $item = $this->createFrontMatterDocumentOfType($classType, $filename, $frontMatter, $body);
228
            $item->evaluateFrontMatter();
229
230
            $results[] = $item;
231
        }
232
233
        return $results;
234
    }
235
236
    /**
237
     * Create a File object from a given path.
238
     *
239
     * @deprecated
240
     *
241
     * @param string $filePath
242
     *
243
     * @return File
244
     */
245
    protected function createFileObjectFromPath($filePath)
246
    {
247
        return new File($filePath);
248
    }
249
250
    ///
251
    // Mock Objects
252
    ///
253
254
    /**
255
     * @return AssetEngineManager|\PHPUnit_Framework_MockObject_MockBuilder
256
     */
257
    protected function getMockAssetEngineManager()
258
    {
259
        return new AssetEngineManager();
260
    }
261
262
    /**
263
     * @return AssetManager
264
     */
265
    protected function getMockAssetManager()
266
    {
267
        return new AssetManager($this->getMockEventDistpatcher(), $this->getMockLogger());
0 ignored issues
show
Bug introduced by
It seems like $this->getMockEventDistpatcher() targeting allejo\stakx\Test\PHPUni...tMockEventDistpatcher() can also be of type object<PHPUnit_Framework_MockObject_MockObject>; however, allejo\stakx\Manager\AssetManager::__construct() does only seem to accept object<Symfony\Component...entDispatcherInterface>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
Bug introduced by
It seems like $this->getMockLogger() targeting allejo\stakx\Test\PHPUni...stCase::getMockLogger() can also be of type object<PHPUnit_Framework_MockObject_MockObject>; however, allejo\stakx\Manager\AssetManager::__construct() does only seem to accept object<Psr\Log\LoggerInterface>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
268
    }
269
270
    /**
271
     * @return Configuration|\PHPUnit_Framework_MockObject_MockObject
272
     */
273
    protected function getMockConfiguration()
274
    {
275
        $stub = $this->getMockBuilder(Configuration::class)
276
            ->disableOriginalConstructor()
277
            ->getMock()
278
        ;
279
280
        $stub->method('getConfiguration')->willReturn([]);
281
        $stub->method('getTwigAutoescape')->willReturn(false);
282
283
        return $stub;
284
    }
285
286
    /**
287
     * @return PageManager|\PHPUnit_Framework_MockObject_MockObject
288
     */
289
    protected function getMockPageManager()
290
    {
291
        $stub = $this->getMockBuilder(PageManager::class)
292
            ->disableOriginalConstructor()
293
            ->getMock()
294
        ;
295
296
        $stub->method('getJailedStaticPageViews')->willReturn([]);
297
298
        return $stub;
299
    }
300
301
    /**
302
     * @return MenuManager|\PHPUnit_Framework_MockObject_MockObject
303
     */
304
    protected function getMockMenuManager()
305
    {
306
        $stub = $this->getMockBuilder(MenuManager::class)
307
            ->disableOriginalConstructor()
308
            ->getMock()
309
        ;
310
311
        $stub->method('getSiteMenu')->willReturn([]);
312
313
        return $stub;
314
    }
315
316
    /**
317
     * @return CollectionManager|\PHPUnit_Framework_MockObject_MockObject
318
     */
319
    protected function getMockCollectionManager()
320
    {
321
        $stub = $this->getMockBuilder(CollectionManager::class)
322
            ->disableOriginalConstructor()
323
            ->getMock()
324
        ;
325
326
        $stub->method('getJailedCollections')->willReturn([]);
327
328
        return $stub;
329
    }
330
331
    /**
332
     * @return DataManager|\PHPUnit_Framework_MockObject_MockObject
333
     */
334
    protected function getMockDataManager()
335
    {
336
        $stub = $this->getMockBuilder(DataManager::class)
337
            ->disableOriginalConstructor()
338
            ->getMock()
339
        ;
340
341
        $stub->method('getJailedDataItems')->willReturn([]);
342
343
        return $stub;
344
    }
345
346
    /**
347
     * @return TwigExtension
348
     */
349
    protected function getMockTwigExtension()
350
    {
351
        // too lazy to actually mock all the methods... just create an actual instance of and dub it a "mock" to match
352
        // all the other mock methods. if this causes problems: sorry, future me!
353
        return new TwigExtension($this->getMockMarkupEngineManager());
354
    }
355
356
    /**
357
     * @return TwigStakxBridge
358
     */
359
    protected function getMockTemplateBridge()
360
    {
361
        return TwigStakxBridgeFactory::createTwigEnvironment(
362
            $this->getMockConfiguration(),
0 ignored issues
show
Bug introduced by
It seems like $this->getMockConfiguration() targeting allejo\stakx\Test\PHPUni...:getMockConfiguration() can also be of type object<PHPUnit_Framework_MockObject_MockObject>; however, allejo\stakx\Templating\...createTwigEnvironment() does only seem to accept object<allejo\stakx\Configuration>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
363
            $this->getMockTwigExtension(),
364
            $this->getMockLogger()
0 ignored issues
show
Bug introduced by
It seems like $this->getMockLogger() targeting allejo\stakx\Test\PHPUni...stCase::getMockLogger() can also be of type object<PHPUnit_Framework_MockObject_MockObject>; however, allejo\stakx\Templating\...createTwigEnvironment() does only seem to accept object<Psr\Log\LoggerInterface>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
365
        );
366
    }
367
368
    /**
369
     * @return MarkupEngineManager
370
     */
371
    protected function getMockMarkupEngineManager()
372
    {
373
        // Just trying to keep the naming convention the same, but create an actual markup engine manager since it's
374
        // necessary in a lot of the unit tests
375
        $markupEngine = new MarkupEngineManager();
376
377
        $markupEngine->addMarkupEngines([
378
            new MarkdownEngine($this->getMockAssetManager()),
379
            new RstEngine($this->getMockAssetManager()),
380
            new PlainTextEngine(),
381
        ]);
382
383
        return $markupEngine;
384
    }
385
386
    /**
387
     * @return RedirectMapper|\PHPUnit_Framework_MockObject_MockObject
388
     */
389
    protected function getMockRedirectMapper()
390
    {
391
        $stub = $this->getMockBuilder(RedirectMapper::class)
392
            ->getMock()
393
        ;
394
395
        $stub->method('getRedirects')->willReturn([]);
396
397
        return $stub;
398
    }
399
400
    /**
401
     * Get a mock EventDispatcher.
402
     *
403
     * @return EventDispatcherInterface|\PHPUnit_Framework_MockObject_MockObject
404
     */
405
    protected function getMockEventDistpatcher()
406
    {
407
        return $this->getMock(EventDispatcherInterface::class);
408
    }
409
410
    /**
411
     * Get a mock logger.
412
     *
413
     * @return LoggerInterface|\PHPUnit_Framework_MockObject_MockObject
414
     */
415
    protected function getMockLogger()
416
    {
417
        return $this->getMock(LoggerInterface::class);
418
    }
419
420
    /**
421
     * Get a real logger instance that will save output to the console.
422
     *
423
     * @return Logger
424
     */
425
    protected function getReadableLogger()
426
    {
427
        stream_filter_register('intercept', StreamInterceptor::class);
428
        $stakxLogger = new Logger(new ConsoleOutput());
429
        stream_filter_append($stakxLogger->getOutputInterface()->getStream(), 'intercept');
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Symfony\Component\Console\Output\OutputInterface as the method getStream() does only exist in the following implementations of said interface: Symfony\Component\Console\Output\ConsoleOutput, Symfony\Component\Console\Output\StreamOutput.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
430
431
        return $stakxLogger;
432
    }
433
434
    ///
435
    // Utility Functions
436
    ///
437
438
    /**
439
     * Get the directory of the unit tests.
440
     *
441
     * @return string
442
     */
443
    protected static function getTestRoot()
444
    {
445
        return __DIR__;
446
    }
447
448
    /**
449
     * Generate a FrontMatter-ready syntax to be used as a file's content.
450
     *
451
     * @param array  $frontMatter
452
     * @param string $body
453
     *
454
     * @return string
455
     */
456
    protected function buildFrontMatterTemplate(array $frontMatter = [], $body = 'Body text')
457
    {
458
        $fm = (empty($frontMatter)) ? '' : Yaml::dump($frontMatter, 2);
459
460
        return sprintf(self::FM_OBJ_TEMPLATE, $fm, $body);
461
    }
462
463
    /**
464
     * @param string               $cls
465
     * @param string               $method
466
     * @param array<string, mixed> $namedParams
467
     *
468
     * @throws \ReflectionException
469
     *
470
     * @return mixed
471
     */
472
    protected function invokeClassFunctionWithNamedParams($cls, $method, $namedParams = [])
473
    {
474
        $clsReflection = new \ReflectionClass($cls);
475
        $fxns = $clsReflection->getMethods();
476
477
        /** @var \ReflectionMethod $fxnToCall */
478
        $fxnToCall = \__::chain($fxns)
479
            ->filter(function (\ReflectionMethod $fxn) use ($method) {
480
                return $fxn->getName() === $method;
481
            })
482
            ->get(0, null)
483
            ->value()
484
        ;
485
486
        if ($fxnToCall === null)
487
        {
488
            throw new \BadFunctionCallException(sprintf('No function by the name of "%s" in this class', $method));
489
        }
490
491
        $arguments = $fxnToCall->getParameters();
492
        $callUserFuncArray = [];
493
494
        /** @var \ReflectionParameter $argument */
495
        foreach ($arguments as $argument)
496
        {
497
            if (isset($namedParams[$argument->getName()]))
498
            {
499
                $callUserFuncArray[] = $namedParams[$argument->getName()];
500
            }
501
            else
502
            {
503
                $callUserFuncArray[] = $argument->getDefaultValue();
504
            }
505
        }
506
507
        return $fxnToCall->invoke(null, ...$callUserFuncArray);
508
    }
509
510
    ///
511
    // Misc Functions
512
    ///
513
514
    protected function bookCollectionProvider($jailed = false)
515
    {
516
        $cm = new CollectionManager(
517
            $this->getMockMarkupEngineManager(),
518
            $this->getMockConfiguration(),
0 ignored issues
show
Bug introduced by
It seems like $this->getMockConfiguration() targeting allejo\stakx\Test\PHPUni...:getMockConfiguration() can also be of type object<PHPUnit_Framework_MockObject_MockObject>; however, allejo\stakx\Manager\Col...nManager::__construct() does only seem to accept object<allejo\stakx\Configuration>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
519
            $this->getMockTemplateBridge(),
520
            $this->getMockEventDistpatcher(),
0 ignored issues
show
Bug introduced by
It seems like $this->getMockEventDistpatcher() targeting allejo\stakx\Test\PHPUni...tMockEventDistpatcher() can also be of type object<PHPUnit_Framework_MockObject_MockObject>; however, allejo\stakx\Manager\Col...nManager::__construct() does only seem to accept object<Symfony\Component...entDispatcherInterface>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
521
            $this->getMockLogger()
0 ignored issues
show
Bug introduced by
It seems like $this->getMockLogger() targeting allejo\stakx\Test\PHPUni...stCase::getMockLogger() can also be of type object<PHPUnit_Framework_MockObject_MockObject>; however, allejo\stakx\Manager\Col...nManager::__construct() does only seem to accept object<Psr\Log\LoggerInterface>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
522
        );
523
        $cm->parseCollections([
524
            [
525
                'name' => 'books',
526
                'folder' => 'tests/allejo/stakx/Test/assets/MyBookCollection/',
527
            ],
528
        ]);
529
530
        return (!$jailed) ? $cm->getCollections() : $cm->getJailedCollections();
531
    }
532
}
533