Skip to content

Commit

Permalink
Test and lint (#11)
Browse files Browse the repository at this point in the history
This PR aims to add tests and linting for #3 as well as GitHub CI. To summarize, the commits add: 

- Mocha tests, similar to the "integration" tests at n_. One test is skipped because it fails on a possible bug. I'm happy to delete that test, or to try to find a fix for it on this or another PR.  
- a GitHub workflow CI similar to the one from shelljs-plugin-inspect. I used this to help me see where tests were passing/failing on Windows and Mac since I use Linux, and to check that applying linting didn't break stuff. Experiments with this CI workflow showed me that the tests work on NodeJS 10 through 14 but fail on NodeJS versions >=15 for Mac, Windows, and Linux with an error message related to `repl.history` in #6. I'm happy to delete/modify this commit though if you don't want it.
- linting styles (copied over from the test folder at ShellJS).
- The new linting applied to index.js.

As always, I'm happy to make any changes. It's been fun learning about REPLs. Thank you for the opportunity to learn more about a really cool project!
  • Loading branch information
JessieFrance authored Feb 20, 2022
1 parent 0f2ad2b commit 24adc87
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 44 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
35 changes: 35 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"env": {
"node": true
},
"extends": "airbnb-base",
"rules": {
"arrow-parens": "off",
"comma-dangle": "off",
"curly": "off",
"global-require": "off",
"import/no-dynamic-require": "off",
"import/no-mutable-exports": "off",
"indent": "off",
"max-len": "off",
"no-bitwise": "off",
"no-console": "off",
"no-param-reassign": "off",
"no-plusplus": "off",
"no-underscore-dangle": "off",
"no-var": "error",
"operator-linebreak": "off",
"prefer-arrow-callback": "off",
"prefer-const": "error",
"prefer-destructuring": "off",
"prefer-numeric-literals": "off",
"prefer-template": "off",
"spaced-comment": ["error", "always", { "markers": ["@", "@include"], "exceptions": ["@"] }],
"vars-on-top": "off",
"new-cap": ["error", {
"capIsNewExceptions": [
"ShellString"
]}
]
}
}
26 changes: 26 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: CI
on:
- push
- pull_request
jobs:
test:
name: Node.js ${{ matrix.node-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
node-version:
- 10
- 12
- 14
os:
- ubuntu-latest
- macos-latest
- windows-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm run test
81 changes: 38 additions & 43 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
#!/usr/bin/env node
'use strict';

// ShellJS plugins
require('shelljs-plugin-clear');
require('shelljs-plugin-inspect');
require('shelljs-plugin-open');
require('shelljs-plugin-sleep');

var util = require('util');
var repl = require('repl');
var argv = require('minimist')(process.argv.slice(2));
var replHistory = require('repl.history');
var osHomedir = require('os-homedir');
var path = require('path');
const util = require('util');
const repl = require('repl');
const argv = require('minimist')(process.argv.slice(2));
const replHistory = require('repl.history');
const osHomedir = require('os-homedir');
const path = require('path');

var shell;
var json;
var isLocal;
let shell;
let json;
let isLocal;
try {
if (argv.path[0] === '~')
argv.path = argv.path.replace('~', osHomedir());
var localShellJS = path.resolve(argv.path);
if (argv.path[0] === '~') argv.path = argv.path.replace('~', osHomedir());
const localShellJS = path.resolve(argv.path);
shell = require('require-relative')(localShellJS, process.cwd());
json = require(path.join(localShellJS, 'package.json'));
isLocal = true;
Expand All @@ -31,82 +29,79 @@ try {
}

// Create the prompt
var myprompt = argv.prompt || 'shelljs %v%l $ ';
myprompt = myprompt.replace(/%./g, (function() {
var option = {
let myprompt = argv.prompt || 'shelljs %v%l $ ';
myprompt = myprompt.replace(/%./g, (function fn() {
const option = {
'%%': '%',
'%v': json.version,
'%l': (isLocal ? ' [local]' : '')
};
return function(match) {
return option[match];
};
})());
return (match) => option[match];
}()));

var replServer = repl.start({
const replServer = repl.start({
prompt: myprompt,
replMode: process.env.NODE_REPL_MODE === 'strict' || argv.use_strict ? repl.REPL_MODE_STRICT : repl.REPL_MODE_MAGIC
});

// save repl history
var HISTORY_FILE = path.join(osHomedir(), '.n_shell_history');
const HISTORY_FILE = path.join(osHomedir(), '.n_shell_history');
replHistory(replServer, HISTORY_FILE);

// Newer versions of node use a symbol called util.inspect.custom.
var inspectAttribute = util.inspect.custom || 'inspect';
const inspectAttribute = util.inspect.custom || 'inspect';

function wrap(fun, key) {
if (typeof fun !== 'function') {
return fun; // not a function
} else {
var outerRet = function() {
var ret = fun.apply(this, arguments);
}
const outerRet = (...args) => {
const ret = fun.apply(this, args);
// Polyfill .inspect() method
function emptyInspect() {
return '';
}

if (ret instanceof Object) {
// Polyfill .inspect() method
function emptyInspect() {
return '';
}
var oldInspect;
let oldInspect;
if (ret[inspectAttribute]) {
oldInspect = ret[inspectAttribute].bind(ret);
} else {
oldInspect = function() { return ''; }
oldInspect = () => '';
}
if (key === 'echo' || key === 'exec') {
ret[inspectAttribute] = emptyInspect;
} else if (key === 'pwd' || key === 'which') {
ret[inspectAttribute] = function () {
var oldResult = oldInspect();
ret[inspectAttribute] = () => {
const oldResult = oldInspect();
return oldResult.match(/\n$/) ? oldResult : oldResult + '\n';
};
}
}
return ret;
};
outerRet[inspectAttribute] = outerRet[inspectAttribute] || function () { return this(); };
outerRet[inspectAttribute] = outerRet[inspectAttribute] || function thisFn() { return this(); };
return outerRet;
}
}

argv.no_global = argv.no_global || argv.local || argv.n;

// Add inspect() method, if it doesn't exist
if (!argv.noinspect) {
for (var key in shell) {
Object.keys(shell).forEach((key) => {
try {
shell[key] = wrap(shell[key], key);
} catch (e) {}
}
} catch (e) { /* empty */ }
});
}

if (argv.no_global) {
if (typeof argv.no_global !== 'string')
argv.no_global = 'shell';
if (typeof argv.no_global !== 'string') argv.no_global = 'shell';
replServer.context[argv.no_global] = shell;
} else {
for (var key in shell) {
Object.keys(shell).forEach((key) => {
replServer.context[key] = shell[key];
}
});
}

module.exports = replServer;
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"n_shell": "index.js"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"test": "mocha test/*.js",
"lint": "eslint .",
"changelog": "shelljs-changelog",
"release:major": "shelljs-release major",
"release:minor": "shelljs-release minor",
Expand Down Expand Up @@ -37,6 +38,10 @@
"shelljs-plugin-sleep": "^0.2.1"
},
"devDependencies": {
"eslint": "^8.9.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.25.4",
"mocha": "^9.2.1",
"shelljs-changelog": "^0.2.6",
"shelljs-release": "^0.5.1"
}
Expand Down
144 changes: 144 additions & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/* eslint-env mocha */
const fs = require('fs');
const { spawn } = require('child_process');
const assert = require('assert');

// Set up variables for testing.
const workDir = process.cwd();
const nodeEntry = `${workDir}/index.js`;
const temporaryFile = 'test/tempFile.txt';
const testString = 'First line^^^^hip\nSecond line>>>>hip\nthird~~~hurray!\nLine number four.';

const runREPL = (child, commands) => new Promise((resolve) => {
// Set up output and errors from stdin and stdout.
const output = [];
const err = [];
child.stdout.on('data', (data) => output.push(data.toString()));
child.stderr.on('data', (data) => err.push(data.toString()));

// Run input commands and end.
child.stdin.setEncoding('utf-8');
commands.forEach(c => child.stdin.write(`${c}\n`));
child.stdin.end();

// Exit.
child.on('exit', (code) => resolve({ code, err, output }));
});

before(() => {
try {
fs.writeFileSync(temporaryFile, testString);
} catch (error) {
throw new Error(`Unable to write temporary file ${temporaryFile} for tests.`);
}
});

after(() => {
try {
fs.unlinkSync(temporaryFile);
} catch (error) {
throw new Error(`Unable to delete file ${temporaryFile} after tests.`);
}
});

describe('running commands inside REPL', () => {
it('executes non-ShellJS commands', async () => {
const inputCommands = [
'function add(a, b) {',
'return a + b;',
'}',
'add(2179242.724, 3198234.878)'
];

const nShell = spawn('node', [nodeEntry]);
const { output, code } = await runREPL(nShell, inputCommands);
assert.equal(code, 0);
assert.equal(output.join().includes('5377477.602'), true);
});

it('executes ShellJS commands', async () => {
const inputCommands = [
'pwd()',
`cat('${temporaryFile}')`
];

const nShell = spawn('node', [nodeEntry]);
const { code, output } = await runREPL(nShell, inputCommands);
const result = output.join();
assert.equal(code, 0);
assert.equal(result.includes(workDir), true);
assert.equal(result.includes(testString), true);
});

it('lets the user pick a prompt to run in the REPL', async () => {
const myPrompt = 'a-w-e-s-o-m-e-s-h-e-l-l-->';
const nShell = spawn('node', [nodeEntry, `--prompt=${myPrompt}`]);
const { code, output } = await runREPL(nShell, ['\n']);
const result = output.join();
assert.equal(code, 0);
assert.equal(result.includes(myPrompt), true);
});
});

describe('running REPL commands in strict mode', () => {
const inputCommands = ['var undefined = 3;'];
const strictModeErrMsg = 'Cannot assign to read only property \'undefined\'';

it('does not enforce strict mode by default', async () => {
const nShell = spawn('node', [nodeEntry]);
const { code, output } = await runREPL(nShell, inputCommands);
const result = output.join();
assert.equal(code, 0);
assert.equal(result.includes(strictModeErrMsg), false);
});

it('allows strict mode as an option', async () => {
const nShell = spawn('node', [nodeEntry, '--use_strict']);
const { code, output } = await runREPL(nShell, inputCommands);
const result = output.join();
assert.equal(code, 0);
assert.equal(result.includes(strictModeErrMsg), true);
});
});

describe('respecting namespaces', () => {
it('says global commands are undefined in local namespace', async () => {
const nShell = spawn('node', [nodeEntry, '--no_global']);
const { code, output } = await runREPL(nShell, ['ls()', 'pwd()']);
assert.equal(code, 0);
const result = output.join();
assert.equal(result.includes('ls is not defined'), true);
assert.equal(result.includes('pwd is not defined'), true);
});

it('runs a command in local shell namespace', async () => {
const inputCommands = [`shell.grep('hurray', '${temporaryFile}')`];
const nShell = spawn('node', [nodeEntry, '--no_global']);
const { code, output } = await runREPL(nShell, inputCommands);
assert.equal(code, 0);
const result = output.join();
assert.equal(result.includes('third~~~hurray'), true);
});

it('runs a command in local user-defined namespace', async () => {
const inputCommands = [`$.grep('hurray', '${temporaryFile}')`];
const nShell = spawn('node', [nodeEntry, '--no_global=$']);
const { code, output } = await runREPL(nShell, inputCommands);
assert.equal(code, 0);
const result = output.join();
assert.equal(result.includes('third~~~hurray'), true);
});

it.skip('does not allow overwriting namespace', async () => {
const inputCommands = [
'shell = "fish"',
`shell.grep('hurray', '${temporaryFile}')`
];

const nShell = spawn('node', [nodeEntry, '--no_global']);
const { code, output } = await runREPL(nShell, inputCommands);
assert.equal(code, 0);
const result = output.join();
assert.equal(result.includes('third~~~hurray'), true);
});
});

0 comments on commit 24adc87

Please sign in to comment.