Skip to content

Commit

Permalink
feat: add the ability to override the default shell used by shelljs.e…
Browse files Browse the repository at this point in the history
…xec (#69)

Co-authored-by: peternhale <[email protected]>
  • Loading branch information
shetzel and peternhale committed Apr 27, 2021
1 parent 84bd215 commit 02d09d0
Show file tree
Hide file tree
Showing 10 changed files with 108 additions and 21 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Using a different file extension will help separate your unit tests from your NU
# Example NUTs

Here are some public github repos for plugins that use this library for NUTs:

- [@salesforce/plugin-alias](https://github.com/salesforcecli/plugin-alias/blob/main/test/commands/alias/set.nut.ts)
- [@salesforce/plugin-auth](https://github.com/salesforcecli/plugin-auth/blob/main/test/commands/auth/list.nut.ts)
- [@salesforce/plugin-config](https://github.com/salesforcecli/plugin-config/blob/main/test/commands/config/list.nut.ts)
Expand Down Expand Up @@ -96,6 +97,7 @@ const result = await execCmd('mycommand --myflag --json');
| TESTKIT_ENABLE_ZIP | Allows zipping the session dir when this is true and `TestSession.zip()` is called during a test. |
| TESTKIT_SETUP_RETRIES | Number of times to retry the setupCommands after the initial attempt before throwing an error. |
| TESTKIT_SETUP_RETRIES_TIMEOUT | Milliseconds to wait before the next retry of setupCommands. Defaults to 5000. |
| TESTKIT_EXEC_SHELL | The shell to use for all testkit shell executions rather than the shelljs default. |
| TESTKIT_HUB_USERNAME | Username of an existing, authenticated devhub org that TestSession will use to auto-authenticate for tests. |
| TESTKIT_JWT_CLIENT_ID | clientId of the connected app that TestSession will use to auto-authenticate for tests. |
| TESTKIT_JWT_KEY | JWT key file **contents** that TestSession will use to auto-authenticate for tests. |
Expand Down
9 changes: 9 additions & 0 deletions SAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ WARNING: THIS IS A GENERATED FILE. DO NOT MODIFY DIRECTLY. USE topics.json
- [Reusing projects during test runs](#reusing-projects-during-test-runs)
- [Using my actual homedir during test runs](#using-my-actual-homedir-during-test-runs)
- [Saving test artifacts after test runs](#saving-test-artifacts-after-test-runs)
- [Overriding the default shell](#overriding-the-default-shell)

### Best Practices

Expand Down Expand Up @@ -660,6 +661,14 @@ export TESTKIT_HOMEDIR=/Users/me
export TESTKIT_SAVE_ARTIFACTS=true
```

## Overriding the default shell

**_Usecase: I want the testkit to use a different shell rather than the default shelljs shell._**

```bash
export TESTKIT_EXEC_SHELL=powershell.exe
```

---

# Testkit Best Practices
Expand Down
5 changes: 5 additions & 0 deletions samples/topics.json
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@
"header": "Saving test artifacts after test runs",
"usecase": "I want to keep the test project, scratch org, and test session dir after the tests are done running to troubleshoot.",
"script": ["export TESTKIT_SAVE_ARTIFACTS=true"]
},
{
"header": "Overriding the default shell",
"usecase": "I want the testkit to use a different shell rather than the default shelljs shell.",
"script": ["export TESTKIT_EXEC_SHELL=powershell.exe"]
}
]
},
Expand Down
6 changes: 5 additions & 1 deletion src/execCmd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,16 @@ export interface ExecCmdResult<T = AnyJson> {
}

const buildCmdOptions = (options?: ExecCmdOptions): ExecCmdOptions => {
const defaults = {
const defaults: shelljs.ExecOptions = {
env: Object.assign({}, process.env),
cwd: process.cwd(),
timeout: 300000, // 5 minutes
silent: true,
};
const shellOverride = env.getString('TESTKIT_EXEC_SHELL');
if (shellOverride) {
defaults.shell = shellOverride;
}
return { ...defaults, ...options };
};

Expand Down
35 changes: 20 additions & 15 deletions src/hubAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ export const prepareForAuthUrl = (homeDir: string): string => {
*/
export const testkitHubAuth = (homeDir: string, authStrategy: AuthStrategy = getAuthStrategy()): void => {
const logger = debug('testkit:authFromStubbedHome');
const execOpts: shell.ExecOptions = { silent: true };
const shellOverride = env.getString('TESTKIT_EXEC_SHELL');
if (shellOverride) {
execOpts.shell = shellOverride;
}

if (authStrategy === AuthStrategy.JWT) {
logger('trying jwt auth');
const jwtKey = prepareForJwt(homeDir);
Expand All @@ -86,8 +92,8 @@ export const testkitHubAuth = (homeDir: string, authStrategy: AuthStrategy = get
'TESTKIT_JWT_CLIENT_ID',
''
)} -f ${jwtKey} -r ${env.getString('TESTKIT_HUB_INSTANCE', DEFAULT_INSTANCE_URL)}`,
{ silent: true }
);
execOpts
) as shell.ShellString;
if (results.code !== 0) {
throw new Error(
`jwt:grant for org ${env.getString(
Expand All @@ -103,7 +109,7 @@ export const testkitHubAuth = (homeDir: string, authStrategy: AuthStrategy = get

const tmpUrl = prepareForAuthUrl(homeDir);

const shellOutput = shell.exec(`sfdx auth:sfdxurl:store -d -f ${tmpUrl}`, { silent: true });
const shellOutput = shell.exec(`sfdx auth:sfdxurl:store -d -f ${tmpUrl}`, execOpts) as shell.ShellString;
logger(shellOutput);
if (shellOutput.code !== 0) {
throw new Error(
Expand Down Expand Up @@ -153,8 +159,9 @@ export const transferExistingAuthToEnv = (authStrategy: AuthStrategy = getAuthSt
if (authStrategy !== AuthStrategy.REUSE) return;

const logger = debug('testkit:AuthReuse');
logger(`reading ${env.getString('TESTKIT_HUB_USERNAME', '')}.json`);
const authFileName = `${env.getString('TESTKIT_HUB_USERNAME', '')}.json`;
const devhub = env.getString('TESTKIT_HUB_USERNAME', '');
logger(`reading ${devhub}.json`);
const authFileName = `${devhub}.json`;
const hubAuthFileSource = path.join(env.getString('HOME') || os.homedir(), '.sfdx', authFileName);
const authFileContents = (fs.readJsonSync(hubAuthFileSource) as unknown) as AuthFields;
if (authFileContents.privateKey) {
Expand All @@ -167,24 +174,22 @@ export const transferExistingAuthToEnv = (authStrategy: AuthStrategy = getAuthSt
return;
}
if (authFileContents.refreshToken) {
const execOpts: shell.ExecOptions = { silent: true, fatal: true };
const shellOverride = env.getString('TESTKIT_EXEC_SHELL');
if (shellOverride) {
execOpts.shell = shellOverride;
}

// this is an org from web:auth or auth:url. Generate the authUrl and set in the env
logger('copying variables to env from org:display for AuthUrl');
const displayContents = JSON.parse(
shell.exec(`sfdx force:org:display -u ${env.getString('TESTKIT_HUB_USERNAME', '')} --verbose --json`, {
silent: true,
fatal: true,
}) as string
shell.exec(`sfdx force:org:display -u ${devhub} --verbose --json`, execOpts) as string
) as OrgDisplayResult;
logger(`found ${displayContents.result.sfdxAuthUrl}`);
env.setString('TESTKIT_AUTH_URL', displayContents.result.sfdxAuthUrl);
return;
}
throw new Error(
`Unable to reuse existing hub ${env.getString('TESTKIT_HUB_USERNAME', '')}. Check file ${env.getString(
'TESTKIT_HUB_USERNAME',
''
)}.json`
);
throw new Error(`Unable to reuse existing hub ${devhub}. Check file ${devhub}.json`);
};

interface OrgDisplayResult {
Expand Down
17 changes: 15 additions & 2 deletions src/testProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { inspect } from 'util';
import { debug, Debugger } from 'debug';
import * as shell from 'shelljs';
import { fs as fsCore } from '@salesforce/core';
import { env } from '@salesforce/kit';
import { genUniqueString } from './genUniqueString';
import { zipDir, ZipDirConfig } from './zip';

Expand All @@ -35,6 +36,9 @@ export class TestProject {
public dir: string;
private debug: Debugger;
private zipDir: (config: ZipDirConfig) => Promise<string>;
private shelljsExecOptions: shell.ExecOptions = {
silent: true,
};

public constructor(options: TestProjectOptions) {
this.debug = debug('testkit:project');
Expand All @@ -44,6 +48,11 @@ export class TestProject {

const destDir = options.destinationDir || tmpdir();

const shellOverride = env.getString('TESTKIT_EXEC_SHELL');
if (shellOverride) {
this.shelljsExecOptions.shell = shellOverride;
}

// Copy a dir containing a SFDX project to a dir for testing.
if (options?.sourceDir) {
const rv = shell.cp('-r', options.sourceDir, destDir);
Expand All @@ -59,7 +68,8 @@ export class TestProject {
throw new Error('git executable not found for creating a project from a git clone');
}
this.debug(`Cloning git repo: ${options.gitClone} to: ${destDir}`);
const rv = shell.exec(`git clone ${options.gitClone}`, { cwd: destDir, silent: true });
const execOpts = { ...this.shelljsExecOptions, ...{ cwd: destDir } };
const rv = shell.exec(`git clone ${options.gitClone}`, execOpts) as shell.ShellString;
if (rv.code !== 0) {
throw new Error(`git clone failed with error:\n${rv.stderr}`);
}
Expand All @@ -75,7 +85,10 @@ export class TestProject {
throw new Error('sfdx executable not found for creating a project using force:project:create command');
}
const name = options.name || genUniqueString('project_%s');
const rv = shell.exec(`sfdx force:project:create -n ${name} -d ${destDir}`, { silent: true });
const rv = shell.exec(
`sfdx force:project:create -n ${name} -d ${destDir}`,
this.shelljsExecOptions
) as shell.ShellString;
if (rv.code !== 0) {
throw new Error(`force:project:create failed with error:\n${rv.stderr}`);
}
Expand Down
13 changes: 11 additions & 2 deletions src/testSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export interface TestSessionOptions {
* TESTKIT_ENABLE_ZIP = allows zipping the session dir when this is true
* TESTKIT_SETUP_RETRIES = number of times to retry the setupCommands after the initial attempt before throwing an error
* TESTKIT_SETUP_RETRIES_TIMEOUT = milliseconds to wait before the next retry of setupCommands. Defaults to 5000
* TESTKIT_EXEC_SHELL = the shell to use for all testkit shell executions rather than the shelljs default.
*
* TESTKIT_HUB_USERNAME = username of an existing hub (authenticated before creating a session)
* TESTKIT_JWT_CLIENT_ID = clientId of connected app for auth:jwt:grant
Expand All @@ -89,6 +90,9 @@ export class TestSession extends AsyncOptionalCreatable<TestSessionOptions> {
private setupRetries: number;
private zipDir: typeof zipDir;
private options: TestSessionOptions;
private shelljsExecOptions: shell.ExecOptions = {
silent: true,
};

public constructor(options: TestSessionOptions = {}) {
super(options);
Expand All @@ -100,6 +104,11 @@ export class TestSession extends AsyncOptionalCreatable<TestSessionOptions> {
this.id = genUniqueString(`${this.createdDate.valueOf()}%s`);
this.setupRetries = env.getNumber('TESTKIT_SETUP_RETRIES', this.options.retries) || 0;

const shellOverride = env.getString('TESTKIT_EXEC_SHELL');
if (shellOverride) {
this.shelljsExecOptions.shell = shellOverride;
}

// Create the test session directory
this.overriddenDir = env.getString('TESTKIT_SESSION_DIR') || this.options.sessionDir;
this.dir = this.overriddenDir || path.join(process.cwd(), `test_session_${this.id}`);
Expand Down Expand Up @@ -220,7 +229,7 @@ export class TestSession extends AsyncOptionalCreatable<TestSessionOptions> {
const orgs = this.orgs.slice();
for (const org of orgs) {
this.debug(`Deleting test org: ${org}`);
const rv = shell.exec(`sfdx force:org:delete -u ${org} -p`, { silent: true });
const rv = shell.exec(`sfdx force:org:delete -u ${org} -p`, this.shelljsExecOptions) as shell.ShellString;
this.orgs = this.orgs.filter((o) => o !== org);
if (rv.code !== 0) {
// Must still delete the session dir if org:delete fails
Expand Down Expand Up @@ -277,7 +286,7 @@ export class TestSession extends AsyncOptionalCreatable<TestSessionOptions> {
}
}

const rv = shell.exec(cmd, { silent: true });
const rv = shell.exec(cmd, this.shelljsExecOptions) as shell.ShellString;
rv.stdout = stripAnsi(rv.stdout);
rv.stderr = stripAnsi(rv.stderr);
if (rv.code !== 0) {
Expand Down
28 changes: 28 additions & 0 deletions test/unit/execCmd.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,20 @@ describe('execCmd (sync)', () => {
expect(result.jsonError?.name).to.equal('JsonParseError');
expect(result.jsonError?.message).to.equal(`Parse error in file unknown on line 1${EOL}try JSON parsing this`);
});

it('should override shell default', () => {
const shellOverride = 'powershell.exe';
stubMethod(sandbox, env, 'getString')
.withArgs('TESTKIT_EXECUTABLE_PATH')
.returns(null)
.withArgs('TESTKIT_EXEC_SHELL')
.returns(shellOverride);
sandbox.stub(fs, 'fileExistsSync').returns(true);
const shellString = new ShellString(JSON.stringify(output));
const execStub = stubMethod(sandbox, shelljs, 'exec').returns(shellString);
execCmd(cmd);
expect(execStub.args[0][1]).to.have.property('shell', shellOverride);
});
});

describe('execCmd (async)', () => {
Expand Down Expand Up @@ -235,4 +249,18 @@ describe('execCmd (async)', () => {
expect(result.jsonError?.name).to.equal('JsonParseError');
expect(result.jsonError?.message).to.equal(`Parse error in file unknown on line 1${EOL}try JSON parsing this`);
});

it('should override shell default', async () => {
const shellOverride = 'powershell.exe';
stubMethod(sandbox, env, 'getString')
.withArgs('TESTKIT_EXECUTABLE_PATH')
.returns(null)
.withArgs('TESTKIT_EXEC_SHELL')
.returns(shellOverride);
sandbox.stub(fs, 'fileExistsSync').returns(true);
const shellString = new ShellString(JSON.stringify(output));
const execStub = stubMethod(sandbox, shelljs, 'exec').yields(0, shellString, '');
await execCmd(cmd, { async: true });
expect(execStub.args[0][1]).to.have.property('shell', shellOverride);
});
});
4 changes: 4 additions & 0 deletions test/unit/testProject.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import * as sinon from 'sinon';
import * as shelljs from 'shelljs';
import { ShellString } from 'shelljs';
import { stubMethod } from '@salesforce/ts-sinon';
import { env } from '@salesforce/kit';
import { fs as fsCore } from '@salesforce/core';
import { TestProject } from '../../lib/testProject';

Expand Down Expand Up @@ -93,6 +94,8 @@ describe('TestProject', () => {

it('should generate from a name', () => {
stubMethod(sandbox, shelljs, 'which').returns(true);
const shellOverride = 'powershell.exe';
stubMethod(sandbox, env, 'getString').returns(shellOverride);
const shellString = new ShellString('');
shellString.code = 0;
const execStub = stubMethod(sandbox, shelljs, 'exec').returns(shellString);
Expand All @@ -102,6 +105,7 @@ describe('TestProject', () => {
expect(testProject.dir).to.equal(pathJoin(destinationDir, name));
const execArg1 = `sfdx force:project:create -n ${name} -d ${destinationDir}`;
expect(execStub.firstCall.args[0]).to.equal(execArg1);
expect(execStub.firstCall.args[1]).to.have.property('shell', shellOverride);
});

it('should generate by default', () => {
Expand Down
10 changes: 9 additions & 1 deletion test/unit/testSession.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,13 @@ describe('TestSession', () => {

it('should create a session with setup commands', async () => {
stubMethod(sandbox, shelljs, 'which').returns(true);
const homedir = path.join('some', 'other', 'home');
const shellOverride = 'powershell.exe';
stubMethod(sandbox, env, 'getString')
.withArgs('TESTKIT_EXEC_SHELL')
.returns(shellOverride)
.withArgs('TESTKIT_HOMEDIR')
.returns(homedir);
const setupCommands = ['sfdx foo:bar -r testing'];
const execRv = { result: { donuts: 'yum' } };
const shellString = new ShellString(JSON.stringify(execRv));
Expand All @@ -162,10 +169,11 @@ describe('TestSession', () => {
expect(session.id).to.be.a('string');
expect(session.createdDate).to.be.a('Date');
expect(session.dir).to.equal(path.join(cwd, `test_session_${session.id}`));
expect(session.homeDir).to.equal(session.dir);
expect(session.homeDir).to.equal(homedir);
expect(session.project).to.equal(undefined);
expect(session.setup).to.deep.equal([execRv]);
expect(execStub.firstCall.args[0]).to.equal(`${setupCommands[0]} --json`);
expect(execStub.firstCall.args[1]).to.have.property('shell', shellOverride);
expect(process.env.HOME).to.equal(session.homeDir);
expect(process.env.USERPROFILE).to.equal(session.homeDir);
});
Expand Down

0 comments on commit 02d09d0

Please sign in to comment.