|
| 1 | +<?php |
| 2 | +/** |
| 3 | + * CROSS PHPUnit Utils |
| 4 | + * |
| 5 | + * @filesource |
| 6 | + * @copyright 2019 Cross Solution <http://cross-solution.de> |
| 7 | + */ |
| 8 | + |
| 9 | +declare(strict_types=1); |
| 10 | + |
| 11 | +namespace Cross\TestUtils\TestCase; |
| 12 | + |
| 13 | +use Cross\TestUtils\Exception\InvalidUsageException; |
| 14 | +use Cross\TestUtils\Utils\Instance; |
| 15 | + |
| 16 | +/** |
| 17 | + * Setup the SUT from specifications. |
| 18 | + * |
| 19 | + * If the testcase using this trait provide its own setup() method, |
| 20 | + * it needs to call the setupTarget() method. |
| 21 | + * |
| 22 | + * @author Mathias Gelhausen <gelhausen@cross-solution.de> |
| 23 | + */ |
| 24 | +trait SetupTargetTrait |
| 25 | +{ |
| 26 | + |
| 27 | + public function setup() |
| 28 | + { |
| 29 | + $this->setupTarget(); |
| 30 | + } |
| 31 | + |
| 32 | + /** |
| 33 | + * Setup the SUT from specification |
| 34 | + * |
| 35 | + * You need to define the property _$target_. |
| 36 | + * This property holds the specification and will be set to the SUT. |
| 37 | + * |
| 38 | + * ### Examples |
| 39 | + * |
| 40 | + * 1. Create one SUT for all tests via the callback "initTarget": |
| 41 | + * _Read more about callbacks futher down._ |
| 42 | + * |
| 43 | + * `$target = true;` |
| 44 | + * |
| 45 | + * |
| 46 | + * 2. Create one SUT for all tests from a specification for use with |
| 47 | + * {@link \Cross\TestUtils\Utils\Instance::create}: |
| 48 | + * |
| 49 | + * ``` |
| 50 | + * $target = FQCN; |
| 51 | + * $target = [FQCN, argument, ...]; |
| 52 | + * ``` |
| 53 | + * |
| 54 | + * 3. If you want to create different SUTs for some tests, you need to |
| 55 | + * use the more verbose specification format: |
| 56 | + * |
| 57 | + * ``` |
| 58 | + * $target = [ |
| 59 | + * 'default' => [], |
| 60 | + * 'create' => [ |
| 61 | + * 'for' => testnamePatterns, |
| 62 | + * 'target' => targetSpec, |
| 63 | + * 'reflection' => FQCN|bool, |
| 64 | + * 'callback' => callable, |
| 65 | + * 'arguments' => [argument, ...], |
| 66 | + * 'use' => presetName |
| 67 | + * ], |
| 68 | + * ]; |
| 69 | + * ``` |
| 70 | + * |
| 71 | + * * 'for' : Specify one or more (as array) patterns matching test names for which this |
| 72 | + * SUT specification should apply. |
| 73 | + * You may use '*' to match all test names starting with the string. |
| 74 | + * __Examples__: |
| 75 | + * * 'testCorrectBehaviour' : will match exactly the test name |
| 76 | + * * 'testCorrect*' : match all tests starting with "testCorrect" |
| 77 | + * * 'testWithProvider|#4': matches the test with dataset #4. |
| 78 | + * * 'testWith*|#2': matches all tests starting with testWith at dataset #2. |
| 79 | + * * '*': matches all tests. This is the default value assumed, if 'for' is not provided. |
| 80 | + * That means, it should always be the last entry, as all following entries are |
| 81 | + * ignored. |
| 82 | + * |
| 83 | + * * 'target' : specification for the target as understood by |
| 84 | + * {@link \Cross\TestUtils\Utils\Instance::create} |
| 85 | + * |
| 86 | + * * 'reflection' : |
| 87 | + * * _string_: Creates a \ReflectionClass from the FQCN |
| 88 | + * * _bool_: Enable or disable the creation of a reflection from |
| 89 | + * target specification (override default value) |
| 90 | + * For example you might want to create a reflection class |
| 91 | + * from specifications returned by a callback. |
| 92 | + * |
| 93 | + * * 'callback' : Specify a callback to be called, which should either return thr SUT instance |
| 94 | + * or a specification understood by {@link \Cross\TestUtils\Utils\Instance::withMappedArguments} |
| 95 | + * |
| 96 | + * * 'arguments' : Array of constructor arguments used to create the SUT. |
| 97 | + * Note: If target specification is an array, this arguments are ignored. |
| 98 | + * |
| 99 | + * * 'use': You can define presets to be used with a specification. |
| 100 | + * Presets are like the default values with a unique key name. |
| 101 | + * If use is present, the options from this preset key are merged in the |
| 102 | + * specification. (order: default -> preset -> spec (later merges override previous ones)) |
| 103 | + * |
| 104 | + * |
| 105 | + * Default values: |
| 106 | + * Each test specification will be merged into the default values, if provided. |
| 107 | + * |
| 108 | + * @return void |
| 109 | + */ |
| 110 | + public function setupTarget(): void |
| 111 | + { |
| 112 | + if (!property_exists($this, 'target')) { |
| 113 | + return; |
| 114 | + } |
| 115 | + |
| 116 | + if (!isset($this->target['create']) && !isset($this->target['default'])) { |
| 117 | + if (false === $this->target) { |
| 118 | + return; |
| 119 | + } |
| 120 | + $spec = true === $this->target ? ['callback' => 'initTarget'] : ['target' => $this->target]; |
| 121 | + $this->target = $this->setupTargetInstance($spec); |
| 122 | + |
| 123 | + return; |
| 124 | + } |
| 125 | + |
| 126 | + $specs = $this->target['create'] ?? []; |
| 127 | + $nameParts = explode(' ', $this->getName()); |
| 128 | + $name = reset($nameParts); |
| 129 | + $set = end($nameParts); |
| 130 | + $set = '#' == $set{0} || '"' == $set{0} ? trim($set, '"') : ''; |
| 131 | + |
| 132 | + foreach ($specs as $spec) { |
| 133 | + $for = isset($spec['for']) ? (array) $spec['for'] : ['*']; |
| 134 | + |
| 135 | + foreach ($for as $pattern) { |
| 136 | + $search = false !== strpos($pattern, '|') ? "$name|$set" : $name; |
| 137 | + $pattern = str_replace(['*', '|'], ['.*', '\|'], $pattern); |
| 138 | + |
| 139 | + if (preg_match('~^' . $pattern . '$~i', "$search")) { |
| 140 | + $defaultSpec = $this->target['default'] ?? []; |
| 141 | + $useSpec = isset($spec['use']) && isset($this->target[$spec['use']]) |
| 142 | + ? $this->target[$spec['use']] |
| 143 | + : [] |
| 144 | + ; |
| 145 | + $spec = array_merge($defaultSpec, $useSpec, $spec); |
| 146 | + $this->target = $this->setupTargetInstance($spec); |
| 147 | + return; |
| 148 | + } |
| 149 | + } |
| 150 | + } |
| 151 | + |
| 152 | + $spec = $this->target['default'] ?? ['callback' => 'initTarget']; |
| 153 | + $this->target = $this->setupTargetInstance($spec); |
| 154 | + } |
| 155 | + |
| 156 | + /** |
| 157 | + * Setup a SUT from specification. |
| 158 | + * |
| 159 | + * @param array $spec |
| 160 | + * @return object|null |
| 161 | + */ |
| 162 | + private function setupTargetInstance($spec): ?object |
| 163 | + { |
| 164 | + $reflection = false; |
| 165 | + $arguments = $spec['arguments'] ?? []; |
| 166 | + |
| 167 | + if (isset($spec['reflection'])) { |
| 168 | + if (is_string($spec['reflection'])) { |
| 169 | + return Instance::reflection($spec['reflection']); |
| 170 | + } |
| 171 | + $reflection = (bool) $spec['reflection']; |
| 172 | + } |
| 173 | + |
| 174 | + if (isset($spec['callback']) && false !== $spec['callback']) { |
| 175 | + if (!is_callable($spec['callback'])) { |
| 176 | + $spec['callback'] = [$this, $spec['callback']]; |
| 177 | + |
| 178 | + if (!is_callable($spec['callback'])) { |
| 179 | + throw InvalidUsageException::fromTrait( |
| 180 | + __TRAIT__, |
| 181 | + __CLASS__, |
| 182 | + 'Invalid callback.' |
| 183 | + ); |
| 184 | + } |
| 185 | + } |
| 186 | + |
| 187 | + $target = $spec['callback'](); |
| 188 | + |
| 189 | + if (!is_object($target)) { |
| 190 | + return $reflection |
| 191 | + ? Instance::reflection($target) |
| 192 | + : Instance::withMappedArguments($target, $arguments, $this); |
| 193 | + } |
| 194 | + |
| 195 | + return $target; |
| 196 | + } |
| 197 | + |
| 198 | + $target = $spec['target'] ?? false; |
| 199 | + |
| 200 | + if (!$target) { |
| 201 | + return null; |
| 202 | + } |
| 203 | + |
| 204 | + if ($reflection) { |
| 205 | + return Instance::reflection($target); |
| 206 | + } |
| 207 | + |
| 208 | + return Instance::withMappedArguments($target, $arguments, $this); |
| 209 | + } |
| 210 | +} |
0 commit comments