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

Commit

Permalink
File coverage viewer
Browse files Browse the repository at this point in the history
This view allows you to select each line of a file
and determine which tests cover that specific line.

This work comes from the UCOSP work from @LinkaiQi and @yuenj.
  • Loading branch information
armenzg committed Jan 3, 2018
1 parent 700e78b commit 6713666
Show file tree
Hide file tree
Showing 12 changed files with 591 additions and 16 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.js
Expand Up @@ -4,6 +4,7 @@ module.exports = {
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
"react/prop-types": "off",
"no-undef": "off",
"no-console": "off"
"no-console": "off",
"no-underscore-dangle": "off",
}
};
3 changes: 3 additions & 0 deletions package.json
Expand Up @@ -14,9 +14,12 @@
"heroku-prebuild": "yarn install && yarn build"
},
"dependencies": {
"chroma-js": "^1.3.4",
"form-serialize": "^0.7.2",
"lodash": "^4.17.4",
"parse-diff": "^0.4.0",
"prop-types": "^15.6.0",
"query-string": "^5.0.1",
"react": "^15.6.1",
"react-dom": "^15.6.1",
"react-interval": "^2.0.2",
Expand Down
5 changes: 5 additions & 0 deletions src/components/app.js
Expand Up @@ -3,6 +3,7 @@ import { Route } from 'react-router-dom';

import ChangesetsViewerContainer from './summaryviewer';
import DiffViewerContainer from './diffviewer';
import FileViewerContainer from './fileviewer';
import '../style.css';

const REPO = 'https://github.com/armenzg/firefox-code-coverage-frontend';
Expand Down Expand Up @@ -57,6 +58,10 @@ export default class App extends Component {
/>
)}
/>
<Route
path="/file"
component={FileViewerContainer}
/>
</div>
);
}
Expand Down
8 changes: 5 additions & 3 deletions src/components/diffviewer.js
Expand Up @@ -23,12 +23,12 @@ export default class DiffViewerContainer extends Component {
};
}

