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

end-to-end tests in circleci [CUMULUS-371] #238

Merged
merged 11 commits into from
Mar 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 7 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,17 @@ jobs:
- ./cumulus/services/sfn-throttler/node_modules

- run:
name: Running Test
name: Running Tests
environment:
LOCALSTACK_HOST: localstack
command: yarn test

- run:
name: Running End to End Tests
environment:
LOCALSTACK_HOST: localstack
command: yarn e2e

build_and_publish:
docker:
- image: circleci/node:6.10
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added
- added tools to @cumulus/integration-tests for local integration testing
- added end to end testing for discovering and parsing of PDRs
- `yarn e2e` command is available for end to end testing
### Fixed

- **CUMULUS-175: "Dashboard providers not in sync with AWS providers."** The root cause of this bug - DynamoDB operations not showing up in Elasticsearch - was shared by collections and rules. The fix was to update providers', collections' and rules; POST, PUT and DELETE endpoints to operate on DynamoDB and using DynamoDB streams to update Elasticsearch. The following packages were made:
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ Run the test commands next

$ yarn test

Run end to end tests by

$ yarn e2e

## Adding New Packages

Create a new folder under `packages` if it is a common library or create folder under `cumulus/tasks` if it is a lambda task. `cd` to the folder and run `npm init`.
Expand Down
15 changes: 14 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "1.0.0",
"description": "Cumulus Framework for ingesting and processing Nasa Earth data streams",
"scripts": {
"e2e": "env TEST=true ava tests/*.js --serial",
"test": "lerna run test",
"bootstrap": "lerna bootstrap",
"ybootstrap": "lerna bootstrap --npm-client=yarn",
Expand All @@ -17,6 +18,14 @@
"type": "git",
"url": "https://github.com/cumulus-nasa/cumulus"
},
"ava": {
"files": "test",
"babel": "inherit",
"require": [
"babel-polyfill",
"babel-register"
]
},
"keywords": [
"GIBS",
"CUMULUS",
Expand All @@ -33,13 +42,14 @@
"author": "Cumulus Authors",
"license": "Apache-2.0",
"devDependencies": {
"ava": "^0.24.0",
"ava": "^0.25.0",
"babel-core": "^6.13.2",
"babel-eslint": "^6.1.2",
"babel-loader": "^6.2.4",
"babel-plugin-transform-async-to-generator": "^6.8.0",
"babel-polyfill": "^6.13.0",
"babel-preset-es2015": "^6.13.2",
"babel-preset-es2017": "^6.24.1",
"copy-webpack-plugin": "^4.0.1",
"eslint": "^3.2.2",
"eslint-config-airbnb": "^10.0.0",
Expand All @@ -55,5 +65,8 @@
"transform-loader": "^0.2.3",
"webpack": "^1.13.3",
"webpack-node-externals": "^1.5.4"
},
"dependencies": {
"fs-extra": "^5.0.0"
}
}
46 changes: 46 additions & 0 deletions packages/common/aws.js
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,52 @@ exports.sendSQSMessage = (queueUrl, message) => {
}).promise();
};

