Skip to content

Commit

Permalink
Merge pull request #238 from cumulus-nasa/CUMULUS-371
Browse files Browse the repository at this point in the history
end-to-end tests in circleci [CUMULUS-371]
  • Loading branch information
Alireza committed Mar 9, 2018
2 parents da2df8a + 29c7d8d commit 1c5ce4c
Show file tree
Hide file tree
Showing 13 changed files with 612 additions and 5 deletions.
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 [];
};

/**
* 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) {
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);
});
});
}
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) {
stepInput = await runStep(step.lambda, step.handler, stepInput, step.name);
trail.stepOutputs[step.name] = clone(stepInput);
}
trail.output = clone(stepInput);

return trail;
}

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

0 comments on commit 1c5ce4c

Please sign in to comment.