Skip to content

Commit

Permalink
update testing with fs
Browse files Browse the repository at this point in the history
  • Loading branch information
ptpaterson committed Nov 26, 2024
1 parent d149bf2 commit 89b3eb3
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 17 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ experiments
.log
coverage
test-results.xml
.history
.history
/test/test-homedir
42 changes: 42 additions & 0 deletions src/commands/shell.mjs
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
//@ts-check

import path from "node:path";
import repl from "node:repl";

import { container } from "../cli.mjs";
import {
// ensureDbScopeClient,
commonConfigurableQueryOptions,
} from "../lib/command-helpers.mjs";
import { dirExists, fileExists } from "../lib/file-util.mjs";
import { performQuery } from "./eval.mjs";

async function doShell(argv) {
const fs = container.resolve("fs");
const logger = container.resolve("logger");
let completionPromise;

if (argv.dbPath) logger.stdout(`Starting shell for database ${argv.dbPath}`);
logger.stdout("Type Ctrl+D or .exit to exit the shell");

// Setup history file
const homedir = container.resolve("homedir");
const historyDir = path.join(homedir.toString(), ".fauna");
if (!dirExists(historyDir)) {
fs.mkdirSync(historyDir, { recursive: true });
}
const historyFile = path.join(historyDir, "history");
if (!fileExists(historyFile)) {
fs.writeFileSync(historyFile, "{}");
}

/** @type {import('node:repl').ReplOptions} */
const replArgs = {
prompt: `${argv.db_path || ""}> `,
Expand All @@ -27,9 +41,18 @@ async function doShell(argv) {
input: container.resolve("stdinStream"),
eval: await buildCustomEval(argv),
terminal: true,
historySize: 1000
};

const shell = repl.start(replArgs);

// Setup history
shell.setupHistory(historyFile, (err) => {
if (err) {
logger.stderr(`Error setting up history: ${err.message}`);
}
});

// eslint-disable-next-line no-console
shell.on("error", console.error);

Expand All @@ -55,6 +78,25 @@ async function doShell(argv) {
shell.prompt();
},
},
{
cmd: "clearhistory",
help: "Clear command history",
action: async () => {
try {
await fs.writeFileSync(historyFile, '');
logger.stdout('History cleared');
// Reinitialize history
shell.setupHistory(historyFile, (err) => {
if (err) {
logger.stderr(`Error reinitializing history: ${err.message}`);
}
});
} catch (err) {
logger.stderr(`Error clearing history: ${err.message}`);
}
shell.prompt();
},
}
].forEach(({ cmd, ...cmdOptions }) => shell.defineCommand(cmd, cmdOptions));