/**
* Receives SQS messages from a given queue. The number of messages received
* can be set and the timeout is also adjustable.
*
* @param {string} queueUrl - url of the SQS queue
* @param {integer} numOfMessages - number of messages to read from the queue
* @param {integer} timeout - number of seconds it takes for a message to timeout
* @returns {Promise.<Array>} an array of messages
*/
exports.receiveSQSMessages = async (queueUrl, numOfMessages = 1, timeout = 30) => {
const params = {
QueueUrl: queueUrl,
AttributeNames: ['All'],
VisibilityTimeout: timeout,
MaxNumberOfMessages: numOfMessages
};

const messages = await exports.sqs().receiveMessage(params).promise();

// convert body from string to js object
if (Object.prototype.hasOwnProperty.call(messages, 'Messages')) {
messages.Messages.forEach((mes) => {
mes.Body = JSON.parse(mes.Body); // eslint-disable-line no-param-reassign
});

return messages.Messages;
}
return [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this function just returns an array of messages (or the empty array) (e.g. not {Promise.<Array>} as documentation above indicates

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

async/await function always return a promise.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right! I learned something.

};

/**
* Delete a given SQS message from a given queue.
*
* @param {string} queueUrl - url of the SQS queue
* @param {integer} receiptHandle - the unique identifier of the sQS message
* @returns {Promise} an AWS SQS response
*/
exports.deleteSQSMessage = (queueUrl, receiptHandle) => {
const params = {
QueueUrl: queueUrl,
ReceiptHandle: receiptHandle
};

return exports.sqs().deleteMessage(params).promise();
};

/**
* Returns execution ARN from a statement machine Arn and executionName
*
Expand Down
6 changes: 3 additions & 3 deletions packages/ingest/consumer.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

const log = require('@cumulus/common/log');
const aws = require('./aws');
const { receiveSQSMessages, deleteSQSMessage } = require('@cumulus/common/aws');

class Consume {
constructor(queueUrl, messageLimit = 1, timeLimit = 90) {
Expand All @@ -15,7 +15,7 @@ class Consume {
async processMessage(message, fn) {
try {
await fn(message);
await aws.SQS.deleteMessage(this.queueUrl, message.ReceiptHandle);
await deleteSQSMessage(this.queueUrl, message.ReceiptHandle);
}
catch (e) {
log.error(e);
Expand All @@ -25,7 +25,7 @@ class Consume {
async processMessages(fn, messageLimit) {
let counter = 0;
while (!this.endConsume) {
const messages = await aws.SQS.receiveMessage(this.queueUrl, messageLimit);
const messages = await receiveSQSMessages(this.queueUrl, messageLimit);
counter += messages.length;

if (messages.length > 0) {
Expand Down
168 changes: 168 additions & 0 deletions packages/integration-tests/local.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/**
* Includes helper functions for replicating Step Function Workflows
* locally
*/
'use strict';

const path = require('path');
const fs = require('fs-extra');
const clone = require('lodash.clonedeep');
const { randomString } = require('@cumulus/common/test-utils');
const { template } = require('@cumulus/deployment/lib/message');
const { fetchMessageAdapter } = require('@cumulus/deployment/lib/adapter');

/**
* Download cumulus message adapter (CMA) and unzip it
*
* @param {string} version - cumulus message adapter version number (optional)
* @returns {Promise.<Object>} an object with path to the zip and extracted CMA
*/
async function downloadCMA(version) {
// download and unzip the message adapter
const gitPath = 'cumulus-nasa/cumulus-message-adapter';
const filename = 'cumulus-message-adapter.zip';
const src = path.join(process.cwd(), 'tests', `${randomString()}.zip`);
const dest = path.join(process.cwd(), 'tests', randomString());
await fetchMessageAdapter(version, gitPath, filename, src, dest);
return {
src,
dest
};
}

/**
* Copy cumulus message adapter python folder to each task
* in the workflow
*
* @param {Object} workflow - a test workflow object
* @param {string} src - the path to the cumulus message adapter folder
* @param {string} cmaFolder - the name of the folder where CMA is copied to
* @returns {Promise.<Array>} an array of undefined values
*/
function copyCMAToTasks(workflow, src, cmaFolder) {
return Promise.all(
workflow.steps.map(
(step) => fs.copy(src, path.join(step.lambda, cmaFolder))
)
);
}

/**
* Delete cumulus message adapter from all tasks in the test workflow
*
* @param {Object} workflow - a test workflow object
* @param {string} cmaFolder - the name of the folder where CMA is copied to
* @returns {Promise.<Array>} an array of undefined values
*/
function deleteCMAFromTasks(workflow, cmaFolder) {
return Promise.all(
workflow.steps.map(
(step) => fs.remove(path.join(step.lambda, cmaFolder))
)
);
}

/**
* Build a cumulus message for a given workflow
*
* @param {Object} workflow - a test workflow object
* @param {Object} configOverride - a cumulus config override object
* @param {Array} cfOutputs - mocked outputs of a CloudFormation template
* @returns {Object} the generated cumulus message
*/
function messageBuilder(workflow, configOverride, cfOutputs) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking but here and elsewhere in this PR it could be more maintainable to use an object argument, e.g.

function messageBuilder({workflow, configOverride, cfOutputs})

this makes it easier to add arguments without being concerned about what order they are in when you make the function call.

const workflowConfigs = {};
workflow.steps.forEach((step) => {
workflowConfigs[step.name] = step.cumulusConfig;
});

const config = {
stack: 'somestack',
workflowConfigs: {
[workflow.name]: workflowConfigs
}
};
Object.assign(config, configOverride);
config.stackName = config.stack;

const message = template(workflow.name, { States: workflowConfigs }, config, cfOutputs);
message.cumulus_meta.message_source = 'local';
return message;
}

/**
* Runs a given workflow step (task)
*
* @param {string} lambdaPath - the local path to the task (e.g. path/to/task)
* @param {string} lambdaHandler - the lambda handler (e.g. index.hanlder)
* @param {Object} message - the cumulus message input for the task
* @param {string} stepName - name of the step/task
* @returns {Promise.<Object>} the cumulus message returned by the task
*/
async function runStep(lambdaPath, lambdaHandler, message, stepName) {
const taskFullPath = path.join(process.cwd(), lambdaPath);
const src = path.join(taskFullPath, 'adapter.zip');
const dest = path.join(taskFullPath, 'cumulus-message-adapter');

process.env.CUMULUS_MESSAGE_ADAPTER_DIR = dest;

// add step name to the message
message.cumulus_meta.task = stepName;

try {
// run the task
const moduleFn = lambdaHandler.split('.');
const moduleFileName = moduleFn[0];
const moduleFunctionName = moduleFn[1];
const task = require(`${taskFullPath}/${moduleFileName}`); // eslint-disable-line global-require

console.log(`Started execution of ${stepName}`);

return new Promise((resolve, reject) => {
task[moduleFunctionName](message, {}, (e, r) => {
if (e) return reject(e);
console.log(`Completed execution of ${stepName}`);
return resolve(r);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think a return is necessary before reject and resolve since this function returns the Promise itself

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's good practice to return something in a function if possible.

});
});
}
finally {
await fs.remove(src);
}
}

/**
* Executes a given workflow by running each step in the workflow
* one after each other
*
* @param {Object} workflow - a test workflow object
* @param {Object} message - input message to the workflow
* @returns {Promise.<Object>} an object that includes the workflow input/output
* plus the output of every step
*/
async function runWorkflow(workflow, message) {
const trail = {
input: clone(message),
stepOutputs: {},
output: {}
};

let stepInput = clone(message);

for (const step of workflow.steps) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious - is there a reason to use this iteration syntax over workflowSteps.forEach((step) => { ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has to happen in sequence. If we don't use for of, we have to use some other library to ensure each async task happen after the other one ended.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that makes sense

stepInput = await runStep(step.lambda, step.handler, stepInput, step.name);
trail.stepOutputs[step.name] = clone(stepInput);
}
trail.output = clone(stepInput);

return trail;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think trail is a promise, as documentation suggests is returned from this function but I could be missing something

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is an async/await function so it always return a promise.

}

module.exports = {
downloadCMA,
copyCMAToTasks,
deleteCMAFromTasks,
runStep,
runWorkflow,
messageBuilder
};
2 changes: 2 additions & 0 deletions packages/integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@
"license": "Apache-2.0",
"dependencies": {
"@cumulus/common": "^1.1.0",
"@cumulus/deployment": "^1.0.1",
"babel-core": "^6.25.0",
"babel-loader": "^6.2.4",
"babel-plugin-transform-async-to-generator": "^6.24.1",
"babel-polyfill": "^6.23.0",
"babel-preset-es2017": "^6.24.1",
"commander": "^2.9.0",
"fs-extra": "^5.0.0",
"lodash.clonedeep": "^4.5.0",
"uuid": "^3.2.1",
"webpack": "^1.12.13"
}
Expand Down
34 changes: 34 additions & 0 deletions tests/fixtures/collections.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"MOD09GQ": {
"name": "MOD09GQ",
"version": "006",
"dataType": "MOD09GQ",
"process": "modis",
"provider_path": "/pdrs",
"granuleId": "^MOD09GQ\\.A[\\d]{7}\\.[\\S]{6}\\.006.[\\d]{13}$",
"sampleFileName": "MOD09GQ.A2017025.h21v00.006.2017034065104.hdf",
"granuleIdExtraction": "(MOD09GQ\\.(.*))\\.hdf",
"files": [
{
"regex": "^MOD09GQ\\.A[\\d]{7}\\.[\\S]{6}\\.006.[\\d]{13}\\.hdf$",
"bucket": "protected",
"sampleFileName": "MOD09GQ.A2017025.h21v00.006.2017034065104.hdf"
},
{
"regex": "^MOD09GQ\\.A[\\d]{7}\\.[\\S]{6}\\.006.[\\d]{13}\\.hdf\\.met$",
"bucket": "private",
"sampleFileName": "MOD09GQ.A2017025.h21v00.006.2017034065104.hdf.met"
},
{
"regex": "^MOD09GQ\\.A[\\d]{7}\\.[\\S]{6}\\.006.[\\d]{13}\\.meta\\.xml$",
"bucket": "protected",
"sampleFileName": "MOD09GQ.A2017025.h21v00.006.2017034065104.meta.xml"
},
{
"regex": "^MOD09GQ\\.A[\\d]{7}\\.[\\S]{6}\\.006.[\\d]{13}_1\\.jpg$",
"bucket": "public",
"sampleFileName": "MOD09GQ.A2017025.h21v00.006.2017034065104_1.jpg"
}
]
}
}
Loading