Skip to content
This repository was archived by the owner on Apr 11, 2019. It is now read-only.

Commit a944c05

Browse files
committed
[WIP] Fetch path coverage, improve how file viewer shows directories.
1 parent aba03f0 commit a944c05

File tree

3 files changed

+119
-33
lines changed

3 files changed

+119
-33
lines changed

src/containers/fileViewer.jsx

Lines changed: 96 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import React, { Component } from 'react';
22

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';
47
import { rawFile } from '../utils/hg';
58
import { TestsSideViewer, CoveragePercentageViewer } from '../components/fileViewer';
69
import { HORIZONTAL_ELLIPSIS, HEAVY_CHECKMARK } from '../utils/symbol';
710
import hash from '../utils/hash';
811

12+
const { low, medium, high } = settings.COVERAGE_THRESHOLDS;
13+
914
// FileViewer loads a raw file for a given revision from Mozilla's hg web.
1015
// It uses test coverage information from Active Data to show coverage
1116
// for runnable lines.
@@ -28,7 +33,8 @@ export default class FileViewerContainer extends Component {
2833
// Reset the state and fetch new data
2934
const newState = {
3035
appErr: undefined,
31-
coverage: undefined,
36+
pathCoverage: undefined,
37+
sourceCoverage: undefined,
3238
parsedFile: undefined,
3339
};
3440
// eslint-disable-next-line react/no-did-update-set-state
@@ -45,52 +51,112 @@ export default class FileViewerContainer extends Component {
4551
}
4652
}
4753

48-
fetchData(repoPath = 'mozilla-central') {
54+
async fetchData(repoPath = 'mozilla-central') {
4955
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)" });
5258
return;
5359
}
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+
/*
64+
http://localhost:5000/#/file?revision=ff0d31843793&path=media/
65+
{
66+
"children": [
67+
{
68+
"coverage": 0.06595940668768085,
69+
"nb": 42,
70+
"path": "media/",
71+
"type": "directory"
72+
}
73+
],
74+
"type": "directory"
75+
}
76+
77+
http://localhost:5000/#/file?revision=ff0d31843793&path=media/webrtc/trunk/webrtc/base/java/src/org/webrtc/ThreadUtils.java
78+
{
79+
"coverage": 0.09302325581395347,
80+
"nb": 7,
81+
"path": "media/webrtc/trunk/webrtc/base/java/src/org/webrtc/ThreadUtils.java",
82+
"type": "file"
83+
}
84+
*/
85+
console.log('backend coverage', data);
86+
this.setState({ pathCoverage: data });
5787
};
58-
// Get coverage from ActiveData
59-
const coverageData = async () => {
60-
const { data } = await fileRevisionWithActiveData(revision, path, repoPath);
61-
this.setState({ coverage: fileRevisionCoverageSummary(data) });
88+
// Get detailed source coverage from ActiveData
89+
const fileCoverage = async () => {
90+
const { data } = await sourceCoverageFromActiveData(revision, path, repoPath);
91+
this.setState({ sourceCoverage: sourceCoverageSummary(data) });
92+
};
93+
// Get raw source code from hg
94+
const fileSource = async () => {
95+
const parsedFile = await rawFile(revision, path, repoPath);
96+
this.setState({ parsedFile });
6297
};
6398
// Fetch source code and coverage in parallel
6499
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-
});
100+
await Promise.all([pathCoverage(), fileCoverage(), fileSource()]);
74101
} catch (error) {
75-
this.setState({ appErr: `${error.name}: ${error.message}` });
102+
console.error(error);
103+
if ((error instanceof RangeError) && (error.message === 'Revision number too short')) {
104+
this.setState({ appErr: 'Revision number is too short. Unable to fetch data.' });
105+
} else {
106+
this.setState({ appErr: `${error.name}: ${error.message}` });
107+
}
76108
throw error;
77109
}
78110
}
79111

