├── .editorconfig ├── .gitignore ├── .npmignore ├── CONTRIBUTING.md ├── Gruntfile.coffee ├── LICENSE ├── README.md ├── git_hooks └── pre-commit │ └── version ├── index.coffee ├── package.json ├── src ├── monkey.coffee └── monkey │ ├── api.coffee │ ├── client.coffee │ ├── command.coffee │ ├── connection.coffee │ ├── multi.coffee │ ├── parser.coffee │ ├── queue.coffee │ └── reply.coffee └── test ├── mock └── duplex.coffee ├── monkey.coffee └── monkey ├── api.coffee ├── client.coffee ├── command.coffee ├── connection.coffee ├── multi.coffee ├── parser.coffee ├── queue.coffee └── reply.coffee /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = tab 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.js] 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.json] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.coffee] 21 | indent_style = space 22 | indent_size = 2 23 | 24 | [*.md] 25 | indent_style = space 26 | indent_size = 4 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /lib/ 3 | /temp/ 4 | /index.js 5 | /*.tgz 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.editorconfig 2 | /.npmignore 3 | /*.tgz 4 | /npm-debug.log 5 | /Gruntfile.coffee 6 | /index.coffee 7 | /CONTRIBUTING.md 8 | /src/ 9 | /test/ 10 | /temp/ 11 | /git_hooks/ 12 | *.!sync 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We are happy to accept any contributions that make sense and respect the rules listed below. 4 | 5 | ## How to contribute 6 | 7 | 1. Fork the repo. 8 | 2. Create a feature branch for your contribution out of the `develop` branch. We use the [git-flow][gitflow-tool] tool to implement the [successful Git branching model][gitflow-post]. Only one contribution per branch is accepted. 9 | 3. Implement your contribution while respecting our rules (see below). 10 | 4. Run `npm test` to make sure you didn't break anything. 11 | 5. Add tests for your contribution so that no one else will break it. 12 | 6. Submit a pull request against our `develop` branch! 13 | 14 | ## Rules 15 | 16 | * **Do** use feature branches. 17 | * **Do** conform to existing coding style so that your contribution fits in. 18 | * **Do** use [EditorConfig] to enforce our [whitespace rules](.editorconfig). If your editor is not supported, enforce the settings manually. 19 | * **Do** run `npm test` for CoffeeLint, JSONLint and unit test coverage. 20 | * **Do not** touch the `version` field in [package.json](package.json). 21 | * **Do not** commit any generated files, unless already in the repo. If absolutely necessary, explain why. 22 | * **Do not** create any top level files or directories. If absolutely necessary, explain why and update [.npmignore](.npmignore). 23 | 24 | ## License 25 | 26 | By contributing your code, you agree to license your contribution under our [LICENSE](LICENSE). 27 | 28 | [gitflow-post]: 29 | [gitflow-tool]: 30 | [editorconfig]: 31 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | grunt.initConfig 4 | pkg: require './package' 5 | coffee: 6 | src: 7 | expand: true 8 | cwd: 'src' 9 | src: '**/*.coffee' 10 | dest: 'lib' 11 | ext: '.js' 12 | index: 13 | src: 'index.coffee' 14 | dest: 'index.js' 15 | clean: 16 | lib: 17 | src: 'lib' 18 | index: 19 | src: 'index.js' 20 | coffeelint: 21 | src: 22 | src: '<%= coffee.src.cwd %>/<%= coffee.src.src %>' 23 | index: 24 | src: '<%= coffee.index.src %>' 25 | test: 26 | src: 'test/**/*.coffee' 27 | gruntfile: 28 | src: 'Gruntfile.coffee' 29 | jsonlint: 30 | packagejson: 31 | src: 'package.json' 32 | watch: 33 | src: 34 | files: '<%= coffee.src.cwd %>/<%= coffee.src.src %>' 35 | tasks: ['coffeelint:src', 'test'] 36 | index: 37 | files: '<%= coffee.index.src %>' 38 | tasks: ['coffeelint:index', 'test'] 39 | test: 40 | files: '<%= coffeelint.test.src %>', 41 | tasks: ['coffeelint:test', 'test'] 42 | gruntfile: 43 | files: '<%= coffeelint.gruntfile.src %>' 44 | tasks: ['coffeelint:gruntfile'] 45 | packagejson: 46 | files: '<%= jsonlint.packagejson.src %>' 47 | tasks: ['jsonlint:packagejson'] 48 | exec: 49 | mocha: 50 | options: [ 51 | '--compilers coffee:coffee-script' 52 | '--reporter spec' 53 | '--colors' 54 | '--recursive' 55 | ], 56 | cmd: './node_modules/.bin/mocha <%= exec.mocha.options.join(" ") %>' 57 | 58 | grunt.loadNpmTasks 'grunt-contrib-clean' 59 | grunt.loadNpmTasks 'grunt-contrib-coffee' 60 | grunt.loadNpmTasks 'grunt-coffeelint' 61 | grunt.loadNpmTasks 'grunt-jsonlint' 62 | grunt.loadNpmTasks 'grunt-contrib-watch' 63 | grunt.loadNpmTasks 'grunt-notify' 64 | grunt.loadNpmTasks 'grunt-exec' 65 | 66 | grunt.registerTask 'test', ['jsonlint', 'coffeelint', 'exec:mocha'] 67 | grunt.registerTask 'build', ['coffee'] 68 | grunt.registerTask 'default', ['test'] 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © CyberAgent, Inc. All Rights Reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # adbkit-monkey 2 | 3 | # Warning 4 | # This project along with other ones in [OpenSTF](https://github.com/openstf) organisation is provided as is for community, without active development. 5 | # You can check any other forks that may be actively developed and offer new/different features [here](https://github.com/openstf/stf/network). 6 | # Active development has been moved to [DeviceFarmer](https://github.com/DeviceFarmer) organisation. 7 | 8 | **adbkit-monkey** provides a [Node.js][nodejs] interface for working with the Android [`monkey` tool][monkey-site]. Albeit undocumented, they monkey program can be started in TCP mode with the `--port` argument. In this mode, it accepts a [range of commands][monkey-proto] that can be used to interact with the UI in a non-random manner. This mode is also used internally by the [`monkeyrunner` tool][monkeyrunner-site], although the documentation claims no relation to the monkey tool. 9 | 10 | ## Getting started 11 | 12 | Install via NPM: 13 | 14 | ```bash 15 | npm install --save adbkit-monkey 16 | ``` 17 | 18 | Note that while adbkit-monkey is written in CoffeeScript, it is compiled to JavaScript before publishing to NPM, which means that you are not required to use CoffeeScript. 19 | 20 | ### Examples 21 | 22 | The following examples assume that monkey is already running (via `adb shell monkey --port 1080`) and a port forwarding (`adb forward tcp:1080 tcp:1080`) has been set up. 23 | 24 | #### Press the home button 25 | 26 | ```javascript 27 | var assert = require('assert'); 28 | var monkey = require('adbkit-monkey'); 29 | 30 | var client = monkey.connect({ port: 1080 }); 31 | 32 | client.press(3 /* KEYCODE_HOME */, function(err) { 33 | assert.ifError(err); 34 | console.log('Pressed home button'); 35 | client.end(); 36 | }); 37 | ``` 38 | 39 | #### Drag out the notification bar 40 | 41 | ```javascript 42 | var assert = require('assert'); 43 | var monkey = require('adbkit-monkey'); 44 | 45 | var client = monkey.connect({ port: 1080 }); 46 | 47 | client.multi() 48 | .touchDown(100, 0) 49 | .sleep(5) 50 | .touchMove(100, 20) 51 | .sleep(5) 52 | .touchMove(100, 40) 53 | .sleep(5) 54 | .touchMove(100, 60) 55 | .sleep(5) 56 | .touchMove(100, 80) 57 | .sleep(5) 58 | .touchMove(100, 100) 59 | .sleep(5) 60 | .touchUp(100, 100) 61 | .sleep(5) 62 | .execute(function(err) { 63 | assert.ifError(err); 64 | console.log('Dragged out the notification bar'); 65 | client.end(); 66 | }); 67 | ``` 68 | 69 | #### Get display size 70 | 71 | ```javascript 72 | var assert = require('assert'); 73 | var monkey = require('adbkit-monkey'); 74 | 75 | var client = monkey.connect({ port: 1080 }); 76 | 77 | client.getDisplayWidth(function(err, width) { 78 | assert.ifError(err); 79 | client.getDisplayHeight(function(err, height) { 80 | assert.ifError(err); 81 | console.log('Display size is %dx%d', width, height); 82 | client.end(); 83 | }); 84 | }); 85 | ``` 86 | 87 | #### Type text 88 | 89 | Note that you should manually focus a text field first. 90 | 91 | ```javascript 92 | var assert = require('assert'); 93 | var monkey = require('adbkit-monkey'); 94 | 95 | var client = monkey.connect({ port: 1080 }); 96 | 97 | client.type('hello monkey!', function(err) { 98 | assert.ifError(err); 99 | console.log('Said hello to monkey'); 100 | client.end(); 101 | }); 102 | ``` 103 | 104 | ## API 105 | 106 | ### Monkey 107 | 108 | #### monkey.connect(options) 109 | 110 | Uses [Net.connect()][node-net] to open a new TCP connection to monkey. Useful when combined with `adb forward`. 111 | 112 | * **options** Any options [`Net.connect()`][node-net] accepts. 113 | * Returns: A new monkey `Client` instance. 114 | 115 | #### monkey.connectStream(stream) 116 | 117 | Attaches a monkey client to an existing monkey protocol stream. 118 | 119 | * **stream** The monkey protocol [`Stream`][node-stream]. 120 | * Returns: A new monkey `Client` instance. 121 | 122 | ### Client 123 | 124 | Implements `Api`. See below for details. 125 | 126 | #### Events 127 | 128 | The following events are available: 129 | 130 | * **error** **(err)** Emitted when an error occurs. 131 | * **err** An `Error`. 132 | * **end** Emitted when the stream ends. 133 | * **finish** Emitted when the stream finishes. 134 | 135 | #### client.end() 136 | 137 | Ends the underlying stream/connection. 138 | 139 | * Returns: The `Client` instance. 140 | 141 | #### client.multi() 142 | 143 | Returns a new API wrapper that buffers commands for simultaneous delivery instead of sending them individually. When used with `api.sleep()`, allows simple gestures to be executed. 144 | 145 | * Returns: A new `Multi` instance. See `Multi` below. 146 | 147 | #### client.send(command, callback) 148 | 149 | Sends a raw protocol command to monkey. 150 | 151 | * **command** The command to send. When `String`, a single command is sent. When `Array`, a series of commands is sent at once. 152 | * **callback(err, value, command)** Called when monkey responds to the command. If multiple commands were sent, the callback will be called once for each command. 153 | * **err** `null` when successful, `Error` otherwise. 154 | * **value** The response value, if any. 155 | * **command** The command the response is for. 156 | * Returns: The `Client` instance. 157 | 158 | ### Api 159 | 160 | The monkey API implemented by `Client` and `Multi`. 161 | 162 | #### api.done(callback) 163 | 164 | Closes the current monkey session and allows a new session to connect. 165 | 166 | * **callback(err)** Called when monkey responds. 167 | * **err** `null` when successful, `Error` otherwise. 168 | * Returns: The `Api` implementation instance. 169 | 170 | #### api.flipClose(callback) 171 | 172 | Simulates closing the keyboard. 173 | 174 | * **callback(err)** Called when monkey responds. 175 | * **err** `null` when successful, `Error` otherwise. 176 | * Returns: The `Api` implementation instance. 177 | 178 | #### api.flipOpen(callback) 179 | 180 | Simulates opening the keyboard. 181 | 182 | * **callback(err)** Called when monkey responds. 183 | * **err** `null` when successful, `Error` otherwise. 184 | * Returns: The `Api` implementation instance. 185 | 186 | #### api.get(name, callback) 187 | 188 | Gets the value of a variable. Use `api.list()` to retrieve a list of supported variables. 189 | 190 | * **name** The name of the variable. 191 | * **callback(err, value)** Called when monkey responds. 192 | * **err** `null` when successful, `Error` otherwise. 193 | * **value** The value of the variable. 194 | * Returns: The `Api` implementation instance. 195 | 196 | #### api.getAmCurrentAction(callback) 197 | 198 | Alias for `api.get('am.current.action', callback)`. 199 | 200 | #### api.getAmCurrentCategories(callback) 201 | 202 | Alias for `api.get('am.current.categories', callback)`. 203 | 204 | #### api.getAmCurrentCompClass(callback) 205 | 206 | Alias for `api.get('am.current.comp.class', callback)`. 207 | 208 | #### api.getAmCurrentCompPackage(callback) 209 | 210 | Alias for `api.get('am.current.comp.package', callback)`. 211 | 212 | #### api.getCurrentData(callback) 213 | 214 | Alias for `api.get('am.current.data', callback)`. 215 | 216 | #### api.getAmCurrentPackage(callback) 217 | 218 | Alias for `api.get('am.current.package', callback)`. 219 | 220 | #### api.getBuildBoard(callback) 221 | 222 | Alias for `api.get('build.board', callback)`. 223 | 224 | #### api.getBuildBrand(callback) 225 | 226 | Alias for `api.get('build.brand', callback)`. 227 | 228 | #### api.getBuildCpuAbi(callback) 229 | 230 | Alias for `api.get('build.cpu_abi', callback)`. 231 | 232 | #### api.getBuildDevice(callback) 233 | 234 | Alias for `api.get('build.device', callback)`. 235 | 236 | #### api.getBuildDisplay(callback) 237 | 238 | Alias for `api.get('build.display', callback)`. 239 | 240 | #### api.getBuildFingerprint(callback) 241 | 242 | Alias for `api.get('build.fingerprint', callback)`. 243 | 244 | #### api.getBuildHost(callback) 245 | 246 | Alias for `api.get('build.host', callback)`. 247 | 248 | #### api.getBuildId(callback) 249 | 250 | Alias for `api.get('build.id', callback)`. 251 | 252 | #### api.getBuildManufacturer(callback) 253 | 254 | Alias for `api.get('build.manufacturer', callback)`. 255 | 256 | #### api.getBuildModel(callback) 257 | 258 | Alias for `api.get('build.model', callback)`. 259 | 260 | #### api.getBuildProduct(callback) 261 | 262 | Alias for `api.get('build.product', callback)`. 263 | 264 | #### api.getBuildTags(callback) 265 | 266 | Alias for `api.get('build.tags', callback)`. 267 | 268 | #### api.getBuildType(callback) 269 | 270 | Alias for `api.get('build.type', callback)`. 271 | 272 | #### api.getBuildUser(callback) 273 | 274 | Alias for `api.get('build.user', callback)`. 275 | 276 | #### api.getBuildVersionCodename(callback) 277 | 278 | Alias for `api.get('build.version.codename', callback)`. 279 | 280 | #### api.getBuildVersionIncremental(callback) 281 | 282 | Alias for `api.get('build.version.incremental', callback)`. 283 | 284 | #### api.getBuildVersionRelease(callback) 285 | 286 | Alias for `api.get('build.version.release', callback)`. 287 | 288 | #### api.getBuildVersionSdk(callback) 289 | 290 | Alias for `api.get('build.version.sdk', callback)`. 291 | 292 | #### api.getClockMillis(callback) 293 | 294 | Alias for `api.get('clock.millis', callback)`. 295 | 296 | #### api.getClockRealtime(callback) 297 | 298 | Alias for `api.get('clock.realtime', callback)`. 299 | 300 | #### api.getClockUptime(callback) 301 | 302 | Alias for `api.get('clock.uptime', callback)`. 303 | 304 | #### api.getDisplayDensity(callback) 305 | 306 | Alias for `api.get('display.density', callback)`. 307 | 308 | #### api.getDisplayHeight(callback) 309 | 310 | Alias for `api.get('display.height', callback)`. Note that the height may exclude any virtual home button row. 311 | 312 | #### api.getDisplayWidth(callback) 313 | 314 | Alias for `api.get('display.width', callback)`. 315 | 316 | #### api.keyDown(keyCode, callback) 317 | 318 | Sends a key down event. Should be coupled with `api.keyUp()`. Note that `api.press()` performs the two events automatically. 319 | 320 | * **keyCode** The [key code][android-keycodes]. All monkeys support numeric keycodes, and some support automatic conversion from key names to key codes (e.g. `'home'` to `KEYCODE_HOME`). This will not work for number keys however. The most portable method is to simply use numeric key codes. 321 | * **callback(err)** Called when monkey responds. 322 | * **err** `null` when successful, `Error` otherwise. 323 | * Returns: The `Api` implementation instance. 324 | 325 | #### api.keyUp(keyCode, callback) 326 | 327 | Sends a key up event. Should be coupled with `api.keyDown()`. Note that `api.press()` performs the two events automatically. 328 | 329 | * **keyCode** See `api.keyDown()`. 330 | * **callback(err)** Called when monkey responds. 331 | * **err** `null` when successful, `Error` otherwise. 332 | * Returns: The `Api` implementation instance. 333 | 334 | #### api.list(callback) 335 | 336 | Lists supported variables. 337 | 338 | * **callback(err, vars)** Called when monkey responds. 339 | * **err** `null` when successful, `Error` otherwise. 340 | * **vars** An array of supported variable names, to be used with `api.get()`. 341 | * Returns: The `Api` implementation instance. 342 | 343 | #### api.press(keyCode, callback) 344 | 345 | Sends a key press event. 346 | 347 | * **keyCode** See `api.keyDown()`. 348 | * **callback(err)** Called when monkey responds. 349 | * **err** `null` when successful, `Error` otherwise. 350 | * Returns: The `Api` implementation instance. 351 | 352 | #### api.quit(callback) 353 | 354 | Closes the current monkey session and quits monkey. 355 | 356 | * **callback(err)** Called when monkey responds. 357 | * **err** `null` when successful, `Error` otherwise. 358 | * Returns: The `Api` implementation instance. 359 | 360 | #### api.sleep(ms, callback) 361 | 362 | Sleeps for the given duration. Can be useful for simulating gestures. 363 | 364 | * **ms** How many milliseconds to sleep for. 365 | * **callback(err)** Called when monkey responds. 366 | * **err** `null` when successful, `Error` otherwise. 367 | * Returns: The `Api` implementation instance. 368 | 369 | #### api.tap(x, y, callback) 370 | 371 | Taps the given coordinates. 372 | 373 | * **x** The x coordinate. 374 | * **y** The y coordinate. 375 | * **callback(err)** Called when monkey responds. 376 | * **err** `null` when successful, `Error` otherwise. 377 | * Returns: The `Api` implementation instance. 378 | 379 | #### api.touchDown(x, y, callback) 380 | 381 | Sends a touch down event on the given coordinates. 382 | 383 | * **x** The x coordinate. 384 | * **y** The y coordinate. 385 | * **callback(err)** Called when monkey responds. 386 | * **err** `null` when successful, `Error` otherwise. 387 | * Returns: The `Api` implementation instance. 388 | 389 | #### api.touchMove(x, y, callback) 390 | 391 | Sends a touch move event on the given coordinates. 392 | 393 | * **x** The x coordinate. 394 | * **y** The y coordinate. 395 | * **callback(err)** Called when monkey responds. 396 | * **err** `null` when successful, `Error` otherwise. 397 | * Returns: The `Api` implementation instance. 398 | 399 | #### api.touchUp(x, y, callback) 400 | 401 | Sends a touch up event on the given coordinates. 402 | 403 | * **x** The x coordinate. 404 | * **y** The y coordinate. 405 | * **callback(err)** Called when monkey responds. 406 | * **err** `null` when successful, `Error` otherwise. 407 | * Returns: The `Api` implementation instance. 408 | 409 | #### api.trackball(x, y, callback) 410 | 411 | Sends a trackball event on the given coordinates. 412 | 413 | * **x** The x coordinate. 414 | * **y** The y coordinate. 415 | * **callback(err)** Called when monkey responds. 416 | * **err** `null` when successful, `Error` otherwise. 417 | * Returns: The `Api` implementation instance. 418 | 419 | #### api.type(text, callback) 420 | 421 | Types the given text. 422 | 423 | * **text** A text `String`. Note that only characters for which [key codes][android-keycodes] exist can be entered. Also note that any IME in use may or may not transform the text. 424 | * **callback(err)** Called when monkey responds. 425 | * **err** `null` when successful, `Error` otherwise. 426 | * Returns: The `Api` implementation instance. 427 | 428 | #### api.wake(callback) 429 | 430 | Wakes the device from sleep and allows user input. 431 | 432 | * **callback(err)** Called when monkey responds. 433 | * **err** `null` when successful, `Error` otherwise. 434 | * Returns: The `Api` implementation instance. 435 | 436 | ### Multi 437 | 438 | Buffers `Api` commands and delivers them simultaneously for greater control over timing. 439 | 440 | Implements all `Api` methods, but without the last `callback` parameter. 441 | 442 | #### multi.execute(callback) 443 | 444 | Sends all buffered commands. 445 | 446 | * **callback(err, values)** Called when monkey has responded to all commands (i.e. just once at the end). 447 | * **err** `null` when successful, `Error` otherwise. 448 | * **values** An array of all response values, identical to individual `Api` responses. 449 | 450 | ## More information 451 | 452 | * [Monkey][monkey-site] 453 | - [Source code][monkey-source] 454 | - [Protocol][monkey-proto] 455 | * [Monkeyrunner][monkeyrunner-site] 456 | 457 | ## Contributing 458 | 459 | See [CONTRIBUTING.md](CONTRIBUTING.md). 460 | 461 | ## License 462 | 463 | See [LICENSE](LICENSE). 464 | 465 | Copyright © CyberAgent, Inc. All Rights Reserved. 466 | 467 | [nodejs]: 468 | [monkey-site]: 469 | [monkey-source]: 470 | [monkey-proto]: 471 | [monkeyrunner-site]: 472 | [node-net]: 473 | [node-stream]: 474 | [android-keycodes]: 475 | -------------------------------------------------------------------------------- /git_hooks/pre-commit/version: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if ! git rev-parse --verify HEAD >/dev/null 2>&1; then 4 | exit 0 5 | fi 6 | 7 | if ! which -s jq 1>/dev/null; then 8 | echo "ERROR: required tool 'jq' is missing, try 'brew install jq'" 9 | exit 4 10 | fi 11 | 12 | CURRENT_BRANCH=`git rev-parse --abbrev-ref HEAD` 13 | HOTFIX_PREFIX=`git config gitflow.prefix.hotfix` 14 | RELEASE_PREFIX=`git config gitflow.prefix.release` 15 | PACKAGE="package.json" 16 | 17 | if [ ! -f $PACKAGE ]; then 18 | echo "ERROR: package.json is missing" 19 | exit 1 20 | fi 21 | 22 | PACKAGE_VERSION=`jq -r .version $PACKAGE` 23 | 24 | function __bail { 25 | echo "ERROR: package.json claims version $PACKAGE_VERSION but branch is $CURRENT_BRANCH" 26 | exit 2 27 | } 28 | 29 | case $CURRENT_BRANCH in 30 | $HOTFIX_PREFIX*) 31 | if [[ $CURRENT_BRANCH != ${HOTFIX_PREFIX}${PACKAGE_VERSION} ]]; then 32 | __bail 33 | fi 34 | ;; 35 | $RELEASE_PREFIX*) 36 | if [[ $CURRENT_BRANCH != ${RELEASE_PREFIX}${PACKAGE_VERSION} ]]; then 37 | __bail 38 | fi 39 | ;; 40 | esac 41 | 42 | exit 0 43 | -------------------------------------------------------------------------------- /index.coffee: -------------------------------------------------------------------------------- 1 | Path = require 'path' 2 | 3 | module.exports = switch Path.extname __filename 4 | when '.coffee' then require './src/monkey' 5 | else require './lib/monkey' 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adbkit-monkey", 3 | "version": "1.0.1", 4 | "description": "A Node.js interface to the Android monkey tool.", 5 | "keywords": [ 6 | "adb", 7 | "adbkit", 8 | "monkey", 9 | "monkeyrunner" 10 | ], 11 | "bugs": { 12 | "url": "https://github.com/CyberAgent/adbkit-monkey/issues" 13 | }, 14 | "license": "Apache-2.0", 15 | "author": { 16 | "name": "CyberAgent, Inc.", 17 | "email": "npm@cyberagent.co.jp", 18 | "url": "http://www.cyberagent.co.jp/" 19 | }, 20 | "main": "./index", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/CyberAgent/adbkit-monkey.git" 24 | }, 25 | "scripts": { 26 | "postpublish": "grunt clean", 27 | "prepublish": "grunt coffee", 28 | "test": "grunt test" 29 | }, 30 | "dependencies": { 31 | "async": "~0.2.9" 32 | }, 33 | "devDependencies": { 34 | "chai": "~1.8.1", 35 | "coffee-script": "~1.6.3", 36 | "grunt": "~0.4.1", 37 | "grunt-cli": "~0.1.11", 38 | "grunt-coffeelint": "~0.0.7", 39 | "grunt-contrib-clean": "~0.5.0", 40 | "grunt-contrib-coffee": "~0.7.0", 41 | "grunt-contrib-watch": "~0.5.3", 42 | "grunt-exec": "~0.4.2", 43 | "grunt-jsonlint": "~1.0.2", 44 | "grunt-notify": "~0.2.16", 45 | "mocha": "~1.14.0", 46 | "sinon": "~1.7.3", 47 | "sinon-chai": "~2.4.0" 48 | }, 49 | "engines": { 50 | "node": ">= 0.10.4" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/monkey.coffee: -------------------------------------------------------------------------------- 1 | Client = require './monkey/client' 2 | Connection = require './monkey/connection' 3 | 4 | class Monkey 5 | 6 | @connect: (options) -> 7 | new Connection().connect options 8 | 9 | @connectStream: (stream) -> 10 | new Client().connect stream 11 | 12 | Monkey.Connection = Connection 13 | Monkey.Client = Client 14 | 15 | module.exports = Monkey 16 | -------------------------------------------------------------------------------- /src/monkey/api.coffee: -------------------------------------------------------------------------------- 1 | {EventEmitter} = require 'events' 2 | 3 | class Api extends EventEmitter 4 | send: -> 5 | throw new Error "send is not implemented" 6 | 7 | keyDown: (keyCode, callback) -> 8 | this.send "key down #{keyCode}", callback 9 | return this 10 | 11 | keyUp: (keyCode, callback) -> 12 | this.send "key up #{keyCode}", callback 13 | return this 14 | 15 | touchDown: (x, y, callback) -> 16 | this.send "touch down #{x} #{y}", callback 17 | return this 18 | 19 | touchUp: (x, y, callback) -> 20 | this.send "touch up #{x} #{y}", callback 21 | return this 22 | 23 | touchMove: (x, y, callback) -> 24 | this.send "touch move #{x} #{y}", callback 25 | return this 26 | 27 | trackball: (dx, dy, callback) -> 28 | this.send "trackball #{dx} #{dy}", callback 29 | return this 30 | 31 | flipOpen: (callback) -> 32 | this.send "flip open", callback 33 | return this 34 | 35 | flipClose: (callback) -> 36 | this.send "flip close", callback 37 | return this 38 | 39 | wake: (callback) -> 40 | this.send "wake", callback 41 | return this 42 | 43 | tap: (x, y, callback) -> 44 | this.send "tap #{x} #{y}", callback 45 | return this 46 | 47 | press: (keyCode, callback) -> 48 | this.send "press #{keyCode}", callback 49 | return this 50 | 51 | type: (str, callback) -> 52 | # Escape double quotes. 53 | str = str.replace /"/g, '\\"' 54 | if str.indexOf(' ') is -1 55 | this.send "type #{str}", callback 56 | else 57 | this.send "type \"#{str}\"", callback 58 | return this 59 | 60 | list: (callback) -> 61 | this.send "listvar", (err, vars) => 62 | return this callback err if err 63 | if err 64 | callback err 65 | else 66 | callback null, vars.split /\s+/g 67 | return this 68 | 69 | get: (name, callback) -> 70 | this.send "getvar #{name}", callback 71 | return this 72 | 73 | quit: (callback) -> 74 | this.send "quit", callback 75 | return this 76 | 77 | done: (callback) -> 78 | this.send "done", callback 79 | return this 80 | 81 | sleep: (ms, callback) -> 82 | this.send "sleep #{ms}", callback 83 | return this 84 | 85 | getAmCurrentAction: (callback) -> 86 | this.get 'am.current.action', callback 87 | return this 88 | 89 | getAmCurrentCategories: (callback) -> 90 | this.get 'am.current.categories', callback 91 | return this 92 | 93 | getAmCurrentCompClass: (callback) -> 94 | this.get 'am.current.comp.class', callback 95 | return this 96 | 97 | getAmCurrentCompPackage: (callback) -> 98 | this.get 'am.current.comp.package', callback 99 | return this 100 | 101 | getAmCurrentData: (callback) -> 102 | this.get 'am.current.data', callback 103 | return this 104 | 105 | getAmCurrentPackage: (callback) -> 106 | this.get 'am.current.package', callback 107 | return this 108 | 109 | getBuildBoard: (callback) -> 110 | this.get 'build.board', callback 111 | return this 112 | 113 | getBuildBrand: (callback) -> 114 | this.get 'build.brand', callback 115 | return this 116 | 117 | getBuildCpuAbi: (callback) -> 118 | this.get 'build.cpu_abi', callback 119 | return this 120 | 121 | getBuildDevice: (callback) -> 122 | this.get 'build.device', callback 123 | return this 124 | 125 | getBuildDisplay: (callback) -> 126 | this.get 'build.display', callback 127 | return this 128 | 129 | getBuildFingerprint: (callback) -> 130 | this.get 'build.fingerprint', callback 131 | return this 132 | 133 | getBuildHost: (callback) -> 134 | this.get 'build.host', callback 135 | return this 136 | 137 | getBuildId: (callback) -> 138 | this.get 'build.id', callback 139 | return this 140 | 141 | getBuildManufacturer: (callback) -> 142 | this.get 'build.manufacturer', callback 143 | return this 144 | 145 | getBuildModel: (callback) -> 146 | this.get 'build.model', callback 147 | return this 148 | 149 | getBuildProduct: (callback) -> 150 | this.get 'build.product', callback 151 | return this 152 | 153 | getBuildTags: (callback) -> 154 | this.get 'build.tags', callback 155 | return this 156 | 157 | getBuildType: (callback) -> 158 | this.get 'build.type', callback 159 | return this 160 | 161 | getBuildUser: (callback) -> 162 | this.get 'build.user', callback 163 | return this 164 | 165 | getBuildVersionCodename: (callback) -> 166 | this.get 'build.version.codename', callback 167 | return this 168 | 169 | getBuildVersionIncremental: (callback) -> 170 | this.get 'build.version.incremental', callback 171 | return this 172 | 173 | getBuildVersionRelease: (callback) -> 174 | this.get 'build.version.release', callback 175 | return this 176 | 177 | getBuildVersionSdk: (callback) -> 178 | this.get 'build.version.sdk', callback 179 | return this 180 | 181 | getClockMillis: (callback) -> 182 | this.get 'clock.millis', callback 183 | return this 184 | 185 | getClockRealtime: (callback) -> 186 | this.get 'clock.realtime', callback 187 | return this 188 | 189 | getClockUptime: (callback) -> 190 | this.get 'clock.uptime', callback 191 | return this 192 | 193 | getDisplayDensity: (callback) -> 194 | this.get 'display.density', callback 195 | return this 196 | 197 | getDisplayHeight: (callback) -> 198 | this.get 'display.height', callback 199 | return this 200 | 201 | getDisplayWidth: (callback) -> 202 | this.get 'display.width', callback 203 | return this 204 | 205 | module.exports = Api 206 | -------------------------------------------------------------------------------- /src/monkey/client.coffee: -------------------------------------------------------------------------------- 1 | Api = require './api' 2 | Command = require './command' 3 | Reply = require './reply' 4 | Queue = require './queue' 5 | Multi = require './multi' 6 | Parser = require './parser' 7 | 8 | class Client extends Api 9 | constructor: -> 10 | @commandQueue = new Queue 11 | @parser = new Parser 12 | @stream = null 13 | 14 | _hook: -> 15 | @stream.on 'data', (data) => 16 | @parser.parse data 17 | @stream.on 'error', (err) => 18 | this.emit 'error', err 19 | @stream.on 'end', => 20 | this.emit 'end' 21 | @stream.on 'finish', => 22 | this.emit 'finish' 23 | @parser.on 'reply', (reply) => 24 | this._consume reply 25 | @parser.on 'error', (err) => 26 | this.emit 'error', err 27 | return 28 | 29 | _consume: (reply) -> 30 | if command = @commandQueue.dequeue() 31 | if reply.isError() 32 | command.callback reply.toError(), null, command.command 33 | else 34 | command.callback null, reply.value, command.command 35 | else 36 | throw new Error "Command queue depleted, but replies still coming in" 37 | return 38 | 39 | connect: (@stream) -> 40 | this._hook() 41 | return this 42 | 43 | end: -> 44 | @stream.end() 45 | return this 46 | 47 | send: (commands, callback) -> 48 | if Array.isArray commands 49 | for command in commands 50 | @commandQueue.enqueue new Command command, callback 51 | @stream.write "#{commands.join('\n')}\n" 52 | else 53 | @commandQueue.enqueue new Command commands, callback 54 | @stream.write "#{commands}\n" 55 | return this 56 | 57 | multi: -> 58 | new Multi this 59 | 60 | module.exports = Client 61 | -------------------------------------------------------------------------------- /src/monkey/command.coffee: -------------------------------------------------------------------------------- 1 | class Command 2 | constructor: (@command, @callback) -> 3 | this.next = null 4 | 5 | module.exports = Command 6 | -------------------------------------------------------------------------------- /src/monkey/connection.coffee: -------------------------------------------------------------------------------- 1 | Net = require 'net' 2 | 3 | Client = require './client' 4 | 5 | class Connection extends Client 6 | connect: (options) -> 7 | stream = Net.connect options 8 | stream.setNoDelay true 9 | super stream 10 | 11 | _hook: -> 12 | @stream.on 'connect', => 13 | this.emit 'connect' 14 | @stream.on 'close', (hadError) => 15 | this.emit 'close', hadError 16 | super() 17 | 18 | module.exports = Connection 19 | -------------------------------------------------------------------------------- /src/monkey/multi.coffee: -------------------------------------------------------------------------------- 1 | Api = require './api' 2 | Command = require './command' 3 | 4 | class Multi extends Api 5 | constructor: (@monkey) -> 6 | @commands = [] 7 | @replies = [] 8 | @errors = [] 9 | @counter = 0 10 | @sent = false 11 | @callback = null 12 | @collector = (err, result, cmd) => 13 | @errors.push "#{cmd}: #{err.message}" if err 14 | @replies.push result 15 | @counter -= 1 16 | this._maybeFinish() 17 | 18 | _maybeFinish: -> 19 | if @counter is 0 20 | if @errors.length 21 | setImmediate => 22 | @callback new Error @errors.join ', ' 23 | else 24 | setImmediate => 25 | @callback null, @replies 26 | return 27 | 28 | _forbidReuse: -> 29 | if @sent 30 | throw new Error "Reuse not supported" 31 | 32 | send: (command) -> 33 | this._forbidReuse() 34 | @commands.push new Command command, @collector 35 | return 36 | 37 | execute: (callback) -> 38 | this._forbidReuse() 39 | @counter = @commands.length 40 | @sent = true 41 | @callback = callback 42 | if @counter is 0 43 | return 44 | parts = [] 45 | for command in @commands 46 | @monkey.commandQueue.enqueue command 47 | parts.push command.command 48 | parts.push '' 49 | @commands = [] 50 | @monkey.stream.write parts.join '\n' 51 | return 52 | 53 | module.exports = Multi 54 | -------------------------------------------------------------------------------- /src/monkey/parser.coffee: -------------------------------------------------------------------------------- 1 | {EventEmitter} = require 'events' 2 | 3 | Reply = require './reply' 4 | 5 | class Parser extends EventEmitter 6 | constructor: (options) -> 7 | @column = 0 8 | @buffer = new Buffer '' 9 | 10 | parse: (chunk) -> 11 | @buffer = Buffer.concat [@buffer, chunk] 12 | while @column < @buffer.length 13 | if @buffer[@column] is 0x0a 14 | this._parseLine @buffer.slice 0, @column 15 | @buffer = @buffer.slice @column + 1 16 | @column = 0 17 | @column += 1 18 | if @buffer.length 19 | @emit 'wait' 20 | else 21 | @emit 'drain' 22 | return 23 | 24 | _parseLine: (line) -> 25 | switch line[0] 26 | when 0x4f # 'O' 27 | if line.length is 2 # 'OK' 28 | @emit 'reply', new Reply Reply.OK, null 29 | else # 'OK:' 30 | @emit 'reply', new Reply Reply.OK, line.toString('ascii', 3) 31 | when 0x45 # 'E' 32 | if line.length is 5 # 'ERROR' 33 | @emit 'reply', new Reply Reply.ERROR, null 34 | else # 'ERROR:' 35 | @emit 'reply', new Reply Reply.ERROR, line.toString('ascii', 6) 36 | else 37 | this._complain line 38 | return 39 | 40 | _complain: (line) -> 41 | @emit 'error', new SyntaxError "Unparseable line '#{line}'" 42 | return 43 | 44 | module.exports = Parser 45 | -------------------------------------------------------------------------------- /src/monkey/queue.coffee: -------------------------------------------------------------------------------- 1 | class Queue 2 | constructor: -> 3 | @head = null 4 | @tail = null 5 | 6 | enqueue: (item) -> 7 | if @tail 8 | @tail.next = item 9 | else 10 | @head = item 11 | @tail = item 12 | return 13 | 14 | dequeue: -> 15 | item = @head 16 | if item 17 | if item is @tail 18 | @tail = null 19 | @head = item.next 20 | item.next = null 21 | return item 22 | 23 | module.exports = Queue 24 | -------------------------------------------------------------------------------- /src/monkey/reply.coffee: -------------------------------------------------------------------------------- 1 | class Reply 2 | @ERROR = 'ERROR' 3 | @OK = 'OK' 4 | 5 | constructor: (@type, @value) -> 6 | 7 | isError: -> 8 | @type is Reply.ERROR 9 | 10 | toError: -> 11 | unless this.isError() 12 | throw new Error 'toError() cannot be called for non-errors' 13 | new Error @value 14 | 15 | module.exports = Reply 16 | -------------------------------------------------------------------------------- /test/mock/duplex.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | 3 | class MockDuplex extends Stream.Duplex 4 | _read: (size) -> 5 | 6 | _write: (chunk, encoding, callback) -> 7 | @emit 'write', chunk, encoding, callback 8 | callback null 9 | return 10 | 11 | causeRead: (chunk) -> 12 | unless Buffer.isBuffer chunk 13 | chunk = new Buffer chunk 14 | this.push chunk 15 | this.push null 16 | return 17 | 18 | module.exports = MockDuplex 19 | -------------------------------------------------------------------------------- /test/monkey.coffee: -------------------------------------------------------------------------------- 1 | Net = require 'net' 2 | {expect} = require 'chai' 3 | 4 | Monkey = require '../' 5 | Connection = require '../src/monkey/connection' 6 | Client = require '../src/monkey/client' 7 | MockDuplex = require './mock/duplex' 8 | 9 | describe 'Monkey', -> 10 | 11 | describe 'Connection', -> 12 | 13 | it "should be exposed", (done) -> 14 | expect(Monkey.Connection).to.equal Connection 15 | done() 16 | 17 | describe 'Client', -> 18 | 19 | it "should be exposed", (done) -> 20 | expect(Monkey.Client).to.equal Client 21 | done() 22 | 23 | describe 'connect(options)', -> 24 | 25 | before (done) -> 26 | @port = 16609 27 | @server = Net.createServer() 28 | @server.listen @port, done 29 | 30 | it "should return a Connection instance", (done) -> 31 | monkey = Monkey.connect port: @port 32 | expect(monkey).to.be.an.instanceOf Connection 33 | done() 34 | 35 | after (done) -> 36 | @server.close() 37 | done() 38 | 39 | describe 'connectStream(stream)', -> 40 | 41 | before (done) -> 42 | @duplex = new MockDuplex 43 | done() 44 | 45 | it "should return a Client instance", (done) -> 46 | monkey = Monkey.connectStream @duplex 47 | expect(monkey).to.be.an.instanceOf Client 48 | done() 49 | 50 | it "should pass stream to Client", (done) -> 51 | monkey = Monkey.connectStream @duplex 52 | expect(monkey.stream).to.equal @duplex 53 | done() 54 | -------------------------------------------------------------------------------- /test/monkey/api.coffee: -------------------------------------------------------------------------------- 1 | Sinon = require 'sinon' 2 | Chai = require 'Chai' 3 | Chai.use require 'sinon-chai' 4 | {expect} = Chai 5 | 6 | Api = require '../../src/monkey/api' 7 | 8 | describe 'Api', -> 9 | 10 | beforeEach -> 11 | @api = new Api 12 | Sinon.stub @api, 'send' 13 | 14 | describe "keyDown(keyCode)", -> 15 | 16 | it "should send a 'key down ' command", (done) -> 17 | @api.keyDown 'a', callback = -> 18 | expect(@api.send).to.have.been.calledWith 'key down a', callback 19 | done() 20 | 21 | describe "keyUp(keyCode)", -> 22 | 23 | it "should send a 'key up ' command", (done) -> 24 | @api.keyUp 'b', callback = -> 25 | expect(@api.send).to.have.been.calledWith 'key up b', callback 26 | done() 27 | 28 | describe "touchDown(x, y)", -> 29 | 30 | it "should send a 'touch down ' command", (done) -> 31 | @api.touchDown 6, 7, callback = -> 32 | expect(@api.send).to.have.been.calledWith 'touch down 6 7', callback 33 | done() 34 | 35 | describe "touchUp(x, y)", -> 36 | 37 | it "should send a 'touch up ' command", (done) -> 38 | @api.touchUp 97, 22, callback = -> 39 | expect(@api.send).to.have.been.calledWith 'touch up 97 22', callback 40 | done() 41 | 42 | describe "touchMove(x, y)", -> 43 | 44 | it "should send a 'touch move ' command", (done) -> 45 | @api.touchMove 27, 88, callback = -> 46 | expect(@api.send).to.have.been.calledWith 'touch move 27 88', callback 47 | done() 48 | 49 | describe "trackball(dx, dy)", -> 50 | 51 | it "should send a 'trackball ' command", (done) -> 52 | @api.trackball 90, 92, callback = -> 53 | expect(@api.send).to.have.been.calledWith 'trackball 90 92', callback 54 | done() 55 | 56 | describe "flipOpen()", -> 57 | 58 | it "should send a 'flip open' command", (done) -> 59 | @api.flipOpen callback = -> 60 | expect(@api.send).to.have.been.calledWith 'flip open', callback 61 | done() 62 | 63 | describe "flipClose()", -> 64 | 65 | it "should send a 'flip close' command", (done) -> 66 | @api.flipClose callback = -> 67 | expect(@api.send).to.have.been.calledWith 'flip close', callback 68 | done() 69 | 70 | describe "wake()", -> 71 | 72 | it "should send a 'wake' command", (done) -> 73 | @api.wake callback = -> 74 | expect(@api.send).to.have.been.calledWith 'wake', callback 75 | done() 76 | 77 | describe "tap(x, y)", -> 78 | 79 | it "should send a 'tap ' command", (done) -> 80 | @api.tap 6, 2, callback = -> 81 | expect(@api.send).to.have.been.calledWith 'tap 6 2', callback 82 | done() 83 | 84 | describe "press(keyCode)", -> 85 | 86 | it "should send a 'press ' command", (done) -> 87 | @api.press 'c', callback = -> 88 | expect(@api.send).to.have.been.calledWith 'press c', callback 89 | done() 90 | 91 | describe "type(string)", -> 92 | 93 | it "should send a 'type ' command", (done) -> 94 | @api.type 'foo', callback = -> 95 | expect(@api.send).to.have.been.calledWith 'type foo', callback 96 | done() 97 | 98 | it "should wrap string in quotes if string contains spaces", (done) -> 99 | @api.type 'a b', callback = -> 100 | expect(@api.send).to.have.been.calledWith 'type "a b"', callback 101 | done() 102 | 103 | it "should escape double quotes with '\\'", (done) -> 104 | @api.type 'a"', callback = -> 105 | expect(@api.send).to.have.been.calledWith 'type a\\"', callback 106 | @api.type 'a" b"', callback = -> 107 | expect(@api.send).to.have.been.calledWith 'type "a\\" b\\""', callback 108 | done() 109 | 110 | describe "list()", -> 111 | 112 | it "should send a 'listvar' command", (done) -> 113 | @api.list callback = -> 114 | # @todo Don't ignore the callback. 115 | expect(@api.send).to.have.been.calledWith 'listvar' 116 | done() 117 | 118 | describe "get(varname)", -> 119 | 120 | it "should send a 'getvar ' command", (done) -> 121 | @api.get 'foo', callback = -> 122 | expect(@api.send).to.have.been.calledWith 'getvar foo', callback 123 | done() 124 | 125 | describe "quit()", -> 126 | 127 | it "should send a 'quit' command", (done) -> 128 | @api.quit callback = -> 129 | expect(@api.send).to.have.been.calledWith 'quit', callback 130 | done() 131 | 132 | describe "done()", -> 133 | 134 | it "should send a 'done' command", (done) -> 135 | @api.done callback = -> 136 | expect(@api.send).to.have.been.calledWith 'done', callback 137 | done() 138 | 139 | describe "sleep()", -> 140 | 141 | it "should send a 'sleep ' command", (done) -> 142 | @api.sleep 500, callback = -> 143 | expect(@api.send).to.have.been.calledWith 'sleep 500', callback 144 | done() 145 | -------------------------------------------------------------------------------- /test/monkey/client.coffee: -------------------------------------------------------------------------------- 1 | Sinon = require 'sinon' 2 | Chai = require 'chai' 3 | Chai.use require 'sinon-chai' 4 | {expect} = Chai 5 | 6 | Client = require '../../src/monkey/client' 7 | Api = require '../../src/monkey/api' 8 | Multi = require '../../src/monkey/multi' 9 | MockDuplex = require '../mock/duplex' 10 | 11 | describe 'Client', -> 12 | 13 | beforeEach -> 14 | @duplex = new MockDuplex 15 | @monkey = new Client().connect @duplex 16 | 17 | it "should implement Api", (done) -> 18 | expect(@monkey).to.be.an.instanceOf Api 19 | done() 20 | 21 | describe "events", -> 22 | 23 | it "should emit 'finish' when underlying stream does", (done) -> 24 | @monkey.on 'finish', -> 25 | done() 26 | @duplex.end() 27 | 28 | it "should emit 'end' when underlying stream does", (done) -> 29 | @monkey.on 'end', -> 30 | done() 31 | @duplex.on 'write', => 32 | @duplex.causeRead 'OK\n' 33 | @monkey.end() 34 | @monkey.send 'foo', -> 35 | 36 | describe "connect(stream)", -> 37 | 38 | it "should set 'stream' property", (done) -> 39 | expect(@monkey.stream).to.be.equal @duplex 40 | done() 41 | 42 | describe "end()", -> 43 | 44 | it "should be chainable", (done) -> 45 | expect(@monkey.end()).to.equal @monkey 46 | done() 47 | 48 | it "should end underlying stream", (done) -> 49 | @duplex.on 'finish', -> 50 | done() 51 | @monkey.end() 52 | 53 | describe "send(command, callback)", -> 54 | 55 | it "should be chainable", (done) -> 56 | expect(@monkey.send 'foo', ->).to.equal @monkey 57 | done() 58 | 59 | describe "with single command", -> 60 | 61 | it "should receive reply", (done) -> 62 | @duplex.on 'write', (chunk) => 63 | expect(chunk.toString()).to.equal 'give5\n' 64 | @duplex.causeRead 'OK:5\n' 65 | @monkey.end() 66 | callback = Sinon.spy() 67 | @monkey.send 'give5', callback 68 | @duplex.on 'finish', -> 69 | expect(callback).to.have.been.calledOnce 70 | expect(callback).to.have.been.calledWith null, '5', 'give5' 71 | done() 72 | 73 | describe "with multiple commands", -> 74 | 75 | it "should receive multiple replies", (done) -> 76 | @duplex.on 'write', (chunk) => 77 | expect(chunk.toString()).to.equal 'give5\ngiveError\ngive7\n' 78 | @duplex.causeRead 'OK:5\nERROR:foo\nOK:7\n' 79 | @monkey.end() 80 | callback = Sinon.spy() 81 | @monkey.send ['give5', 'giveError', 'give7'], callback 82 | @duplex.on 'finish', -> 83 | expect(callback).to.have.been.calledThrice 84 | expect(callback).to.have.been.calledWith null, '5', 'give5' 85 | expect(callback).to.have.been.calledWith \ 86 | Sinon.match.instanceOf(Error), null, 'giveError' 87 | expect(callback).to.have.been.calledWith null, '7', 'give7' 88 | done() 89 | 90 | describe "multi()", -> 91 | 92 | it "should return a Multi instance", (done) -> 93 | expect(@monkey.multi()).to.be.an.instanceOf Multi 94 | done() 95 | 96 | it "should be be bound to the Client instance", (done) -> 97 | multi = @monkey.multi() 98 | expect(multi.monkey).to.equal @monkey 99 | done() 100 | -------------------------------------------------------------------------------- /test/monkey/command.coffee: -------------------------------------------------------------------------------- 1 | {expect} = require 'chai' 2 | 3 | Command = require '../../src/monkey/command' 4 | 5 | describe 'Command', -> 6 | 7 | it "should have a 'command' property set", (done) -> 8 | cmd = new Command 'a', -> 9 | expect(cmd.command).to.equal 'a' 10 | done() 11 | 12 | it "should have a 'callback' property set", (done) -> 13 | callback = -> 14 | cmd = new Command 'b', callback 15 | expect(cmd.callback).to.equal callback 16 | done() 17 | 18 | it "should have a 'next' property for the queue", (done) -> 19 | cmd = new Command 'c', -> 20 | expect(cmd.next).to.be.null 21 | done() 22 | -------------------------------------------------------------------------------- /test/monkey/connection.coffee: -------------------------------------------------------------------------------- 1 | Net = require 'net' 2 | Path = require 'path' 3 | Sinon = require 'sinon' 4 | Chai = require 'chai' 5 | Chai.use require 'sinon-chai' 6 | {spawn} = require 'child_process' 7 | {expect} = Chai 8 | 9 | Connection = require '../../src/monkey/connection' 10 | Client = require '../../src/monkey/client' 11 | 12 | describe 'Connection', -> 13 | 14 | before (done) -> 15 | @options = port: 16610 16 | @server = Net.createServer() 17 | @server.listen @options.port, done 18 | 19 | after (done) -> 20 | @server.close() 21 | done() 22 | 23 | it "should extend Client", (done) -> 24 | monkey = new Connection 25 | expect(monkey).to.be.an.instanceOf Client 26 | done() 27 | 28 | it "should not create a connection immediately", (done) -> 29 | Sinon.spy Net, 'connect' 30 | monkey = new Connection 31 | expect(Net.connect).to.not.have.been.called 32 | Net.connect.restore() 33 | done() 34 | 35 | describe "events", -> 36 | 37 | it "should emit 'connect' when underlying stream does", (done) -> 38 | monkey = new Connection().connect @options 39 | monkey.on 'connect', -> 40 | done() 41 | 42 | describe 'connect(options)', -> 43 | 44 | it "should create a connection", (done) -> 45 | Sinon.spy Net, 'connect' 46 | monkey = new Connection().connect @options 47 | expect(Net.connect).to.have.been.calledWith @options 48 | Net.connect.restore() 49 | done() 50 | -------------------------------------------------------------------------------- /test/monkey/multi.coffee: -------------------------------------------------------------------------------- 1 | Sinon = require 'sinon' 2 | Chai = require 'Chai' 3 | Chai.use require 'sinon-chai' 4 | {expect} = Chai 5 | 6 | Multi = require '../../src/monkey/multi' 7 | Client = require '../../src/monkey/client' 8 | MockDuplex = require '../mock/duplex' 9 | Api = require '../../src/monkey/api' 10 | 11 | describe 'Multi', -> 12 | 13 | beforeEach -> 14 | @duplex = new MockDuplex 15 | @monkey = new Client().connect @duplex 16 | @multi = new Multi @monkey 17 | 18 | it "should implement Api", (done) -> 19 | expect(@multi).to.be.an.instanceOf Api 20 | done() 21 | 22 | it "should set 'monkey' property", (done) -> 23 | expect(@multi.monkey).to.be.equal @monkey 24 | done() 25 | 26 | describe "send(command)", -> 27 | 28 | it "should not write to stream", (done) -> 29 | Sinon.spy @duplex, 'write' 30 | @multi.send 'foo' 31 | expect(@duplex.write).to.not.have.been.called 32 | done() 33 | 34 | it "should throw an Error if run after execute()", (done) -> 35 | Sinon.spy @duplex, 'write' 36 | @multi.execute -> 37 | expect(=> @multi.send 'foo').to.throw Error 38 | done() 39 | 40 | describe "execute(callback)", -> 41 | 42 | it "should write to stream if commands were sent", (done) -> 43 | Sinon.spy @duplex, 'write' 44 | @multi.send 'foo' 45 | @multi.execute -> 46 | expect(@duplex.write).to.have.been.calledOnce 47 | done() 48 | 49 | it "should not write to stream if commands were not sent", (done) -> 50 | Sinon.spy @duplex, 'write' 51 | @multi.execute -> 52 | expect(@duplex.write).to.not.have.been.called 53 | done() 54 | 55 | it "should throw an Error if reused", (done) -> 56 | Sinon.spy @duplex, 'write' 57 | @multi.execute -> 58 | expect(=> @multi.execute ->).to.throw Error 59 | done() 60 | 61 | it "should write command to stream", (done) -> 62 | Sinon.spy @duplex, 'write' 63 | @multi.send 'foo' 64 | @multi.execute -> 65 | expect(@duplex.write).to.have.been.calledWith 'foo\n' 66 | done() 67 | 68 | it "should write multiple commands to stream at once", (done) -> 69 | Sinon.spy @duplex, 'write' 70 | @multi.send 'tap 1 2' 71 | @multi.send 'getvar foo' 72 | @multi.execute -> 73 | expect(@duplex.write).to.have.been.calledWith 'tap 1 2\ngetvar foo\n' 74 | done() 75 | 76 | describe "callback", -> 77 | 78 | it "should be called just once with all results", (done) -> 79 | @duplex.on 'write', => 80 | @duplex.causeRead 'OK\nOK:bar\n' 81 | @multi.send 'tap 1 2' 82 | @multi.send 'getvar foo' 83 | @multi.execute (err, results) -> 84 | done() 85 | -------------------------------------------------------------------------------- /test/monkey/parser.coffee: -------------------------------------------------------------------------------- 1 | {expect} = require 'chai' 2 | 3 | Parser = require '../../src/monkey/parser' 4 | Reply = require '../../src/monkey/reply' 5 | 6 | describe 'Parser', -> 7 | 8 | it "should emit 'wait' when waiting for more data", (done) -> 9 | parser = new Parser 10 | parser.on 'wait', done 11 | parser.parse new Buffer 'OK' 12 | 13 | it "should emit 'drain' when all data has been consumed", (done) -> 14 | parser = new Parser 15 | parser.on 'drain', done 16 | parser.parse new Buffer 'OK\n' 17 | 18 | it "should parse a successful reply", (done) -> 19 | parser = new Parser 20 | parser.on 'reply', (reply) -> 21 | expect(reply.type).to.equal 'OK' 22 | expect(reply.value).to.be.null 23 | expect(reply.isError()).to.equal false 24 | done() 25 | parser.parse new Buffer 'OK\n' 26 | 27 | it "should parse a successful reply with value", (done) -> 28 | parser = new Parser 29 | parser.on 'reply', (reply) -> 30 | expect(reply.type).to.equal 'OK' 31 | expect(reply.value).to.equal '2' 32 | done() 33 | parser.parse new Buffer 'OK:2\n' 34 | 35 | it "should parse a successful reply with spaces in value", (done) -> 36 | parser = new Parser 37 | parser.on 'reply', (reply) -> 38 | expect(reply.type).to.equal 'OK' 39 | expect(reply.value).to.equal 'a b c' 40 | done() 41 | parser.parse new Buffer 'OK:a b c\n' 42 | 43 | it "should parse an empty successful reply", (done) -> 44 | parser = new Parser 45 | parser.on 'reply', (reply) -> 46 | expect(reply.type).to.equal 'OK' 47 | expect(reply.value).to.equal '' 48 | done() 49 | parser.parse new Buffer 'OK:\n' 50 | 51 | it "should not trim values in successful replies", (done) -> 52 | parser = new Parser 53 | parser.on 'reply', (reply) -> 54 | expect(reply.type).to.equal 'OK' 55 | expect(reply.value).to.equal ' test ' 56 | done() 57 | parser.parse new Buffer 'OK: test \n' 58 | 59 | it "should not trim values in error replies", (done) -> 60 | parser = new Parser 61 | parser.on 'reply', (reply) -> 62 | expect(reply.type).to.equal 'ERROR' 63 | expect(reply.value).to.equal ' test ' 64 | done() 65 | parser.parse new Buffer 'ERROR: test \n' 66 | 67 | it "should parse an error reply with value", (done) -> 68 | parser = new Parser 69 | parser.on 'reply', (reply) -> 70 | expect(reply.type).to.equal 'ERROR' 71 | expect(reply.value).to.equal 'unknown var' 72 | expect(reply.isError()).to.equal true 73 | expect(reply.toError()).to.be.an.instanceof Error 74 | expect(reply.toError().message).to.equal 'unknown var' 75 | done() 76 | parser.parse new Buffer 'ERROR:unknown var\n' 77 | 78 | it "should throw a SyntaxError for an unknown reply", (done) -> 79 | parser = new Parser 80 | parser.on 'error', (err) -> 81 | expect(err).to.be.an.instanceOf SyntaxError 82 | done() 83 | parser.parse new Buffer 'FOO:bar\n' 84 | 85 | it "should parse multiple replies from one chunk", (done) -> 86 | parser = new Parser 87 | parser.once 'reply', (reply) -> 88 | expect(reply.type).to.equal 'OK' 89 | expect(reply.value).to.equal '2' 90 | parser.once 'reply', (reply) -> 91 | expect(reply.type).to.equal 'OK' 92 | expect(reply.value).to.equal 'okay' 93 | done() 94 | parser.parse new Buffer 'OK:2\nOK:okay\n' 95 | -------------------------------------------------------------------------------- /test/monkey/queue.coffee: -------------------------------------------------------------------------------- 1 | {expect} = require 'chai' 2 | 3 | Queue = require '../../src/monkey/queue' 4 | Command = require '../../src/monkey/command' 5 | 6 | describe 'Queue', -> 7 | 8 | describe "when empty", -> 9 | 10 | before (done) -> 11 | @queue = new Queue 12 | done() 13 | 14 | it "should have null tail and tail", (done) -> 15 | expect(@queue.tail).to.be.null 16 | expect(@queue.head).to.be.null 17 | done() 18 | 19 | it "dequeue should return null", (done) -> 20 | expect(@queue.dequeue()).to.be.null 21 | done() 22 | 23 | describe "with one command", -> 24 | 25 | before (done) -> 26 | @queue = new Queue 27 | @command = new Command 'a', -> 28 | @queue.enqueue @command 29 | done() 30 | 31 | it "should have the command as head", (done) -> 32 | expect(@queue.head).to.equal @command 33 | done() 34 | 35 | it "should have tail same as head", (done) -> 36 | expect(@queue.head).to.equal @queue.tail 37 | done() 38 | 39 | it "should have command.next be null", (done) -> 40 | expect(@command.next).to.be.null 41 | done() 42 | 43 | it "dequeue should return the command and update tail and head", (done) -> 44 | expect(@queue.dequeue()).to.equal @command 45 | expect(@queue.head).to.be.null 46 | expect(@queue.tail).to.be.null 47 | done() 48 | 49 | describe "with multiple commands", -> 50 | 51 | before (done) -> 52 | @queue = new Queue 53 | @command1 = new Command 'a', -> 54 | @command2 = new Command 'b', -> 55 | @command3 = new Command 'c', -> 56 | @queue.enqueue @command1 57 | @queue.enqueue @command2 58 | @queue.enqueue @command3 59 | done() 60 | 61 | it "should set head to the first command", (done) -> 62 | expect(@queue.head).to.equal @command1 63 | done() 64 | 65 | it "should set tail to the last command", (done) -> 66 | expect(@queue.tail).to.equal @command3 67 | done() 68 | 69 | it "should set command.next properly", (done) -> 70 | expect(@command1.next).to.equal @command2 71 | expect(@command2.next).to.equal @command3 72 | expect(@command3.next).to.be.null 73 | done() 74 | 75 | it "dequeue should return the first command and update head", (done) -> 76 | expect(@queue.dequeue()).to.equal @command1 77 | expect(@command1.next).to.be.null 78 | expect(@queue.head).to.equal @command2 79 | done() 80 | -------------------------------------------------------------------------------- /test/monkey/reply.coffee: -------------------------------------------------------------------------------- 1 | {expect} = require 'chai' 2 | 3 | Reply = require '../../src/monkey/reply' 4 | 5 | describe 'Reply', -> 6 | 7 | describe 'isError()', -> 8 | 9 | it "should return false for OK reply", (done) -> 10 | reply = new Reply Reply.OK, null 11 | expect(reply.isError()).to.equal false 12 | done() 13 | 14 | it "should return true for ERROR reply", (done) -> 15 | reply = new Reply Reply.ERROR, null 16 | expect(reply.isError()).to.equal true 17 | done() 18 | 19 | describe 'toError()', -> 20 | 21 | it "should throw an Error is called on an OK reply", (done) -> 22 | reply = new Reply Reply.OK, null 23 | expect(-> reply.toError()).to.throw Error 24 | done() 25 | 26 | it "should return an Error with the value as the message", (done) -> 27 | reply = new Reply Reply.ERROR, 'a b' 28 | err = reply.toError() 29 | expect(err).to.be.an.instanceOf Error 30 | expect(err.message).to.equal 'a b' 31 | done() 32 | --------------------------------------------------------------------------------