From 937d8ab55efb8586e836a941ab107d91d40f2f90 Mon Sep 17 00:00:00 2001 From: rlgomes Date: Tue, 8 Mar 2016 11:45:24 -0800 Subject: [PATCH] added adapter end to end tests fixes #73 add adapter tests for all of the adapters we intend on shipping with `juttle-engine` and want to minimally validate they can be started up and used to simply read/write data from the expected backend rework example tests to use the same docker utilities to running the various docker containers and waiting for those containers to be up and running before running tests --- .travis.yml | 1 + package.json | 5 +- test/README.md | 3 + test/adapters/adapters.spec.js | 196 ++++++++++++++++++++++ test/examples/examples.spec.js | 113 +++++++------ test/examples/start-example-containers.sh | 23 --- test/examples/stop-example-containers.sh | 18 -- test/lib/cmd.js | 51 ++++++ test/lib/docker.js | 150 +++++++++++++++++ 9 files changed, 465 insertions(+), 95 deletions(-) create mode 100644 test/adapters/adapters.spec.js delete mode 100755 test/examples/start-example-containers.sh delete mode 100755 test/examples/stop-example-containers.sh create mode 100644 test/lib/cmd.js create mode 100644 test/lib/docker.js diff --git a/.travis.yml b/.travis.yml index 0c595a7..f147c3e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,7 @@ before_install: - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose - chmod +x docker-compose - sudo mv docker-compose /usr/local/bin + - docker build -t juttle/juttle-engine:latest . script: - gulp lint diff --git a/package.json b/package.json index a01d16f..3eaf1f0 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "juttle-cloudwatch-adapter": "^0.3.0", "juttle-elastic-adapter": "^0.5.0", "juttle-gmail-adapter": "^0.5.0", - "juttle-graphite-adapter": "^0.4.0", + "juttle-graphite-adapter": "^0.4.2", "juttle-influx-adapter": "^0.5.0", "juttle-mysql-adapter": "^0.5.0", "juttle-opentsdb-adapter": "^0.2.0", @@ -69,6 +69,7 @@ "selenium-grid-status": "^0.2.0", "selenium-webdriver": "^2.48.2", "tmp": "0.0.28", - "underscore": "^1.8.3" + "underscore": "^1.8.3", + "uuid": "^2.0.1" } } diff --git a/test/README.md b/test/README.md index 741ba9a..117f469 100644 --- a/test/README.md +++ b/test/README.md @@ -14,6 +14,9 @@ To run the built in system tests simply run: gulp test --sys ``` +**NOTE:** be sure to run `docker build -t juttle-engine:local .` after each +code change if you intend on running the system tests locally. + The system tests rely on having docker to bring up the selenium containers and other supporting [docker](https://www.docker.com/) containers. If you happen to want to run the selenium tests locally by using your own available chrome diff --git a/test/adapters/adapters.spec.js b/test/adapters/adapters.spec.js new file mode 100644 index 0000000..2b9e75e --- /dev/null +++ b/test/adapters/adapters.spec.js @@ -0,0 +1,196 @@ +'use strict'; + +let Promise = require('bluebird'); +let docker = require('../lib/docker'); +let expect = require('chai').expect; +let fs = Promise.promisifyAll(require('fs')); +let logger = require('mocha-logger'); +let path = require('path'); +let retry = require('bluebird-retry'); +let tmp = require('tmp'); +let uuid = require('uuid'); + +function juttleBin(juttle) { + logger.log(`juttle -e "${juttle}"`); + return docker.exec('juttle-engine-local', + ['/opt/juttle-engine/bin/juttle', + '-e', + juttle], { quiet: true }); +} + +describe('adapters', () => { + + before(() => { + var tmpdir = tmp.dirSync().name; + return Promise.all([ + docker.destroy('elasticsearch-adapter-test'), + docker.destroy('influxdb-adapter-test'), + docker.destroy('graphite-adapter-test'), + docker.destroy('juttle-engine-local') + ]) + .then(() => { + return docker.checkJuttleEngineLocalExists(); + }) + .then(() => { + return Promise.all([ + docker.run({ + name: 'elasticsearch-adapter-test', + image: 'elasticsearch:1.5.2', + detach: true + }), + docker.run({ + name: 'influxdb-adapter-test', + image: 'tutum/influxdb:0.10', + detach: true + }), + docker.run({ + name: 'graphite-adapter-test', + image: 'sitespeedio/graphite:0.9.14', + detach: true + }) + ]); + }) + .then(() => { + var config = JSON.stringify({ + adapters: { + elastic: { + address: 'elasticsearch', + port: 9200 + }, + influx: { + url: 'http://influxdb:8086' + }, + graphite: { + carbon: { + host: 'graphite', + port: 2003 + }, + webapp: { + host: 'graphite', + port: 80, + username: 'guest', + password: 'guest' + } + } + } + }); + + var filename = path.join(tmpdir, '.juttle-config.json') + return fs.writeFileAsync(filename, config) + }) + .then(() => { + return docker.run({ + name: 'juttle-engine-local', + image: 'juttle/juttle-engine:latest', + ports: ['8080:8080'], + links: [ + 'elasticsearch-adapter-test:elasticsearch', + 'influxdb-adapter-test:influxdb', + 'graphite-adapter-test:graphite' + ], + volumes: [`${tmpdir}:/tmp`], + workdir: '/tmp', + detach: true + }); + }) + .then(() => { + return Promise.all([ + docker.waitForSuccess('elasticsearch-adapter-test', + ['curl', 'http://localhost:9200']), + docker.waitForSuccess('influxdb-adapter-test', + ['influx', '-execute', 'show databases']), + docker.waitForSuccess('graphite-adapter-test', + ['curl', '-u', 'guest:guest', 'http://localhost:80']) + ]); + }); + }); + + after(() => { + return Promise.all([ + docker.destroy('elasticsearch-adapter-test'), + docker.destroy('influxdb-adapter-test'), + docker.destroy('graphite-adapter-test'), + docker.destroy('juttle-engine-local') + ]); + }); + + describe('juttle-elastic-adapter', () => { + it('can write data and read back data using the juttle CLI', () => { + var id = uuid.v1().slice(0, 8); + + return juttleBin(`emit -limit 5 -from :2014-01-01: | put name="test-${id}",value=count() | write elastic`) + .then(() => { + return retry(() => { + return juttleBin(`read elastic -from :2014-01-01: name="test-${id}" | view text`) + .then((output) => { + var data = JSON.parse(output); + expect(data).to.deep.equal([ + { time: '2014-01-01T00:00:00.000Z', name: `test-${id}`, value: 1 }, + { time: '2014-01-01T00:00:01.000Z', name: `test-${id}`, value: 2 }, + { time: '2014-01-01T00:00:02.000Z', name: `test-${id}`, value: 3 }, + { time: '2014-01-01T00:00:03.000Z', name: `test-${id}`, value: 4 }, + { time: '2014-01-01T00:00:04.000Z', name: `test-${id}`, value: 5 } + ]); + }); + },{ timeout: 50000 } ); + }); + }); + }); + + describe('juttle-influx-adapter', () => { + it('can write data and read back data using the juttle CLI', () => { + var id = uuid.v1().slice(0, 8); + var db = `testdb_${id}`; + + return juttleBin(`read influx -db 'dummy' -raw 'CREATE DATABASE ${db}';`) + .then(() => { + return juttleBin(`emit -limit 5 -from :2014-01-01: | put name="test-${id}", value=count() | write influx -db '${db}'`); + }) + .then(() => { + return retry(() => { + return juttleBin(`read influx -db '${db}' -from :2014-01-01: name="test-${id}" | view text`) + .then((output) => { + var data = JSON.parse(output); + expect(data).to.deep.equal([ + { time: '2014-01-01T00:00:00.000Z', name: `test-${id}`, value: 1 }, + { time: '2014-01-01T00:00:01.000Z', name: `test-${id}`, value: 2 }, + { time: '2014-01-01T00:00:02.000Z', name: `test-${id}`, value: 3 }, + { time: '2014-01-01T00:00:03.000Z', name: `test-${id}`, value: 4 }, + { time: '2014-01-01T00:00:04.000Z', name: `test-${id}`, value: 5 } + ]); + }); + }, { timeout: 5000 }); + }); + }); + }); + + describe('juttle-graphite-adapter', () => { + it('can write data and read back data using the juttle CLI', () => { + var id = uuid.v1().slice(0, 8); + + // default data retention for graphite server is: + // + // retentions = 5m:1d,15m:21d,30m:60d + // + // so we'll generate data from 30 minutes ago every 5 minutes and + // then read back the exact points + return juttleBin(`emit -limit 5 -every :5m: -from :30 minutes ago: | put name="test-${id}", value=count() | write graphite`) + .then(() => { + return retry(() => { + return juttleBin(`read graphite -from :60 minutes ago: name="test-${id}" | keep name, value | view text`) + .then((output) => { + var data = JSON.parse(output); + expect(data).to.deep.equal([ + { name: `test-${id}`, value: 1 }, + { name: `test-${id}`, value: 2 }, + { name: `test-${id}`, value: 3 }, + { name: `test-${id}`, value: 4 }, + { name: `test-${id}`, value: 5 } + ]); + }) + }, { timeout: 5000 }); + }); + }); + }); + +}); diff --git a/test/examples/examples.spec.js b/test/examples/examples.spec.js index 962f173..e9f8eed 100644 --- a/test/examples/examples.spec.js +++ b/test/examples/examples.spec.js @@ -3,47 +3,40 @@ let _ = require('underscore'); let Promise = require('bluebird'); let chakram = require('chakram'); +let docker = require('../lib/docker'); var expect = chakram.expect; -let execAsync = Promise.promisify(require('child_process').exec); let fs = require('fs'); let http = require('http'); -let logger = require('mocha-logger'); let path = require('path'); +let cmd = require('../lib/cmd'); let retry = require('bluebird-retry'); -let spawn = require('child_process').spawn; let WebSocket = require('ws'); -function spawnify(command, args, options) { - return new Promise((resolve, reject) => { - args = args || []; - logger.log('spawning "' + command + ' ' + args.join(' ')); - options = _.extend( { - detached: true, - stdio: ['ignore', 'pipe', 'pipe'] - }, options); - - var spawned = spawn(command, args, options); - var stdout = ''; - var stderr = ''; - - spawned.stdout.on('data', (data) => { - logger.log('STDOUT:', data); - stdout += data; - }); +const YMLS = [ + 'dc-juttle-engine.yml', + 'cadvisor-influx/dc-cadvisor-influx.yml', + 'postgres-diskstats/dc-postgres.yml' +] - spawned.stderr.on('data', (data) => { - logger.log('STDERR:', data); - stderr += data; - }); +function dockerCompose(ymls, command, commandArgs) { + commandArgs = commandArgs || []; + var args = []; - spawned.on('close', (code) => { - if (code === 0) { - resolve(stdout, stderr); - } else { - reject(Error('command: "' + command + ' ' + args.join(' ') + - '", failed with ' + code)); - } - }); + _.each(ymls, (yml) => { + args.push('-f'); + args.push(yml); + }); + + args.push(command); + args = args.concat(commandArgs); + + var env = Object.create(process.env); + // we need to set the PWD used by those yml files to . otherwise we inherit + // from the current process running at the root of the source code + env.PWD = '.'; + return cmd.spawnAsync('docker-compose', args, { + cwd: 'examples', + env: env }); } @@ -56,38 +49,54 @@ describe('examples', () => { let retryHTTPOptions = { interval: 500, timeout: 10000 - } + }; before(() => { - return execAsync('docker run ubuntu route | grep default | awk \'{print $2}\'') - .then((stdout) => { - hostAddress = stdout.trim(); - return spawnify('docker', ['build', '-q', '-t', 'juttle/juttle-engine:latest', '.']); + return docker.getHostAddress() + .then((address) => { + hostAddress = address; + return docker.checkJuttleEngineLocalExists(); + }) + .then(() => { + return dockerCompose(YMLS, 'stop'); }) .then(() => { - return spawnify('test/examples/start-example-containers.sh'); + return dockerCompose(YMLS, 'rm', ['--force']); }) .then(() => { - return retry(() => { - return new Promise((resolve, reject) => { - http.get(baseUrl, (resp) => { - if (resp.statusCode !== 200) { - reject(Error(`juttle-engine not responding on ${baseUrl}`)); - } else { - resolve(); - } - }) - .on('error', (err) => { - reject(err); + return dockerCompose(YMLS, 'up', ['-d']); + }) + .then(() => { + // wait for various underlying storages to be up and running + return Promise.all([ + docker.waitForSuccess('examples_postgres_1', + ['pgrep', 'postgres']), + docker.waitForSuccess('examples_influxdb_1', + ['influx', '-execute', 'show databases']), + retry(() => { + return new Promise((resolve, reject) => { + http.get(baseUrl, (resp) => { + if (resp.statusCode !== 200) { + reject(Error(`juttle-engine not responding on ${baseUrl}`)); + } else { + resolve(); + } + }) + .on('error', (err) => { + reject(err); + }); }); - }); - }, retryHTTPOptions); + }, retryHTTPOptions) + ]); }); }); after(() => { // shutdown and remove containers - return spawnify('test/examples/stop-example-containers.sh'); + return dockerCompose(YMLS, 'stop') + .then(() => { + return dockerCompose(YMLS, 'rm', ['--force']); + }); }); _.each({ diff --git a/test/examples/start-example-containers.sh b/test/examples/start-example-containers.sh deleted file mode 100755 index fd051bd..0000000 --- a/test/examples/start-example-containers.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -# -# helper script to start/stop example docker containers -# - -pushd examples - -docker-compose -f dc-juttle-engine.yml \ - -f cadvisor-influx/dc-cadvisor-influx.yml \ - -f postgres-diskstats/dc-postgres.yml \ - stop - -docker-compose -f dc-juttle-engine.yml \ - -f cadvisor-influx/dc-cadvisor-influx.yml \ - -f postgres-diskstats/dc-postgres.yml \ - rm -f - -docker-compose -f dc-juttle-engine.yml \ - -f cadvisor-influx/dc-cadvisor-influx.yml \ - -f postgres-diskstats/dc-postgres.yml \ - up -d - -popd diff --git a/test/examples/stop-example-containers.sh b/test/examples/stop-example-containers.sh deleted file mode 100755 index 84af103..0000000 --- a/test/examples/stop-example-containers.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -# -# helper script to start example docker containers -# - -pushd examples - -docker-compose -f dc-juttle-engine.yml \ - -f cadvisor-influx/dc-cadvisor-influx.yml \ - -f postgres-diskstats/dc-postgres.yml \ - stop - -docker-compose -f dc-juttle-engine.yml \ - -f cadvisor-influx/dc-cadvisor-influx.yml \ - -f postgres-diskstats/dc-postgres.yml \ - rm -f - -popd diff --git a/test/lib/cmd.js b/test/lib/cmd.js new file mode 100644 index 0000000..a8be931 --- /dev/null +++ b/test/lib/cmd.js @@ -0,0 +1,51 @@ +'use strict'; + +let _ = require('underscore'); +let Promise = require('bluebird'); +let logger = require('mocha-logger'); +let spawn = require('child_process').spawn; + +module.exports = { + spawnAsync: function(command, args, options) { + return new Promise((resolve, reject) => { + options = options || {}; + args = args || []; + + if (!options.quiet) { + logger.log('spawning "' + command + ' ' + args.join(' ') + '"'); + } + + options = _.extend( { + detached: true, + stdio: ['ignore', 'pipe', 'pipe'] + }, options); + + var spawned = spawn(command, args, options); + var stdout = ''; + var stderr = ''; + + spawned.stdout.on('data', (data) => { + if (!options.quiet) { + logger.log('STDOUT:', data); + } + stdout += data; + }); + + spawned.stderr.on('data', (data) => { + if (!options.quiet) { + logger.log('STDERR:', data); + } + stderr += data; + }); + + spawned.on('close', (code) => { + if (code === 0) { + resolve(stdout); + } else { + reject(Error('command: "' + command + ' ' + args.join(' ') + + '", failed with ' + code)); + } + }); + }); + } +}; diff --git a/test/lib/docker.js b/test/lib/docker.js new file mode 100644 index 0000000..fd88f83 --- /dev/null +++ b/test/lib/docker.js @@ -0,0 +1,150 @@ +'use strict'; + +let _ = require('underscore'); +let cmd = require('./cmd'); +let logger = require('mocha-logger'); +let retry = require('bluebird-retry'); +let uuid = require('uuid'); + +module.exports = { + getHostAddress: function() { + var name = uuid.v1(); + return cmd.spawnAsync('docker', ['run', '--name', name, 'ubuntu', 'route'], { quiet: true }) + .then((stdout) => { + var address; + _.each(stdout.split('\n'), (line) => { + if (line.indexOf('default') !== -1) { + address = line.split(/[\r\n\t ]+/)[1]; + } + }); + return address; + }) + .finally(() => { + return this.destroy(name); + }); + }, + + getContainerIPAddress: function(name) { + return cmd.spawnAsync('docker', ['inspect', name], { quiet: true }) + .then((stdout) => { + var data = JSON.parse(stdout); + return data[0].NetworkSettings.IPAddress; + }); + }, + + buildLocalJuttleEngine: function(options) { + // this obviously only works when running tests from the root of the + // repository + var tag = options.tag ? options.tag : 'juttle/juttle-engine:latest'; + return cmd.spawnAsync('docker', ['build', '-q', '-t', tag, '.']); + }, + + run: function(options) { + var args = ['run', '--name', options.name]; + + if (options.links) { + _.each(options.links, (link) => { + args.push('--link'); + args.push(link); + }); + } + + if (options.ports) { + _.each(options.ports, (port) => { + args.push('-p'); + args.push(port); + }); + } + + if (options.volumes) { + _.each(options.volumes, (volume) => { + args.push('--volume'); + args.push(volume); + }); + } + + if (options.workdir) { + args.push('--workdir'); + args.push(options.workdir); + } + + if (options.detach) { + args.push('-d'); + } + + args.push(options.image); + + return cmd.spawnAsync('docker', args); + }, + + stop: function(name, options) { + return cmd.spawnAsync('docker', ['stop', name], options); + }, + + rm: function(name, options) { + return cmd.spawnAsync('docker', ['rm', name], options); + }, + + containerExists: function(name) { + return cmd.spawnAsync('docker', ['inspect', name], { quiet: true }) + .then(() => { + return true; + }) + .catch(() => { + return false; + }); + }, + + destroy: function(name, options) { + return this.containerExists(name) + .then((exists) => { + if (exists) { + return this.stop(name, { quiet: true }) + .then(() => { + return this.rm(name, { quiet: true }); + }); + } + }); + }, + + exec: function(name, args, options) { + var dockerArgs = ['exec', name].concat(args); + return cmd.spawnAsync('docker', dockerArgs, options); + }, + + imageExists: function(image, tag, message) { + return cmd.spawnAsync('docker', ['images', image], { quiet: true }) + .then((output) => { + if (output.indexOf(tag) === -1) { + throw Error(message); + } + }); + }, + + checkJuttleEngineLocalExists: function() { + var message = 'You need to build the local juttle-engine container: ' + + '`docker build -t juttle/juttle-engine:latest .` before you ' + + 'can run these tests'; + return this.imageExists('juttle/juttle-engine', 'latest', message); + }, + + waitForSuccess: function(containerName, args, options) { + /* + * runs the command supplied as a list of arguments in the container + * specified until it exits with a zero return code + */ + options = options || {}; + options = _.defaults(options, { quiet: true }); + logger.log(`${containerName}: "${args.join(' ')}" waiting for success`); + return retry(() => { + return this.exec(containerName, args, options); + }, { interval: 1000, max_tries: 10 }) + .then(() => { + logger.log(`${containerName}: "${args.join(' ')}" succeeded`); + }) + .catch((err) => { + logger.log(`${containerName}: "${args.join(' ')}" failed`); + throw err; + }); + } +};