This repository has been archived by the owner on Apr 11, 2019. It is now read-only.
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
12 changed files
with
591 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> | ||
); | ||
}; |
Oops, something went wrong.