Completed
Push — renovate/css-loader-5.x ( 70942c...c43e3d )
by
unknown
124:26 queued 114:47
created

ParserTestCase::testFixture()   F

Complexity

Conditions 29
Paths > 20000

Size

Total Lines 100

Duplication

Lines 28
Ratio 28 %

Importance

Changes 0
Metric Value
cc 29
nc 26248
nop 1
dl 28
loc 100
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2
/**
3
 * Test base class for changelog parsers.
4
 *
5
 * @package automattic/jetpack-changelogger
6
 */
7
8
// phpcs:disable WordPress.WP.AlternativeFunctions, WordPress.NamingConventions.ValidVariableName
9
10
namespace Automattic\Jetpack\Changelog\Tests;
11
12
use Automattic\Jetpack\Changelog\Changelog;
13
use Automattic\Jetpack\Changelog\Parser;
14
use Exception;
15
use PHPUnit\Framework\TestCase;
16
17
/**
18
 * Test base class for changelog parsers.
19
 */
20
class ParserTestCase extends TestCase {
21
	use \Yoast\PHPUnitPolyfills\Polyfills\AssertIsType;
22
	use \Yoast\PHPUnitPolyfills\Polyfills\AssertStringContains;
23
24
	/**
25
	 * Parser class being tested.
26
	 *
27
	 * @var string
28
	 */
29
	protected $className;
30
31
	/**
32
	 * Fixture file glob.
33
	 *
34
	 * @var string
35
	 */
36
	protected $fixtures;
37
38
	/**
39
	 * Set to update fixture files after running tests.
40
	 *
41
	 * It's recommended to run tests with this set during development to standardize the formatting
42
	 * of the fixture files.
43
	 *
44
	 * @var bool
45
	 */
46
	protected $updateFixtures = false;
47
48
	/**
49
	 * Create the parser object to test.
50
	 *
51
	 * @param array $args Arguments to pass to the constructor.
52
	 * @return Parser
53
	 */
54
	protected function newParser( array $args ) {
55
		$class = $this->className;
56
		return new $class( ...$args );
57
	}
58
59
	/**
60
	 * Add two spaces at the start of all lines in a string.
61
	 *
62
	 * @param string $s String.
63
	 * @return string
64
	 */
65
	private function indent( $s ) {
66
		$ret = '  ' . str_replace( "\n", "\n  ", $s );
67
		if ( substr( $ret, -3 ) === "\n  " ) {
68
			$ret = substr( $ret, 0, -2 );
69
		}
70
		do {
71
			$l   = strlen( $ret );
72
			$ret = str_replace( "\n  \n", "\n\n", $ret );
73
		} while ( strlen( $ret ) !== $l ); // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found
74
		return $ret;
75
	}
76
77
	/**
78
	 * Write a fixture file.
79
	 *
80
	 * The fixture file consists of markdown-style fenced code blocks using `~~~~~~~~` as the fence,
81
	 * with the opening fence having an optional syntax-highlighting name definition (unspaced),
82
	 * followed by a space then a keyword, then a `~~~~~~~~` trailer.
83
	 *
84
	 * See `$data` for the keywords recognized and their interpretation.
85
	 *
86
	 * All other content in the file is ignored; here we output some headers for human readability.
87
	 *
88
	 * @param string $filename Filename to write.
89
	 * @param array  $data Fixture data.
90
	 *   - args: (array) Arguments to pass to the constructor.
91
	 *   - changelog: (string) Changelog file. Required for testing `parse()`.
92
	 *   - object: (Changelog) Changelog object. Required for testing `format()`.
93
	 *   - parse-output: (Changelog) Changelog object to expect from `parse()`. If this and `parse-exception` are omitted, `object` will be expected.
94
	 *     If this is supplied but equal to `object`, it will not be written to the file.
95
	 *   - parse-exception: (Exception) Exception to expect from `parse()`. If this and `parse-output` are omitted, `object` will be expected.
96
	 *   - format-output: (string) Changelog text to expect from `format()`. If this and `format-exception` are omitted, `changelog` will be expected.
97
	 *     If this is supplied but equal to `changelog`, it will not be written to the file.
98
	 *   - format-exception: (Exception) Exception to expect from `format()`. If this and `format-output` are omitted, `changelog` will be expected.
99
	 */
100
	protected function writeFixture( $filename, array $data ) {
101
		$this->assertTrue( defined( 'JSON_THROW_ON_ERROR' ) );
102
		$this->assertTrue( isset( $data['changelog'] ) || isset( $data['object'] ), 'Must provide at least one of "changelog" or "object"' );
103
		$this->assertFalse( isset( $data['parse-output'] ) && isset( $data['parse-exception'] ), 'Cannot provide both "parse-output" and "parse-exception".' );
104
		$this->assertFalse( isset( $data['format-output'] ) && isset( $data['format-exception'] ), 'Cannot provide both "format-output" and "format-exception".' );
105
		$jsonFlags = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR; // phpcs:ignore PHPCompatibility.Constants.NewConstants.json_throw_on_errorFound
106
107
		$contents = "# {$this->className} test fixture file\n";
108 View Code Duplication
		if ( ! empty( $data['args'] ) ) {
109
			$this->assertIsArray( $data['args'] );
110
			$contents .= "\n## Constructor args\n";
111
			$contents .= "  ~~~~~~~~json args\n";
112
			$contents .= $this->indent( json_encode( $data['args'], $jsonFlags ) ) . "\n";
113
			$contents .= "  ~~~~~~~~\n";
114
		}
115
		if ( isset( $data['changelog'] ) ) {
116
			$this->assertIsString( $data['changelog'] );
117
			$contents .= "\n## Changelog file\n";
118
			$contents .= "  ~~~~~~~~markdown changelog\n";
119
			$contents .= $this->indent( $data['changelog'] ) . "\n";
120
			$contents .= "  ~~~~~~~~\n";
121
		}
122 View Code Duplication
		if ( isset( $data['object'] ) ) {
123
			$this->assertInstanceOf( Changelog::class, $data['object'] );
124
			$contents .= "\n## Changelog object\n";
125
			$contents .= "  ~~~~~~~~json object\n";
126
			$contents .= $this->indent( json_encode( $data['object'], $jsonFlags ) ) . "\n";
127
			$contents .= "  ~~~~~~~~\n";
128
		}
129 View Code Duplication
		if ( isset( $data['changelog'] ) ) {
130
			if ( isset( $data['parse-exception'] ) ) {
131
				$this->assertInstanceOf( Exception::class, $data['parse-exception'] );
132
				$contents .= "\n## Expected exception from `parse()`\n";
133
				$contents .= "  ~~~~~~~~text parse-exception\n";
134
				$contents .= '  ' . get_class( $data['parse-exception'] ) . "\n";
135
				$contents .= $this->indent( $data['parse-exception']->getMessage() ) . "\n";
136
				$contents .= "  ~~~~~~~~\n";
137
			} elseif ( isset( $data['parse-output'] ) && ! ( isset( $data['object'] ) && $data['object'] == $data['parse-output'] ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
138
				$this->assertInstanceOf( Changelog::class, $data['parse-output'] );
139
				$contents .= "\n## Expected output from `parse()`\n";
140
				$contents .= "  ~~~~~~~~json parse-output\n";
141
				$contents .= $this->indent( json_encode( $data['parse-output'], $jsonFlags ) ) . "\n";
142
				$contents .= "  ~~~~~~~~\n";
143
			} elseif ( ! isset( $data['object'] ) ) {
144
				$this->fail( 'At least one of "object", "parse-output", or "parse-exception" is required when "changelog" is given.' );
145
			}
146
		}
147 View Code Duplication
		if ( isset( $data['object'] ) ) {
148
			if ( isset( $data['format-exception'] ) ) {
149
				$this->assertInstanceOf( Exception::class, $data['format-exception'] );
150
				$contents .= "\n## Expected exception from `format()`\n";
151
				$contents .= "  ~~~~~~~~text format-exception\n";
152
				$contents .= '  ' . get_class( $data['format-exception'] ) . "\n";
153
				$contents .= $this->indent( $data['format-exception']->getMessage() ) . "\n";
154
				$contents .= "  ~~~~~~~~\n";
155
			} elseif ( isset( $data['format-output'] ) && ! ( isset( $data['changelog'] ) && $data['changelog'] == $data['format-output'] ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
156
				$this->assertIsString( $data['format-output'] );
157
				$contents .= "\n## Expected output from `format()`\n";
158
				$contents .= "  ~~~~~~~~markdown format-output\n";
159
				$contents .= $this->indent( $data['format-output'] ) . "\n";
160
				$contents .= "  ~~~~~~~~\n";
161
			} elseif ( ! isset( $data['changelog'] ) ) {
162
				$this->fail( 'At least one of "changelog", "format-output", or "format-exception" is required when "object" is given.' );
163
			}
164
		}
165
166
		$this->assertNotFalse( file_put_contents( $filename, $contents ) );
167
	}
168
169
	/**
170
	 * Run tests using fixture files.
171
	 *
172
	 * @dataProvider provideFixture
173
	 * @param string $filename Fixture file name.
174
	 * @throws Exception On all sorts of failures. Duh.
175
	 */
176
	public function testFixture( $filename ) {
177
		// Load fixture file. The important parts are the bits delimited with `~~~~~~~~`, the rest is ignored.
178
		$contents = file_get_contents( $filename );
179
		$this->assertIsString( $contents, 'Fixture contents cannot be fetched' );
180
		if ( ! preg_match_all( '/^( {0,3})~~~~~~~~\S* (\S+)\n(.*?)\n {0,3}~~~~~~~~$/sm', $contents, $m, PREG_SET_ORDER ) ) {
181
			$this->fail( 'Fixture is invalid' );
182
		}
183
		$data = array( 'args' => array() );
184
		foreach ( $m as list( , $indent, $key, $value ) ) {
0 ignored issues
show
Bug introduced by
The expression $m of type null|array<integer,array<integer,string>> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
185
			if ( strlen( $indent ) > 0 ) {
186
				// We take advantage of markdown's ability to indent fenced code blocks for readability.
187
				// Unindent them here before processing the contents.
188
				$value = preg_replace( '/^ {0,' . strlen( $indent ) . '}/m', '', $value );
189
			}
190
			switch ( $key ) {
191
				case 'args':
192
					$data[ $key ] = json_decode( $value, true );
193
					$this->assertIsArray( $data[ $key ] );
194
					break;
195
				case 'object':
196
				case 'parse-output':
197
					$data[ $key ] = Changelog::jsonUnserialize( json_decode( $value, true ) );
198
					break;
199
				case 'parse-exception':
200
				case 'format-exception':
201
					list( $class, $message ) = explode( "\n", $value, 2 );
202
					$this->assertTrue( is_a( $class, Exception::class, true ), "$class is not an Exception" );
203
					$data[ $key ] = new $class( $message );
204
					break;
205
				case 'changelog':
206
				case 'format-output':
207
					$data[ $key ] = $value;
208
					break;
209
				default:
210
					$this->fail( "Unknown fixture key $key" );
211
			}
212
		}
213
		$this->assertTrue( isset( $data['changelog'] ) || isset( $data['object'] ), 'Must provide at least one of "changelog" or "object"' );
214
		$this->assertFalse( isset( $data['parse-output'] ) && isset( $data['parse-exception'] ), 'Cannot provide both "parse-output" and "parse-exception".' );
215
		$this->assertFalse( isset( $data['format-output'] ) && isset( $data['format-exception'] ), 'Cannot provide both "format-output" and "format-exception".' );
216
217
		// Run the tests!
218
		$parser = $this->newParser( $data['args'] );
219
		try {
220 View Code Duplication
			if ( isset( $data['changelog'] ) ) {
221
				if ( isset( $data['parse-exception'] ) ) {
222
					try {
223
						$parser->parse( $data['changelog'] );
224
						$this->fail( 'Expected exception not thrown from parse()' );
225
					} catch ( Exception $ex ) {
226
						$this->assertInstanceOf( get_class( $data['parse-exception'] ), $ex, 'Expected exception from parse()' );
227
						$this->assertStringContainsString( $data['parse-exception']->getMessage(), $ex->getMessage(), 'Expected exception from parse()' );
228
					}
229
				} else {
230
					$expect = isset( $data['parse-output'] ) ? $data['parse-output'] : $data['object'];
231
					$this->assertEquals( $expect, $parser->parse( $data['changelog'] ), 'Output from parse()' );
232
				}
233
			}
234 View Code Duplication
			if ( isset( $data['object'] ) ) {
235
				if ( isset( $data['format-exception'] ) ) {
236
					try {
237
						$parser->format( $data['object'] );
238
						$this->fail( 'Expected exception not thrown from format()' );
239
					} catch ( Exception $ex ) {
240
						$this->assertInstanceOf( get_class( $data['format-exception'] ), $ex, 'Expected exception from format()' );
241
						$this->assertStringContainsString( $data['format-exception']->getMessage(), $ex->getMessage(), 'Expected exception from format()' );
242
					}
243
				} else {
244
					$expect = isset( $data['format-output'] ) ? $data['format-output'] : $data['changelog'];
245
					$this->assertEquals( $expect, $parser->format( $data['object'] ), 'Output from format()' );
246
				}
247
			}
248
		} catch ( Exception $ex ) {
249
			if ( $this->updateFixtures ) {
250
				// Re-run parse and format to get the new outputs for the fixture update.
251
				// writeFixture() will take care of deduplication.
252
				unset( $data['parse-output'], $data['parse-exception'], $data['format-output'], $data['format-exception'] );
253
				if ( isset( $data['changelog'] ) ) {
254
					try {
255
						$data['parse-output'] = $parser->parse( $data['changelog'] );
256
					} catch ( Exception $ex ) {
257
						$data['parse-exception'] = $ex;
258
					}
259
				}
260
				if ( isset( $data['object'] ) ) {
261
					try {
262
						$data['format-output'] = $parser->format( $data['object'] );
263
					} catch ( Exception $ex ) {
264
						$data['format-exception'] = $ex;
265
					}
266
				}
267
				$this->writeFixture( $filename, $data );
268
			}
269
			throw $ex;
270
		}
271
		if ( $this->updateFixtures ) {
272
			// The test passed, so the fixture data is good. But re-write it to clean up formatting.
273
			$this->writeFixture( $filename, $data );
274
		}
275
	}
276
277
	/**
278
	 * Data provider for testFixture.
279
	 */
280
	public function provideFixture() {
281
		$ret = array();
282
		foreach ( glob( $this->fixtures ) as $filename ) {
283
			$ret[ basename( $filename ) ] = array( $filename );
284
		}
285
		return $ret;
286
	}
287
288
	/**
289
	 * Test that updateFixtures is not set, so CI will not allow merge if it is.
290
	 */
291
	public function testUpdateFixtures() {
292
		$this->assertFalse( $this->updateFixtures, static::class . '::$updateFixtures must be false for tests to pass.' );
293
	}
294
295
}
296