Completed
Push — add/changelog-tooling ( fa9ac3...7f5585 )
by
unknown
517:08 queued 507:24
created

ParserTestCase::testFixture()   F

Complexity

Conditions 29
Paths > 20000

Size

Total Lines 94

Duplication

Lines 28
Ratio 29.79 %

Importance

Changes 0
Metric Value
cc 29
nc 26248
nop 1
dl 28
loc 94
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 instead of running tests.
40
	 *
41
	 * @var bool
42
	 */
43
	protected $updateFixtures = false;
44
45
	/**
46
	 * Create the parser object to test.
47
	 *
48
	 * @param array $args Arguments to pass to the constructor.
49
	 * @return Parser
50
	 */
51
	protected function newParser( array $args ) {
52
		$class = $this->className;
53
		return new $class( ...$args );
54
	}
55
56
	/**
57
	 * Write a fixture file.
58
	 *
59
	 * @param string $filename Filename to write.
60
	 * @param array  $data Fixture data.
61
	 *   - args: (array) Arguments to pass to the constructor.
62
	 *   - changelog: (string) Changelog file. Required for testing `parse()`.
63
	 *   - object: (Changelog) Changelog object. Required for testing `format()`.
64
	 *   - parse-output: (Changelog) Changelog object to expect from `parse()`. If this and `parse-exception` are omitted, `object` will be expected.
65
	 *   - format-output: (string|Exception) Changelog text or Exception to expect from `format()`. If omitted, `changelog` will be expected.
66
	 */
