Completed
Push — fix/autoloader-code-coverage ( c5ee08 )
by
unknown
150:59 queued 141:18
created

test-coverage.php ➔ get_output_file()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 0
dl 0
loc 11
rs 9.9
c 0
b 0
f 0
1
<?php
2
/**
3
 * A utility for transforming a raw code coverage
4
 * report into a clover.xml file for consumption.
5
 *
6
 * @package automattic/jetpack-autoloader
7
 */
8
9
use SebastianBergmann\CodeCoverage\Report\Clover;
10
use SebastianBergmann\CodeCoverage\Version;
11
12
// phpcs:disabled WordPress.Security.EscapeOutput.OutputNotEscaped
13
14
define( 'ROOT_DIR', realpath( implode( DIRECTORY_SEPARATOR, array( __DIR__, '..', '..', '..' ) ) ) );
15
16
require_once ROOT_DIR . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
17
18
/**
19
 * Returns the path to the clover.xml output file.
20
 *
21
 * @return string The path to the output file.
22
 */
23
function get_output_file() {
24
	global $argc;
25
	global $argv;
26
27
	if ( $argc < 2 ) {
28
		echo "Usage: test-coverage [clover.xml file]\n";
29
		exit( -1 );
30
	}
31
32
	return $argv[1];
33
}
34
35
/**
36
 * Attempts to load the report from the tmp file we should have generated.
37
 *
38
 * @return SebastianBergmann\CodeCoverage\CodeCoverage The unserialized code coverage object.
39
 */
40
function load_report() {
41
	$coverage_report = implode( DIRECTORY_SEPARATOR, array( ROOT_DIR, 'tests', 'php', 'tmp', 'coverage-report.php' ) );
42
	if ( ! file_exists( $coverage_report ) ) {
43
		echo "There is no coverage report to process.\n";
44
		exit( -1 );
45
	}
46
47
	return require_once $coverage_report;
48
}
49
50
/**
51
 * Evaluates the version of sebastianbergmann/php-code-coverage that we've generated the coverage report using.
52
 *
53
 * @return string The version for the code coverage package.
54
 */
55
function get_coverage_version() {
56
	return Version::id();
57
}
58
59
/**
60
 * Counts the number of lines in a file before the `class` keyword.
61
 *
62
 * @param string $file The file to check.
63
 * @return int|null The number of lines or null if there is no class keyword.
64
 */
