Skip to content

Commit

Permalink
feat: CDK Build Integration Test (#1219)
Browse files Browse the repository at this point in the history
* feat: CDK Build Integration Test

Adds @jsii/integ-test private module for defining new integration tests.
Adds a new integration test that downloads the latest CDK release source
code and builds it with the local version of jsii and jsii-pacmak.

This unit test requires a github access token defined in the environment
to get the latest release version and download the asset.

Fixes: #1209
  • Loading branch information
MrArnoldPalmer authored Feb 14, 2020
1 parent aa28ef6 commit e99d722
Show file tree
Hide file tree
Showing 10 changed files with 325 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"fetch-dotnet-snk": "bash scripts/fetch-dotnet-snk.sh",
"package": "bash scripts/package.sh",
"test": "lerna run test --stream",
"test:integ": "lerna run test:integ --stream",
"test:update": "lerna run test:update --stream"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions packages/@jsii/integ-test/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
GITHUB_TOKEN=personal_access_token
2 changes: 2 additions & 0 deletions packages/@jsii/integ-test/.eslintrc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
extends: ../../../eslint-config.yaml
2 changes: 2 additions & 0 deletions packages/@jsii/integ-test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env
*.js
10 changes: 10 additions & 0 deletions packages/@jsii/integ-test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# JSII Integration Tests

A suite of integration tests for JSII and related modules.

## Running

Running the integration tests locally requires a github access token. Copy the
`.env.example` file and replace the dummy value with a personal access token.

then run `yarn run test:integ`
41 changes: 41 additions & 0 deletions packages/@jsii/integ-test/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "@jsii/integ-test",
"version": "0.22.0",
"description": "A suite of integration tests for JSII and related modules.",
"private": true,
"scripts": {
"build": "tsc --build && npm run lint",
"lint": "eslint . --ext .ts --ignore-path=.gitignore",
"test:integ": "jest"
},
"keywords": [],
"author": {
"name": "Amazon Web Services",
"url": "https://aws.amazon.com"
},
"license": "Apache-2.0",
"dependencies": {
"tar": "^6.0.1"
},
"devDependencies": {
"@octokit/rest": "^16.36.0",
"@types/dotenv": "^8.2.0",
"@types/fs-extra": "^8.0.1",
"@types/tar": "^4.0.3",
"dotenv": "^8.2.0",
"eslint": "^6.8.0",
"fs-extra": "^8.1.0",
"jest": "^25.1.0",
"jsii": "^0.22.0",
"jsii-pacmak": "^0.22.0",
"jsii-rosetta": "^0.22.0",
"typescript": "~3.7.5"
},
"jest": {
"errorOnDeprecated": true,
"testEnvironment": "node",
"testMatch": [
"**/?(*.)+(spec|test).js"
]
}
}
88 changes: 88 additions & 0 deletions packages/@jsii/integ-test/test/build-cdk.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import * as Octokit from '@octokit/rest';
import * as dotenv from 'dotenv';
import { mkdtemp, remove } from 'fs-extra';
import * as path from 'path';
import { downloadReleaseAsset, minutes, ProcessManager, extractFileStream } from '../utils';

dotenv.config();
const JSII_DIR = path.dirname(require.resolve('jsii/package.json'));
const JSII_PACMAK_DIR = path.dirname(require.resolve('jsii-pacmak/package.json'));
const JSII_ROSETTA_DIR = path.dirname(require.resolve('jsii-rosetta/package.json'));

const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN
});

