Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sandbox e2e test #206

Merged
merged 35 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c92c58e
fixup
edwardfoyle Sep 1, 2023
8e7a1e7
e2e working
edwardfoyle Sep 2, 2023
331ccea
remove unnecessary promise
edwardfoyle Sep 5, 2023
fe1c2f7
make process controller more ergonomic
edwardfoyle Sep 5, 2023
7fea057
fix pathing
edwardfoyle Sep 5, 2023
b54443e
refactor macros
edwardfoyle Sep 5, 2023
ca0f257
fix build
edwardfoyle Sep 5, 2023
db007f8
Merge remote-tracking branch 'origin/main' into e2e-tests
edwardfoyle Sep 5, 2023
09be50e
update e2e runner
edwardfoyle Sep 5, 2023
b2c7ba1
add debugging config
edwardfoyle Sep 5, 2023
8a6b31b
update debug config
edwardfoyle Sep 5, 2023
980703c
update setup
edwardfoyle Sep 5, 2023
8bad67d
add debug config
edwardfoyle Sep 5, 2023
5b9eaf5
try this
edwardfoyle Sep 5, 2023
5e323fb
add randomness to project names
edwardfoyle Sep 5, 2023
8566546
update workflow deps
edwardfoyle Sep 5, 2023
e511c74
fix indexing
edwardfoyle Sep 5, 2023
a0c24a6
fix types
edwardfoyle Sep 5, 2023
75ea586
revert package json change
edwardfoyle Sep 5, 2023
6b9dae0
remove debug config
edwardfoyle Sep 5, 2023
12f66d0
Merge remote-tracking branch 'origin/main' into e2e-tests
edwardfoyle Sep 6, 2023
5dc0262
update test glob patterns
edwardfoyle Sep 6, 2023
f2de8fa
remove e2e prefix
edwardfoyle Sep 6, 2023
0852f6c
update gitignore
edwardfoyle Sep 6, 2023
0e5173b
refactor creating test dir
edwardfoyle Sep 6, 2023
3f0d9eb
renaming things
edwardfoyle Sep 6, 2023
1423594
update comment
edwardfoyle Sep 6, 2023
0855fa1
fix path
edwardfoyle Sep 6, 2023
ede58fd
ignore integration-test package in coverage analysis
edwardfoyle Sep 6, 2023
d996c8e
update parcel watcher
edwardfoyle Sep 6, 2023
c6c5fe6
regenerate package lock
edwardfoyle Sep 6, 2023
3cc7d7d
fix queue shifting
edwardfoyle Sep 6, 2023
a90e954
revert some auto renaming
edwardfoyle Sep 6, 2023
b7e5fd6
rename ControllerAction => StdioInteraction and related names
edwardfoyle Sep 7, 2023
9718ab6
add empty changeset
edwardfoyle Sep 7, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
e2e working
  • Loading branch information
edwardfoyle committed Sep 2, 2023
commit 8e7a1e79d018acc266a985663524a769f73a7005
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"clean:npm-proxy": "npm run stop:npm-proxy && rimraf verdaccio-cache verdaccio-logs.txt",
"diff:check": "tsx scripts/check_pr_size.ts",
"docs": "typedoc",
"e2e": "tsx --test --test-reporter spec --test-name-pattern /^\\[E2E\\]/",
"e2e": "tsx --test --test-reporter spec --test-name-pattern \"/^\\[E2E\\]/\"",
"install:local": "npm install && npm run build && npm run install:local --workspaces --if-present",
"lint": "eslint --max-warnings 0 . && prettier --check .",
"lint:fix": "eslint --cache --fix . && prettier --write .",
Expand Down
14 changes: 0 additions & 14 deletions packages/e2e-tests/.npmignore

This file was deleted.

3 changes: 0 additions & 3 deletions packages/e2e-tests/api-extractor.json

This file was deleted.

17 changes: 0 additions & 17 deletions packages/e2e-tests/package.json

This file was deleted.

10 changes: 0 additions & 10 deletions packages/e2e-tests/src/sandbox.test.ts

This file was deleted.

5 changes: 0 additions & 5 deletions packages/e2e-tests/tsconfig.json

