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

Commit 6f34e23

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

File tree

3 files changed

+96
-33
lines changed

3 files changed

+96
-33
lines changed

src/containers/fileViewer.jsx

Lines changed: 73 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,89 @@ 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+
this.setState({ pathCoverage: data });
5764
};
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 });
6274
};
6375
// Fetch source code and coverage in parallel
6476
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()]);
7478
} 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+
}
7685
throw error;
7786
}
7887
}
7988

8089
render() {
90+
const { revision, path } = this.props;
8191
const {
82-
parsedFile, coverage, selectedLine, appErr,
92+
pathCoverage, sourceCoverage, parsedFile, selectedLine, appErr,
8393
} = this.state;
8494

8595
return (
8696
<div>
8797
<div className="file-view">
8898
<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 &&
90133
<FileViewer {...this.state} onLineClick={this.setSelectedLine} /> }
91134
</div>
92135
<TestsSideViewer
93-
coverage={coverage}
136+
coverage={sourceCoverage}
94137
lineNumber={selectedLine}
95138
/>
96139
</div>
@@ -100,7 +143,7 @@ export default class FileViewerContainer extends Component {
100143

101144
// This component renders each line of the file with its line number
102145
const FileViewer = ({
103-
parsedFile, coverage, selectedLine, onLineClick,
146+
parsedFile, sourceCoverage, selectedLine, onLineClick,
104147
}) => (
105148
<table className="file-view-table">
106149
<tbody>
@@ -111,7 +154,7 @@ const FileViewer = ({
111154
key={uniqueId}
112155
lineNumber={lineNumber + 1}
113156
text={text}
114-
coverage={coverage}
157+
coverage={sourceCoverage}
115158
selectedLine={selectedLine}
116159
onLineClick={onLineClick}
117160
/>
@@ -156,7 +199,7 @@ const Line = ({
156199

157200
// This component contains metadata of the file
158201
const FileViewerMeta = ({
159-
revision, path, appErr, parsedFile, coverage,
202+
revision, path, appErr, parsedFile, sourceCoverage,
160203
}) => {
161204
const showStatus = (label, data) => (
162205
<li className="file-meta-li">
@@ -168,11 +211,11 @@ const FileViewerMeta = ({
168211
<div>
169212
<div className="file-meta-center">
170213
<div className="file-meta-title">File Coverage</div>
171-
{ (coverage) && <CoveragePercentageViewer coverage={coverage} /> }
214+
{ (sourceCoverage) && <CoveragePercentageViewer coverage={sourceCoverage} /> }
172215
<div className="file-meta-status">
173216
<ul className="file-meta-ul">
174217
{ showStatus('Source code', parsedFile) }
175-
{ showStatus('Coverage', coverage) }
218+
{ showStatus('Coverage', sourceCoverage) }
176219
</ul>
177220
</div>
178221
</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)