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

ParserTestCase   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 237
Duplicated Lines 35.86 %

Coupling/Cohesion

Components 2
Dependencies 2

Importance

Changes 0
Metric Value
dl 85
loc 237
rs 7.44
c 0
b 0
f 0
wmc 52
lcom 2
cbo 2

5 Methods

Rating   Name   Duplication   Size   Complexity  
A newParser() 0 4 1
F writeFixture() 57 68 19
F testFixture() 28 94 29
A provideFixture() 0 7 2
A testUpdateFixtures() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ParserTestCase often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ParserTestCase, and based on these observations, apply Extract Interface, too.

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