refactor: DiffViewer to use same common code to generate code coverage status

This commit is contained in:
Armen Zambrano G 2017-11-08 09:08:52 -05:00 коммит произвёл Armen Zambrano
Родитель 83b50612ad
Коммит e8e5b5aad4
6 изменённых файлов: 192 добавлений и 250 удалений

Просмотреть файл

@ -1,9 +1,9 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import * as FetchAPI from '../utils/fetch_data';
import { csetWithCcovData } from '../utils/data';
import hash from '../utils/hash';
import { DiffMeta, CoverageMeta } from './diffviewermeta';
import * as FetchAPI from '../utils/fetch_data';
const parse = require('parse-diff');
@ -16,60 +16,63 @@ export default class DiffViewerContainer extends Component {
super(props);
this.state = {
appError: undefined,
coverage: undefined,
csetMeta: {
coverage: undefined,
},
parsedDiff: [],
};
}
componentDidMount() {
async componentDidMount() {
const { changeset } = this.props;
await this.fetchCsetData(changeset);
}
FetchAPI.getDiff(changeset)
.then(response =>
response.text())
.then(text =>
this.setState({ parsedDiff: parse(text) }))
.catch((error) => {
console.error(error);
this.setState({
appError: 'We did not manage to parse the diff correctly.',
});
async fetchCsetData(changeset) {
try {
this.setState({ csetMeta: await csetWithCcovData({ node: changeset }) });
} catch (error) {
console.error(error);
this.setState({
appError: 'There was an error fetching the code coverage data.',
});
}
FetchAPI.getChangesetCoverage(changeset)
.then(response =>
response.text())
.then(text =>
this.setState({ coverage: JSON.parse(text) }))
.catch((error) => {
console.error(error);
this.setState({
appError: 'There was an error fetching the code coverage data.',
});
try {
const text = await (await FetchAPI.getDiff(changeset)).text();
this.setState({ parsedDiff: parse(text) });
} catch (error) {
console.error(error);
this.setState({
appError: 'We did not manage to parse the diff correctly.',
});
}
}
render() {
const { changeset } = this.props;
const { appError, coverage, parsedDiff } = this.state;
const { appError, csetMeta, parsedDiff } = this.state;
return (
<DiffViewer
{...csetMeta}
appError={appError}
changeset={changeset}
coverage={coverage}
parsedDiff={parsedDiff}
/>
);
}
}
const DiffViewer = ({ appError, changeset, coverage, parsedDiff }) => (
<div className="page_body codecoverage-diffviewer">
<Link className="return-home" to="/">Return to main page</Link>
<DiffMeta changeset={changeset} />
<CoverageMeta coverage={coverage} parsedDiff={parsedDiff} />
const DiffViewer = ({ appError, coverage, node, parsedDiff, summary }) => (
<div className="codecoverage-diffviewer">
<div className="return-home"><Link to="/">Return to main page</Link></div>
{(coverage) &&
<CoverageMeta
{...coverage.parentMeta(coverage)}
{...coverage.diffMeta(node)}
coverage={coverage}
node={node}
summary={summary}
/>}
<span className="error_message">{appError}</span>
<br />
{parsedDiff.map(diffBlock =>
// We only push down the subset of code coverage data
// applicable to a file
@ -83,6 +86,30 @@ const DiffViewer = ({ appError, changeset, coverage, parsedDiff }) => (
</div>
);
const CoverageMeta = ({ ccovBackend, codecov, coverage, gh, hgRev, pushlog, summary }) => (
<div className="coverage-meta">
<div className="coverage-meta-row">
<span className="meta parent-meta-subtitle">Parent meta</span>
<span className="meta">
{`Current coverage: ${coverage.overall_cur.substring(0, 4)}%`}
</span>
<span className="meta meta-right">
<a href={pushlog} target="_blank">Push log</a>&nbsp;
<a href={gh} target="_blank">GitHub</a>&nbsp;
<a href={codecov} target="_blank">Codecov</a>
</span>
</div>
<div className="coverage-meta-row">
<span className="meta parent-meta-subtitle">Changeset meta</span>
<span className="meta">{summary}</span>
<span className="meta meta-right">
<a href={hgRev} target="_blank">Hg diff</a>&nbsp;
<a href={ccovBackend} target="_blank">Coverage backend</a>
</span>
</div>
</div>
);
/* A DiffLine contains all diff changes for a specific file */
const DiffFile = ({ coverage, diffBlock }) => {
// We try to see if the file modified shows up in the code

Просмотреть файл

@ -1,144 +0,0 @@
import React from 'react';
import * as FetchAPI from '../utils/fetch_data';
export const coverageSummary = (coverage) => {
const s = {
addedLines: 0,
coveredLines: 0,
};
if (coverage.diffs.length > 0) {
coverage.diffs.forEach((diff) => {
diff.changes.forEach((change) => {
if (change.coverage === 'Y') {
s.coveredLines += 1;
}
if (change.coverage !== '?') {
s.addedLines += 1;
}
});
s.percentage = (s.addedLines === 0) ?
undefined :
100 * (s.coveredLines / s.addedLines);
});
}
return s;
};
export const DiffMeta = ({ changeset }) => {
const hgRev = `${FetchAPI.hgHost}/mozilla-central/rev/${changeset}`;
const shipitUrl = `${FetchAPI.ccovBackend}/coverage/changeset/${changeset}`;
return (
<div className="diff-meta">
<span><b>Meta for diff ({changeset})</b></span>
<table>
<tbody>
<tr>
<td>
<a href={hgRev} target="_blank">Hg diff</a>&nbsp;-&nbsp;
<a href={shipitUrl} target="_blank">Shipit backend</a>
</td>
</tr>
</tbody>
</table>
</div>
);
};
export const determineCoverageMeta = (coverage, parsedDiff) => {
if (parsedDiff.length > 0) {
// addedLines: The total number of new executable lines
const addedLines = (coverage.diffs.length !== 0) ?
coverage.diffs.reduce((sum, file) => (
('changes' in file) ?
sum + file.changes.reduce((acumm, lineCov) => (
(lineCov.coverage === 'Y' || lineCov.coverage === 'N') ? acumm + 1 : acumm), 0) :
sum), 0) : 0;
// coveredLines: The total number of new covered lines
const coveredLines = (coverage.diffs.length !== 0) ?
coverage.diffs.reduce((sum, file) => (
('changes' in file) ?
sum + file.changes.reduce((acumm, lineCov) => (
(lineCov.coverage === 'Y') ? acumm + 1 : acumm), 0) :
sum), 0) : 0;
let netGain = ((addedLines !== 0) ?
(coveredLines / addedLines) * 100 : 0);
netGain = netGain.toPrecision(3);
return {
addedLines,
coveredLines,
netGain,
};
}
return undefined;
};
const NetCoverageContainer = ({ coverage, parsedDiff }) => {
const status = determineCoverageMeta(coverage, parsedDiff);
return (status) ? (
<NetCoverage {...status} />
) : null;
};
const NetCoverage = ({ addedLines, coveredLines, netGain }) => (
<div className="net-coverage-meta">
<span className="net-lines-coverage-change">
New lines coverage change: {netGain}%
</span>
{`Added lines: ${addedLines} / Covered lines: ${coveredLines}`}
</div>
);
const ParentMeta = ({ coverage }) => {
const pushlog = `https://hg.mozilla.org/mozilla-central/pushloghtml?changeset=${coverage.build_changeset}`;
const codecov = `https://codecov.io/gh/marco-c/gecko-dev/commit/${coverage.git_build_changeset}`;
const gh = `https://github.com/mozilla/gecko-dev/commit/${coverage.git_build_changeset}`;
return (
<div className="parent-meta">
<div>
<b>Meta for parent code coverage build ({coverage.build_changeset})</b>
</div>
<div>
{`Current coverage: ${coverage.overall_cur.substring(0, 4)}%`}&nbsp;-&nbsp;
<a href={pushlog} target="_blank">Push log</a>&nbsp;-&nbsp;
<a href={gh} target="_blank">GitHub</a>&nbsp;-&nbsp;
<a href={codecov} target="_blank">Codecov</a>
</div>
</div>
);
};
export const CoverageMeta = ({ coverage, parsedDiff }) => {
let errorMessage;
if (!coverage || coverage.error) {
if (!coverage) {
errorMessage = "We're waiting for coverage data from the backend.";
} else if (coverage.error) {
errorMessage = coverage.error;
}
return (
<div className="error_message">{errorMessage}</div>
);
}
if (coverage.diffs.length === 0) {
errorMessage = `There is no code coverage for this diff
or no new lines are being added to this diff.`;
}
return (
<div>
<ParentMeta coverage={coverage} />
{(parsedDiff.length > 0 && coverage) &&
<NetCoverageContainer
coverage={coverage}
parsedDiff={parsedDiff}
/>}
<div className="error_message">{errorMessage}</div>
</div>
);
};

Просмотреть файл

@ -2,13 +2,9 @@ import { Link } from 'react-router-dom';
import React, { Component } from 'react';
import ReactInterval from 'react-interval';
// XXX: Create module that fetches ccov data + diff data
import { coverageSummary } from './diffviewermeta';
import * as FetchAPI from '../utils/fetch_data';
import { arrayToMap, mapToArray } from '../utils/data';
import SETTINGS from '../settings';
const PENDING = 'Pending';
import { PENDING } from '../settings';
import { arrayToMap, csetWithCcovData, mapToArray } from '../utils/data';
const ChangesetInfo = ({ changeset }) => {
const { author, desc, hidden, linkify, node, summary, summaryClassName } = changeset;
@ -56,68 +52,6 @@ const PollingStatus = ({ pollingEnabled }) => (
</div>) : (null)
);
const coverageSummaryText = (coverage) => {
const { coverageThresholds } = SETTINGS;
const { low, medium, high } = coverageThresholds;
const s = coverageSummary(coverage);
const result = { className: 'no-change', text: 'No changes' };
if (typeof s.percentage !== 'undefined') {
const perc = parseInt(s.percentage, 10);
if (perc < low.threshold) {
result.className = low.className;
} else if (perc < medium.threshold) {
result.className = medium.className;
} else {
result.className = high.className;
}
result.text = `${perc}% - ${s.coveredLines} lines covered out of ${s.addedLines} added`;
}
return result;
};
const csetWithCcovData = async (cset) => {
if (!cset.node) {
throw Error(`No node for cset: ${cset}`);
}
const newCset = Object.assign({}, cset);
// XXX: fetch does not support timeouts. I would like to add a 5 second
// timeout rather than wait Heroku's default 30 second timeout. Specially
// since we're doing sequential fetches.
// XXX: Wrap fetch() in a Promise; add a setTimeout and call reject() if
// it goes off, otherwise resolve with the result of the fetch()
try {
const res = await FetchAPI.getChangesetCoverage(cset.node);
if (res.status === 202) {
// This is the only case when we poll again
newCset.summary = PENDING;
} else if (res.status === 200) {
const ccSum = await res.json();
// XXX: Document in which cases we would not have overall_cur
if (ccSum.overall_cur) {
// We have coverage data, thus, adding links to the coverage diff viewer
// and unhiding the csets
newCset.linkify = true;
newCset.hidden = false;
const result = coverageSummaryText(ccSum);
newCset.summary = result.text;
newCset.summaryClassName = result.className;
} else {
console.error(`No overall_cur: ${ccSum}`);
}
} else if (res.status === 500) {
newCset.summary = res.statusText;
} else {
console.log(`Unexpected HTTP code (${res.status}) for ${newCset}`);
}
return newCset;
} catch (e) {
console.log(e);
console.log(`Failed to fetch data for ${cset.node}`);
return cset;
}
};
// Return list of changesets
const pushesToCsets = async (pushes, hiddenDefault) => {
const ignore = ({ desc, author }) => {

Просмотреть файл

@ -1,4 +1,4 @@
const SETTINGS = {
export const SETTINGS = {
coverageThresholds: {
low: {
threshold: 20,
@ -14,4 +14,4 @@ const SETTINGS = {
},
};
export default SETTINGS;
export const PENDING = 'Pending';

Просмотреть файл

@ -19,8 +19,32 @@ a {
/* XXX: The lines inside of pre overrun if the window is very little */
pre { margin: 0; }
.codecoverage-diffviewer table,th,tr,td {
border-collapse: collapse;
padding: 0px;
border-collapse: collapse;
padding: 0px;
}
.return-home {
margin: 0em 0em 1em;
}
.coverage-meta {
display: flex;
flex-direction: column;
margin: 0em 0em 1em;
min-width: 530px;
max-width: 600px;
}
.coverage-meta-row {
display: flex;
justify-content: space-between;
}
.parent-meta-subtitle {
font-weight: 400;
text-align: left;
}
.meta {
flex-direction: row;
}
.meta-right {
text-align: right;
}
.diff-block {
border: 1px solid #eaeaea;
@ -34,6 +58,7 @@ pre { margin: 0; }
}
.diff-file {
margin: 0em 0em 1em;
min-width: 530px;
width: 100%;
}
.file-path {

Просмотреть файл

@ -1,3 +1,6 @@
import { PENDING, SETTINGS } from '../settings';
import * as FetchAPI from '../utils/fetch_data';
export const arrayToMap = (csets) => {
const newCsets = {};
csets.forEach((cset) => {
@ -9,3 +12,100 @@ export const arrayToMap = (csets) => {
export const mapToArray = csets => (
Object.keys(csets).map(node => csets[node])
);
const coverageSummary = (coverage) => {
const s = {
addedLines: 0,
coveredLines: 0,
};
if (coverage.diffs.length > 0) {
coverage.diffs.forEach((diff) => {
diff.changes.forEach((change) => {
if (change.coverage === 'Y') {
s.coveredLines += 1;
}
if (change.coverage !== '?') {
s.addedLines += 1;
}
});
s.percentage = (s.addedLines === 0) ?
undefined :
100 * (s.coveredLines / s.addedLines);
});
}
return s;
};
export const coverageSummaryText = (coverage) => {
const { coverageThresholds } = SETTINGS;
const { low, medium, high } = coverageThresholds;
const s = coverageSummary(coverage);
const result = { className: 'no-change', text: 'No changes' };
if (typeof s.percentage !== 'undefined') {
const perc = parseInt(s.percentage, 10);
if (perc < low.threshold) {
result.className = low.className;
} else if (perc < medium.threshold) {
result.className = medium.className;
} else {
result.className = high.className;
}
result.text = `${perc}% - ${s.coveredLines} lines covered out of ${s.addedLines} added`;
}
return result;
};
export const csetWithCcovData = async (cset) => {
if (!cset.node) {
throw Error(`No node for cset: ${cset}`);
}
const newCset = Object.assign({}, cset);
// XXX: fetch does not support timeouts. I would like to add a 5 second
// timeout rather than wait Heroku's default 30 second timeout. Specially
// since we're doing sequential fetches.
// XXX: Wrap fetch() in a Promise; add a setTimeout and call reject() if
// it goes off, otherwise resolve with the result of the fetch()
try {
const res = await FetchAPI.getChangesetCoverage(cset.node);
if (res.status === 202) {
// This is the only case when we poll again
newCset.summary = PENDING;
} else if (res.status === 200) {
const coverageData = await res.json();
// XXX: Document in which cases we would not have overall_cur
if (coverageData.overall_cur) {
// We have coverage data, thus, adding links to the coverage diff viewer
// and unhiding the csets
newCset.linkify = true;
newCset.hidden = false;
newCset.coverage = {
...coverageData,
diffMeta: node => ({
hgRev: `${FetchAPI.hgHost}/mozilla-central/rev/${node}`,
ccovBackend: `${FetchAPI.ccovBackend}/coverage/changeset/${node}`,
}),
parentMeta: coverage => ({
pushlog: `https://hg.mozilla.org/mozilla-central/pushloghtml?changeset=${coverage.build_changeset}`,
codecov: `https://codecov.io/gh/marco-c/gecko-dev/commit/${coverage.git_build_changeset}`,
gh: `https://github.com/mozilla/gecko-dev/commit/${coverage.git_build_changeset}`,
}),
};
const result = coverageSummaryText(coverageData);
newCset.summary = result.text;
newCset.summaryClassName = result.className;
} else {
console.error(`No overall_cur: ${coverageData}`);
}
} else if (res.status === 500) {
newCset.summary = res.statusText;
} else {
console.log(`Unexpected HTTP code (${res.status}) for ${newCset}`);
}
return newCset;
} catch (e) {
console.log(e);
console.log(`Failed to fetch data for ${cset.node}`);
return cset;
}
};