67
	protected function writeFixture( $filename, array $data ) {
68
		$this->assertTrue( defined( 'JSON_THROW_ON_ERROR' ) );
69
		$this->assertTrue( isset( $data['changelog'] ) || isset( $data['object'] ), 'Must provide at least one of "changelog" or "object"' );
70
		$this->assertFalse( isset( $data['parse-output'] ) && isset( $data['parse-exception'] ), 'Cannot provide both "parse-output" and "parse-exception".' );
71
		$this->assertFalse( isset( $data['format-output'] ) && isset( $data['format-exception'] ), 'Cannot provide both "format-output" and "format-exception".' );
72
		$jsonFlags = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR; // phpcs:ignore PHPCompatibility.Constants.NewConstants.json_throw_on_errorFound
73
74
		$contents = "# Parser text fixture file\n";
75 View Code Duplication
		if ( ! empty( $data['args'] ) ) {
76
			$this->assertIsArray( $data['args'] );
77
			$contents .= "\n## Constructor args\n";
78
			$contents .= "  ~~~~~~~~json args\n";
79
			$contents .= '  ' . str_replace( "\n", "\n  ", json_encode( $data['args'], $jsonFlags ) ) . "\n";
80
			$contents .= "  ~~~~~~~~\n";
81
		}
82 View Code Duplication
		if ( isset( $data['changelog'] ) ) {
83
			$this->assertIsString( $data['changelog'] );
84
			$contents .= "\n## Changelog file\n";
85
			$contents .= "  ~~~~~~~~markdown changelog\n";
86
			$contents .= '  ' . str_replace( "\n", "\n  ", $data['changelog'] ) . "\n";
87
			$contents .= "  ~~~~~~~~\n";
88
		}
89 View Code Duplication
		if ( isset( $data['object'] ) ) {
90
			$this->assertInstanceOf( Changelog::class, $data['object'] );
91
			$contents .= "\n## Changelog object\n";
92
			$contents .= "  ~~~~~~~~json object\n";
93
			$contents .= '  ' . str_replace( "\n", "\n  ", json_encode( $data['object'], $jsonFlags ) ) . "\n";
94
			$contents .= "  ~~~~~~~~\n";
95
		}
96 View Code Duplication
		if ( isset( $data['changelog'] ) ) {
97
			if ( isset( $data['parse-exception'] ) ) {
98
				$this->assertInstanceOf( Exception::class, $data['parse-exception'] );
99
				$contents .= "\n## Expected exception from `parse()`\n";
100
				$contents .= "  ~~~~~~~~text parse-exception\n";
101
				$contents .= '  ' . get_class( $data['parse-exception'] ) . "\n";
102
				$contents .= '  ' . str_replace( "\n", "\n  ", $data['parse-exception']->getMessage() ) . "\n";
103
				$contents .= "  ~~~~~~~~\n";
104
			} elseif ( isset( $data['parse-output'] ) && ! ( isset( $data['object'] ) && $data['object'] == $data['parse-output'] ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
105
				$this->assertInstanceOf( Changelog::class, $data['parse-output'] );
106
				$contents .= "\n## Expected output from `parse()`\n";
107
				$contents .= "  ~~~~~~~~json parse-output\n";
108
				$contents .= '  ' . str_replace( "\n", "\n  ", json_encode( $data['parse-output'], $jsonFlags ) ) . "\n";
109
				$contents .= "  ~~~~~~~~\n";
110
			} elseif ( ! isset( $data['object'] ) ) {
111
				$this->fail( 'At least one of "object", "parse-output", or "parse-exception" is required when "changelog" is given.' );
112
			}
113
		}
114 View Code Duplication
		if ( isset( $data['object'] ) ) {
115
			if ( isset( $data['format-exception'] ) ) {
116
				$this->assertInstanceOf( Exception::class, $data['format-exception'] );
117
				$contents .= "\n## Expected exception from `format()`\n";
118
				$contents .= "  ~~~~~~~~text format-exception\n";
119
				$contents .= '  ' . get_class( $data['format-exception'] ) . "\n";
120
				$contents .= '  ' . str_replace( "\n", "\n  ", $data['format-exception']->getMessage() ) . "\n";
121
				$contents .= "  ~~~~~~~~\n";
122
			} elseif ( isset( $data['format-output'] ) && ! ( isset( $data['changelog'] ) && $data['changelog'] == $data['format-output'] ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
123
				$this->assertIsString( $data['format-output'] );
124
				$contents .= "\n## Expected output from `format()`\n";
125
				$contents .= "  ~~~~~~~~markdown format-output\n";
126
				$contents .= '  ' . str_replace( "\n", "\n  ", $data['format-output'] ) . "\n";
127
				$contents .= "  ~~~~~~~~\n";
128
			} elseif ( ! isset( $data['changelog'] ) ) {
129
				$this->fail( 'At least one of "changelog", "format-output", or "format-exception" is required when "object" is given.' );
130
			}
131
		}
132
133
		file_put_contents( $filename, $contents );
134
	}
135
136
	/**
137
	 * Run tests using fixture files.
138
	 *
139
	 * @dataProvider provideFixture
140
	 * @param string $filename Fixture file name.
141
	 * @throws Exception On all sorts of failures. Duh.
142
	 */
143
	public function testFixture( $filename ) {
144
		$contents = file_get_contents( $filename );
145
		$this->assertIsString( $contents, 'Fixture contents cannot be fetched' );
146
		if ( ! preg_match_all( '/^( {0,3})~~~~~~~~\S* (\S+)\n(.*?)\n {0,3}~~~~~~~~$/sm', $contents, $m, PREG_SET_ORDER ) ) {
147
			$this->fail( 'Fixture is invalid' );
148
		}
149
		$data = array( 'args' => array() );
150
		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...
151
			if ( strlen( $indent ) > 0 ) {
152
				$value = preg_replace( '/^ {0,' . strlen( $indent ) . '}/m', '', $value );
153
			}
154
			switch ( $key ) {
155
				case 'args':
156
					$data[ $key ] = json_decode( $value, true );
157
					$this->assertIsArray( $data[ $key ] );
158
					break;
159
				case 'object':
160
				case 'parse-output':
161
					$data[ $key ] = Changelog::jsonUnserialize( json_decode( $value, true ) );
162
					break;
163
				case 'parse-exception':
164
				case 'format-exception':
165
					list( $class, $message ) = explode( "\n", $value, 2 );
166
					$this->assertTrue( is_a( $class, Exception::class, true ), "$class is not an Exception" );
167
					$data[ $key ] = new $class( $message );
168
					break;
169
				case 'changelog':
170
				case 'format-output':
171
					$data[ $key ] = $value;
172
					break;
173
				default:
174
					$this->fail( "Unknown fixture key $key" );
175
			}
176
		}
177
		$this->assertTrue( isset( $data['changelog'] ) || isset( $data['object'] ), 'Must provide at least one of "changelog" or "object"' );
178
		$this->assertFalse( isset( $data['parse-output'] ) && isset( $data['parse-exception'] ), 'Cannot provide both "parse-output" and "parse-exception".' );
179
		$this->assertFalse( isset( $data['format-output'] ) && isset( $data['format-exception'] ), 'Cannot provide both "format-output" and "format-exception".' );
180
181
		$parser = $this->newParser( $data['args'] );
182
183
		try {
184 View Code Duplication
			if ( isset( $data['changelog'] ) ) {
185
				if ( isset( $data['parse-exception'] ) ) {
186
					try {
187
						$parser->parse( $data['changelog'] );
188
						$this->fail( 'Expected exception not thrown from parse()' );
189
					} catch ( Exception $ex ) {
190
						$this->assertInstanceOf( get_class( $data['parse-exception'] ), $ex, 'Expected exception from parse()' );
191
						$this->assertStringContainsString( $data['parse-exception']->getMessage(), $ex->getMessage(), 'Expected exception from parse()' );
192
					}
193
				} else {
194
					$expect = isset( $data['parse-output'] ) ? $data['parse-output'] : $data['object'];
195
					$this->assertEquals( $expect, $parser->parse( $data['changelog'] ), 'Output from parse()' );
196
				}
197
			}
198 View Code Duplication
			if ( isset( $data['object'] ) ) {
199
				if ( isset( $data['format-exception'] ) ) {
200
					try {
201
						$parser->format( $data['object'] );
202
						$this->fail( 'Expected exception not thrown from format()' );
203
					} catch ( Exception $ex ) {
204
						$this->assertInstanceOf( get_class( $data['format-exception'] ), $ex, 'Expected exception from format()' );
205
						$this->assertStringContainsString( $data['format-exception']->getMessage(), $ex->getMessage(), 'Expected exception from format()' );
206
					}
207
				} else {
208
					$expect = isset( $data['format-output'] ) ? $data['format-output'] : $data['changelog'];
209
					$this->assertEquals( $expect, $parser->format( $data['object'] ), 'Output from format()' );
210
				}
211
			}
212
		} catch ( Exception $ex ) {
213
			if ( $this->updateFixtures ) {
214
				unset( $data['parse-output'], $data['parse-exception'], $data['format-output'], $data['format-exception'] );
215
				if ( isset( $data['changelog'] ) ) {
216
					try {
217
						$data['parse-output'] = $parser->parse( $data['changelog'] );
218
					} catch ( Exception $ex ) {
219
						$data['parse-exception'] = $ex;
220
					}
221
				}
222
				if ( isset( $data['object'] ) ) {
223
					try {
224
						$data['format-output'] = $parser->format( $data['object'] );
225
					} catch ( Exception $ex ) {
226
						$data['format-exception'] = $ex;
227
					}
228
				}
229
				$this->writeFixture( $filename, $data );
230
			}
231
			throw $ex;
232
		}
233
		if ( $this->updateFixtures ) {
234
			$this->writeFixture( $filename, $data );
235
		}
236
	}
237
238
	/**
239
	 * Data provider for testFixture.
240
	 */
241
	public function provideFixture() {
242
		$ret = array();
243
		foreach ( glob( $this->fixtures ) as $filename ) {
244
			$ret[ basename( $filename ) ] = array( $filename );
245
		}
246
		return $ret;
247
	}
248
249
	/**
250
	 * Test that updateFixtures is not set, so CI will not allow merge if it is.
251
	 */
252
	public function testUpdateFixtures() {
253
		$this->assertFalse( $this->updateFixtures );
254
	}
255
256
}
257