11// Copyright Kani Contributors
22// SPDX-License-Identifier: Apache-2.0 OR MIT
3+ import { ExecFileException , execFile } from 'child_process' ;
34import * as fs from 'fs' ;
45import * as path from 'path' ;
56
@@ -9,13 +10,22 @@ import Config from './config';
910import GlobalConfig from '../../globalConfig' ;
1011import { fullToRelativePath , getRootDir } from '../../utils' ;
1112
12- const { execFile } = require ( 'child_process' ) ;
13+ enum CoverageStatus {
14+ COVERED = 'COVERED' ,
15+ UNCOVERED = 'UNCOVERED' ,
16+ }
17+
18+ // Coverage information for a given file.
19+ type FileCoverageMap = Map < vscode . Range , CoverageStatus > ;
20+
21+ // Map files to their coverage information.
22+ type CoverageMap = Map < string , FileCoverageMap > ;
1323
14- // Interface for storing the coverage status for each line of a document .
15- export interface CoverageLines {
16- full : vscode . Range [ ] ;
17- partial : vscode . Range [ ] ;
18- none : vscode . Range [ ] ;
24+ // Result of running the coverage command .
25+ interface CoverageCommandResult {
26+ statusCode : number ;
27+ coverage ?: CoverageMap ;
28+ error ?: string ;
1929}
2030
2131const warningMessage = `Source-based coverage is an unstable feature.` ;
@@ -32,15 +42,17 @@ export async function runCodeCoverageAction(
3242
3343 // Run this command: cargo kani --coverage -Z source-coverage --harness verify_success
3444 // to generate the source coverage output
35- const playbackCommand : string = `${ kaniBinaryPath } --coverage -Z source-coverage --harness ${ functionName } ` ;
36- const processOutput = await runCoverageCommand ( playbackCommand , functionName ) ;
37- if ( processOutput . statusCode == 0 ) {
38- const coverageGlobalMap = processOutput . result ;
45+ const coverageCommand : string = `${ kaniBinaryPath } --coverage -Z source-coverage --harness ${ functionName } ` ;
46+ const processOutput = await runCoverageCommand ( coverageCommand , functionName ) ;
3947
48+ if ( processOutput . statusCode === 0 && processOutput . coverage ) {
4049 // Convert the array of (file, line, status) objects into Map<file <line, status>>
4150 // since we need to cache this globally
42- globalConfig . setCoverage ( coverageGlobalMap ) ;
43- renderer . renderInterface ( vscode . window . visibleTextEditors , coverageGlobalMap ) ;
51+ globalConfig . setCoverage ( processOutput . coverage ) ;
52+ renderer . renderInterface ( vscode . window . visibleTextEditors , processOutput . coverage ) ;
53+ } else if ( processOutput . error ) {
54+ renderer . clearAllHighlights ( ) ; // Clear highlights if there's an error
55+ vscode . window . showErrorMessage ( `Coverage generation failed: ${ processOutput . error } ` ) ;
4456 }
4557}
4658
@@ -51,7 +63,10 @@ export async function runCodeCoverageAction(
5163 * @param harnessName - name of the harness
5264 * @returns - the result of executing the --coverage command and parsing the output
5365 */
54- async function runCoverageCommand ( command : string , harnessName : string ) : Promise < any > {
66+ async function runCoverageCommand (
67+ command : string ,
68+ harnessName : string ,
69+ ) : Promise < CoverageCommandResult > {
5570 // Get the full resolved path for the root directory of the crate
5671 const directory = path . resolve ( getRootDir ( ) ) ;
5772 const commmandSplit = command . split ( ' ' ) ;
@@ -68,13 +83,27 @@ async function runCoverageCommand(command: string, harnessName: string): Promise
6883 const kaniBinaryPath = globalConfig . getFilePath ( ) ;
6984
7085 vscode . window . showInformationMessage ( `Generating coverage for ${ harnessName } ` ) ;
71- return new Promise ( ( resolve , _reject ) => {
72- execFile ( kaniBinaryPath , args , options , async ( _error : any , stdout : any , _stderr : any ) => {
73- if ( stdout ) {
74- const parseResult = await parseKaniCoverageOutput ( stdout ) ;
75- resolve ( { statusCode : 0 , result : parseResult } ) ;
76- }
77- } ) ;
86+ return new Promise < CoverageCommandResult > ( ( resolve ) => {
87+ execFile (
88+ kaniBinaryPath ,
89+ args ,
90+ options ,
91+ ( error : ExecFileException | null , stdout : string | Buffer , stderr : string | Buffer ) => {
92+ if ( error ) {
93+ resolve ( {
94+ statusCode : typeof error . code === 'number' ? error . code : 1 ,
95+ error : error . message ,
96+ } ) ;
97+ } else if ( stdout ) {
98+ parseKaniCoverageOutput ( stdout . toString ( ) ) . then ( resolve ) ;
99+ } else {
100+ resolve ( {
101+ statusCode : 1 ,
102+ error : stderr . toString ( ) || 'No output from coverage command' ,
103+ } ) ;
104+ }
105+ } ,
106+ ) ;
78107 } ) ;
79108}
80109
@@ -107,10 +136,9 @@ function readJsonFromPath(filePath: string): any {
107136 * @returns - undefined (error) or a result that indicates if the extension is executed on a local
108137 * or remote environment. The result includes a `path` (if local) or a `command` (if remote).
109138 */
110- async function parseKaniCoverageOutput ( stdout : string ) : Promise < any | undefined > {
111- const kaniOutput : string = stdout ;
112- const jsonFilePath = getCoverageJsonPath ( kaniOutput ) ;
113- const coverageMap : Map < string , Map < any , string > > = new Map ( ) ;
139+ async function parseKaniCoverageOutput ( stdout : string ) : Promise < CoverageCommandResult > {
140+ const jsonFilePath = getCoverageJsonPath ( stdout ) ;
141+ const coverageMap : CoverageMap = new Map ( ) ;
114142
115143 if ( jsonFilePath ) {
116144 const files = fs . readdirSync ( jsonFilePath ) ;
@@ -123,20 +151,19 @@ async function parseKaniCoverageOutput(stdout: string): Promise<any | undefined>
123151 const filePath = path . join ( jsonFilePath , kanirawJson ) ;
124152 const jsonContent = readJsonFromPath ( filePath ) ;
125153
126- // Take this jsonContent and create an Map<file <region, status>>
127- if ( jsonContent ) {
128- for ( const file in jsonContent [ 'data' ] ) {
129- const regions = jsonContent [ 'data' ] [ file ] ;
130- const regionMap : Map < any , string > = new Map ( ) ;
154+ if ( jsonContent && 'data' in jsonContent ) {
155+ for ( const file in jsonContent . data ) {
156+ const regions = jsonContent . data [ file ] ;
157+ const regionMap : FileCoverageMap = new Map ( ) ;
131158
132159 for ( const region of regions ) {
133160 const startLine = region . region . start [ 0 ] ;
134161 const startCol = region . region . start [ 1 ] ;
135162 const endLine = region . region . end [ 0 ] ;
136163 const endCol = region . region . end [ 1 ] ;
137164
138- const status = region . status ;
139- // Create a VSCode range with start line and column
165+ const status =
166+ region . status === 'COVERED' ? CoverageStatus . COVERED : CoverageStatus . UNCOVERED ;
140167 const start = new vscode . Position ( startLine - 1 , startCol - 1 ) ;
141168 const end = new vscode . Position ( endLine - 1 , endCol - 1 ) ;
142169 const range = new vscode . Range ( start , end ) ;
@@ -151,8 +178,11 @@ async function parseKaniCoverageOutput(stdout: string): Promise<any | undefined>
151178 }
152179 }
153180
154- if ( coverageMap === undefined || coverageMap . size == 0 ) {
155- return '' ;
181+ if ( coverageMap . size === 0 ) {
182+ return {
183+ statusCode : 1 ,
184+ error : 'No coverage data found' ,
185+ } ;
156186 }
157187
158188 // If the global setting for showing output is on, show the output
@@ -168,8 +198,10 @@ async function parseKaniCoverageOutput(stdout: string): Promise<any | undefined>
168198 terminal . show ( ) ;
169199 }
170200
171- // No command found from Kani
172- return coverageMap ;
201+ return {
202+ statusCode : 0 ,
203+ coverage : coverageMap ,
204+ } ;
173205}
174206
175207// Class representing the Renderer that handles rendering coverage highlights in the editor.
@@ -180,136 +212,49 @@ export class CoverageRenderer {
180212 }
181213
182214 /**
183- * Renders coverage highlights for multiple files.
184- * @param editors - An array of text editor files to render coverage highlights for.
185- * @param coverageMap - A map containing coverage data for each file.
215+ * Clears all existing coverage highlights.
186216 */
187- public renderInterface (
188- editors : readonly vscode . TextEditor [ ] ,
189- coverageMap : Map < string , Map < any , any > > ,
190- ) : void {
191- editors . forEach ( ( editor ) => {
192- // If coverageMap is empty, de-highlight the files
193- if ( coverageMap . size == 0 ) {
194- const coverageLines : CoverageLines = {
195- full : [ ] ,
196- none : [ ] ,
197- partial : [ ] ,
198- } ;
199-
200- this . renderHighlight ( editor , coverageLines ) ;
201- return ;
202- }
203-
204- // Fetch the coverage data for a file from the coverageMap.
205- const relativePath = fullToRelativePath ( editor . document . fileName ) ;
206- const fileMap = coverageMap . get ( relativePath ) ! ;
207-
208- const coverageLines = this . convertMapToLines ( fileMap ) ;
209- this . renderHighlight ( editor , coverageLines ) ;
210- return ;
217+ public clearAllHighlights ( ) : void {
218+ vscode . window . visibleTextEditors . forEach ( ( editor ) => {
219+ this . renderHighlight ( editor , new Map ( ) ) ;
211220 } ) ;
212221 }
213222
214- public convertMapToLines ( coverageMap : Map < any , any > ) : CoverageLines {
215- // Parse this into coverageLines i.e if the status of the range in coverageMap is COVERED, add it to full or else if it is UNCOVERED, add it to None
216- const coverageLines : CoverageLines = {
217- full : [ ] ,
218- none : [ ] ,
219- partial : [ ] ,
220- } ;
221-
222- for ( const [ range , status ] of coverageMap ) {
223- if ( status === 'COVERED' ) {
224- coverageLines . full . push ( range ) ;
225- } else if ( status === 'UNCOVERED' ) {
226- coverageLines . none . push ( range ) ;
227- }
228- }
229-
230- return coverageLines ;
223+ /**
224+ * Renders coverage highlights for multiple files.
225+ * @param editors - An array of text editor files to render coverage highlights for.
226+ * @param coverageMap - A map containing coverage data for each file.
227+ */
228+ public renderInterface ( editors : readonly vscode . TextEditor [ ] , coverageMap : CoverageMap ) : void {
229+ this . clearAllHighlights ( ) ;
230+ editors . forEach ( ( editor ) => this . renderInterfaceForFile ( editor , coverageMap ) ) ;
231231 }
232232
233233 /**
234234 * Renders coverage highlights for a single text editor file.
235235 * @param editor - The text editor to render coverage highlights for.
236236 * @param coverageMap - A map containing coverage data for each file.
237237 */
238- public renderInterfaceForFile (
239- editor : vscode . TextEditor ,
240- coverageMap : Map < string , Map < any , string > > ,
241- ) : void {
242- if ( coverageMap . size == 0 ) {
243- const coverageLines : CoverageLines = {
244- full : [ ] ,
245- none : [ ] ,
246- partial : [ ] ,
247- } ;
248-
249- this . renderHighlight ( editor , coverageLines ) ;
250- }
251-
238+ public renderInterfaceForFile ( editor : vscode . TextEditor , coverageMap : CoverageMap ) : void {
252239 const relativePath = fullToRelativePath ( editor . document . fileName ) ;
253- const fileMap = coverageMap . get ( relativePath ) ! ;
254- if ( fileMap === undefined ) {
255- return ;
256- }
257-
258- const coverageLines = this . convertMapToLines ( fileMap ) ;
259- this . renderHighlight ( editor , coverageLines ) ;
240+ const fileMap = coverageMap . get ( relativePath ) ;
241+ this . renderHighlight ( editor , fileMap || new Map ( ) ) ;
260242 }
261243
262244 /**
263- * Creates coverage highlights for a given text document based on the coverageFileMap.
264- * @param doc - The text document for which coverage highlights are to be created.
265- * @param coverageFileMap - A map containing coverage status for each line number.
266- * @returns An object containing coverage highlights categorized as 'full', 'partial', and 'none'.
245+ * Applies the coverage highlights to the given text editor.
246+ * @param editor - The text editor to apply the coverage highlights to.
247+ * @param fileCoverageMap - A map containing coverage highlights for the file.
267248 */
268- public createCoverage (
269- doc : vscode . TextDocument ,
270- coverageFileMap : Map < number , string > ,
271- ) : CoverageLines {
272- const coverageLines : CoverageLines = {
273- full : [ ] ,
274- none : [ ] ,
275- partial : [ ] ,
276- } ;
249+ public renderHighlight ( editor : vscode . TextEditor , fileCoverageMap : FileCoverageMap ) : void {
250+ const coveredRanges : vscode . Range [ ] = [ ] ;
251+ const uncoveredRanges : vscode . Range [ ] = [ ] ;
277252
278- for ( let lineNum = 1 ; lineNum <= doc . lineCount ; lineNum ++ ) {
279- const line = doc . lineAt ( lineNum - 1 ) ;
280- const status = coverageFileMap . get ( lineNum ) ;
281-
282- if ( status === undefined ) {
283- continue ;
284- }
285-
286- const range = new vscode . Range ( line . range . start , line . range . end ) ;
287- switch ( status ) {
288- case 'FULL' :
289- coverageLines . full . push ( range ) ;
290- break ;
291- case 'PARTIAL' :
292- coverageLines . partial . push ( range ) ;
293- break ;
294- case 'NONE' :
295- coverageLines . none . push ( range ) ;
296- break ;
297- default :
298- break ;
299- }
253+ for ( const [ range , status ] of fileCoverageMap ) {
254+ ( status === CoverageStatus . COVERED ? coveredRanges : uncoveredRanges ) . push ( range ) ;
300255 }
301256
302- return coverageLines ;
303- }
304-
305- /**
306- * Applies the coverage highlights to the given text editor.
307- * @param editor - The text editor to apply the coverage highlights to.
308- * @param coverageLinesInfo - An object containing coverage highlights categorized as 'full', 'partial', and 'none'.
309- */
310- public renderHighlight ( editor : vscode . TextEditor , coverageLinesInfo : CoverageLines ) : void {
311- editor . setDecorations ( this . configStore . covered , coverageLinesInfo . full ) ;
312- editor . setDecorations ( this . configStore . partialcovered , coverageLinesInfo . partial ) ;
313- editor . setDecorations ( this . configStore . uncovered , coverageLinesInfo . none ) ;
257+ editor . setDecorations ( this . configStore . covered , coveredRanges ) ;
258+ editor . setDecorations ( this . configStore . uncovered , uncoveredRanges ) ;
314259 }
315260}
0 commit comments