65
function count_lines_before_class_keyword( $file ) {
66
	// Find the line that the `class` keyword occurs on so that we can use it to calculate an offset from the header.
67
	$content = file_get_contents( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
68
69
	// Find the class keyword and capture the number of characters in the string before this point.
70
	if ( 0 === preg_match_all( '/^class /m', $content, $matches, PREG_OFFSET_CAPTURE ) ) {
71
		return null;
72
	}
73
74
	// Support both styles of line endings.
75
	$newlines = substr_count( $content, "\r\n", 0, $matches[0][0][1] );
76
	if ( $newlines > 0 ) {
77
		return $newlines + 1;
78
	}
79
	$newlines = substr_count( $content, "\n", 0, $matches[0][0][1] );
80
	if ( $newlines > 0 ) {
81
		return $newlines + 1;
82
	}
83
84
	return null;
85
}
86
87
/**
88
 * Creates a map for converting file paths to src paths.
89
 *
90
 * @param string[] $report_file_paths An array containing all of the paths for the report.
91
 * @return array A map describing how to transform built files into src coverage.
92
 */
93
function get_path_transformation_map( $report_file_paths ) {
94
	// We're going to create a map describing how to transform files to src files.
95
	// We're also going to store any metadata needed to perform the merge safetly.
96
	$transformation_map = array();
97
98
	// Scan the src directory so that we can create the map to convert between files.
99
	$raw_src_files = scandir( ROOT_DIR . DIRECTORY_SEPARATOR . 'src' );
100
	$src_file_map  = array();
101
	foreach ( $raw_src_files as $file ) {
102
		// Only PHP files will be copied.
103
		if ( substr( $file, -4 ) !== '.php' ) {
104
			continue;
105
		}
106
107
		$file = ROOT_DIR . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . $file;
108
		if ( ! file_exists( $file ) ) {
109
			continue;
110
		}
111
112
		// We need to use the class keywork to address the line offset from injecting the header.
113
		$class_line = count_lines_before_class_keyword( $file );
114
		if ( ! isset( $class_line ) ) {
115
			// The autoloader only has class files and so this is fine.
116
			continue;
117
		}
118
119
		$src_file_map[ $file ] = array(
120
			'file'       => $file,
121
			'class_line' => $class_line,
122
		);
123
	}
124
125
	// Create a map describing the file transformations.
126
	foreach ( $report_file_paths as $report_file_path ) {
127
		// We will use the class line from the report file to calculate the offset from the src file to apply to coverage lines.
128
		$class_line = count_lines_before_class_keyword( $report_file_path );
129
		if ( ! isset( $class_line ) ) {
130
			continue;
131
		}
132
133
		// Attempt to find the original file.
134
		// Note: This does not support nested directories!
135
		$src_file_path = null;
136
		foreach ( $src_file_map as $src_file ) {
137
			// We don't need to perform any transformations if the file path is the same.
138
			if ( $src_file['file'] === $report_file_path ) {
139
				continue;
140
			}
141
142
			if ( basename( $src_file['file'] ) === basename( $report_file_path ) ) {
143
				$src_file_path = $src_file['file'];
144
				break;
145
			}
146
		}
147
		if ( ! isset( $src_file_path ) ) {
148
			continue;
149
		}
150
151
		// We can finally calculate the line offset since we have the class line for both.
152
		$line_offset = $class_line - $src_file_map[ $src_file_path ]['class_line'];
153
154
		// Record the file in the transformation map.
155
		$transformation_map[ $report_file_path ] = array(
156
			'src'         => $src_file_path,
157
			'line_offset' => $line_offset,
158
		);
159
	}
160
161
	return $transformation_map;
162
}
163
164
/**
165
 * Processes a v9 CodeCoverage report.
166
 *
167
 * @param SebastianBergmann\CodeCoverage\CodeCoverage $report The report to process.
168
 * @return SebastianBergmann\CodeCoverage\CodeCoverage The processed report.
169
 */
170
function process_coverage_9( $report ) {
171
	$data = $report->getData( true );
172
173
	// We're going to merge the line coverage from compiled files into the src files.
174
	$line_coverage   = $data->lineCoverage();
175
	$transformations = get_path_transformation_map( array_keys( $line_coverage ) );
176
177
	$removed_files = array();
178
	foreach ( $line_coverage as $file => $lines ) {
179
		if ( ! isset( $transformations[ $file ] ) ) {
180
			continue;
181
		}
182
183
		// Prepare the transformations we are going to make.
184
		$src_file    = $transformations[ $file ]['src'];
185
		$line_offset = $transformations[ $file ]['line_offset'];
186
187
		// Create a new line coverage mapped to the src file.
188
		$new_coverage = array();
189
		foreach ( $lines as $line => $coverage ) {
190
			$new_coverage[ $src_file ][ $line - $line_offset ] = $coverage;
191
		}
192
193
		// Merge the coverage since multiple compiled files may map to a single src file.
194
		$merge = new SebastianBergmann\CodeCoverage\ProcessedCodeCoverageData();
195
		$merge->setLineCoverage( $new_coverage );
196
		$data->merge( $merge );
197
198
		// Mark the file for removal from the original coverage.
199
		$removed_files[] = $file;
200
	}
201
202
	// Remove all of the files that we've transformed from the coverage.
203
	$line_coverage = $data->lineCoverage();
204
	foreach ( $removed_files as $file ) {
205
		// Make sure the uncovered file does not show up in the report.
206
		$report->filter()->excludeFile( $file );
207
		unset( $line_coverage[ $file ] );
208
	}
209
	$data->setLineCoverage( $line_coverage );
210
211
	return $report;
212
}
213
214
/**
215
 * Processes the code coverage report and outputs a clover.xml file.
216
 */
217
function process_coverage() {
218
	echo "Aggregating compiled coverage into unified code coverage report\n";
219
220
	// We're going to transform the code coverage object into a Clover XML report.
221
	$output_file = get_output_file();
222
223
	// Since there is no backwards compatibility guarantee in place for the code coverage
224
	// object we need to handle it according to each major version independently.
225
	$coverage_version = get_coverage_version();
226
	$major_version    = substr( $coverage_version, 0, strpos( $coverage_version, '.' ) );
227
228
	// We can finally load the report that we're wanting to process.
229
	$report = load_report();
230
231
	$function = 'process_coverage_' . $major_version;
232
	if ( ! function_exists( $function ) ) {
233
		echo "No handler defined for major version $major_version\n";
234
		die( -1 );
235
	}
236
237
	// Process the report using the handler.
238
	$report = call_user_func( $function, $report );
239
240
	// Generate the XML file for the report.
241
	$clover = new Clover();
242
	$clover->process( $report, $output_file );
243
	echo "Generated code coverage report in Clover format\n";
244
}
245
246
// Process the coverage report into the new output.
247
process_coverage();
248