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

Commit 826f45b

Browse files
committed
[WIP] Add a directory coverage viewer.
1 parent aba03f0 commit 826f45b

File tree

3 files changed

+199
-0
lines changed

3 files changed

+199
-0
lines changed

src/components/routes.jsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Route } from 'react-router-dom';
33

44
import AppDisclaimer from './disclaimer';
55
import DiffViewerContainer from '../containers/diffViewer';
6+
import DirectoryViewerContainer from '../containers/directoryViewer';
67
import FileViewerContainer from '../containers/fileViewer';
78
import GitHubRibbon from './githubRibbon';
89
import SummaryContainer from '../containers/summary';
@@ -31,6 +32,15 @@ const Routes = () => (
3132
path="/file"
3233
render={({ location }) => {
3334
const { path = '', revision } = parse(location.search);
35+
// FIXME: There must be a better way to know if this is a directory
36+
if (path[path.length - 1] === '/') {
37+
return (
38+
<DirectoryViewerContainer
39+
revision={revision}
40+
path={path.startsWith('/') ? path.slice(1) : path}
41+
/>
42+
);
43+
}
3444
// Remove beginning '/' in the path parameter to fetch from source,
3545
// makes both path=/path AND path=path acceptable in the URL query
3646
// Ex. "path=/accessible/atk/Platform.cpp" AND "path=accessible/atk/Platform.cpp"

src/containers/directoryViewer.jsx

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import React, { Component } from 'react';
2+
3+
import FileOutlineIcon from 'mdi-react/FileOutlineIcon';
4+
import FolderOutlineIcon from 'mdi-react/FolderOutlineIcon';
5+
import { directoryRevisionWithActiveData } from '../utils/coverage';
6+
import settings from '../settings';
7+
import { HORIZONTAL_ELLIPSIS, HEAVY_CHECKMARK } from '../utils/symbol';
8+
9+
const { low, medium, high } = settings.COVERAGE_THRESHOLDS;
10+
11+
// DirectoryViewer loads a directory for a given revision from Mozilla's hg web.
12+
// It uses test coverage information from Active Data to show coverage
13+
// per sub-directory or file.
14+
export default class DirectoryViewerContainer extends Component {
15+
constructor(props) {
16+
super(props);
17+
this.state = {};
18+
}
19+
20+
componentDidMount() {
21+
this.fetchData();
22+
}
23+
24+
componentDidUpdate(prevProps) {
25+
const { revision, path } = this.props;
26+
if (revision === prevProps.revision && path === prevProps.path) {
27+
return;
28+
}
29+
// Reset the state and fetch new data
30+
const newState = {
31+
appErr: undefined,
32+
coverage: undefined,
33+
};
34+
// eslint-disable-next-line react/no-did-update-set-state
35+
this.setState(newState);
36+
this.fetchData();
37+
}
38+
39+
async fetchData(repoPath = 'mozilla-central') {
40+
const { revision, path } = this.props;
41+
if (!revision) {
42+
this.setState({ appErr: "Undefined URL query ('revision' field is required)" });
43+
return;
44+
}
45+
// Get coverage from ActiveData
46+
try {
47+
let { data: coverage } = await directoryRevisionWithActiveData(revision, path, repoPath);
48+
49+
// Filter weird files from the coverage results (e.g. 'chrome:', 'data:...', 'NONE', etc)
50+
coverage = coverage.filter(([fileName]) => !/^(\/|chrome:|data:|obj-firefox|resource:|NONE)/.test(fileName || '/'));
51+
52+
// Group by type (directory/file), sort alphabetically
53+
coverage = coverage.sort(([fileNameA, isDirectoryA], [fileNameB, isDirectoryB]) => {
54+
if (isDirectoryA !== isDirectoryB) {
55+
return isDirectoryB - isDirectoryA;
56+
}
57+
if (fileNameA < fileNameB) {
58+
return -1;
59+
}
60+
return 1;
61+
});
62+
63+
this.setState({ coverage });
64+
} catch (error) {
65+
this.setState({ appErr: `${error.name}: ${error.message}` });
66+
throw error;
67+
}
68+
}
69+
70+
render() {
71+
const {
72+
coverage, appErr,
73+
} = this.state;
74+
75+
return (
76+
<div>
77+
<div className="file-view">
78+
<DirectoryViewerMeta {...this.props} {...this.state} />
79+
{ !appErr && coverage &&
80+
<table className="changeset-viewer">
81+
<tbody>
82+
<tr>
83+
<th>File</th>
84+
<th>Coverage summary</th>
85+
</tr>
86+
{coverage.map((file) => {
87+
const [fileName, isDirectory, totalCovered, totalUncovered] = file;
88+
const coveragePercent =
89+
Math.round(100 * (totalCovered / (totalCovered + totalUncovered)));
90+
let summaryClassName = high.className;
91+
if (coveragePercent < medium.threshold) {
92+
summaryClassName =
93+
(coveragePercent < low.threshold ? low.className : medium.className);
94+
}
95+
return (
96+
<tr className="changeset" key={fileName}>
97+
<td className="changeset-author">
98+
{isDirectory ? <FolderOutlineIcon /> : <FileOutlineIcon />}
99+
<span className="changeset-eIcon-align">{fileName}</span>
100+
</td>
101+
<td className={`changeset-summary ${summaryClassName}`}>{coveragePercent}%</td>
102+
</tr>
103+
);
104+
})}
105+
</tbody>
106+
</table>
107+
}
108+
</div>
109+
</div>
110+
);
111+
}
112+
}
113+
114+
// This component contains metadata of the file
115+
const DirectoryViewerMeta = ({
116+
revision, path, appErr, coverage,
117+
}) => {
118+
const showStatus = (label, data) => (
119+
<li className="file-meta-li">
120+
{label}: {(data) ? HEAVY_CHECKMARK : HORIZONTAL_ELLIPSIS}
121+
</li>
122+
);
123+
124+
return (
125+
<div>
126+
<div className="file-meta-center">
127+
<div className="file-meta-title">Directory Coverage</div>
128+
<div className="file-meta-status">
129+
<ul className="file-meta-ul">
130+
{ showStatus('Coverage', coverage) }
131+
</ul>
132+
</div>
133+
</div>
134+
{appErr && <span className="error-message">{appErr}</span>}
135+
136+
<div className="file-summary">
137+
<span className="file-path">{path}</span>
138+
</div>
139+
<div className="file-meta-revision">revision number: {revision}</div>
140+
</div>
141+
);
142+
};
143+

src/utils/coverage.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,3 +252,49 @@ export const fileRevisionWithActiveData = async (revision, path, repoPath) => {
252252
throw new Error(`Failed to fetch data for revision: ${revision}, path: ${path}\n${e}`);
253253
}
254254
};
255+
256+
export const directoryRevisionWithActiveData = async (revision, path /* , repoPath */) => {
257+
try {
258+
if (revision.length < settings.MIN_REVISION_LENGTH) {
259+
throw new RangeError('Revision number too short');
260+
}
261+
const res = await queryActiveData({
262+
from: 'coverage',
263+
where: {
264+
and: [
265+
{ prefix: { 'source.file.name': path } },
266+
{ eq: { 'repo.changeset.id12': revision.slice(0, 12) } },
267+
],
268+
},
269+
select: [
270+
{
271+
aggregate: 'min',
272+
name: 'is_dir',
273+
value: {
274+
when: { start: path.length, find: { 'source.file.name': '/' } },
275+
then: 1,
276+
else: 0,
277+
},
278+
},
279+
{ aggregate: 'sum', name: 'total_covered', value: 'source.file.total_covered' },
280+
{ aggregate: 'sum', name: 'total_uncovered', value: 'source.file.total_uncovered' },
281+
],
282+
groupby: [{
283+
name: 'subdir',
284+
value: {
285+
when: { start: path.length, find: { 'source.file.name': '/' } },
286+
then: { between: { 'source.file.name': [path.length, '/'] } },
287+
else: { not_left: { 'source.file.name': path.length } },
288+
},
289+
}],
290+
limit: 1000,
291+
});
292+
if (res.status !== 200) {
293+
throw new Error();
294+
}
295+
return res.json();
296+
} catch (e) {
297+
console.error(`Failed to fetch data for revision: ${revision}, path: ${path}\n${e}`);
298+
throw e;
299+
}
300+
};

0 commit comments

Comments
 (0)