80112
render() {
113+
const { revision, path } = this.props;
81114
const {
82-
parsedFile, coverage, selectedLine, appErr,
115+
pathCoverage, sourceCoverage, parsedFile, selectedLine, appErr,
83116
} = this.state;
84117

85118
return (
86119
<div>
87120
<div className="file-view">
88121
<FileViewerMeta {...this.props} {...this.state} />
89-
{ !appErr && (parsedFile) &&
122+
{ pathCoverage && pathCoverage.type === 'directory' &&
123+
<table className="changeset-viewer">
124+
<tbody>
125+
<tr>
126+
<th>File</th>
127+
<th>Coverage summary</th>
128+
</tr>
129+
{pathCoverage.children.map((file) => {
130+
const fileName = file.path.replace(new RegExp(`^${path}`, 'g'), '');
131+
const coveragePercent = Math.round(100 * file.coverage);
132+
let summaryClassName = high.className;
133+
if (coveragePercent < medium.threshold) {
134+
summaryClassName =
135+
(coveragePercent < low.threshold ? low.className : medium.className);
136+
}
137+
const href =
138+
`/#/file?revision=${revision}&path=${path}${fileName}`;
139+
return (
140+
<tr className="changeset" key={fileName}>
141+
<td className="changeset-author">
142+
<a href={href}>
143+
{file.type === 'directory' ? <FolderOutlineIcon /> : <FileOutlineIcon />}
144+
<span className="changeset-eIcon-align">{fileName}</span>
145+
</a>
146+
</td>
147+
<td className={`changeset-summary ${summaryClassName}`}>{coveragePercent}%</td>
148+
</tr>
149+
);
150+
})}
151+
</tbody>
152+
</table> }
153+
{ pathCoverage && pathCoverage.type === 'file' &&
154+
<div style={{ textAlign: 'center' }}>Coverage: {Math.round(pathCoverage.coverage * 100)}%</div> }
155+
{ parsedFile &&
90156
<FileViewer {...this.state} onLineClick={this.setSelectedLine} /> }
91157
</div>
92158
<TestsSideViewer
93-
coverage={coverage}
159+
coverage={sourceCoverage}
94160
lineNumber={selectedLine}
95161
/>
96162
</div>
@@ -100,7 +166,7 @@ export default class FileViewerContainer extends Component {
100166

101167
// This component renders each line of the file with its line number
102168
const FileViewer = ({
103-
parsedFile, coverage, selectedLine, onLineClick,
169+
parsedFile, sourceCoverage, selectedLine, onLineClick,
104170
}) => (
105171
<table className="file-view-table">
106172
<tbody>
@@ -111,7 +177,7 @@ const FileViewer = ({
111177
key={uniqueId}
112178
lineNumber={lineNumber + 1}
113179
text={text}
114-
coverage={coverage}
180+
coverage={sourceCoverage}
115181
selectedLine={selectedLine}
116182
onLineClick={onLineClick}
117183
/>
@@ -156,7 +222,7 @@ const Line = ({
156222

157223
// This component contains metadata of the file
158224
const FileViewerMeta = ({
159-
revision, path, appErr, parsedFile, coverage,
225+
revision, path, appErr, parsedFile, sourceCoverage,
160226
}) => {
161227
const showStatus = (label, data) => (
162228
<li className="file-meta-li">
@@ -168,11 +234,11 @@ const FileViewerMeta = ({
168234
<div>
169235
<div className="file-meta-center">
170236
<div className="file-meta-title">File Coverage</div>
171-
{ (coverage) && <CoveragePercentageViewer coverage={coverage} /> }
237+
{ (sourceCoverage) && <CoveragePercentageViewer coverage={sourceCoverage} /> }
172238
<div className="file-meta-status">
173239
<ul className="file-meta-ul">
174240
{ showStatus('Source code', parsedFile) }
175-
{ showStatus('Coverage', coverage) }
241+
{ showStatus('Coverage', sourceCoverage) }
176242
</ul>
177243
</div>
178244
</div>

src/settings.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const settings = {
88
ENABLED: process.env.ENABLE_CACHE === 'true' || process.env.NODE_ENV === 'production',
99
},
1010
CCOV_BACKEND: 'https://coverage.moz.tools',
11+
CCOV_STAGING_BACKEND: 'https://coverage.staging.moz.tools',
1112
CODECOV_GECKO_DEV: 'https://codecov.io/gh/mozilla/gecko-dev',
1213
COVERAGE_THRESHOLDS: {
1314
low: {

src/utils/coverage.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { jsonFetch, jsonPost, plainFetch } from './fetch';
44
import { queryCacheWithFallback, saveInCache } from './localCache';
55

66
const {
7-
ACTIVE_DATA, CCOV_BACKEND, CODECOV_GECKO_DEV, GH_GECKO_DEV,
7+
ACTIVE_DATA, CCOV_BACKEND, CCOV_STAGING_BACKEND, CODECOV_GECKO_DEV, GH_GECKO_DEV,
88
} = settings;
99

1010
const { INTERNAL_ERROR, PENDING } = settings.STRINGS;
@@ -16,6 +16,9 @@ export const ccovBackendUrl = node => (`${CCOV_BACKEND}/coverage/changeset/${nod
1616
const queryChangesetCoverage = node =>
1717
plainFetch(`${CCOV_BACKEND}/coverage/changeset/${node}`);
1818

19+
export const queryPathCoverage = (path, revision) =>
20+
jsonFetch(`${CCOV_STAGING_BACKEND}/v2/path?path=${path}${revision ? `&changeset=${revision}` : ''}`);
21+
1922
const queryActiveData = body =>
2023
jsonPost(`${ACTIVE_DATA}/query`, body);
2124

@@ -44,7 +47,7 @@ const coverageStatistics = (coverage) => {
4447
};
4548

4649
// get the coverage summary for a particular revision and file
47-
export const fileRevisionCoverageSummary = (coverage) => {
50+
export const sourceCoverageSummary = (coverage) => {
4851
const s = {
4952
coveredLines: [],
5053
uncoveredLines: [],
@@ -227,7 +230,7 @@ export const getPendingCoverage = async (changesetsCoverage) => {
227230
};
228231
};
229232

230-
export const fileRevisionWithActiveData = async (revision, path, repoPath) => {
233+
export const sourceCoverageFromActiveData = async (revision, path, repoPath) => {
231234
try {
232235
if (revision.length < settings.MIN_REVISION_LENGTH) {
233236
throw new RangeError('Revision number too short');
@@ -252,3 +255,19 @@ export const fileRevisionWithActiveData = async (revision, path, repoPath) => {
252255
throw new Error(`Failed to fetch data for revision: ${revision}, path: ${path}\n${e}`);
253256
}
254257
};
258+
259+
export const pathCoverageFromBackend = async (revision, path, repoPath) => {
260+
try {
261+
if (revision.length < settings.MIN_REVISION_LENGTH) {
262+
throw new RangeError('Revision number too short');
263+
}
264+
const data = await queryPathCoverage(path /* , revision */);
265+
if (data.status && data.staus !== 200) {
266+
throw new Error(`HTTP response ${data.status}`);
267+
}
268+
return data;
269+
} catch (error) {
270+
// FIXME: If you start using this method, please replace the `console.error()` with a `throw`.
271+
console.error(new Error(`Failed to fetch data for revision: ${revision}, path: ${path}\n${error}`));
272+
}
273+
};

0 commit comments

Comments
 (0)