diff --git a/gulpfile.babel.js b/gulpfile.babel.js new file mode 100644 index 0000000000..fd94feccd0 --- /dev/null +++ b/gulpfile.babel.js @@ -0,0 +1,125 @@ +import { exec } from 'child_process'; +import psTree from 'ps-tree'; +import gulp from 'gulp'; +import net from 'net'; +import Q from 'q'; + +const TEST_SERVER_PORT = 3001 +const TEST_DB = 'habitrpg_test' + +const TEST_DB_URI = `mongodb://localhost/${TEST_DB}` + +/* + * This is a helper function that allows us to kill background tasks, such as + * the Selenium webdriver. We need to recurse through any child processes they + * have spun up, or gulp will hang after task completion. + */ +let kill = (proc) => { + ((pid) => { + psTree(pid, (_, pids) => { + if(pids.length) { + pids.forEach(kill); return + } + try { + exec(/^win/.test(process.platform) + ? `taskkill /PID ${pid} /T /F` + : `kill -9 ${pid}`) + } + catch(e) { console.log(e) } + }); + }(proc.PID || proc.pid)); +}; + +/* + * Another helper function, returns a promise that will resolve when a response + * is received on the specified port. Accepts a second argument indicating the + * maximum seconds to wait before failing. + */ +let awaitPort = (port, max = 60) => { + let socket, timeout, interval; + let deferred = Q.defer(); + + timeout = setTimeout(() => { + clearInterval(interval); + deferred.reject(`Timed out after ${max} seconds`); + }, max * 1000); + + interval = setInterval(() => { + socket = net.connect({port: port}, () => { + clearInterval(interval); + clearTimeout(timeout); + socket.destroy(); + deferred.resolve(); + }).on('error', () => { socket.destroy }); + }, 1000); + + return deferred.promise +}; + +/* + * And another helper function to add "noisy" listeners (pipe child process + * stdout and stderr to the parent. + */ +let listen = (child) => { + child.stdout.on('data', (data) => { process.stdout.write(data) }); + child.stderr.on('data', (data) => { process.stderr.write(data) }); +}; + +gulp.task('test:common', (cb) => { + listen(exec('NODE_ENV=testing ./node_modules/.bin/mocha test/common', cb)); +}); + +gulp.task('test:api', (cb) => { + listen(exec('NODE_ENV=testing ./node_modules/.bin/mocha test/api', cb)); +}); + +gulp.task('test:karma', (cb) => { + listen(exec('NODE_ENV=testing ./node_modules/.bin/grunt karma:continuous', cb)); +}); + +gulp.task('test:prepare:build', (cb) => { + exec('grunt build:test', cb); +}); + +gulp.task('test:prepare:mongo', (cb) => { + exec(`mongo "${TEST_DB}" --eval "db.dropDatabase()"`, cb); +}); + +gulp.task('test:prepare', [ + 'test:prepare:build', + 'test:prepare:webdriver', + 'test:prepare:mongo' +]); + +gulp.task('test:prepare:webdriver', (cb) => { + exec('./node_modules/protractor/bin/webdriver-manager update', cb); +}); + +gulp.task('test:e2e', ['test:prepare'], (cb) => { + let support = [ + 'Xvfb :99 -screen 0 1024x768x24 -extension RANDR', + `NODE_DB_URI="${TEST_DB_URI}" PORT="${TEST_SERVER_PORT}" node ./website/src/server.js`, + './node_modules/protractor/bin/webdriver-manager start', + ].map(exec); + + awaitPort(3001) + .then(awaitPort.bind(null, 4444)) + .then(() => { + listen( + exec('DISPLAY=:99 NODE_ENV=testing ./node_modules/protractor/bin/protractor protractor.conf.js', () => { + support.forEach(kill); + cb(); + }) + ); + }); + +}); + +gulp.task('test', [ + 'test:common', + 'test:api', + 'test:karma', + 'test:e2e' +]); + +gulp.task('default', ['test']); diff --git a/package.json b/package.json index d9089d2c73..9b4832f5f0 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "dependencies": { "async": "~0.9.0", "aws-sdk": "^2.0.25", + "babel": "^5.5.4", "bower": "~1.3.12", "browserify": "~3.30.2", "coffee-script": "1.6.x", @@ -30,6 +31,7 @@ "grunt-karma": "~0.6.2", "grunt-nodemon": "~0.3.0", "grunt-spritesmith": "~3.5.0", + "gulp": "^3.9.0", "icalendar": "git://github.com/lefnire/node-icalendar#master", "image-size": "~0.3.2", "in-app-purchase": "^0.2.0", @@ -52,7 +54,9 @@ "paypal-ipn": "2.1.0", "paypal-rest-sdk": "^1.2.1", "pretty-data": "git://github.com/vkiryukhin/pretty-data#master", + "ps-tree": "^1.0.0", "push-notify": "^1.1.1", + "q": "^1.4.1", "qs": "^2.3.2", "request": "~2.44.0", "s3-upload-stream": "^1.0.6", @@ -74,7 +78,7 @@ "node": "0.10.x" }, "scripts": { - "test": "./node_modules/coffee-script/bin/coffee ./test/runTests.coffee -n", + "test": "./node_modules/.bin/gulp", "start": "grunt run:dev", "postinstall": "./node_modules/bower/bin/bower --config.interactive=false install -f; ./node_modules/.bin/grunt;", "coverage": "COVERAGE=true mocha --require register-handlers.js --reporter html-cov > coverage.html; open coverage.html" diff --git a/test/runTests.coffee b/test/runTests.coffee deleted file mode 100644 index a1cbec49f0..0000000000 --- a/test/runTests.coffee +++ /dev/null @@ -1,99 +0,0 @@ -sh = require('shelljs') -async = require('async') -TEST_DB = 'habitrpg_test' -TEST_DB_URI = "mongodb://localhost/#{TEST_DB}" -TEST_SERVER_PORT = 3001 -MAX_WAIT = 60 - -announce = (msg) -> - sh.echo '\x1b[36m%s\x1b[0m', "TEST SUITE: #{msg}" - -Suite = - # Primary Task - run: -> - announce "Preparing the test environment." - Suite.prepareEnvironment -> - announce "Test prep complete. Waiting for server availability." - Suite.awaitServers -> - announce "Servers are ready. Beginning tests." - Suite.summarize - "API Specs": Suite.runApiSpecs() - "Common Specs": Suite.runCommonSpecs() - "End-to-End Specs": Suite.runE2ESpecs() - "Karma Specs": Suite.runKarmaSpecs() - - # Output summary report when tests are done. - summarize: (results) -> - anyFailed = 0 - sh.echo "" - announce "Tests complete!\n\nSummary\n-------\n" - for name, result of results - if result is 0 - sh.echo '\x1b[36m%s\x1b[0m', "#{name}: \x1b[32mpassing" - else - anyFailed = 1 - sh.echo '\x1b[36m%s\x1b[0m', "#{name}: \x1b[31mfailing" - sh.echo "" - announce "Thanks for helping keep Habitica clean!" - process.exit(anyFailed) - - # Prepare files, db, and spin up servers. - prepareEnvironment: (cb) -> - sh.exec "grunt build:test" - sh.exec "mongo \"#{TEST_DB}\" --eval \"db.dropDatabase()\"" - sh.exec "./node_modules/protractor/bin/webdriver-manager update" - - # Spin this up even if we're not in a headless environment. Shouldn't matter. - sh.exec "Xvfb :99 -screen 0 1024x768x24 -extension RANDR", silent: true, async: true - - sh.exec "./node_modules/protractor/bin/webdriver-manager start", silent: true, async: true - sh.exec "NODE_DB_URI=\"#{TEST_DB_URI}\" PORT=\"#{TEST_SERVER_PORT}\" node ./website/src/server.js", silent: true, async: true - cb() - - # Ensure both the selenium and node servers are available - awaitServers: (cb) -> - async.parallel [Suite.awaitSelenium, Suite.awaitNode], (err, results) -> - throw err if err? - cb() - - awaitSelenium: (cb) -> - waited = 0 - interval = setInterval -> - if sh.exec('nc -z localhost 4444').code is 0 - clearInterval(interval) - cb() - waited += 1 - if waited > MAX_WAIT - clearInterval(interval) - cb(new Error("Timed out waiting for Selenium")) - , 1000 - - awaitNode: (cb) -> - waited = 0 - interval = setInterval -> - if sh.exec('nc -z localhost 3001').code is 0 - clearInterval(interval) - cb() - waited += 1 - if waited > MAX_WAIT - clearInterval(interval) - cb(new Error("Timed out waiting for Node server")) - , 1000 - - runApiSpecs: -> - announce "Running API Specs (Mocha)" - sh.exec("NODE_ENV=testing ./node_modules/mocha/bin/mocha test/api").code - - runCommonSpecs: -> - announce "Running Common Specs (Mocha)" - sh.exec("NODE_ENV=testing ./node_modules/mocha/bin/mocha test/common").code - - runE2ESpecs: -> - announce "Running End-to-End Specs (Protractor)" - sh.exec("DISPLAY=:99 NODE_ENV=testing ./node_modules/protractor/bin/protractor protractor.conf.js").code - - runKarmaSpecs: -> - announce "Running Karma Specs" - sh.exec("NODE_ENV=testing grunt karma:continuous").code - -Suite.run()