describe('Build CDK', () => {
let buildDir: string;
let processes: ProcessManager;

beforeAll(async () => {
processes = new ProcessManager();
buildDir = await mkdtemp(path.join(__dirname, 'build'));
});

afterAll(async () => {
await processes.killAll();
await remove(buildDir);
});

test('can build latest cdk release', async () => {
// download latest release info
const release = await octokit.repos.getLatestRelease({
owner: 'aws',
repo: 'aws-cdk'
});

// download and extract code
const code = await downloadReleaseAsset(`https://api.github.com/repos/aws/aws-cdk/tarball/${release.data.tag_name}`);
const srcDir = path.join(buildDir, `aws-aws-cdk-${release.data.target_commitish.substring(0, 7)}`);
await extractFileStream(code, buildDir);

// install cdk dependencies
await processes.spawn('yarn', ['install', '--frozen-lockfile'], {
cwd: srcDir
});

// build cdk build tools
await processes.spawn('npx', [
'lerna',
'--stream',
'--scope', 'cdk-build-tools',
'--scope', 'pkglint',
'--scope', 'awslint',
'run', 'build',
], { cwd: srcDir });

// build jsii modules
await processes.spawn('npx', [
'lerna',
'--stream',
'--scope', '@aws-cdk/*',
'--scope', 'aws-cdk',
'run', 'build',
'--', '--jsii', path.join(JSII_DIR, 'bin', 'jsii'),
], { cwd: srcDir });

// build the rest
await processes.spawn('npx', [
'lerna',
'--stream',
'--ignore', '@aws-cdk/*',
'--ignore', 'aws-cdk',
'--ignore', 'cdk-build-tools',
'--ignore', 'pkglint',
'--ignore', 'awslint',
'run', 'build',
], { cwd: srcDir });

// package modules
await processes.spawn('./pack.sh', [], {
cwd: srcDir,
env: {
PACMAK: path.join(JSII_PACMAK_DIR, 'bin', 'jsii-pacmak'),
ROSETTA: path.join(JSII_ROSETTA_DIR, 'bin', 'jsii-rosetta')
}
});
}, minutes(60));
});
8 changes: 8 additions & 0 deletions packages/@jsii/integ-test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"composite": false,
"declaration": false
},
"extends": "../../../tsconfig-base",
"include": ["**/*.ts"]
}
134 changes: 134 additions & 0 deletions packages/@jsii/integ-test/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import * as cp from 'child_process';
import { IncomingMessage } from 'http';
import * as https from 'https';
import { Readable } from 'stream';
import { extract } from 'tar';

/**
* @param num a quantity of minutes (could be fractional)
*
* @return equivalent number of milliseconds
*/
export function minutes(num: number): number {
return num * 1000 * 60;
}

/**
* Used to track and clean up processes if tests fail or timeout
*/
export class ProcessManager {
private readonly processes: {
[pid: string]: {
proc: cp.ChildProcess;
promise: Promise<void>;
};
} = {};

/**
* kill all still running processes
*
* @param signal sent to terminate process
*/
public async killAll(signal?: string) {
const values = Object.values(this.processes);
await Promise.all(values.map(({ proc, promise }) => async() => {
proc.kill(signal);
await promise;
this.remove(proc);
}));
}

private add(proc: cp.ChildProcess, promise: Promise<void>) {
this.processes[proc.pid] = { proc, promise };
}

private remove(proc: cp.ChildProcess) {
delete this.processes[proc.pid];
}

/**
* spawn new child process
*
* @param shell command being called
* @param arguments passed to command
* @param options passed to child process spawn
*/
public async spawn(cmd: string, args: string[], opts: any = {}): Promise<void> {
const proc = cp.spawn(cmd, args, { stdio: 'inherit', ...opts });

const promise = new Promise<void>((ok, ko) => {
proc.once('exit', code => {
const message = `child process exited with code: ${code}`;
if (code === 0) {
process.stdout.write(message);
ok();
} else {
process.stderr.write(message);
ko(new Error(message));
}

this.remove(proc);
});

proc.once('error', error => {
process.stderr.write(`Process ${proc.pid} error: ${error}`);
ko();
});
});

this.add(proc, promise);
return promise;
}
}

/**
* write downloaded asset to file
*
* @param source stream
* @param destination directory for extracted files
*/
export async function extractFileStream(source: Readable, destination: string) {
return new Promise((ok, ko) => {
const destStream = extract({ cwd: destination });
destStream.once('close', ok);
destStream.once('error', ko);
source.once('error', ko);

source.pipe(destStream);
});
}

