├── .gitignore ├── drones ├── stop-local.sh ├── accounts ├── stop.sh ├── start-local.sh ├── immortalCasper.sh ├── start.sh ├── Readme.md └── parties-stress.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | captures/done 3 | -------------------------------------------------------------------------------- /drones: -------------------------------------------------------------------------------- 1 | 192.168.2.231 2 | 192.168.2.232 3 | 192.168.2.233 4 | 192.168.2.234 5 | -------------------------------------------------------------------------------- /stop-local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | kill $(ps aux | grep 'immortalCasper' | awk '{print $2}') 3 | -------------------------------------------------------------------------------- /accounts: -------------------------------------------------------------------------------- 1 | casper1@example.org 2 | casper2@example.org 3 | casper3@example.org 4 | casper4@example.org 5 | casper5@example.org 6 | casper6@example.org 7 | casper7@example.org 8 | casper8@example.org 9 | casper9@example.org 10 | casper10@example.org 11 | casper11@example.org 12 | casper12@example.org 13 | -------------------------------------------------------------------------------- /stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # populate Drones 4 | declare -a Drones=() 5 | while read line; do Drones=("${Drones[@]}" "$line"); done < drones 6 | 7 | # kill the immortalCasper 8 | for i in "${Drones[@]}" 9 | do 10 | ssh $i "pid=\$(ps aux | grep 'immortalCasper.sh' | awk '{print \$2}' | head -1); echo \$pid |xargs kill" 11 | echo Stopped the immortal Casper on $i 12 | done 13 | -------------------------------------------------------------------------------- /start-local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # read options 3 | while getopts ":i:" opt; do 4 | case $opt in 5 | i) 6 | echo "Will run $OPTARG times" >&2 7 | drones="$OPTARG" 8 | ;; 9 | \?) 10 | echo "Invalid option: -$OPTARG" >&2 11 | ;; 12 | :) 13 | echo "Option -$OPTARG requires an argument." >&2 14 | exit 1 15 | ;; 16 | esac 17 | done 18 | 19 | i=0 20 | # run casper locally 21 | while [[ $i -lt $drones ]] 22 | do 23 | nohup ./immortalCasper.sh -u http://localhost:3000 -p password > /dev/null 2>&1 & 24 | i=$[$i+1] 25 | done -------------------------------------------------------------------------------- /immortalCasper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # read options 4 | while getopts ":u:p:" opt; do 5 | case $opt in 6 | u) 7 | echo "URL set to $OPTARG" >&2 8 | url="$OPTARG" 9 | ;; 10 | e) 11 | echo "EMAIL set to $OPTARG" >&2 12 | email="$OPTARG" 13 | ;; 14 | p) 15 | echo "PASSWORD set to $OPTARG" >&2 16 | password="$OPTARG" 17 | ;; 18 | \?) 19 | echo "Invalid option: -$OPTARG" >&2 20 | ;; 21 | :) 22 | echo "Option -$OPTARG requires an argument." >&2 23 | exit 1 24 | ;; 25 | esac 26 | done 27 | 28 | if [ -z "$url" -o -z "$password" ]; then 29 | echo "Usage: ./immortalCasper.sh -u -p " 30 | exit 31 | fi 32 | 33 | # populate Accounts 34 | declare -a Accounts=() 35 | while read line; do Accounts=("${Accounts[@]}" "$line"); done < accounts 36 | num_accounts=${#Accounts[*]} 37 | 38 | while : 39 | do 40 | casperjs parties-stress.js --email=${Accounts[$((RANDOM%num_accounts))]} --url=$url --password=$password 41 | done 42 | 43 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # read options 4 | while getopts ":u:p:d:" opt; do 5 | case $opt in 6 | u) 7 | echo "URL set to $OPTARG" >&2 8 | url="$OPTARG" 9 | ;; 10 | p) 11 | echo "PASSWORD set to $OPTARG" >&2 12 | password="$OPTARG" 13 | ;; 14 | d) 15 | echo "DIRECTORY set to $OPTARG" >&2 16 | directory="$OPTARG" 17 | ;; 18 | \?) 19 | echo "Invalid option: -$OPTARG" >&2 20 | ;; 21 | :) 22 | echo "Option -$OPTARG requires an argument." >&2 23 | exit 1 24 | ;; 25 | esac 26 | done 27 | 28 | if [ -z "$directory" -o -z "$url" -o -z "$password" ]; then 29 | echo "Usage: ./start.sh -u -p -d " 30 | exit 31 | fi 32 | 33 | # populate Drones 34 | declare -a Drones=() 35 | while read line; do Drones=("${Drones[@]}" "$line"); done < drones 36 | 37 | # copy all files to the Drones 38 | for i in "${Drones[@]}" 39 | do 40 | scp parties-stress.js $i:$directory 41 | scp immortalCasper.sh $i:$directory 42 | scp accounts $i:$directory 43 | ssh $i chmod +x $directory/immortalCasper.sh 44 | done 45 | 46 | 47 | # run casper in all drones 48 | for i in "${Drones[@]}" 49 | do 50 | ssh $i "nohup $directory/immortalCasper.sh -u $url -p $password > /dev/null 2>&1 & " 51 | echo Started the immortal Casper on $i 52 | done 53 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Meteor Parties Stresstest 2 | 3 | This is an example load testing application that may be run against the [Meteor Parties example](https://www.meteor.com/examples/parties). It actually simulates clients and their behavior rather than just the messages exchanged between client and server. 4 | 5 | Remember: 6 | **DO NOT RUN THIS AGAINST ANY SERVERS THAT ARE NOT YOURS! ESPECIALLY NOT AGAINST METEOR.COM!** 7 | 8 | ## Prerequisites 9 | 10 | This example expects one running Meteor server. Also there should be at least one `drone` (machine to run the client simulation). 11 | 12 | ### The application under test 13 | 14 | Create the meteor parties example and run it on the server you would like to test: 15 | 16 | $ meteor create --example parties 17 | 18 | *Hint*: This test will not differentiate between MongoDB and Meteor. 19 | 20 | ### Required Software 21 | 22 | The machines used for testing (client drones) must be [accessible via key](http://sshkeychain.sourceforge.net/mirrors/SSH-with-Keys-HOWTO/SSH-with-Keys-HOWTO-4.html) and not ask for a password when you try to connect via ssh. 23 | Furthermore you must install both [PhantomJS]:http://phantomjs.org/download.html and [CasperJS]:http://docs.casperjs.org/en/latest/installation.html on the drones. 24 | 25 | ## Usage 26 | 27 | 1. Clone this repository 28 | 2. Set up SSH keys to connect to your drones 29 | 3. Add the IP/address of your drones to the `drones` file 30 | 4. Add the e-mail addresses to be used for the test in the `accounts file` 31 | 5. Run the parties example and make sure the drones can access the URL 32 | 6. Start the drones using `$./start.sh -u http://mytesturl:3000 -d /home/meteor -p password` 33 | 7. Open your URL and watch the Party Frenzy begin! 34 | 8. Use `stop.sh` when the parents are home again. 35 | 36 | ## Files 37 | 38 | This is in the package: 39 | 40 | ### parties-stress.js 41 | 42 | This is the actual user simulation. It tries to register an account with the app. If that fails, it tries to log in instead. If that fails as well, it gives up. 43 | Once authenticated it creates a party somewhere and then RSVPs for a random other party. 44 | 45 | You can call this script directly using 46 | 47 | $ casperjs parties-stress.js --url=http://mytesturl:3000 --email=casper@example.com --password=password 48 | 49 | If you do not provide any arguments, default values will be used (most importantly it will run against http://127.0.0.1). 50 | 51 | ### drones 52 | 53 | Inside the drones file enter the URL or IP for each of your client machines used during testing. Make sure to hit return after each entry! 54 | 55 | ### accounts 56 | 57 | List all the email addresses you wish to use as accounts for your drones. They will use them randomly. Watch out - there is no guarantee that one account is only logged in once at a time! 58 | Also you need to add a newline after the last entry. 59 | 60 | In case you want to keep the password private it is not stored inside the accounts file. You will have to provide it on the command line instead. 61 | 62 | ### The Bash scripts 63 | 64 | These help you scale the testing. `start.sh` copies all files to the drones, `immortalCasper.sh` calls the `parties-stress.js` infinitely, and `stop.sh` will kill it from all drones. 65 | 66 | #### start.sh 67 | 68 | `start.sh` is used like that: 69 | 70 | $ ./start.sh -u http://mytesturl:3000 -p password -d /home/meteor 71 | 72 | Once executed the testing begins and your URL will get hammered with never-ending requests. 73 | 74 | #### stop.sh 75 | 76 | It doesn't take any arguments, just call it to stop the immortalCasper instances. 77 | 78 | #### immortalCasper.sh 79 | 80 | This script initiates the casper process. Once finished, it will call it again. And again. And again. And... 81 | 82 | ./immortalCasper.sh -u http://mytesturl:3000 -p password 83 | 84 | 85 | ## Monitoring the Results 86 | 87 | Because these scripts will only generate load or stress on your server, you should contemplate how to monitor your servers behavior. One suggestion is to add [Kadira](https://kadira.io/) to your application. Alternatively, if you are running on your own infrastructure, you should use dedicated monitoring software such as [Munin](http://munin-monitoring.org/) to observe your applications behavior. 88 | 89 | ## Known Issues 90 | 91 | * Sometimes a selector for a party throws an error - no idea why. 92 | * Between runs users are not logged out. 93 | * There is no easy way to retrieve the number of currently running clients, you need to keep track manually. 94 | * Running multiple times on the same drone is quirky. Try using a different account for a second instance. 95 | 96 | ## Meta 97 | Created by [Stephan Hochhaus](mailto:stephan@yauh.de), July 2014. -------------------------------------------------------------------------------- /parties-stress.js: -------------------------------------------------------------------------------- 1 | // Testing the parties example with an automated ghost 2 | var casper = require('casper').create(); 3 | 4 | // get variables from command line or use defaults as a fallback 5 | if (casper.cli.has("email")) { 6 | var email = casper.cli.get("email"); 7 | } 8 | else { 9 | var email = 'casper@example.com'; 10 | } 11 | if (casper.cli.has("password")) { 12 | var password = casper.cli.get("password"); 13 | } 14 | else { 15 | var password = 'password'; 16 | } 17 | if (casper.cli.has("url")) { 18 | var url = casper.cli.get("url"); 19 | } 20 | else { 21 | var url = 'http://localhost:3000/'; 22 | } 23 | 24 | var partyTitles = [ 25 | "Alive and kicking", 26 | "Accelerate", 27 | "Beerfest", 28 | "Breakfast at Tiffany's", 29 | "Browncoats unite", 30 | "Burning down the House", 31 | "BYOB", 32 | "City Shag", 33 | "Cloud city", 34 | "Das Fest", 35 | "Dude, where's my car?", 36 | "Exterminate!", 37 | "Have you met Ted?", 38 | "House Party", 39 | "Kelly's Call", 40 | "Kwanzaa", 41 | "La Boum", 42 | "Lollapalooza", 43 | "Meet the Feebles", 44 | "No siesta fiesta", 45 | "Out all night", 46 | "Party PEOPLE!!!!", 47 | "Risky Business", 48 | "Rock Da House", 49 | "Rockin' the Night Away", 50 | "Slayerfest '98", 51 | "Slurms MacKenzie's Frenzy", 52 | "Stan's previously owned party", 53 | "The more the merrier", 54 | "The Party", 55 | "TurboDiesel", 56 | "Woo! Party!" 57 | ]; 58 | 59 | var rsvpChoices = ["rsvp_yes", "rsvp_no", "rsvp_maybe"]; 60 | var rsvpChoices_weight = [5, 2, 1]; 61 | var rsvpChoices_totalWeight = eval(rsvpChoices_weight.join("+")); 62 | var weighedChoices=new Array() //new array to hold "weighted" fruits 63 | var currentChoice=0 64 | 65 | while (currentChoice div.login-form.login-password-form > div.message.error-message', 92 | function then() { // if we find the selector we got an error 93 | if (getErrorMsg(this) === "Email already exists.") { 94 | this.echo("Let's try if I can log in"); 95 | logIn(this); 96 | } 97 | else { // for all other errors, e.g. wrong password, we have to stop here 98 | this.echo('Sorry, I do not know how to authenticate.'); 99 | } 100 | }, 101 | function () { // this precents us from 102 | this.echo('No errors encountered. Time for Party!'); 103 | }); 104 | }); 105 | 106 | // CREATE A PARTY 107 | createParty(); 108 | 109 | // RSVP TO A PARTY - 2 ACTUALLY 110 | rsvpParty(); 111 | rsvpParty(); 112 | 113 | // WAIT AND TAKE A PICTURE 114 | // ONLY WORKS WELL, IF NOT TOO MANY INSTANCES RUN IN PARALLEL 115 | //casper.then(function () { 116 | // this.wait(1000, function () { 117 | // this.capture('captures/done', undefined, { 118 | // format: 'png' 119 | // }); 120 | // }); 121 | //}); 122 | 123 | casper.then(function () { 124 | this.echo("I'm done. I quit"); 125 | this.exit(); 126 | }); 127 | 128 | // RUN THE CASPER 129 | casper.run(); 130 | 131 | // HELPFUL FUNCTIONS 132 | function getErrorMsg(obj) { 133 | return obj.getHTML('#login-dropdown-list > div.login-form.login-password-form > div.message.error-message'); 134 | } 135 | 136 | function signUp(obj) { 137 | obj.echo("Performing sign up for " + email + " (password: " + password + ") at " + url); 138 | obj.waitForSelector('#login-sign-in-link', function () { 139 | obj.click('#login-sign-in-link'); 140 | obj.click('#signup-link'); 141 | }); 142 | obj.waitForSelector('.login-form', function () { 143 | obj.fillSelectors('.login-form', { 144 | '#login-email': email, 145 | '#login-password': password 146 | }, true); 147 | obj.click('#login-buttons-password'); 148 | }); 149 | } 150 | 151 | function logIn(obj) { 152 | obj.echo('Performing Log in'); 153 | if (obj.exists('#login-dropdown-list > a')) { 154 | obj.click('#login-dropdown-list > a'); 155 | } 156 | casper.waitForSelector('#login-sign-in-link', function () { 157 | this.click('#login-sign-in-link'); 158 | this.click('#signup-link'); 159 | }); 160 | casper.waitForSelector('.login-form', function () { 161 | this.fillSelectors('.login-form', { 162 | '#login-email': email, 163 | '#login-password': password 164 | }, true); 165 | if (obj.exists('#back-to-login-link')) { 166 | obj.click('#back-to-login-link'); 167 | } 168 | this.click('#login-buttons-password'); 169 | }); 170 | obj.waitForSelector('#login-name-link', function () { 171 | obj.echo('Logged in as ' + obj.getHTML('#login-name-link')); 172 | }); 173 | } 174 | 175 | function logOut(obj) { 176 | obj.echo('Performing Log out'); 177 | if (obj.exists('#login-name-link')) { 178 | obj.echo('logging out user ' + obj.getHTML('#login-name-link')); 179 | obj.click('#login-name-link'); 180 | obj.click('#login-buttons-logout'); 181 | } else { 182 | obj.echo("Nobody was logged in."); 183 | } 184 | } 185 | 186 | function createParty() { 187 | casper.then(function () { 188 | this.echo('Start a new party!'); 189 | this.mouse.doubleclick(randomCoordinates()[0], randomCoordinates()[1]); 190 | }); 191 | casper.waitForSelector('body > div.modal > div.modal-footer > a.btn.btn-primary.save', function () { 192 | this.echo('Adding a party'); 193 | this.fillSelectors('.modal', { 194 | 'body > div.modal > div.modal-body > input': returnRandomChoice(partyTitles), 195 | 'body > div.modal > div.modal-body > textarea': "This party was brought to you by the power of automated testing and Mr or Ms " + email 196 | }, true); 197 | this.click('body > div.modal > div.modal-footer > a.btn.btn-primary.save'); 198 | }); 199 | } 200 | 201 | function rsvpParty() { 202 | casper.then(function () { 203 | // return a random party ID, but never an empty one 204 | // TODO: There is still an issue with *some* Caspers but only with certain *IDs (reproducable) 205 | //this.echo(this.getElementsAttribute('circle', 'id').filter(function(e){return e})); 206 | var partyIdSelector = 'circle#' + returnRandomChoice(this.getElementsAttribute('circle', 'id').filter(function(e){return e})); 207 | var rsvpSelector = '.' + returnRandomChoice(weighedChoices); 208 | this.echo('For ' + partyIdSelector + ' I will ' + rsvpSelector) 209 | casper.waitForSelector(partyIdSelector, function () { 210 | this.click(partyIdSelector); 211 | }); 212 | casper.waitForSelector(rsvpSelector, function () { 213 | this.click(rsvpSelector); 214 | }); 215 | }); 216 | } 217 | 218 | function randomCoordinates() { 219 | // watch out - these may only work with a viewport of 1280x768 220 | var x = Math.floor(Math.random() * (650 - 155 + 1) + 155); 221 | var y = Math.floor(Math.random() * (590 - 90 + 1) + 90); 222 | return [x, y]; 223 | } 224 | 225 | function returnRandomChoice(array) { 226 | return array[Math.floor(Math.random() * array.length)]; 227 | } --------------------------------------------------------------------------------