async componentDidMount() {
componentDidMount() {
const { changeset } = this.props;
await this.fetchCsetData(changeset);
Promise.all([this.fetchSetCoverageData(changeset), this.fetchSetDiff(changeset)]);
}

async fetchCsetData(changeset) {
async fetchSetCoverageData(changeset) {
try {
this.setState({ csetMeta: await csetWithCcovData({ node: changeset }) });
} catch (error) {
Expand All @@ -37,7 +37,9 @@ export default class DiffViewerContainer extends Component {
appError: 'There was an error fetching the code coverage data.',
});
}
}

async fetchSetDiff(changeset) {
try {
const text = await (await FetchAPI.getDiff(changeset)).text();
this.setState({ parsedDiff: parse(text) });
Expand Down
168 changes: 168 additions & 0 deletions src/components/fileviewer.js
@@ -0,0 +1,168 @@
import React, { Component } from 'react';
import * as queryString from 'query-string';

import { fileRevisionCoverageSummary, fileRevisionWithActiveData, rawFile } from '../utils/data';
import { TestsSideViewer, CoveragePercentageViewer } from './fileviewercov';
import { HORIZONTAL_ELLIPSIS, HEAVY_CHECKMARK } from '../utils/symbol';
import hash from '../utils/hash';

// FileViewer loads a raw file for a given revision from Mozilla's hg web.
// It uses test coverage information from Active Data to show coverage
// for runnable lines.
export default class FileViewerContainer extends Component {
constructor(props) {
super(props);
this.state = this.parseQueryParams();
this.setSelectedLine = this.setSelectedLine.bind(this);
}

componentDidMount() {
const { revision, path } = this.state;
this.fetchData(revision, path);
}

setSelectedLine(selectedLineNumber) {
// click on a selected line to deselect the line
if (selectedLineNumber === this.state.selectedLine) {
this.setState({ selectedLine: undefined });
} else {
this.setState({ selectedLine: selectedLineNumber });
}
}

fetchData(revision, path, repoPath = 'mozilla-central') {
// Get source code from hg
const fileSource = async () => {
this.setState({ parsedFile: (await rawFile(revision, path, repoPath)) });
};
// Get coverage from ActiveData
const coverageData = async () => {
const { data } = await fileRevisionWithActiveData(revision, path, repoPath);
this.setState({ coverage: fileRevisionCoverageSummary(data) });
};
// Fetch source code and coverage in parallel
try {
Promise.all([fileSource(), coverageData()]);
} catch (error) {
this.setState({ appErr: `${error.name}: ${error.message}` });
}
}

parseQueryParams() {
const parsedQuery = queryString.parse(this.props.location.search);
const out = {
appError: undefined,
revision: undefined,
path: undefined,
};
if (!parsedQuery.revision || !parsedQuery.path) {
out.appErr = "Undefined URL query ('revision', 'path' fields are required)";
} else {
// Remove beginning '/' in the path parameter to fetch from source,
// makes both path=/path AND path=path acceptable in the URL query
// Ex. "path=/accessible/atk/Platform.cpp" AND "path=accessible/atk/Platform.cpp"
out.revision = parsedQuery.revision;
out.path = parsedQuery.path.startsWith('/') ? parsedQuery.path.slice(1) : parsedQuery.path;
}
return out;
}

render() {
const { parsedFile, coverage, selectedLine } = this.state;

return (
<div>
<div className="file-view">
<FileViewerMeta {...this.state} />
{ (parsedFile) && <FileViewer {...this.state} onLineClick={this.setSelectedLine} /> }
</div>
<TestsSideViewer
coverage={coverage}
lineNumber={selectedLine}
/>
</div>
);
}
}

// This component renders each line of the file with its line number
const FileViewer = ({ parsedFile, coverage, selectedLine, onLineClick }) => (
<table className="file-view-table">
<tbody>
{parsedFile.map((text, lineNumber) => {
const uniqueId = hash(text) + lineNumber;
return (
<Line
key={uniqueId}
lineNumber={lineNumber + 1}
text={text}
coverage={coverage}
selectedLine={selectedLine}
onLineClick={onLineClick}
/>
);
})}
</tbody>
</table>
);

const Line = ({ lineNumber, text, coverage, selectedLine, onLineClick }) => {
const handleOnClick = () => {
onLineClick(lineNumber);
};

const select = (lineNumber === selectedLine) ? 'selected' : '';

let nTests;
let color;
if (coverage) {
// hit line
if (coverage.coveredLines.find(element => element === lineNumber)) {
nTests = coverage.testsPerHitLine[lineNumber].length;
color = 'hit';
// miss line
} else if (coverage.uncoveredLines.find(element => element === lineNumber)) {
color = 'miss';
}
}

return (
<tr className={`file-line ${select} ${color}`} onClick={handleOnClick}>
<td className="file-line-number">{lineNumber}</td>
<td className="file-line-tests">
{ nTests && <span className="tests">{nTests}</span> }
</td>
<td className="file-line-text"><pre>{text}</pre></td>
</tr>
);
};

// This component contains metadata of the file
const FileViewerMeta = ({ revision, path, appErr, parsedFile, coverage }) => {
const showStatus = (label, data) => (
<li className="file-meta-li">
{label}: {(data) ? HEAVY_CHECKMARK : HORIZONTAL_ELLIPSIS}
</li>
);

return (
<div>
<div className="file-meta-center">
<div className="file-meta-title">File Coverage</div>
{ (coverage) && <CoveragePercentageViewer coverage={coverage} /> }
<div className="file-meta-status">
<ul className="file-meta-ul">
{ showStatus('Source code', parsedFile) }
{ showStatus('Coverage', coverage) }
</ul>
</div>
</div>
{appErr && <span className="error-message">{appErr}</span>}

<div className="file-summary">
<span className="file-path">{path}</span>
</div>
<div className="file-meta-revision">revision number: {revision}</div>
</div>
);
};
116 changes: 116 additions & 0 deletions src/components/fileviewercov.js
@@ -0,0 +1,116 @@
// This file contains coverage information for a particular revision of a source file
import React, { Component } from 'react';

import getPercentCovColor from '../utils/color';
import { TRIANGULAR_BULLET } from '../utils/symbol';

// Sidebar component, show which tests will cover the given selected line
export class TestsSideViewer extends Component {
constructor(props) {
super(props);
this.state = {
expandTest: undefined,
};
this.handleTestOnExpand = this.handleTestOnExpand.bind(this);
}

componentWillReceiveProps() {
// collapse expanded test when selected line is changed
this.setState({ expandTest: undefined });
}

getTestList(tests) {
return (
<ul className="test-viewer-ul">
{tests.map((test, row) => (
<Test
key={test._id}
row={row}
test={test}
expand={(row === this.state.expandTest) ? 'expanded' : ''}
handleTestOnExpand={this.handleTestOnExpand}
/>
))}
</ul>
);
}

handleTestOnExpand(row) {
if (this.state.expandTest === row) {
this.setState({ expandTest: undefined });
} else {
this.setState({ expandTest: row });
}
}

render() {
const { coverage, lineNumber } = this.props;
let testTitle;
let testList;
if (!coverage) {
testTitle = 'Fetching coverage from backend...';
} else if (!lineNumber) {
testTitle = 'All test that cover this file';
testList = this.getTestList(coverage.allTests);
} else {
testTitle = `Line: ${lineNumber}`;
if (coverage.testsPerHitLine[lineNumber]) {
testList = this.getTestList(coverage.testsPerHitLine[lineNumber]);
} else {
testList = (<p>No test covers this line</p>);
}
}
return (
<div className="tests-viewer">
<div className="tests-viewer-title">Covered Tests</div>
<h3>{testTitle}</h3>
{testList}
</div>
);
}
}

// Test list item in the TestsSideViewer
const Test = ({ row, test, expand, handleTestOnExpand }) => (
<li>
<button className="test-switch" onClick={() => handleTestOnExpand(row)}>
<span className={`test-symbol ${expand}`}>{TRIANGULAR_BULLET}</span>
<span className="test-name">
{ test.run.name.substring(test.run.name.indexOf('/') + 1) }
</span>
</button>
<div className={`expandable-test-info ${expand}`}>
<ul className="test-detail-ul">
<li>{`platform : ${test.run.machine.platform}`}</li>
<li>{`suite : ${test.run.suite.fullname}`}</li>
<li>{`chunk : ${test.run.chunk}`}</li>
</ul>
</div>
</li>
);

// shows coverage percentage of a file
export const CoveragePercentageViewer = ({ coverage }) => {
const coveredLines = coverage.coveredLines.length;
const totalLines = coveredLines + coverage.uncoveredLines.length;
let percentageCovered;
if (coveredLines !== 0 || coverage.uncoveredLines.length !== 0) {
percentageCovered = (
<div
className="coverage-percentage"
style={{ backgroundColor: `${getPercentCovColor(coveredLines / totalLines)}` }}
>
{((coveredLines / totalLines) * 100).toPrecision(4)}
% - {coveredLines} lines covered out of {totalLines} coverable lines
</div>
);
} else {
percentageCovered = (<div className="coverage-percentage">No changes</div>);
}

return (
<div className="coverage-percentage-viewer">
{ percentageCovered }
</div>
);
};

0 comments on commit 6713666

Please sign in to comment.