/**
* Wrap http calls to download release asset in a promise. Github responds with
* a 302 sometimes which is required to be handled. Returns the buffer to be
* streamed to destination fs stream.
*
* @param url of downloadable asset
*
* @returns readable stream of asset data
*/
export async function downloadReleaseAsset(url: string): Promise<Readable> {
return new Promise((ok, ko) => {
const config = {
headers: {
'User-Agent': '@jsii/integ-test',
Authorization: `token ${process.env.GITHUB_TOKEN}`
}
};

https.get(url, config, (response: IncomingMessage) => {
if (response.statusCode === 302) {

if (!response.headers.location) {
throw new Error('Bad redirect, no location header found');
}

return https.get(response.headers.location, config, ok);
} else if (response.statusCode && (response.statusCode < 200 || response.statusCode > 300)) {
return ko(new Error(`Status Code: ${response.statusCode}`));
}

return ok(response);
});
});
}
38 changes: 38 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1221,6 +1221,13 @@
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==

"@octokit/auth-token@^2.4.0":
version "2.4.0"
resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.0.tgz#b64178975218b99e4dfe948253f0673cbbb59d9f"
integrity sha512-eoOVMjILna7FVQf96iWc3+ZtE/ZT6y8ob8ZzcqKY1ibSQCnu4O/B7pJvzMx5cyZ/RjAff6DAdEb0O0Cjcxidkg==
dependencies:
"@octokit/types" "^2.0.0"

"@octokit/endpoint@^5.5.0":
version "5.5.1"
resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-5.5.1.tgz#2eea81e110ca754ff2de11c79154ccab4ae16b3f"
Expand Down Expand Up @@ -1276,6 +1283,25 @@
once "^1.4.0"
universal-user-agent "^4.0.0"

"@octokit/rest@^16.36.0":
version "16.38.1"
resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-16.38.1.tgz#be24e0faa7d0bdb9459fbc089ec866ed11774b72"
integrity sha512-zyNFx+/Bd1EXt7LQjfrc6H4wryBQ/oDuZeZhGMBSFr1eMPFDmpEweFQR3R25zjKwBQpDY7L5GQO6A3XSaOfV1w==
dependencies:
"@octokit/auth-token" "^2.4.0"
"@octokit/request" "^5.2.0"
"@octokit/request-error" "^1.0.2"
atob-lite "^2.0.0"
before-after-hook "^2.0.0"
btoa-lite "^1.0.0"
deprecation "^2.0.0"
lodash.get "^4.4.2"
lodash.set "^4.3.2"
lodash.uniq "^4.5.0"
octokit-pagination-methods "^1.1.0"
once "^1.4.0"
universal-user-agent "^4.0.0"

"@octokit/types@^2.0.0":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@octokit/types/-/types-2.0.2.tgz#0888497f5a664e28b0449731d5e88e19b2a74f90"
Expand Down Expand Up @@ -1348,6 +1374,13 @@
resolved "https://registry.yarnpkg.com/@types/deep-equal/-/deep-equal-1.0.1.tgz#71cfabb247c22bcc16d536111f50c0ed12476b03"
integrity sha512-mMUu4nWHLBlHtxXY17Fg6+ucS/MnndyOWyOe7MmwkoMYxvfQU2ajtRaEvqSUv+aVkMqH/C0NCI8UoVfRNQ10yg==

"@types/dotenv@^8.2.0":
version "8.2.0"
resolved "https://registry.yarnpkg.com/@types/dotenv/-/dotenv-8.2.0.tgz#5cd64710c3c98e82d9d15844375a33bf1b45d053"
integrity sha512-ylSC9GhfRH7m1EUXBXofhgx4lUWmFeQDINW5oLuS+gxWdfUeW4zJdeVTYVkexEW+e2VUvlZR2kGnGGipAWR7kw==
dependencies:
dotenv "*"

"@types/eslint-visitor-keys@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
Expand Down Expand Up @@ -3213,6 +3246,11 @@ dot-prop@^4.2.0:
dependencies:
is-obj "^1.0.0"

dotenv@*, dotenv@^8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==

duplexer@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
Expand Down

0 comments on commit e99d722

Please sign in to comment.