Completed
Push — add/business-hours-tests ( a06e6d...88b2e3 )
by Jeremy
68:17 queued 57:51
created

test-coverage.php ➔ process_coverage_9()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
nc 8
nop 1
dl 0
loc 43
rs 8.9208
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', dirname( dirname( dirname( __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
	// Count the line endings leading up to the `class` keyword.
75
	$newlines = substr_count( $content, "\n", 0, $matches[0][0][1] );
76
	if ( $newlines > 0 ) {
77
		return $newlines + 1;
78
	}
79
80
	return null;
81
}
82
83
/**
84
 * Creates a map for converting file paths to src paths.
85
 *
86
 * @param string[] $report_file_paths An array containing all of the paths for the report.
87
 * @return array A map describing how to transform built files into src coverage.
88
 */
89
function get_path_transformation_map( $report_file_paths ) {
90
	// We're going to create a map describing how to transform files to src files.
91
	// We're also going to store any metadata needed to perform the merge safetly.
92
	$transformation_map = array();
93
94
	// Scan the src directory so that we can create the map to convert between files.
95
	$raw_src_files = scandir( ROOT_DIR . DIRECTORY_SEPARATOR . 'src' );
96
	$src_file_map  = array();
97
	foreach ( $raw_src_files as $file ) {
98
		// Only PHP files will be copied.
99
		if ( substr( $file, -4 ) !== '.php' ) {
100
			continue;
101
		}
102
103
		$file = ROOT_DIR . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . $file;
104
		if ( ! file_exists( $file ) ) {
105
			continue;
106
		}
107
108
		// We need to use the class keyword to address the line offset from injecting the header.
109
		$class_line = count_lines_before_class_keyword( $file );
110
		if ( null === $class_line ) {
111
			// The autoloader only has class files and so this is fine.
112
			continue;
113
		}
114
115
		$src_file_map[ $file ] = array(
116
			'file'       => $file,
117
			'class_line' => $class_line,
118
		);
119
	}
120
121
	// Create a map describing the file transformations.
122
	foreach ( $report_file_paths as $report_file_path ) {
123
		// We will use the class line from the report file to calculate the offset from the src file to apply to coverage lines.
124
		$class_line = count_lines_before_class_keyword( $report_file_path );
125
		if ( ! isset( $class_line ) ) {
126
			continue;
127
		}
128
129
		// Attempt to find the original file.
130
		// Note: This does not support nested directories!
131
		$src_file_path = null;
132
		foreach ( $src_file_map as $src_file ) {
133
			// We don't need to perform any transformations if the file path is the same.
134
			if ( $src_file['file'] === $report_file_path ) {
135
				continue;
136
			}
137
138
			if ( basename( $src_file['file'] ) === basename( $report_file_path ) ) {
139
				$src_file_path = $src_file['file'];
140
				break;
141
			}
142
		}
143
		if ( ! $src_file_path ) {
144
			continue;
145
		}
146
147
		// We can finally calculate the line offset since we have the class line for both.
148
		$line_offset = $class_line - $src_file_map[ $src_file_path ]['class_line'];
149
150
		// Record the file in the transformation map.
151
		$transformation_map[ $report_file_path ] = array(
152
			'src'         => $src_file_path,
153
			'line_offset' => $line_offset,
154
		);
155
	}
156
157
	return $transformation_map;
158
}
159
160
/**
161
 * Processes a v9 CodeCoverage report.
162
 *
163
 * @param SebastianBergmann\CodeCoverage\CodeCoverage $report The report to process.
164
 * @return SebastianBergmann\CodeCoverage\CodeCoverage The processed report.
165
 */
166
function process_coverage_9( $report ) {
167
	$data = $report->getData( true );
168
169
	// We're going to merge the line coverage from compiled files into the src files.
170
	$line_coverage   = $data->lineCoverage();
171
	$transformations = get_path_transformation_map( array_keys( $line_coverage ) );
172
173
	$removed_files = array();
174
	foreach ( $line_coverage as $file => $lines ) {
175
		if ( ! isset( $transformations[ $file ] ) ) {
176
			continue;
177
		}
178
179
		// Prepare the transformations we are going to make.
180
		$src_file    = $transformations[ $file ]['src'];
181
		$line_offset = $transformations[ $file ]['line_offset'];
182
183
		// Create a new line coverage mapped to the src file.
184
		$new_coverage = array();
185
		foreach ( $lines as $line => $coverage ) {
186
			$new_coverage[ $src_file ][ $line - $line_offset ] = $coverage;
187
		}
188
189
		// Merge the coverage since multiple compiled files may map to a single src file.
190
		$merge = new SebastianBergmann\CodeCoverage\ProcessedCodeCoverageData();
191
		$merge->setLineCoverage( $new_coverage );
192
		$data->merge( $merge );
193
194
		// Mark the file for removal from the original coverage.
195
		$removed_files[] = $file;
196
	}
197
198
	// Remove all of the files that we've transformed from the coverage.
199
	$line_coverage = $data->lineCoverage();
200
	foreach ( $removed_files as $file ) {
201
		// Make sure the uncovered file does not show up in the report.
202
		$report->filter()->excludeFile( $file );
203
		unset( $line_coverage[ $file ] );
204
	}
205
	$data->setLineCoverage( $line_coverage );
206
207
	return $report;
208
}
209
210
/**
211
 * Processes the code coverage report and outputs a clover.xml file.
212
 */
213
function process_coverage() {
214
	echo "Aggregating compiled coverage into unified code coverage report\n";
215
216
	// We're going to transform the code coverage object into a Clover XML report.
217
	$output_file = get_output_file();
218
219
	// Since there is no backwards compatibility guarantee in place for the code coverage
220
	// object we need to handle it according to each major version independently.
221
	$coverage_version = get_coverage_version();
222
	$major_version    = substr( $coverage_version, 0, strpos( $coverage_version, '.' ) );
223
224
	$function = 'process_coverage_' . $major_version;
225
	if ( ! function_exists( $function ) ) {
226
		echo "No handler defined for major version $major_version\n";
227
		die( -1 );
228
	}
229
230
	// We can finally load the report that we're wanting to process.
231
	$report = load_report();
232
233
	// Process the report using the handler.
234
	$report = call_user_func( $function, $report );
235
236
	// Generate the XML file for the report.
237
	$clover = new Clover();
238
	$clover->process( $report, $output_file );
239
	echo "Generated code coverage report in Clover format\n";
240
}
241
242
// Process the coverage report into the new output.
243
process_coverage();
244