├── .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 |
--------------------------------------------------------------------------------