From 17e15bb45d9d9d542e0e05f8ca20121aa5476e63 Mon Sep 17 00:00:00 2001 From: Jan Keromnes Date: Wed, 22 Aug 2018 15:13:02 +0200 Subject: [PATCH] [WIP] Fetch and show directory coverage in the file viewer. --- src/containers/fileViewer.jsx | 103 ++++++++++++++++++++++++---------- src/settings.js | 1 + src/utils/coverage.js | 25 ++++++++- 3 files changed, 96 insertions(+), 33 deletions(-) diff --git a/src/containers/fileViewer.jsx b/src/containers/fileViewer.jsx index 1d89bf33..1dfe18b8 100644 --- a/src/containers/fileViewer.jsx +++ b/src/containers/fileViewer.jsx @@ -1,11 +1,16 @@ import React, { Component } from 'react'; -import { fileRevisionCoverageSummary, fileRevisionWithActiveData } from '../utils/coverage'; +import FileOutlineIcon from 'mdi-react/FileOutlineIcon'; +import FolderOutlineIcon from 'mdi-react/FolderOutlineIcon'; +import settings from '../settings'; +import { sourceCoverageSummary, sourceCoverageFromActiveData, pathCoverageFromBackend } from '../utils/coverage'; import { rawFile } from '../utils/hg'; import { TestsSideViewer, CoveragePercentageViewer } from '../components/fileViewer'; import { HORIZONTAL_ELLIPSIS, HEAVY_CHECKMARK } from '../utils/symbol'; import hash from '../utils/hash'; +const { low, medium, high } = settings.COVERAGE_THRESHOLDS; + // 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. @@ -28,7 +33,8 @@ export default class FileViewerContainer extends Component { // Reset the state and fetch new data const newState = { appErr: undefined, - coverage: undefined, + pathCoverage: undefined, + sourceCoverage: undefined, parsedFile: undefined, }; // eslint-disable-next-line react/no-did-update-set-state @@ -45,52 +51,89 @@ export default class FileViewerContainer extends Component { } } - fetchData(repoPath = 'mozilla-central') { + async fetchData(repoPath = 'mozilla-central') { const { revision, path } = this.props; - if (!revision || !path) { - this.setState({ appErr: "Undefined URL query ('revision', 'path' fields are required)" }); + if (!revision) { + this.setState({ appErr: "Undefined URL query (field 'revision' is required)" }); return; } - // Get source code from hg - const fileSource = async () => { - this.setState({ parsedFile: (await rawFile(revision, path, repoPath)) }); + // Get overall path coverage from backend + const pathCoverage = async () => { + const data = await pathCoverageFromBackend(revision, path, repoPath); + this.setState({ pathCoverage: data }); }; - // Get coverage from ActiveData - const coverageData = async () => { - const { data } = await fileRevisionWithActiveData(revision, path, repoPath); - this.setState({ coverage: fileRevisionCoverageSummary(data) }); + // Get detailed source coverage from ActiveData + const fileCoverage = async () => { + const { data } = await sourceCoverageFromActiveData(revision, path, repoPath); + this.setState({ sourceCoverage: sourceCoverageSummary(data) }); + }; + // Get raw source code from hg + const fileSource = async () => { + const parsedFile = await rawFile(revision, path, repoPath); + this.setState({ parsedFile }); }; // Fetch source code and coverage in parallel try { - Promise.all([fileSource(), coverageData()]) - .catch((e) => { - if ((e instanceof RangeError) && (e.message === 'Revision number too short')) { - this.setState({ appErr: 'Revision number is too short. Unable to fetch tests.' }); - } else { - this.setState({ appErr: `${e.name}: ${e.message}` }); - } - throw e; - }); + await Promise.all([pathCoverage(), fileCoverage(), fileSource()]); } catch (error) { - this.setState({ appErr: `${error.name}: ${error.message}` }); + console.error(error); + if ((error instanceof RangeError) && (error.message === 'Revision number too short')) { + this.setState({ appErr: 'Revision number is too short. Unable to fetch data.' }); + } else { + this.setState({ appErr: `${error.name}: ${error.message}` }); + } throw error; } } render() { + const { revision, path } = this.props; const { - parsedFile, coverage, selectedLine, appErr, + pathCoverage, sourceCoverage, parsedFile, selectedLine, appErr, } = this.state; return (
- { !appErr && (parsedFile) && + { pathCoverage && pathCoverage.type === 'directory' && + + + + + + + {pathCoverage.children.map((file) => { + const fileName = file.path.replace(new RegExp(`^${path}`, 'g'), ''); + const coveragePercent = Math.round(100 * file.coverage); + let summaryClassName = high.className; + if (coveragePercent < medium.threshold) { + summaryClassName = + (coveragePercent < low.threshold ? low.className : medium.className); + } + const href = + `/#/file?revision=${revision}&path=${path}${fileName}`; + return ( + + + + + ); + })} + +
FileCoverage summary
+ + {file.type === 'directory' ? : } + {fileName} + + {coveragePercent}%
} + { pathCoverage && pathCoverage.type === 'file' && +
Coverage: {Math.round(pathCoverage.coverage * 100)}%
} + { parsedFile && }
@@ -100,7 +143,7 @@ export default class FileViewerContainer extends Component { // This component renders each line of the file with its line number const FileViewer = ({ - parsedFile, coverage, selectedLine, onLineClick, + parsedFile, sourceCoverage, selectedLine, onLineClick, }) => ( @@ -111,7 +154,7 @@ const FileViewer = ({ key={uniqueId} lineNumber={lineNumber + 1} text={text} - coverage={coverage} + coverage={sourceCoverage} selectedLine={selectedLine} onLineClick={onLineClick} /> @@ -156,7 +199,7 @@ const Line = ({ // This component contains metadata of the file const FileViewerMeta = ({ - revision, path, appErr, parsedFile, coverage, + revision, path, appErr, parsedFile, sourceCoverage, }) => { const showStatus = (label, data) => (
  • @@ -168,11 +211,11 @@ const FileViewerMeta = ({
    File Coverage
    - { (coverage) && } + { (sourceCoverage) && }
      { showStatus('Source code', parsedFile) } - { showStatus('Coverage', coverage) } + { showStatus('Coverage', sourceCoverage) }
    diff --git a/src/settings.js b/src/settings.js index b31b9017..607c830d 100644 --- a/src/settings.js +++ b/src/settings.js @@ -8,6 +8,7 @@ const settings = { ENABLED: process.env.ENABLE_CACHE === 'true' || process.env.NODE_ENV === 'production', }, CCOV_BACKEND: 'https://coverage.moz.tools', + CCOV_STAGING_BACKEND: 'https://coverage.staging.moz.tools', CODECOV_GECKO_DEV: 'https://codecov.io/gh/mozilla/gecko-dev', COVERAGE_THRESHOLDS: { low: { diff --git a/src/utils/coverage.js b/src/utils/coverage.js index 7cb36dbe..d76f716a 100644 --- a/src/utils/coverage.js +++ b/src/utils/coverage.js @@ -4,7 +4,7 @@ import { jsonFetch, jsonPost, plainFetch } from './fetch'; import { queryCacheWithFallback, saveInCache } from './localCache'; const { - ACTIVE_DATA, CCOV_BACKEND, CODECOV_GECKO_DEV, GH_GECKO_DEV, + ACTIVE_DATA, CCOV_BACKEND, CCOV_STAGING_BACKEND, CODECOV_GECKO_DEV, GH_GECKO_DEV, } = settings; const { INTERNAL_ERROR, PENDING } = settings.STRINGS; @@ -16,6 +16,9 @@ export const ccovBackendUrl = node => (`${CCOV_BACKEND}/coverage/changeset/${nod const queryChangesetCoverage = node => plainFetch(`${CCOV_BACKEND}/coverage/changeset/${node}`); +export const queryPathCoverage = (path, revision) => + jsonFetch(`${CCOV_STAGING_BACKEND}/v2/path?path=${path}${revision ? `&changeset=${revision}` : ''}`); + const queryActiveData = body => jsonPost(`${ACTIVE_DATA}/query`, body); @@ -44,7 +47,7 @@ const coverageStatistics = (coverage) => { }; // get the coverage summary for a particular revision and file -export const fileRevisionCoverageSummary = (coverage) => { +export const sourceCoverageSummary = (coverage) => { const s = { coveredLines: [], uncoveredLines: [], @@ -227,7 +230,7 @@ export const getPendingCoverage = async (changesetsCoverage) => { }; }; -export const fileRevisionWithActiveData = async (revision, path, repoPath) => { +export const sourceCoverageFromActiveData = async (revision, path, repoPath) => { try { if (revision.length < settings.MIN_REVISION_LENGTH) { throw new RangeError('Revision number too short'); @@ -252,3 +255,19 @@ export const fileRevisionWithActiveData = async (revision, path, repoPath) => { throw new Error(`Failed to fetch data for revision: ${revision}, path: ${path}\n${e}`); } }; + +export const pathCoverageFromBackend = async (revision, path, repoPath) => { + try { + if (revision.length < settings.MIN_REVISION_LENGTH) { + throw new RangeError('Revision number too short'); + } + const data = await queryPathCoverage(path /* , revision */); + if (data.status && data.staus !== 200) { + throw new Error(`HTTP response ${data.status}`); + } + return data; + } catch (error) { + // FIXME: If you start using this method, please replace the `console.error()` with a `throw`. + console.error(new Error(`Failed to fetch data for revision: ${revision}, path: ${path}\n${error}`)); + } +};