Skip to content

Commit

Permalink
Integration tests setup (#212)
Browse files Browse the repository at this point in the history
* Make it testable
* Introduce a simple integration test, so it is easier to spot problems with ESLint version upgrade
* Add a few changes to Makefile:
  - `make integration`
  - `DEBUG=true make integration`
  - `DEBUG=true make test`
  - `make citest` -> runs `npm run test integration`
  • Loading branch information
filipesperandio authored Mar 16, 2017
1 parent d532765 commit f21a8bf
Show file tree
Hide file tree
Showing 10 changed files with 342 additions and 247 deletions.
17 changes: 15 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
.PHONY: image test citest
.PHONY: image test citest integration

IMAGE_NAME ?= codeclimate/codeclimate-eslint

NPM_TEST_TARGET ?= test
NPM_INTEGRATION_TARGET ?= integration

DEBUG ?= false
ifeq ($(DEBUG),true)
NPM_TEST_TARGET = test.debug
NPM_INTEGRATION_TARGET = integration.debug
endif

image:
docker build --rm -t $(IMAGE_NAME) .

integration: image
docker run -ti --rm \
--volume $(PWD):/code \
--workdir /code \
$(IMAGE_NAME) npm run $(NPM_INTEGRATION_TARGET)

test: image
docker run -ti --rm \
--volume $(PWD):/code \
Expand All @@ -16,4 +29,4 @@ test: image
citest:
docker run --rm \
--workdir /usr/src/app \
$(IMAGE_NAME) npm run test
$(IMAGE_NAME) npm run test integration
243 changes: 4 additions & 239 deletions bin/eslint.js
Original file line number Diff line number Diff line change
@@ -1,244 +1,9 @@
#!/usr/src/app/bin/node_gc

var CODE_DIR = "/code";
const CODE_DIR = "/code";
process.chdir(CODE_DIR);

// Redirect `console.log` so that we are the only ones
// writing to STDOUT
var stdout = console.log;
console.log = console.error;
const ESLint = require("../lib/eslint");
const exitCode = ESLint.run({ dir: CODE_DIR });


var eslint = require('../lib/eslint-patch')();

var CLIEngine = eslint.CLIEngine;
var docs = require('../lib/docs')();
var fs = require("fs");
var glob = require("glob");
var options = { extensions: [".js"], ignore: true, reset: false, useEslintrc: true };
var cli; // instantiation delayed until after options are (potentially) modified
var debug = false;
var BatchSanitizer = require("../lib/batch_sanitizer");
var ignoreWarnings = false;
var ESLINT_WARNING_SEVERITY = 1;
var checks = require("../lib/checks");
var validateConfig = require("../lib/validate_config");
var computeFingerprint = require("../lib/compute_fingerprint");
const ConfigUpgrader = require("../lib/config_upgrader");

// a wrapper for emitting perf timing
function runWithTiming(name, fn) {
var start = new Date()
, rv = fn()
, duration = (new Date() - start) / 1000;
if (debug) {
console.error("eslint.timing." + name + ": " + duration + "s");
}
return rv;
}

function contentBody(check) {
var content = docs.get(check) || "For more information visit ";
return content + "Source: http://eslint.org/docs/rules/\n";
}

function buildIssueJson(message, path) {
// ESLint doesn't emit a ruleId in the
// case of a fatal error (such as an invalid
// token)
var checkName = message.ruleId;
if(message.fatal) {
checkName = "fatal";
}
var line = message.line || 1;
var column = message.column || 1;

var issue = {
type: "issue",
categories: checks.categories(checkName),
check_name: checkName,
description: message.message,
content: {
body: contentBody(checkName)
},
location: {
path: path,
positions: {
begin: {
line: line,
column: column
},
end: {
line: line,
column: column
}
}
},
remediation_points: checks.remediationPoints(checkName, message, cli.getConfigForFile(path))
};

var fingerprint = computeFingerprint(path, checkName, message.message);

if (fingerprint) {
issue["fingerprint"] = fingerprint;
}

return JSON.stringify(issue);
}

function isFileWithMatchingExtension(file, extensions) {
var stats = fs.lstatSync(file);
var extension = "." + file.split(".").pop();
return (
stats.isFile() &&
!stats.isSymbolicLink()
&& extensions.indexOf(extension) >= 0
);
}

function isFileIgnoredByLibrary(file) {
return cli.isPathIgnored(file);
}

function prunePathsWithinSymlinks(paths) {
// Extracts symlinked paths and filters them out, including any child paths
var symlinks = paths.filter(function(path) {
return fs.lstatSync(path).isSymbolicLink();
});

return paths.filter(function(path) {
var withinSymlink = false;
symlinks.forEach(function(symlink) {
if (path.indexOf(symlink) === 0) {
withinSymlink = true;
}
});
return !withinSymlink;
});
}

function inclusionBasedFileListBuilder(includePaths) {
// Uses glob to expand the files and directories in includePaths, filtering
// down to match the list of desired extensions.
return function(extensions) {
var analysisFiles = [];

includePaths.forEach(function(fileOrDirectory, i) {
if ((/\/$/).test(fileOrDirectory)) {
// if it ends in a slash, expand and push
var filesInThisDirectory = glob.sync(
fileOrDirectory + "/**/**"
);
prunePathsWithinSymlinks(filesInThisDirectory).forEach(function(file, j){
if (!isFileIgnoredByLibrary(file) && isFileWithMatchingExtension(file, extensions)) {
analysisFiles.push(file);
}
});
} else {
if (!isFileIgnoredByLibrary(fileOrDirectory) && isFileWithMatchingExtension(fileOrDirectory, extensions)) {
analysisFiles.push(fileOrDirectory);
}
}
});

return analysisFiles;
};
}

var buildFileList;
runWithTiming("engineConfig", function () {
if (fs.existsSync("/config.json")) {
var engineConfig = JSON.parse(fs.readFileSync("/config.json"));

if (engineConfig.include_paths) {
buildFileList = inclusionBasedFileListBuilder(
engineConfig.include_paths
);
} else {
// No explicit includes, let's try with everything
buildFileList = inclusionBasedFileListBuilder(["./"]);
}

var userConfig = engineConfig.config || {};
if (userConfig.config) {
options.configFile = CODE_DIR + "/" + userConfig.config;
options.useEslintrc = false;
}

if (userConfig.extensions) {
options.extensions = userConfig.extensions;
}

if (userConfig.ignore_path) {
options.ignorePath = userConfig.ignore_path;
}

if (userConfig.ignore_warnings) {
ignoreWarnings = true;
}

if (userConfig.debug) {
debug = true;
}
}

cli = new CLIEngine(options);
});

var analysisFiles = runWithTiming("buildFileList", function() {
return buildFileList(options.extensions);
});

function analyzeFiles() {
var batchNum = 0
, batchSize = 10
, batchFiles
, batchReport
, sanitizedBatchFiles;

while(analysisFiles.length > 0) {
batchFiles = analysisFiles.splice(0, batchSize);
sanitizedBatchFiles = (new BatchSanitizer(batchFiles)).sanitizedFiles();

if (debug) {
process.stderr.write("Analyzing: " + batchFiles + "\n");
}

runWithTiming("analyze-batch-" + batchNum, function() {
batchReport = cli.executeOnFiles(sanitizedBatchFiles);
});
runWithTiming("report-batch" + batchNum, function() {
batchReport.results.forEach(function(result) {
var path = result.filePath.replace(/^\/code\//, "");

result.messages.forEach(function(message) {
if (ignoreWarnings && message.severity === ESLINT_WARNING_SEVERITY) { return; }

var issueJson = buildIssueJson(message, path);
process.stdout.write(issueJson + "\u0000\n");
});
});
});
runWithTiming("gc-batch-" + batchNum, function() {
batchFiles = null;
batchReport = null;
global.gc();
});

batchNum++;
}
}

if (validateConfig(options.configFile)) {
console.error("ESLint is running with the " + cli.getConfigForFile(null).parser + " parser.");

for (const line of ConfigUpgrader.upgradeInstructions(options.configFile, analysisFiles, process.cwd())) {
console.error(line);
}

analyzeFiles();
} else {
console.error("No rules are configured. Make sure you have added a config file with rules enabled.");
console.error("See our documentation at https://docs.codeclimate.com/docs/eslint for more information.");
process.exit(1);
}
process.exit(exitCode);
39 changes: 39 additions & 0 deletions integration/eslint_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const sinon = require("sinon");
const expect = require("chai").expect;

global.gc = function(){};

const STDOUT = console.log;
const STDERR = console.error;

describe("eslint integration", function() {
describe("eslintrc has not supported plugins", function() {
before(function() {
console.log = sinon.spy();
console.error = sinon.spy();
});

after(function() {
console.log = STDOUT;
console.error = STDERR;
});

it("does not raise any error", function() {
this.timeout(3000);

var consoleStub = {
log: sinon.spy(),
error: sinon.spy()
};

function execute() {
const ESLint = require('../lib/eslint');
ESLint.run({ dir: __dirname, configPath: `${__dirname}/with_unsupported_plugins/config.json`});
}

expect(execute).to.not.throw();
expect(console.log.called).to.be.ok;
});
});

});
10 changes: 10 additions & 0 deletions integration/with_unsupported_plugins/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"enabled": true,
"config": {
"config": "with_unsupported_plugins/eslintrc.yml",
"debug": "false"
},
"include_paths": [
"/usr/src/app/integration/with_unsupported_plugins/index.js"
]
}
21 changes: 21 additions & 0 deletions integration/with_unsupported_plugins/eslintrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
env:
es6: true
node: true
parserOptions:
sourceType: module
plugins:
- node
- not_supported
extends:
- not_valid
- 'plugin:invalidplugin/recommended'
- 'eslint:recommended'
- 'plugin:node/recommended'
rules:
invalidplugin/rule: 1
node/exports-style: [error, module.exports]
indent: [error, 4]
linebreak-style: [error, unix]
quotes: [error, double]
semi: [error, always]
no-console: off
2 changes: 2 additions & 0 deletions integration/with_unsupported_plugins/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
function dummy() {
}
4 changes: 2 additions & 2 deletions lib/docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ var fs = require('fs')

function Docs() {

var docs = Object.create(null);
var docs = {};

function get(ruleId) {
return docs[ruleId];
}

var docsDir = path.join(__dirname, '/docs/rules');

fs.readdirSync(docsDir).forEach(function(file) {
fs.existsSync(docsDir) && fs.readdirSync(docsDir).forEach(function(file) {
var content = fs.readFileSync(docsDir + '/' + file, 'utf8');

// Remove the .md extension from the filename
Expand Down
8 changes: 4 additions & 4 deletions lib/eslint-patch.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ module.exports = function patch() {

const skippedModules = [];
function warnModuleNotSupported(name) {
if(skippedModules.indexOf(name) < 0) {
skippedModules.push(name);
console.error(`Module not supported: ${name}`);
}
if(skippedModules.indexOf(name) < 0) {
skippedModules.push(name);
console.error(`Module not supported: ${name}`);
}
}

const resolve = ModuleResolver.prototype.resolve;
Expand Down
Loading

0 comments on commit f21a8bf

Please sign in to comment.