From 07d50702e5800a9e7e018b07eab8833a84337a8f Mon Sep 17 00:00:00 2001 From: Ryan Luker Date: Wed, 22 Mar 2017 22:18:40 -0700 Subject: [PATCH] Add relative lcov path support (#33) * fix changelog link * create wrappers and interfaces * use wrappers in a type safe way * cleanup tests are increased type safety * update wrapper interfaces * add alternative source file compare * add relative path option with multi os support * add new setting to readme, cleanup english * add tests to cover absolute and relative modes * make readme more descriptive for alt option --- README.md | 3 +- package.json | 5 ++ src/config.ts | 40 +++++----- src/gutters.ts | 31 +++----- src/indicators.ts | 56 +++++++++++--- src/lcov.ts | 24 +++--- src/wrappers/fs.ts | 13 +++- src/wrappers/lcov-parse.ts | 12 ++- src/wrappers/vscode.ts | 47 +++++++---- test/indicators.test.ts | 76 +++++++++++++++++- test/lcov.test.ts | 155 +++++++++++++++++-------------------- 11 files changed, 290 insertions(+), 172 deletions(-) diff --git a/README.md b/README.md index 13c3336..ae854fb 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ |Setting | Description |--------|------------ |`coverage-gutters.lcovname`|Allows specification of a custom lcov file name +|`coverage-gutters.altSfCompare`|Uses a relative method of comparing lcov source file paths |`coverage-gutters.highlightlight`|Changes the highlight for light themes |`coverage-gutters.highlightdark`|Changes the Highlight for dark themes |`coverage-gutters.gutterIconPathDark`|Relative path to an icon in the extension for dark themes @@ -40,7 +41,7 @@ Some examples for the highlight colour are as follows: - none ( just missing functionality :) ) ## Release Notes -### [Changelog](CHANGELOG.mb) +### [Changelog](https://github.com/ryanluker/vscode-coverage-gutters/releases) ## Contribution Guidelines - test backed code changes diff --git a/package.json b/package.json index 396b040..901445e 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,11 @@ "default": "lcov.info", "description": "name of your lcov file" }, + "coverage-gutters.altSfCompare": { + "type": "boolean", + "default": false, + "description": "uses a relative method of comparing lcov source file paths" + }, "coverage-gutters.highlightlight": { "type": "string", "default": "rgba(166, 220, 142, 0.75)", diff --git a/src/config.ts b/src/config.ts index edb4ecf..97ae461 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,32 +1,25 @@ 'use strict'; - +import {VscodeInterface} from "./wrappers/vscode"; import {TextEditorDecorationType, ExtensionContext} from "vscode"; export interface configStore { - lcovFileName: string, - coverageDecorationType: TextEditorDecorationType, - gutterDecorationType: TextEditorDecorationType + lcovFileName: string; + coverageDecorationType: TextEditorDecorationType; + gutterDecorationType: TextEditorDecorationType; + altSfCompare: boolean; } export class Config { - private createTextEditorDecorationType; - private executeCommand; - private workspaceConfig; + private vscode: VscodeInterface; private context: ExtensionContext; private lcovFileName: string; private coverageDecorationType: TextEditorDecorationType; private gutterDecorationType: TextEditorDecorationType; + private altSfCompare: boolean; - constructor( - createTextEditorDecorationType, - executeCommand, - workspaceConfig, - context: ExtensionContext - ) { - this.createTextEditorDecorationType = createTextEditorDecorationType; - this.executeCommand = executeCommand; - this.workspaceConfig = workspaceConfig; + constructor(vscode: VscodeInterface, context: ExtensionContext) { + this.vscode = vscode; this.context = context; } @@ -34,30 +27,33 @@ export class Config { return { lcovFileName: this.lcovFileName, coverageDecorationType: this.coverageDecorationType, - gutterDecorationType: this.gutterDecorationType + gutterDecorationType: this.gutterDecorationType, + altSfCompare: this.altSfCompare } } public setup(): configStore { //Customizable UI configurations - const rootCustomConfig = this.workspaceConfig("coverage-gutters.customizable"); + const rootCustomConfig = this.vscode.getConfiguration("coverage-gutters.customizable"); const configsCustom = Object.keys(rootCustomConfig); for(let element of configsCustom) { - this.executeCommand( + this.vscode.executeCommand( "setContext", "config.coverage-gutters.customizable." + element, rootCustomConfig.get(element)); } //Basic configurations - const rootConfig = this.workspaceConfig("coverage-gutters"); + const rootConfig = this.vscode.getConfiguration("coverage-gutters"); this.lcovFileName = rootConfig.get("lcovname") as string; + this.altSfCompare = rootConfig.get("altSfCompare") as boolean; + const coverageLightBackgroundColour = rootConfig.get("highlightlight") as string; const coverageDarkBackgroundColour = rootConfig.get("highlightdark") as string; const gutterIconPathDark = rootConfig.get("gutterIconPathDark") as string; const gutterIconPathLight = rootConfig.get("gutterIconPathLight") as string; - this.coverageDecorationType = this.createTextEditorDecorationType({ + this.coverageDecorationType = this.vscode.createTextEditorDecorationType({ isWholeLine: true, light: { backgroundColor: coverageLightBackgroundColour @@ -67,7 +63,7 @@ export class Config { } }); - this.gutterDecorationType = this.createTextEditorDecorationType({ + this.gutterDecorationType = this.vscode.createTextEditorDecorationType({ light: { gutterIconPath: this.context.asAbsolutePath(gutterIconPathLight) }, diff --git a/src/gutters.ts b/src/gutters.ts index 73d7487..104b632 100644 --- a/src/gutters.ts +++ b/src/gutters.ts @@ -1,34 +1,27 @@ 'use strict'; -import { - createTextEditorDecorationType, - executeCommand, - getConfiguration, - setDecorations, - findFiles -} from "./wrappers/vscode"; -import {readFile} from "./wrappers/fs"; +import {vscode} from "./wrappers/vscode"; +import {fs} from "./wrappers/fs"; import {lcovParse} from "./wrappers/lcov-parse"; -import {Range, window, ExtensionContext} from "vscode"; +import {ExtensionContext, window} from "vscode"; import {Lcov, lcov} from "./lcov"; import {Indicators, indicators} from "./indicators"; import {Config, configStore} from "./config"; +const vscodeImpl = new vscode(); +const fsImpl = new fs(); +const parseImpl = new lcovParse(); + export class Gutters { private configStore: configStore; private lcov: lcov; private indicators: indicators; constructor(context: ExtensionContext) { - this.configStore = new Config( - createTextEditorDecorationType, - executeCommand, - getConfiguration, - context - ).setup(); - this.lcov = new Lcov(this.configStore, findFiles, readFile); - this.indicators = new Indicators(this.configStore, lcovParse, setDecorations); + this.configStore = new Config(vscodeImpl, context).setup(); + this.lcov = new Lcov(this.configStore, vscodeImpl, fsImpl); + this.indicators = new Indicators(parseImpl, vscodeImpl, this.configStore); } public async displayCoverageForActiveFile() { @@ -44,7 +37,7 @@ export class Gutters { } public dispose() { - setDecorations(this.configStore.coverageDecorationType, []); - setDecorations(this.configStore.gutterDecorationType, []); + vscodeImpl.setDecorations(this.configStore.coverageDecorationType, []); + vscodeImpl.setDecorations(this.configStore.gutterDecorationType, []); } } \ No newline at end of file diff --git a/src/indicators.ts b/src/indicators.ts index f1f004c..68688cd 100644 --- a/src/indicators.ts +++ b/src/indicators.ts @@ -1,23 +1,30 @@ "use strict"; import {configStore} from "./config"; +import {LcovParseInterface} from "./wrappers/lcov-parse"; +import {VscodeInterface} from "./wrappers/vscode"; + import {Range} from "vscode"; import {Detail} from "lcov-parse"; export interface indicators { - render(lines: Array): Promise, - extract(lcovFile: string, file: string): Promise> + render(lines: Array): Promise; + extract(lcovFile: string, file: string): Promise>; } export class Indicators implements indicators{ - private parseLcov; + private parse: LcovParseInterface; + private vscode: VscodeInterface; private configStore: configStore; - private setDecorations; - constructor(configStore, parseLcov, setDecorations) { + constructor( + parse: LcovParseInterface, + vscode: VscodeInterface, + configStore: configStore + ) { + this.parse = parse; + this.vscode = vscode; this.configStore = configStore; - this.parseLcov = parseLcov; - this.setDecorations = setDecorations; } public render(lines: Detail[]): Promise { @@ -28,19 +35,18 @@ export class Indicators implements indicators{ renderLines.push(new Range(detail.line - 1, 0, detail.line - 1, 0)); } }); - this.setDecorations(this.configStore.coverageDecorationType, renderLines); - this.setDecorations(this.configStore.gutterDecorationType, renderLines); + this.vscode.setDecorations(this.configStore.coverageDecorationType, renderLines); + this.vscode.setDecorations(this.configStore.gutterDecorationType, renderLines); return resolve(); }); } public extract(lcovFile: string, file: string): Promise> { return new Promise>((resolve, reject) => { - this.parseLcov(lcovFile, (err, data) => { + this.parse.source(lcovFile, (err, data) => { if(err) return reject(err); let section = data.find((section) => { - //prevent hazardous casing mishaps - return section.file.toLocaleLowerCase() === file.toLocaleLowerCase(); + return this.compareFilePaths(section.file, file); }); if(!section) return reject(new Error("No coverage for file!")); @@ -48,4 +54,30 @@ export class Indicators implements indicators{ }); }); } + + private compareFilePaths(lcovFile: string, file: string): boolean { + if(this.configStore.altSfCompare) { + //consider windows and linux file paths + const sourceFile = lcovFile.split(/[\\\/]/).reverse(); + const openFile = file.split(/[\\\/]/).reverse(); + const folderName = this.vscode.getRootPath().split(/[\\\/]/).reverse()[0]; + let match = true; + let index = 0; + + //work backwards from the file folder leaf to folder node + do { + if(sourceFile[index] === openFile[index]) { + index++; + } else { + match = false; + break; + } + } while(folderName !== openFile[index]); + + return match; + } else { + //prevent hazardous casing mishaps + return lcovFile.toLocaleLowerCase() === file.toLocaleLowerCase(); + } + } } \ No newline at end of file diff --git a/src/lcov.ts b/src/lcov.ts index 7905753..fd73c82 100644 --- a/src/lcov.ts +++ b/src/lcov.ts @@ -1,25 +1,31 @@ 'use strict'; import {configStore} from "./config"; +import {VscodeInterface} from "./wrappers/vscode"; +import {FsInterface} from "./wrappers/fs"; export interface lcov { - find(): Promise, - load(lcovPath: string): Promise + find(): Promise; + load(lcovPath: string): Promise; } export class Lcov implements lcov { private configStore: configStore; - private findFiles; - private readFile; + private vscode: VscodeInterface; + private fs: FsInterface; - constructor(configStore: configStore, findFiles, readFile) { + constructor( + configStore: configStore, + vscode: VscodeInterface, + fs: FsInterface + ) { this.configStore = configStore; - this.findFiles = findFiles; - this.readFile = readFile; + this.vscode = vscode; + this.fs = fs; } public find(): Promise { return new Promise((resolve, reject) => { - this.findFiles("**/" + this.configStore.lcovFileName, "**/node_modules/**", 1) + this.vscode.findFiles("**/" + this.configStore.lcovFileName, "**/node_modules/**", 1) .then((uriLcov) => { if(!uriLcov.length) return reject(new Error("Could not find a lcov file!")); return resolve(uriLcov[0].fsPath); @@ -29,7 +35,7 @@ export class Lcov implements lcov { public load(lcovPath: string): Promise { return new Promise((resolve, reject) => { - this.readFile(lcovPath, (err, data) => { + this.fs.readFile(lcovPath, (err, data) => { if(err) return reject(err); return resolve(data.toString()); }); diff --git a/src/wrappers/fs.ts b/src/wrappers/fs.ts index a0e2106..b87d963 100644 --- a/src/wrappers/fs.ts +++ b/src/wrappers/fs.ts @@ -1,5 +1,12 @@ import {readFile as readFileFS} from "fs"; -export function readFile(filename: string, callback: (err: NodeJS.ErrnoException, data: Buffer) => void) { - return readFileFS(filename, callback); -} \ No newline at end of file +export interface FsInterface { + readFile(filename: string, callback: (err: NodeJS.ErrnoException, data: Buffer) => void): void; +} + +export class fs implements FsInterface { + public readFile(filename: string, callback: (err: NodeJS.ErrnoException, data: Buffer) => void): void { + return readFileFS(filename, callback); + } +} + diff --git a/src/wrappers/lcov-parse.ts b/src/wrappers/lcov-parse.ts index 87f0094..5441d58 100644 --- a/src/wrappers/lcov-parse.ts +++ b/src/wrappers/lcov-parse.ts @@ -1,7 +1,13 @@ "use strict"; -import {LcovSection, source} from "lcov-parse"; +import {LcovSection, source as sourceLcovParse} from "lcov-parse"; -export function lcovParse(file: string, cb: (err: Error, data: Array) => void): void { - return source(file, cb); +export interface LcovParseInterface { + source(file: string, cb: (err: Error, data: Array) => void): void; +} + +export class lcovParse implements LcovParseInterface { + public source(file: string, cb: (err: Error, data: Array) => void): void { + return sourceLcovParse(file, cb); + } } \ No newline at end of file diff --git a/src/wrappers/vscode.ts b/src/wrappers/vscode.ts index 5dbb556..9484382 100644 --- a/src/wrappers/vscode.ts +++ b/src/wrappers/vscode.ts @@ -1,7 +1,9 @@ "use strict"; -import {window, commands, workspace} from "vscode"; import { + window, + commands, + workspace, DecorationRenderOptions, TextEditorDecorationType, WorkspaceConfiguration, @@ -11,22 +13,37 @@ import { Uri } from "vscode" -export function createTextEditorDecorationType(options: DecorationRenderOptions): TextEditorDecorationType { - return window.createTextEditorDecorationType(options); +export interface VscodeInterface { + createTextEditorDecorationType(options: DecorationRenderOptions): TextEditorDecorationType; + setDecorations(decorationType: TextEditorDecorationType, rangesOrOptions: Range[] | DecorationOptions[]): void; + executeCommand(command: string, ...rest: any[]): Thenable<{}>; + findFiles(include: string, exclude: string, maxResults?: number, token?: CancellationToken): Thenable; + getConfiguration(section?: string): WorkspaceConfiguration; + getRootPath(): string; } -export function setDecorations(decorationType: TextEditorDecorationType, rangesOrOptions: Range[] | DecorationOptions[]) { - return window.activeTextEditor.setDecorations(decorationType, rangesOrOptions); -} +export class vscode implements VscodeInterface { + public createTextEditorDecorationType(options: DecorationRenderOptions): TextEditorDecorationType { + return window.createTextEditorDecorationType(options); + } -export function executeCommand(command: string, ...rest: any[]): Thenable<{}> { - return commands.executeCommand(command, rest); -} + public setDecorations(decorationType: TextEditorDecorationType, rangesOrOptions: Range[] | DecorationOptions[]) { + return window.activeTextEditor.setDecorations(decorationType, rangesOrOptions); + } -export function findFiles(include: string, exclude: string, maxResults?: number, token?: CancellationToken): Thenable { - return workspace.findFiles(include, exclude, maxResults, token); -} + public executeCommand(command: string, ...rest: any[]): Thenable<{}> { + return commands.executeCommand(command, rest); + } -export function getConfiguration(section?: string): WorkspaceConfiguration { - return workspace.getConfiguration(section); -} \ No newline at end of file + public findFiles(include: string, exclude: string, maxResults?: number, token?: CancellationToken): Thenable { + return workspace.findFiles(include, exclude, maxResults, token); + } + + public getConfiguration(section?: string): WorkspaceConfiguration { + return workspace.getConfiguration(section); + } + + public getRootPath(): string { + return workspace.rootPath; + } +} diff --git a/test/indicators.test.ts b/test/indicators.test.ts index e7cc380..0ba7692 100644 --- a/test/indicators.test.ts +++ b/test/indicators.test.ts @@ -2,11 +2,83 @@ import * as assert from "assert"; -import * as vscode from "vscode"; +import {vscode} from "../src/wrappers/vscode"; +import {lcovParse} from "../src/wrappers/lcov-parse"; import {Indicators} from "../src/indicators"; suite("Indicators Tests", function() { - test("Constructor should setup properly", function() { + const fakeConfig = { + lcovFileName: "test.ts", + coverageDecorationType: { + key: "testKey", + dispose() {} + }, + gutterDecorationType: { + key: "testKey2", + dispose() {} + }, + altSfCompare: false + }; + test("Constructor should setup properly", function(done) { + try { + const vscodeImpl = new vscode(); + const parseImpl = new lcovParse(); + const indicators = new Indicators( + parseImpl, + vscodeImpl, + fakeConfig + ); + return done(); + } catch(e) { + assert.equal(1,2); + return done(); + } + }); + + test("#extract: should find a matching file with absolute match mode", function(done) { + fakeConfig.altSfCompare = false; + const vscodeImpl = new vscode(); + const parseImpl = new lcovParse(); + + const fakeLcov = "TN:\nSF:c:\/dev\/vscode-coverage-gutters\/example\/test-coverage.js\nDA:1,1\nend_of_record"; + const fakeFile = "c:\/dev\/vscode-coverage-gutters\/example\/test-coverage.js"; + const indicators = new Indicators( + parseImpl, + vscodeImpl, + fakeConfig + ); + + indicators.extract(fakeLcov, fakeFile) + .then(function(data) { + assert.equal(data.length, 1); + return done(); + }) + .catch(function(error) { + return done(error); + }); + }); + + test("#extract: should find a matching file with relative match mode", function(done) { + fakeConfig.altSfCompare = true; + const vscodeImpl = new vscode(); + vscodeImpl.getRootPath = function() { return "vscode-coverage-gutters"; }; + const parseImpl = new lcovParse(); + const fakeLinuxLcov = "TN:\nSF:/mnt/c/dev/vscode-coverage-gutters/example/test-coverage.js\nDA:1,1\nend_of_record"; + const fakeFile = "c:\/dev\/vscode-coverage-gutters\/example\/test-coverage.js"; + const indicators = new Indicators( + parseImpl, + vscodeImpl, + fakeConfig + ); + + indicators.extract(fakeLinuxLcov, fakeFile) + .then(function(data) { + assert.equal(data.length, 1); + return done(); + }) + .catch(function(error) { + return done(error); + }); }); }); \ No newline at end of file diff --git a/test/lcov.test.ts b/test/lcov.test.ts index 95452b2..df75cf3 100644 --- a/test/lcov.test.ts +++ b/test/lcov.test.ts @@ -2,26 +2,32 @@ import * as assert from "assert"; -import * as vscode from "vscode"; +import {vscode} from "../src/wrappers/vscode"; +import {fs} from "../src/wrappers/fs"; import {Lcov} from "../src/lcov"; suite("Lcov Tests", function() { + const fakeConfig = { + lcovFileName: "test.ts", + coverageDecorationType: { + key: "testKey", + dispose() {} + }, + gutterDecorationType: { + key: "testKey2", + dispose() {} + }, + altSfCompare: true + }; + test("Constructor should setup properly", function(done) { try { + const vscodeImpl = new vscode(); + const fsImpl = new fs(); const lcov = new Lcov( - { - lcovFileName: "test.ts", - coverageDecorationType: { - key: "testKey", - dispose() {} - }, - gutterDecorationType: { - key: "testKey2", - dispose() {} - } - }, - function(){}, - function(path: string){} + fakeConfig, + vscodeImpl, + fsImpl ); return done(); } catch(e) { @@ -31,27 +37,21 @@ suite("Lcov Tests", function() { }); test("#find: Should return error if no file found for lcovFileName", function(done) { + const vscodeImpl = new vscode(); + const fsImpl = new fs(); + + vscodeImpl.findFiles = function(path, exclude, filesToFind) { + assert.equal(path, "**/test.ts"); + assert.equal(exclude, "**/node_modules/**"); + assert.equal(filesToFind, 1); + return new Promise(function(resolve, reject) { + return resolve([]); + }); + }; const lcov = new Lcov( - { - lcovFileName: "test.ts", - coverageDecorationType: { - key: "testKey", - dispose() {} - }, - gutterDecorationType: { - key: "testKey2", - dispose() {} - } - }, - function(path, exclude, filesToFind) { - assert.equal(path, "**/test.ts"); - assert.equal(exclude, "**/node_modules/**"); - assert.equal(filesToFind, 1); - return new Promise(function(resolve, reject) { - return resolve([]); - }); - }, - function(path: string){} + fakeConfig, + vscodeImpl, + fsImpl ); lcov.find() @@ -66,27 +66,21 @@ suite("Lcov Tests", function() { }); test("#find: Should return a file system path", function(done) { + const vscodeImpl = new vscode(); + const fsImpl = new fs(); + + vscodeImpl.findFiles = function(path, exclude, filesToFind) { + assert.equal(path, "**/test.ts"); + assert.equal(exclude, "**/node_modules/**"); + assert.equal(filesToFind, 1); + return new Promise(function(resolve, reject) { + return resolve([{fsPath: "path/to/greatness/test.ts"}]); + }); + }; const lcov = new Lcov( - { - lcovFileName: "test.ts", - coverageDecorationType: { - key: "testKey", - dispose() {} - }, - gutterDecorationType: { - key: "testKey2", - dispose() {} - } - }, - function(path, exclude, filesToFind) { - assert.equal(path, "**/test.ts"); - assert.equal(exclude, "**/node_modules/**"); - assert.equal(filesToFind, 1); - return new Promise(function(resolve, reject) { - return resolve([{fsPath: "path/to/greatness/test.ts"}]); - }); - }, - function(path: string){} + fakeConfig, + vscodeImpl, + fsImpl ); lcov.find() @@ -100,23 +94,18 @@ suite("Lcov Tests", function() { }); test("#load: Should reject when readFile returns an error", function(done) { + const vscodeImpl = new vscode(); + const fsImpl = new fs(); + + fsImpl.readFile = function(path: string, cb) { + assert.equal(path, "pathtofile"); + const error: NodeJS.ErrnoException = new Error("could not read from fs"); + return cb(error, null); + }; const lcov = new Lcov( - { - lcovFileName: "test.ts", - coverageDecorationType: { - key: "testKey", - dispose() {} - }, - gutterDecorationType: { - key: "testKey2", - dispose() {} - } - }, - function(path, exclude, filesToFind) {}, - function(path: string, cb) { - assert.equal(path, "pathtofile"); - return cb(new Error("could not read from fs")); - } + fakeConfig, + vscodeImpl, + fsImpl ); lcov.load("pathtofile") @@ -131,23 +120,17 @@ suite("Lcov Tests", function() { }); test("#load: Should return a data string", function(done) { + const vscodeImpl = new vscode(); + const fsImpl = new fs(); + + fsImpl.readFile = function(path: string, cb) { + assert.equal(path, "pathtofile"); + return cb(null, new Buffer("lcovhere")); + }; const lcov = new Lcov( - { - lcovFileName: "test.ts", - coverageDecorationType: { - key: "testKey", - dispose() {} - }, - gutterDecorationType: { - key: "testKey2", - dispose() {} - } - }, - function(path, exclude, filesToFind) {}, - function(path: string, cb) { - assert.equal(path, "pathtofile"); - return cb(null, "lcovhere"); - } + fakeConfig, + vscodeImpl, + fsImpl ); lcov.load("pathtofile")