Skip to content

Commit

Permalink
chore: migrate aws-cdk from nodeunit to jest (#5659)
Browse files Browse the repository at this point in the history
There is some interesting magic happening around the runtime-info
module: `jest` replaces the standard `require` function so it can honor
module mocking requirements, however this does (intentionally) not
implement `require.cache`, which is used to determine which CDK
libraries are loaded during a particular execution (in order to populate
the `AWS::CDK::Metadata` resource as needed).

In order to work around this, the `require.cache` reading was indirected
through a proxy module, so it can be stubbed, too, with a pretend cache
content, in order to make the test still workable.
  • Loading branch information
RomainMuller authored and mergify[bot] committed Jan 6, 2020
1 parent 1085a27 commit 59cbdc0
Show file tree
Hide file tree
Showing 56 changed files with 2,714 additions and 3,056 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ cdk.context.json
.cdk.staging/
cdk.out/
*.tabl.json

# Yarn error log
yarn-error.log
18 changes: 15 additions & 3 deletions packages/aws-cdk/lib/api/util/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ export interface SDKOptions {
* @default No certificate bundle
*/
caBundlePath?: string;

/**
* The custom suer agent to use.
*
* @default - <package-name>/<package-version>
*/
userAgent?: string;
}

/**
Expand Down Expand Up @@ -191,9 +198,14 @@ export class SDK implements ISDK {
private async configureSDKHttpOptions(options: SDKOptions) {
const config: {[k: string]: any} = {};
const httpOptions: {[k: string]: any} = {};
// Find the package.json from the main toolkit
const pkg = (require.main as any).require('../package.json');
config.customUserAgent = `${pkg.name}/${pkg.version}`;

let userAgent = options.userAgent;
if (userAgent == null) {
// Find the package.json from the main toolkit
const pkg = (require.main as any).require('../package.json');
userAgent = `${pkg.name}/${pkg.version}`;
}
config.customUserAgent = userAgent;

// https://aws.amazon.com/blogs/developer/using-the-aws-sdk-for-javascript-from-behind-a-proxy/
options.proxyAddress = options.proxyAddress || httpsProxyFromEnvironment();
Expand Down
58 changes: 29 additions & 29 deletions packages/aws-cdk/lib/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const CDK_HOME = process.env.CDK_HOME ? path.resolve(process.env.CDK_HOME) : pat
/**
* Initialize a CDK package in the current directory
*/
export async function cliInit(type?: string, language?: string, canUseNetwork = true, generateOnly = false) {
export async function cliInit(type?: string, language?: string, canUseNetwork = true, generateOnly = false, workDir = process.cwd()) {
if (!type && !language) {
await printAvailableTemplates();
return;
Expand All @@ -43,7 +43,7 @@ export async function cliInit(type?: string, language?: string, canUseNetwork =
throw new Error('No language was selected');
}

await initializeProject(template, language, canUseNetwork, generateOnly);
await initializeProject(template, language, canUseNetwork, generateOnly, workDir);
}

/**
Expand Down Expand Up @@ -237,13 +237,13 @@ export async function printAvailableTemplates(language?: string) {
}
}

async function initializeProject(template: InitTemplate, language: string, canUseNetwork: boolean, generateOnly: boolean) {
await assertIsEmptyDirectory();
async function initializeProject(template: InitTemplate, language: string, canUseNetwork: boolean, generateOnly: boolean, workDir: string) {
await assertIsEmptyDirectory(workDir);
print(`Applying project template ${colors.green(template.name)} for ${colors.blue(language)}`);
await template.install(language, process.cwd());
await template.install(language, workDir);
if (!generateOnly) {
await initializeGitRepository();
await postInstall(language, canUseNetwork);
await initializeGitRepository(workDir);
await postInstall(language, canUseNetwork, workDir);
}
if (await fs.pathExists('README.md')) {
print(colors.green(await fs.readFile('README.md', { encoding: 'utf-8' })));
Expand All @@ -252,43 +252,43 @@ async function initializeProject(template: InitTemplate, language: string, canUs
}
}

async function assertIsEmptyDirectory() {
const files = await fs.readdir(process.cwd());
async function assertIsEmptyDirectory(workDir: string) {
const files = await fs.readdir(workDir);
if (files.filter(f => !f.startsWith('.')).length !== 0) {
throw new Error('`cdk init` cannot be run in a non-empty directory!');
}
}

async function initializeGitRepository() {
if (await isInGitRepository(process.cwd())) { return; }
async function initializeGitRepository(workDir: string) {
if (await isInGitRepository(workDir)) { return; }
print('Initializing a new git repository...');
try {
await execute('git', 'init');
await execute('git', 'add', '.');
await execute('git', 'commit', '--message="Initial commit"', '--no-gpg-sign');
await execute('git', ['init'], { cwd: workDir });
await execute('git', ['add', '.'], { cwd: workDir });
await execute('git', ['commit', '--message="Initial commit"', '--no-gpg-sign'], { cwd: workDir });
} catch (e) {
warning('Unable to initialize git repository for your project.');
}
}

async function postInstall(language: string, canUseNetwork: boolean) {
async function postInstall(language: string, canUseNetwork: boolean, workDir: string) {
switch (language) {
case 'javascript':
return await postInstallJavascript(canUseNetwork);
return await postInstallJavascript(canUseNetwork, workDir);
case 'typescript':
return await postInstallTypescript(canUseNetwork);
return await postInstallTypescript(canUseNetwork, workDir);
case 'java':
return await postInstallJava(canUseNetwork);
return await postInstallJava(canUseNetwork, workDir);
case 'python':
return await postInstallPython();
return await postInstallPython(workDir);
}
}

async function postInstallJavascript(canUseNetwork: boolean) {
return postInstallTypescript(canUseNetwork);
async function postInstallJavascript(canUseNetwork: boolean, cwd: string) {
return postInstallTypescript(canUseNetwork, cwd);
}

async function postInstallTypescript(canUseNetwork: boolean) {
async function postInstallTypescript(canUseNetwork: boolean, cwd: string) {
const command = 'npm';

if (!canUseNetwork) {
Expand All @@ -298,27 +298,27 @@ async function postInstallTypescript(canUseNetwork: boolean) {

print(`Executing ${colors.green(`${command} install`)}...`);
try {
await execute(command, 'install');
await execute(command, ['install'], { cwd });
} catch (e) {
throw new Error(`${colors.green(`${command} install`)} failed: ` + e.message);
}
}

async function postInstallJava(canUseNetwork: boolean) {
async function postInstallJava(canUseNetwork: boolean, cwd: string) {
if (!canUseNetwork) {
print(`Please run ${colors.green(`mvn package`)}!`);
return;
}

print(`Executing ${colors.green('mvn package')}...`);
await execute('mvn', 'package');
await execute('mvn', ['package'], { cwd });
}

async function postInstallPython() {
async function postInstallPython(cwd: string) {
const python = pythonExecutable();
print(`Executing ${colors.green('Creating virtualenv...')}`);
try {
await execute(python, '-m venv', '.env');
await execute(python, ['-m venv', '.env'], { cwd });
} catch (e) {
print('Unable to create virtualenv automatically');
print(`Please run ${colors.green(python + ' -m venv .env')}!`);
Expand Down Expand Up @@ -353,8 +353,8 @@ function isRoot(dir: string) {
*
* @returns STDOUT (if successful).
*/
async function execute(cmd: string, ...args: string[]) {
const child = childProcess.spawn(cmd, args, { shell: true, stdio: [ 'ignore', 'pipe', 'inherit' ] });
async function execute(cmd: string, args: string[], { cwd }: { cwd: string }) {
const child = childProcess.spawn(cmd, args, { cwd, shell: true, stdio: [ 'ignore', 'pipe', 'inherit' ] });
let stdout = '';
child.stdout.on('data', chunk => stdout += chunk.toString());
return new Promise<string>((ok, fail) => {
Expand Down
36 changes: 25 additions & 11 deletions packages/aws-cdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,6 @@
]
}
},
"nyc": {
"statements": 8,
"lines": 8,
"branches": 3
},
"author": {
"name": "Amazon Web Services",
"url": "https://aws.amazon.com",
Expand All @@ -47,11 +42,11 @@
"@types/archiver": "^3.0.0",
"@types/fs-extra": "^8.0.1",
"@types/glob": "^7.1.1",
"@types/jest": "^24.0.25",
"@types/jszip": "^3.1.6",
"@types/minimatch": "^3.0.3",
"@types/mockery": "^1.4.29",
"@types/node": "^10.17.13",
"@types/nodeunit": "^0.0.30",
"@types/promptly": "^3.0.0",
"@types/request": "^2.48.4",
"@types/semver": "^6.2.0",
Expand All @@ -62,18 +57,19 @@
"@types/yargs": "^13.0.4",
"aws-sdk-mock": "^5.0.0",
"cdk-build-tools": "1.19.0",
"jest": "^24.9.0",
"jszip": "^3.2.2",
"mockery": "^2.1.0",
"nodeunit": "^0.11.3",
"pkglint": "1.19.0",
"sinon": "^8.0.2"
"sinon": "^8.0.2",
"ts-jest": "^24.2.0"
},
"dependencies": {
"@aws-cdk/cloudformation-diff": "1.19.0",
"@aws-cdk/cx-api": "1.19.0",
"@aws-cdk/region-info": "1.19.0",
"archiver": "^3.1.1",
"aws-sdk": "^2.597.0",
"aws-sdk": "^2.596.0",
"camelcase": "^5.3.1",
"colors": "^1.4.0",
"decamelize": "^3.2.0",
Expand All @@ -88,7 +84,7 @@
"table": "^5.4.6",
"uuid": "^3.3.3",
"yaml": "^1.7.2",
"yargs": "^15.1.0"
"yargs": "^15.0.2"
},
"repository": {
"url": "https://github.com/aws/aws-cdk.git",
Expand All @@ -103,5 +99,23 @@
"engines": {
"node": ">= 10.3.0"
},
"stability": "stable"
"stability": "stable",
"jest": {
"collectCoverage": true,
"coverageReporters": [
"lcov",
"html",
"text-summary"
],
"coverageThreshold": {
"global": {
"branches": 45,
"statements": 60
}
},
"preset": "ts-jest",
"testMatch": [
"**/?(*.)+(spec|test).ts?(x)"
]
}
}
91 changes: 91 additions & 0 deletions packages/aws-cdk/test/account-cache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import { AccountAccessKeyCache } from '../lib/api/util/account-cache';

async function makeCache() {
const dir = await fs.mkdtemp('/tmp/account-cache-test');
const file = path.join(dir, 'cache.json');
return {
cacheDir: dir,
cacheFile: file,
cache: new AccountAccessKeyCache(file),
};
}

async function nukeCache(cacheDir: string) {
await fs.remove(cacheDir);
}

test('get(k) when cache is empty', async () => {
const { cacheDir, cacheFile, cache } = await makeCache();
try {
expect(await cache.get('foo')).toBeUndefined();
expect(await fs.pathExists(cacheFile)).toBeFalsy();
} finally {
await nukeCache(cacheDir);
}
});

test('put(k,v) and then get(k)', async () => {
const { cacheDir, cacheFile, cache } = await makeCache();

try {
await cache.put('key', 'value');
await cache.put('boo', 'bar');
expect(await cache.get('key')).toBe('value');

// create another cache instance on the same file, should still work
const cache2 = new AccountAccessKeyCache(cacheFile);
expect(await cache2.get('boo')).toBe('bar');

// whitebox: read the file
expect(await fs.readJson(cacheFile)).toEqual({
key: 'value',
boo: 'bar'
});
} finally {
await nukeCache(cacheDir);
}
});

test('fetch(k, resolver) can be used to "atomically" get + resolve + put', async () => {
const { cacheDir, cache } = await makeCache();

try {
expect(await cache.get('foo')).toBeUndefined();
expect(await cache.fetch('foo', async () => 'bar')).toBe('bar');
expect(await cache.get('foo')).toBe('bar');
} finally {
await nukeCache(cacheDir);
}
});

test(`cache is nuked if it exceeds ${AccountAccessKeyCache.MAX_ENTRIES} entries`, async () => {
// This makes a lot of promises, so it can queue for a while...
jest.setTimeout(30_000);

const { cacheDir, cacheFile, cache } = await makeCache();

try {
for (let i = 0; i < AccountAccessKeyCache.MAX_ENTRIES; ++i) {
await cache.put(`key${i}`, `value${i}`);
}

// verify all values are on disk
const otherCache = new AccountAccessKeyCache(cacheFile);
for (let i = 0; i < AccountAccessKeyCache.MAX_ENTRIES; ++i) {
expect(await otherCache.get(`key${i}`)).toBe(`value${i}`);
}

// add another value
await cache.put('nuke-me', 'genesis');

// now, we expect only `nuke-me` to exist on disk
expect(await otherCache.get('nuke-me')).toBe('genesis');
for (let i = 0; i < AccountAccessKeyCache.MAX_ENTRIES; ++i) {
expect(await otherCache.get(`key${i}`)).toBeUndefined();
}
} finally {
await nukeCache(cacheDir);
}
});
Loading

0 comments on commit 59cbdc0

Please sign in to comment.