return completionPromise;
Expand Down
5 changes: 4 additions & 1 deletion src/config/setup-test-container.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ export function setupTestContainer() {
// wrap it in a spy so we can record calls, but use the
// real implementation
parseYargs: awilix.asValue(spy(parseYargs)),
fs: awilix.asValue(stub(fs)),
// Stubbing node:fs globally breaks tests where we want fs to work normally.
// Let tests decide how fs should be stubbed out if they don't want the real
// implementation.
fs: awilix.asValue(fs),
fsp: awilix.asValue({
unlink: stub(),
writeFile: stub(),
Expand Down
28 changes: 15 additions & 13 deletions src/lib/file-util.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
//@ts-check

import fs from "node:fs";
import path from "node:path";

import { container } from "../cli.mjs";
Expand All @@ -22,15 +20,8 @@ export function fixPath(path) {
* @returns {boolean}
*/
export function dirExists(path) {
const stat = fs.statSync(fixPath(path), {
// returns undefined instead of throwing if the file doesn't exist
throwIfNoEntry: false,
});
if (stat === undefined || !stat.isDirectory()) {
return false;
} else {
return true;
}
const fs = container.resolve("fs");
return fs.existsSync(fixPath(path));
}

/**
Expand All @@ -39,6 +30,7 @@ export function dirExists(path) {
* @returns {boolean}
*/
export function dirIsWriteable(path) {
const fs = container.resolve("fs");
try {
fs.accessSync(fixPath(path), fs.constants.W_OK);
} catch (e) {
Expand All @@ -54,7 +46,8 @@ export function dirIsWriteable(path) {
* @param {string} path - The path to the file.
* @returns {boolean} - Returns true if the file exists, otherwise false.
*/
function fileExists(path) {
export function fileExists(path) {
const fs = container.resolve("fs");
try {
fs.readFileSync(fixPath(path));
return true;
Expand All @@ -69,6 +62,7 @@ function fileExists(path) {
* @returns {Object.<string, any>} - The parsed JSON content of the file.
*/
function getJSONFileContents(path) {
const fs = container.resolve("fs");
// Open file for reading and writing without truncating
const fileContent = fs.readFileSync(path, { flag: "r+" }).toString();
if (!fileContent) {
Expand Down Expand Up @@ -103,11 +97,13 @@ export class Credentials {
* @param {string} [filename=""] - The name of the credentials file.
*/
constructor(filename = "") {
const fs = container.resolve("fs");

this.logger = container.resolve("logger");
this.filename = filename;

const homedir = container.resolve("homedir");
this.credsDir = path.join(homedir.toString(),".fauna/credentials");
this.credsDir = path.join(homedir.toString(), ".fauna/credentials");

if (!dirExists(this.credsDir)) {
fs.mkdirSync(this.credsDir, { recursive: true });
Expand Down Expand Up @@ -146,6 +142,8 @@ export class Credentials {
* @param {string} params.key - The key to index the creds under
*/
save({ creds, overwrite = false, key }) {
const fs = container.resolve("fs");

try {
const existingContent = overwrite ? {} : this.get();
const newContent = {
Expand All @@ -159,6 +157,8 @@ export class Credentials {
}

delete(key) {
const fs = container.resolve("fs");

try {
const existingContent = this.get();
delete existingContent[key];
Expand Down Expand Up @@ -193,6 +193,8 @@ export class SecretKey extends Credentials {
* @param {string} opts.creds.role - The role to save the secret
*/
this.save = ({ creds, overwrite = false, key }) => {
const fs = container.resolve("fs");

try {
const existingContent = overwrite ? {} : this.get();
const existingAccountSecrets = existingContent[key] || {};
Expand Down
2 changes: 2 additions & 0 deletions test/login.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//@ts-check
import node_fs from "node:fs";
import path from "node:path";

import * as awilix from "awilix";
Expand Down Expand Up @@ -72,6 +73,7 @@ describe("login", function () {
accountClient: awilix.asFunction(mockAccountClient).scoped(),
accountCreds: awilix.asClass(AccountKey).scoped(),
homedir: awilix.asFunction(() => homedir).scoped(),
fs: awilix.asValue(stub(node_fs)),
});
fs = container.resolve("fs");
});
Expand Down
2 changes: 2 additions & 0 deletions test/schema/pull.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//@ts-check
import node_fs from "node:fs";

import * as awilix from "awilix";
import { expect } from "chai";
Expand Down Expand Up @@ -30,6 +31,7 @@ describe("schema pull", function () {
deleteUnusedSchemaFiles: awilix.asValue(
sinon.spy(deleteUnusedSchemaFiles),
),
fs: awilix.asValue(sinon.stub(node_fs)),
});
logger = container.resolve("logger");
fetch = container.resolve("fetch");
Expand Down
58 changes: 56 additions & 2 deletions test/shell.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
//@ts-check

import { EOL } from "node:os";
import path from "node:path";

import * as awilix from "awilix";
import { expect } from "chai";
import sinon from "sinon";

Expand Down Expand Up @@ -45,10 +46,19 @@ const v4Object2 = `{

describe("shell", function () {
let container, stdin, stdout, logger;
let prompt = `${EOL}\x1B[1G\x1B[0J> \x1B[3G`;

const promptReset = "\x1B[1G\x1B[0J> ";
const prompt = `${EOL}${promptReset}\x1B[3G`;
const getHistoryPrompt = (text) => `${promptReset}${text}\u001b[${3 + text.length}G`

beforeEach(() => {
const __dirname = import.meta.dirname;
const homedir = path.join(__dirname, "../test/test-homedir");

container = setupContainer();
container.register({
homedir: awilix.asFunction(() => homedir).scoped(),
});
stdin = container.resolve("stdinStream");
stdout = container.resolve("stdoutStream");
logger = container.resolve("logger");
Expand Down Expand Up @@ -89,6 +99,50 @@ describe("shell", function () {
it.skip("can read input from a file", async function () {});

it.skip("can set a connection timeout", async function () {});

const upArrow = "\x1b[A";
const downArrow = "\x1b[B";

it("can keep track of history", async function () {
// start the shell
const runPromise = run(`shell --secret "secret" --typecheck`, container);

// send our first command
stdin.push(`1\n2\n3\n`);
await stdout.waitForWritten();

// navigate up through history
stdout.clear();
stdin.push(upArrow);
await stdout.waitForWritten();
expect(stdout.getWritten()).to.equal(getHistoryPrompt("3"));
stdout.clear();
stdin.push(upArrow);
await stdout.waitForWritten();
expect(stdout.getWritten()).to.equal(getHistoryPrompt("2"));
stdout.clear();
stdin.push(upArrow);
await stdout.waitForWritten();
expect(stdout.getWritten()).to.equal(getHistoryPrompt("1"));
stdout.clear();
stdin.push(downArrow);
await stdout.waitForWritten();
expect(stdout.getWritten()).to.equal(getHistoryPrompt("2"));
stdout.clear();
stdin.push(downArrow);
await stdout.waitForWritten();
expect(stdout.getWritten()).to.equal(getHistoryPrompt("3"));

expect(container.resolve("stderrStream").getWritten()).to.equal("");

stdin.push(null);

return runPromise;
});

it.skip("can clear history", async function () {});

it.skip("can save history between sessions", async function () {});
});

describe("v10", function () {
Expand Down

0 comments on commit 89b3eb3

Please sign in to comment.