1
1
import React , { Component } from 'react' ;
2
2
3
- import { fileRevisionCoverageSummary , fileRevisionWithActiveData } from '../utils/coverage' ;
3
+ import FileOutlineIcon from 'mdi-react/FileOutlineIcon' ;
4
+ import FolderOutlineIcon from 'mdi-react/FolderOutlineIcon' ;
5
+ import settings from '../settings' ;
6
+ import { sourceCoverageSummary , sourceCoverageFromActiveData , pathCoverageFromBackend } from '../utils/coverage' ;
4
7
import { rawFile } from '../utils/hg' ;
5
8
import { TestsSideViewer , CoveragePercentageViewer } from '../components/fileViewer' ;
6
9
import { HORIZONTAL_ELLIPSIS , HEAVY_CHECKMARK } from '../utils/symbol' ;
7
10
import hash from '../utils/hash' ;
8
11
12
+ const { low, medium, high } = settings . COVERAGE_THRESHOLDS ;
13
+
9
14
// FileViewer loads a raw file for a given revision from Mozilla's hg web.
10
15
// It uses test coverage information from Active Data to show coverage
11
16
// for runnable lines.
@@ -28,7 +33,8 @@ export default class FileViewerContainer extends Component {
28
33
// Reset the state and fetch new data
29
34
const newState = {
30
35
appErr : undefined ,
31
- coverage : undefined ,
36
+ pathCoverage : undefined ,
37
+ sourceCoverage : undefined ,
32
38
parsedFile : undefined ,
33
39
} ;
34
40
// eslint-disable-next-line react/no-did-update-set-state
@@ -45,52 +51,89 @@ export default class FileViewerContainer extends Component {
45
51
}
46
52
}
47
53
48
- fetchData ( repoPath = 'mozilla-central' ) {
54
+ async fetchData ( repoPath = 'mozilla-central' ) {
49
55
const { revision, path } = this . props ;
50
- if ( ! revision || ! path ) {
51
- this . setState ( { appErr : "Undefined URL query ('revision', 'path' fields are required)" } ) ;
56
+ if ( ! revision ) {
57
+ this . setState ( { appErr : "Undefined URL query (field 'revision' is required)" } ) ;
52
58
return ;
53
59
}
54
- // Get source code from hg
55
- const fileSource = async ( ) => {
56
- this . setState ( { parsedFile : ( await rawFile ( revision , path , repoPath ) ) } ) ;
60
+ // Get overall path coverage from backend
61
+ const pathCoverage = async ( ) => {
62
+ const data = await pathCoverageFromBackend ( revision , path , repoPath ) ;
63
+ this . setState ( { pathCoverage : data } ) ;
57
64
} ;
58
- // Get coverage from ActiveData
59
- const coverageData = async ( ) => {
60
- const { data } = await fileRevisionWithActiveData ( revision , path , repoPath ) ;
61
- this . setState ( { coverage : fileRevisionCoverageSummary ( data ) } ) ;
65
+ // Get detailed source coverage from ActiveData
66
+ const fileCoverage = async ( ) => {
67
+ const { data } = await sourceCoverageFromActiveData ( revision , path , repoPath ) ;
68
+ this . setState ( { sourceCoverage : sourceCoverageSummary ( data ) } ) ;
69
+ } ;
70
+ // Get raw source code from hg
71
+ const fileSource = async ( ) => {
72
+ const parsedFile = await rawFile ( revision , path , repoPath ) ;
73
+ this . setState ( { parsedFile } ) ;
62
74
} ;
63
75
// Fetch source code and coverage in parallel
64
76
try {
65
- Promise . all ( [ fileSource ( ) , coverageData ( ) ] )
66
- . catch ( ( e ) => {
67
- if ( ( e instanceof RangeError ) && ( e . message === 'Revision number too short' ) ) {
68
- this . setState ( { appErr : 'Revision number is too short. Unable to fetch tests.' } ) ;
69
- } else {
70
- this . setState ( { appErr : `${ e . name } : ${ e . message } ` } ) ;
71
- }
72
- throw e ;
73
- } ) ;
77
+ await Promise . all ( [ pathCoverage ( ) , fileCoverage ( ) , fileSource ( ) ] ) ;
74
78
} catch ( error ) {
75
- this . setState ( { appErr : `${ error . name } : ${ error . message } ` } ) ;
79
+ console . error ( error ) ;
80
+ if ( ( error instanceof RangeError ) && ( error . message === 'Revision number too short' ) ) {
81
+ this . setState ( { appErr : 'Revision number is too short. Unable to fetch data.' } ) ;
82
+ } else {
83
+ this . setState ( { appErr : `${ error . name } : ${ error . message } ` } ) ;
84
+ }
76
85
throw error ;
77
86
}
78
87
}
79
88
80
89
render ( ) {
90
+ const { revision, path } = this . props ;
81
91
const {
82
- parsedFile , coverage , selectedLine, appErr,
92
+ pathCoverage , sourceCoverage , parsedFile , selectedLine, appErr,
83
93
} = this . state ;
84
94
85
95
return (
86
96
< div >
87
97
< div className = "file-view" >
88
98
< FileViewerMeta { ...this . props } { ...this . state } />
89
- { ! appErr && ( parsedFile ) &&
99
+ { pathCoverage && pathCoverage . type === 'directory' &&
100
+ < table className = "changeset-viewer" >
101
+ < tbody >
102
+ < tr >
103
+ < th > File</ th >
104
+ < th > Coverage summary</ th >
105
+ </ tr >
106
+ { pathCoverage . children . map ( ( file ) => {
107
+ const fileName = file . path . replace ( new RegExp ( `^${ path } ` , 'g' ) , '' ) ;
108
+ const coveragePercent = Math . round ( 100 * file . coverage ) ;
109
+ let summaryClassName = high . className ;
110
+ if ( coveragePercent < medium . threshold ) {
111
+ summaryClassName =
112
+ ( coveragePercent < low . threshold ? low . className : medium . className ) ;
113
+ }
114
+ const href =
115
+ `/#/file?revision=${ revision } &path=${ path } ${ fileName } ` ;
116
+ return (
117
+ < tr className = "changeset" key = { fileName } >
118
+ < td className = "changeset-author" >
119
+ < a href = { href } >
120
+ { file . type === 'directory' ? < FolderOutlineIcon /> : < FileOutlineIcon /> }
121
+ < span className = "changeset-eIcon-align" > { fileName } </ span >
122
+ </ a >
123
+ </ td >
124
+ < td className = { `changeset-summary ${ summaryClassName } ` } > { coveragePercent } %</ td >
125
+ </ tr >
126
+ ) ;
127
+ } ) }
128
+ </ tbody >
129
+ </ table > }
130
+ { pathCoverage && pathCoverage . type === 'file' &&
131
+ < div style = { { textAlign : 'center' } } > Coverage: { Math . round ( pathCoverage . coverage * 100 ) } %</ div > }
132
+ { parsedFile &&
90
133
< FileViewer { ...this . state } onLineClick = { this . setSelectedLine } /> }
91
134
</ div >
92
135
< TestsSideViewer
93
- coverage = { coverage }
136
+ coverage = { sourceCoverage }
94
137
lineNumber = { selectedLine }
95
138
/>
96
139
</ div >
@@ -100,7 +143,7 @@ export default class FileViewerContainer extends Component {
100
143
101
144
// This component renders each line of the file with its line number
102
145
const FileViewer = ( {
103
- parsedFile, coverage , selectedLine, onLineClick,
146
+ parsedFile, sourceCoverage , selectedLine, onLineClick,
104
147
} ) => (
105
148
< table className = "file-view-table" >
106
149
< tbody >
@@ -111,7 +154,7 @@ const FileViewer = ({
111
154
key = { uniqueId }
112
155
lineNumber = { lineNumber + 1 }
113
156
text = { text }
114
- coverage = { coverage }
157
+ coverage = { sourceCoverage }
115
158
selectedLine = { selectedLine }
116
159
onLineClick = { onLineClick }
117
160
/>
@@ -156,7 +199,7 @@ const Line = ({
156
199
157
200
// This component contains metadata of the file
158
201
const FileViewerMeta = ( {
159
- revision, path, appErr, parsedFile, coverage ,
202
+ revision, path, appErr, parsedFile, sourceCoverage ,
160
203
} ) => {
161
204
const showStatus = ( label , data ) => (
162
205
< li className = "file-meta-li" >
@@ -168,11 +211,11 @@ const FileViewerMeta = ({
168
211
< div >
169
212
< div className = "file-meta-center" >
170
213
< div className = "file-meta-title" > File Coverage</ div >
171
- { ( coverage ) && < CoveragePercentageViewer coverage = { coverage } /> }
214
+ { ( sourceCoverage ) && < CoveragePercentageViewer coverage = { sourceCoverage } /> }
172
215
< div className = "file-meta-status" >
173
216
< ul className = "file-meta-ul" >
174
217
{ showStatus ( 'Source code' , parsedFile ) }
175
- { showStatus ( 'Coverage' , coverage ) }
218
+ { showStatus ( 'Coverage' , sourceCoverage ) }
176
219
</ ul >
177
220
</ div >
178
221
</ div >
0 commit comments