This file was deleted.

3 changes: 0 additions & 3 deletions packages/e2e-tests/typedoc.json

This file was deleted.

70 changes: 40 additions & 30 deletions packages/integration-tests/src/e2e/sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { after, afterEach, before, beforeEach, describe, it } from 'node:test';
import path from 'path';
import { existsSync } from 'fs';
import fs from 'fs/promises';
import assert from 'node:assert';
import { fileURLToPath } from 'url';
import { ProcessController } from '../process_controller.js';
import assert from 'node:assert';

describe('[E2E] sandbox', () => {
const e2eSandboxDir = new URL('./e2e-sandboxes', import.meta.url);
edwardfoyle marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -17,55 +18,64 @@ describe('[E2E] sandbox', () => {
await fs.rm(e2eSandboxDir, { recursive: true });
});

let testDir: string;
let testProjectRoot: string;
let testAmplifyDir: string;
beforeEach(async () => {
testDir = await fs.mkdtemp(
path.join(e2eSandboxDir.toString(), 'test-create-amplify')
testProjectRoot = await fs.mkdtemp(
path.join(fileURLToPath(e2eSandboxDir), 'test-sandbox')
);
// creating a package.json beforehand skips the npm init in create-amplify
await fs.writeFile(
path.join(testDir, 'package.json'),
JSON.stringify({ name: 'test-project-name' }, null, 2)
path.join(testProjectRoot, 'package.json'),
JSON.stringify({ name: 'test-sandbox' }, null, 2)
);

testAmplifyDir = path.join(testProjectRoot, 'amplify');
await fs.mkdir(testAmplifyDir);
});

afterEach(async () => {
await fs.rm(testAmplifyDir, { recursive: true });
});

const testProjects = [
{
initialStatePath: new URL(
'../test-projects/basic-auth-data-storage-function',
initialAmplifyDirPath: new URL(
'../../test-projects/basic-auth-data-storage-function/amplify',
import.meta.url
),
name: 'basic-auth-data-storage-function',
},
];

afterEach(async () => {
await fs.rm(testDir, { recursive: true });
});
testProjects.forEach((testProject) => {
it(`[E2E] ${testProject.name}`, async () => {
await fs.cp(testProject.initialAmplifyDirPath, testAmplifyDir, {
recursive: true,
});

describe('sandbox', () => {
it('deploys fully loaded project', async () => {
const sandboxProcess = ProcessController.fromCommand(
'npx',
['amplify', 'sandbox'],
{ cwd: testDir }
);
await sandboxProcess.waitForLineIncludes(
'[Sandbox] Watching for file changes'
);
sandboxProcess.kill();
const clientConfig = await import(
path.join(testDir, 'amplifyconfiguration.js')
const proc = new ProcessController('amplify', ['sandbox'], {
cwd: testProjectRoot,
})
.waitForLineIncludes('[Sandbox] Watching for file changes')
.sendCtrlC()
.waitForLineIncludes(
'Would you like to delete all the resources in your sandbox environment'
)
.sendNo();

await proc.run();
const { default: clientConfig } = await import(
path.join(testProjectRoot, 'amplifyconfiguration.js')
);
assert.deepStrictEqual(Object.keys(clientConfig).sort(), [
sobolk marked this conversation as resolved.
Show resolved Hide resolved
'aws_user_pools_id',
'aws_user_pools_web_client_id',
'aws_cognito_region',
'aws_appsync_authenticationType',
'aws_appsync_graphqlEndpoint',
'aws_appsync_region',
'aws_appsync_authenticationType',
'aws_user_files_s3_bucket_region',
'aws_cognito_region',
'aws_user_files_s3_bucket',
'aws_user_files_s3_bucket_region',
'aws_user_pools_id',
'aws_user_pools_web_client_id',
]);
});
});
Expand Down
129 changes: 80 additions & 49 deletions packages/integration-tests/src/process_controller.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,50 @@
/**
*
*/
import { ExecaChildProcess, Options, execa } from 'execa';
import { Options, execa } from 'execa';
import * as os from 'os';
import * as readline from 'readline';
import { Interface } from 'readline';
import readline from 'readline';

type ExpectedLineAction = {
predicate: (line: string) => boolean;
thenSend: string[];
};

const CONTROL_C = '\x03';

/**
* Provides an abstractions for sending and receiving data on stdin/out of a child process
*
* The general strategy is a builder pattern which appends actions to a queue
* Then when .run() is called, the child process is spawned and the actions are executed one by one
*
* Each action is essentially a condition to wait for stdout to satisfy and some data to send on stdin once the wait condition is met
*
* For example `.waitForLineIncludes('Do you like M&Ms').sendLine('yes')`
* will wait until a line that includes "Do you like M&Ms" is printed on stdout of the child process,
* then send "yes" on stdin of the process
*/
export class ProcessController {
private readonly processInterface: Interface;
private readonly expectedLineQueue: ExpectedLineAction[] = [];
/**
* Private ctor that initializes a readline interface around the execa process
*/
private constructor(private readonly execaProcess: ExecaChildProcess) {
if (!execaProcess.stdout || !execaProcess.stdin) {
throw new Error(`Process does not have stdout and stdin`);
}
// We are creating an interface with the child process stdout as the input and stdin as the output
// This is because the _output_ of the child process in the input that we want to process here
// and the _input_ of the child process is where we want to send commands (the output from this controller)
this.processInterface = readline.createInterface(
execaProcess.stdout,
execaProcess.stdin
);
this.processInterface.pause();
}
constructor(
private readonly command: string,
private readonly args: string[] = [],
private readonly options?: Pick<Options, 'cwd'>
) {}

/**
* Factory method to initialize with a command and args
*/
static fromCommand = (
command: string,
args: string[] = [],
options?: Pick<Options, 'cwd'>
) => {
return new ProcessController(execa(command, args, options));
waitForLineIncludes = (str: string) => {
this.expectedLineQueue.push({
predicate: (line) => line.includes(str),
thenSend: [],
});
return this;
};

send = (str: string) => {
this.processInterface.write(str);
if (this.expectedLineQueue.length === 0) {
throw new Error('Must wait for a line before sending');
}
this.expectedLineQueue.at(-1)?.thenSend.push(str);
return this;
};

Expand All @@ -49,29 +53,56 @@ export class ProcessController {
return this;
};

waitForLineIncludes = async (expected: string) => {
// attach a line listener that will wait until the expected line is found
// once found, it will pause the input stream and remove itself from the line listeners
const foundExpectedStringPromise = new Promise<void>((resolve) => {
const selfRemovingListener = (line: string) => {
if (line.includes(expected)) {
this.processInterface.pause();
this.processInterface.removeListener('line', selfRemovingListener);
resolve();
}
};
this.processInterface.on('line', selfRemovingListener);
});
sendNo = () => {
this.sendLine('N');
return this;
};

// now that the line listener is attached, we can resume the input stream
this.processInterface.resume();
sendYes = () => {
this.sendLine('Y');
return this;
};

// wait for the line to be found
await foundExpectedStringPromise;
sendCtrlC = () => {
this.send(CONTROL_C);
return this;
};

kill = () => {
this.execaProcess.kill();
/**
* Execute the sequence of actions queued on the process
*/
run = async () => {
const execaProcess = execa(this.command, this.args, this.options);

if (process.stdout) {
void execaProcess.pipeStdout?.(process.stdout);
}

if (!execaProcess.stdout) {
throw new Error('Child process does not have stdout stream');
}
const reader = readline.createInterface(execaProcess.stdout);

for await (const line of reader) {
const expectedLine = this.expectedLineQueue[0];
if (!expectedLine?.predicate(line)) {
continue;
}
// if we got here, the line matched the predicate
for (const chunk of expectedLine.thenSend) {
if (chunk === CONTROL_C) {
execaProcess.kill('SIGINT');
} else {
await new Promise<void>((resolve, reject) => {
execaProcess.stdin?.write(chunk, (err) => {
return err ? reject(err) : resolve();
});
});
}
}
this.expectedLineQueue.shift();
}

await execaProcess;
};
}

This file was deleted.

This file was deleted.