├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── index.js ├── lib ├── logcat.js └── logcat │ ├── entry.js │ ├── parser │ └── binary.js │ ├── priority.js │ ├── reader.js │ └── transform.js ├── package-lock.json ├── package.json └── test ├── fixtures ├── 1-broken.dat ├── 1-crlf.dat ├── 1-default.dat └── 3-default.dat ├── logcat.js ├── logcat ├── entry.js ├── parser │ └── binary.js ├── priority.js ├── reader.js └── transform.js └── mock └── duplex.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.md] 14 | indent_size = 4 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'browser': true, 4 | 'es6': true, 5 | 'node': true 6 | }, 7 | 'extends': 'eslint:recommended', 8 | 'rules': { 9 | 'indent': [ 10 | 'error', 11 | 2 12 | ], 13 | 'linebreak-style': [ 14 | 'error', 15 | 'unix' 16 | ], 17 | 'quotes': [ 18 | 'error', 19 | 'single' 20 | ], 21 | 'semi': [ 22 | 'error', 23 | 'never' 24 | ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*.tgz 2 | /node_modules/ 3 | /temp/ 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.!sync 2 | /*.tgz 3 | /.editorconfig 4 | /.eslintrc.js 5 | /.npmignore 6 | /CONTRIBUTING.md 7 | /Gruntfile.coffee 8 | /git_hooks/ 9 | /npm-debug.log 10 | /temp/ 11 | /test/ 12 | -------------------------------------------------------------------------------- /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 `master` branch. Only one contribution per branch is accepted. 9 | 3. Implement your contribution while respecting our rules (see below). 10 | 4. If possible, add tests for your contribution to make sure it actually works. 11 | 5. Don't forget to run `npm test` just right before submitting, it also checks for code styling issues. 12 | 6. Submit a pull request against our `master` 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 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 | [editorconfig]: 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2013 CyberAgent, Inc. 2 | Copyright © 2016 The OpenSTF Project 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # adbkit-logcat 2 | 3 | # Warning 4 | 5 | This repository was superseded by https://github.com/DeviceFarmer/adbkit-logcat 6 | 7 | # Warning 8 | 9 | This repository was supreseded by https://github.com/DeviceFarmer/adbkit-logcat 10 | 11 | **adbkit-logcat** provides a [Node.js][nodejs] interface for working with output produced by the Android [`logcat` tool][logcat-site]. It takes a log stream (that you must create separately), parses it, and emits log entries in real-time as they occur. Possible use cases include storing logs in a database, forwarding logs via [MessagePack][msgpack], or just advanced filtering. 12 | 13 | ## Requirements 14 | 15 | * [Node.js](http://nodejs.org/) 4.x or newer. Older versions are not supported. 16 | 17 | ## Getting started 18 | 19 | Install via NPM: 20 | 21 | ```bash 22 | npm install --save adbkit-logcat 23 | ``` 24 | 25 | ### Examples 26 | 27 | #### Output all log messages 28 | 29 | ##### JavaScript 30 | 31 | ```javascript 32 | const logcat = require('adbkit-logcat') 33 | const {spawn} = require('child_process') 34 | 35 | // Retrieve a binary log stream 36 | const proc = spawn('adb', ['logcat', '-B']) 37 | 38 | // Connect logcat to the stream 39 | reader = logcat.readStream(proc.stdout) 40 | reader.on('entry', entry => { 41 | console.log(entry.message) 42 | }) 43 | 44 | // Make sure we don't leave anything hanging 45 | process.on('exit', () => { 46 | proc.kill() 47 | }) 48 | ``` 49 | 50 | ## API 51 | 52 | ### Logcat 53 | 54 | #### logcat.Priority 55 | 56 | Exposes `Priority`. See below for details. 57 | 58 | #### logcat.Reader 59 | 60 | Exposes `Reader`. See below for details. 61 | 62 | #### logcat.readStream(stream[, options]) 63 | 64 | Creates a logcat reader instance from the provided logcat event [`Stream`][node-stream]. Note that you must create the stream separately. 65 | 66 | **IMPORTANT: The default line break behavior has changed on newer Android versions. Since adbkit-logcat merely parses output and is not able to communicate with ADB, you must _externally_ detect whether you need the `fixLineFeeds` option enabled or not. For newer versions it should be off.** 67 | 68 | * **stream** The event stream to read. 69 | * **options** Optional. The following options are supported: 70 | - **format** The format of the stream. Currently, the only supported value is `'binary'`, which (for example) `adb logcat -B` produces. Defaults to `'binary'`. 71 | - **fixLineFeeds** On older Android versions, ADB shell automatically transformed any `'\n'` into `'\r\n'`, which broke binary content. If set, this option reverses the transformation before parsing the stream. Defaults to `true` for backwards compatibility. You **MUST** set this option to `false` on newer versions. 72 | * Returns: The `Reader` instance. 73 | 74 | ### Priority 75 | 76 | #### Constants 77 | 78 | The following static properties are available: 79 | 80 | * **Priority.UNKNOWN** i.e. `0`. 81 | * **Priority.DEFAULT** i.e. `1`. Not available when reading a stream. 82 | * **Priority.VERBOSE** i.e. `2`. 83 | * **Priority.DEBUG** i.e. `3`. 84 | * **Priority.INFO** i.e. `4`. 85 | * **Priority.WARN** i.e. `5`. 86 | * **Priority.ERROR** i.e. `6`. 87 | * **Priority.FATAL** i.e. `7`. 88 | * **Priority.SILENT** i.e. `8`. Not available when reading a stream. 89 | 90 | #### Priority.fromLetter(letter) 91 | 92 | Static method to convert the given `letter` into a numeric priority. For example, `Priority.fromName('d')` would return `Priority.DEBUG`. 93 | 94 | * **letter** The priority as a `String`. Any single, case-insensitive character matching the first character of any `Priority` constant is accepted. 95 | * Returns: The priority as a `Number`, or `undefined`. 96 | 97 | #### Priority.fromName(name) 98 | 99 | Static method to convert the given `name` into a numeric priority. For example, `Priority.fromName('debug')` (or `Priority.fromName('d')`) would return `Priority.DEBUG`. 100 | 101 | * **name** The priority as a `String`. Any full, case-insensitive match of the `Priority` constants is accepted. If no match is found, falls back to `Priority.fromLetter()`. 102 | * Returns: The priority as a `Number`, or `undefined`. 103 | 104 | #### Priority.toLetter(priority) 105 | 106 | Static method to convert the numeric priority into its letter representation. For example, `Priority.toLetter(Priority.DEBUG)` would return `'D'`. 107 | 108 | * **priority** The priority as a `Number`. Any `Priority` constant value is accepted. 109 | * Returns: The priority as a `String` letter, or `undefined`. 110 | 111 | #### Priority.toName(priority) 112 | 113 | Static method to convert the numeric priority into its full string representation. For example, `Priority.toLetter(Priority.DEBUG)` would return `'DEBUG'`. 114 | 115 | * **priority** The priority as a `Number`. Any `Priority` constant value is accepted. 116 | * Returns: The priority as a `String`, or `undefined`. 117 | 118 | ### Reader 119 | 120 | A reader instance, which is an [`EventEmitter`][node-events]. 121 | 122 | #### Events 123 | 124 | The following events are available: 125 | 126 | * **error** **(err)** Emitted when an error occurs. 127 | * **err** An `Error`. 128 | * **end** Emitted when the stream ends. 129 | * **finish** Emitted when the stream finishes. 130 | * **entry** **(entry)** Emitted when the stream finishes. 131 | * **entry** A log `Entry`. See below for details. 132 | 133 | #### constructor([options]) 134 | 135 | For advanced users. Manually constructs a `Reader` instance. Useful for testing and/or playing around. Normally you would use `logcat.readStream()` to create the instance. 136 | 137 | * **options** See `logcat.readStream()` for details. 138 | * Returns: N/A 139 | 140 | #### reader.connect(stream) 141 | 142 | For advanced users. When instantiated manually (not via `logcat.readStream()`), connects the `Reader` instance to the given stream. 143 | 144 | * **stream** See `logcat.readStream()` for details. 145 | * Returns: The `Reader` instance. 146 | 147 | #### reader.end() 148 | 149 | Convenience method for ending the stream. 150 | 151 | * Returns: The `Reader` instance. 152 | 153 | #### reader.exclude(tag) 154 | 155 | Skip entries with the provided tag. Alias for `reader.include(tag, Priority.SILENT)`. Note that even skipped events have to be parsed so that they can be ignored. 156 | 157 | * **tag** The tag string to exclude. If `'*'`, works the same as `reader.excludeAll()`. 158 | * Returns: The `Reader` instance. 159 | 160 | #### reader.excludeAll() 161 | 162 | Skip **ALL** entries. Alias for `reader.includeAll(Priority.SILENT)`. Any entries you wish to see must be included via `include()`/`includeAll()`. 163 | 164 | * Returns: The `Reader` instance. 165 | 166 | #### reader.include(tag[, priority]) 167 | 168 | Include all entries with the given tag and a priority higher or equal to the given `priority`. 169 | 170 | * **tag** The tag string to include. If `'*'`, works the same as `reader.includeAll(priority)`. 171 | * **priority** Optional. A lower bound for the priority. Any numeric `Priority` constant or any `String` value accepted by `Priority.fromName()` is accepted. Defaults to `Priority.DEBUG`. 172 | * Returns: The `Reader` instance. 173 | 174 | #### reader.includeAll([priority]) 175 | 176 | Include all entries with a priority higher or equal to the given `priority`. 177 | 178 | * **tag** The tag string to exclude. 179 | * **priority** Optional. See `reader.include()` for details. 180 | * Returns: The `Reader` instance. 181 | 182 | #### reader.resetFilters() 183 | 184 | Resets all inclusions/exclusions. 185 | 186 | * Returns: The `Reader` instance. 187 | 188 | ### Entry 189 | 190 | A log entry. 191 | 192 | #### Properties 193 | 194 | The following properties are available: 195 | 196 | * **date** Event time as a `Date`. 197 | * **pid** Process ID as a `Number`. 198 | * **tid** Thread ID as a `Number`. 199 | * **priority** Event priority as a `Number`. You can use `logcat.Priority` to convert the value into a `String`. 200 | * **tag** Event tag as a `String`. 201 | * **message** Message as a `String`. 202 | 203 | #### entry.toBinary() 204 | 205 | Converts the entry back to the binary log format. 206 | 207 | * Returns: The binary event as a [`Buffer`][node-buffer]. 208 | 209 | ## More information 210 | 211 | * [logprint.c](https://github.com/android/platform_system_core/blob/master/liblog/logprint.c) 212 | * [logcat.cpp](https://github.com/android/platform_system_core/blob/master/logcat/logcat.cpp) 213 | * [logger.h](https://github.com/android/platform_system_core/blob/master/include/log/logger.h) 214 | 215 | ## Contributing 216 | 217 | See [CONTRIBUTING.md](CONTRIBUTING.md). 218 | 219 | ## License 220 | 221 | See [LICENSE](LICENSE). 222 | 223 | Copyright © The OpenSTF Project. All Rights Reserved. 224 | 225 | [nodejs]: 226 | [msgpack]: 227 | [logcat-site]: 228 | [node-stream]: 229 | [node-events]: 230 | [node-buffer]: 231 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/logcat') 2 | -------------------------------------------------------------------------------- /lib/logcat.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Reader = require('./logcat/reader') 4 | const Priority = require('./logcat/priority') 5 | 6 | class Logcat { 7 | static readStream(stream, options) { 8 | return new Reader(options).connect(stream) 9 | } 10 | } 11 | 12 | Logcat.Reader = Reader 13 | Logcat.Priority = Priority 14 | 15 | module.exports = Logcat 16 | -------------------------------------------------------------------------------- /lib/logcat/entry.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class Entry { 4 | constructor() { 5 | this.date = null 6 | this.pid = -1 7 | this.tid = -1 8 | this.priority = null 9 | this.tag = null 10 | this.message = null 11 | } 12 | 13 | setDate(date) { 14 | this.date = date 15 | } 16 | 17 | setPid(pid) { 18 | this.pid = pid 19 | } 20 | 21 | setTid(tid) { 22 | this.tid = tid 23 | } 24 | 25 | setPriority(priority) { 26 | this.priority = priority 27 | } 28 | 29 | setTag(tag) { 30 | this.tag = tag 31 | } 32 | 33 | setMessage(message) { 34 | this.message = message 35 | } 36 | 37 | toBinary() { 38 | let length = 20 // header 39 | length += 1 // priority 40 | length += this.tag.length 41 | length += 1 // NULL-byte 42 | length += this.message.length 43 | length += 1 // NULL-byte 44 | const buffer = new Buffer(length) 45 | let cursor = 0 46 | buffer.writeUInt16LE(length - 20, cursor) 47 | cursor += 4 // include 2 bytes of padding 48 | buffer.writeInt32LE(this.pid, cursor) 49 | cursor += 4 50 | buffer.writeInt32LE(this.tid, cursor) 51 | cursor += 4 52 | buffer.writeInt32LE(Math.floor(this.date.getTime() / 1000), cursor) 53 | cursor += 4 54 | buffer.writeInt32LE((this.date.getTime() % 1000) * 1000000, cursor) 55 | cursor += 4 56 | buffer[cursor] = this.priority 57 | cursor += 1 58 | buffer.write(this.tag, cursor, this.tag.length) 59 | cursor += this.tag.length 60 | buffer[cursor] = 0x00 61 | cursor += 1 62 | buffer.write(this.message, cursor, this.message.length) 63 | cursor += this.message.length 64 | buffer[cursor] = 0x00 65 | return buffer 66 | } 67 | } 68 | 69 | module.exports = Entry 70 | -------------------------------------------------------------------------------- /lib/logcat/parser/binary.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const EventEmitter = require('events').EventEmitter 4 | 5 | const Entry = require('../entry') 6 | 7 | const HEADER_SIZE_V1 = 20 8 | const HEADER_SIZE_MAX = 100 9 | 10 | class Binary extends EventEmitter { 11 | constructor(options) { 12 | super(options) 13 | this.buffer = new Buffer(0) 14 | } 15 | 16 | parse(chunk) { 17 | this.buffer = Buffer.concat([this.buffer, chunk]) 18 | 19 | while (this.buffer.length > 4) { 20 | let cursor = 0 21 | const length = this.buffer.readUInt16LE(cursor) 22 | cursor += 2 23 | let headerSize = this.buffer.readUInt16LE(cursor) 24 | // On v1, headerSize SHOULD be 0, but isn't on some devices. Attempt to 25 | // avoid that situation by discarding values that are obviously incorrect. 26 | if ((headerSize < HEADER_SIZE_V1) || (headerSize > HEADER_SIZE_MAX)) { 27 | headerSize = HEADER_SIZE_V1 28 | } 29 | cursor += 2 30 | if (this.buffer.length < (headerSize + length)) { 31 | break 32 | } 33 | const entry = new Entry() 34 | entry.setPid(this.buffer.readInt32LE(cursor)) 35 | cursor += 4 36 | entry.setTid(this.buffer.readInt32LE(cursor)) 37 | cursor += 4 38 | const sec = this.buffer.readInt32LE(cursor) 39 | cursor += 4 40 | const nsec = this.buffer.readInt32LE(cursor) 41 | entry.setDate(new Date((sec * 1000) + (nsec / 1000000))) 42 | cursor += 4 43 | // Make sure that we don't choke if new fields are added 44 | cursor = headerSize 45 | const data = this.buffer.slice(cursor, cursor + length) 46 | cursor += length 47 | this.buffer = this.buffer.slice(cursor) 48 | this._processEntry(entry, data) 49 | } 50 | 51 | if (this.buffer.length) { 52 | this.emit('wait') 53 | } else { 54 | this.emit('drain') 55 | } 56 | } 57 | 58 | _processEntry(entry, data) { 59 | entry.setPriority(data[0]) 60 | 61 | let cursor = 1 62 | while (cursor < data.length) { 63 | if (data[cursor] === 0) { 64 | entry.setTag(data.slice(1, cursor).toString()) 65 | entry.setMessage(data.slice(cursor + 1, data.length - 1).toString()) 66 | this.emit('entry', entry) 67 | return 68 | } 69 | cursor += 1 70 | } 71 | 72 | this.emit('error', new Error(`Unprocessable entry data '${data}'`)) 73 | } 74 | } 75 | 76 | module.exports = Binary 77 | -------------------------------------------------------------------------------- /lib/logcat/priority.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const codes = { 4 | UNKNOWN: 0, 5 | DEFAULT: 1, 6 | VERBOSE: 2, 7 | DEBUG: 3, 8 | INFO: 4, 9 | WARN: 5, 10 | ERROR: 6, 11 | FATAL: 7, 12 | SILENT: 8 13 | } 14 | 15 | const names = { 16 | 0: 'UNKNOWN', 17 | 1: 'DEFAULT', 18 | 2: 'VERBOSE', 19 | 3: 'DEBUG', 20 | 4: 'INFO', 21 | 5: 'WARN', 22 | 6: 'ERROR', 23 | 7: 'FATAL', 24 | 8: 'SILENT' 25 | } 26 | 27 | const letters = { 28 | '?': codes.UNKNOWN, 29 | 'V': codes.VERBOSE, 30 | 'D': codes.DEBUG, 31 | 'I': codes.INFO, 32 | 'W': codes.WARN, 33 | 'E': codes.ERROR, 34 | 'F': codes.FATAL, 35 | 'S': codes.SILENT 36 | } 37 | 38 | const letterNames = { 39 | 0: '?', 40 | 1: '?', 41 | 2: 'V', 42 | 3: 'D', 43 | 4: 'I', 44 | 5: 'W', 45 | 6: 'E', 46 | 7: 'F', 47 | 8: 'S' 48 | } 49 | 50 | class Priority { 51 | static fromName(name) { 52 | const value = codes[name.toUpperCase()] 53 | 54 | if (value || (value === 0)) { 55 | return value 56 | } 57 | return Priority.fromLetter(name) 58 | } 59 | 60 | static toName(value) { 61 | return names[value] 62 | } 63 | 64 | static fromLetter(letter) { 65 | return letters[letter.toUpperCase()] 66 | } 67 | 68 | static toLetter(value) { 69 | return letterNames[value] 70 | } 71 | } 72 | 73 | Priority.UNKNOWN = codes.UNKNOWN 74 | Priority.DEFAULT = codes.DEFAULT 75 | Priority.VERBOSE = codes.VERBOSE 76 | Priority.DEBUG = codes.DEBUG 77 | Priority.INFO = codes.INFO 78 | Priority.WARN = codes.WARN 79 | Priority.ERROR = codes.ERROR 80 | Priority.FATAL = codes.FATAL 81 | Priority.SILENT = codes.SILENT 82 | 83 | module.exports = Priority 84 | -------------------------------------------------------------------------------- /lib/logcat/reader.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const EventEmitter = require('events').EventEmitter 4 | 5 | const BinaryParser = require('./parser/binary') 6 | const Transform = require('./transform') 7 | const Priority = require('./priority') 8 | 9 | const ANY = '*' 10 | 11 | class Reader extends EventEmitter { 12 | constructor(options) { 13 | super(options) 14 | 15 | const defaults = { 16 | format: 'binary', 17 | fixLineFeeds: true, 18 | priority: Priority.DEBUG 19 | } 20 | 21 | this.options = Object.assign({}, defaults, options) 22 | 23 | this.filters = { 24 | all: -1, 25 | tags: {} 26 | } 27 | 28 | if (this.options.format !== 'binary') { 29 | throw new Error(`Unsupported format '${this.options.format}'`) 30 | } 31 | 32 | this.parser = new BinaryParser() 33 | this.stream = null 34 | } 35 | 36 | exclude(tag) { 37 | if (tag === Reader.ANY) { 38 | return this.excludeAll() 39 | } 40 | 41 | this.filters.tags[tag] = Priority.SILENT 42 | return this 43 | } 44 | 45 | excludeAll() { 46 | this.filters.all = Priority.SILENT 47 | return this 48 | } 49 | 50 | include(tag, priority) { 51 | if (typeof priority === 'undefined') { 52 | priority = this.options.priority 53 | } 54 | 55 | if (tag === Reader.ANY) { 56 | return this.includeAll(priority) 57 | } 58 | 59 | this.filters.tags[tag] = this._priority(priority) 60 | return this 61 | } 62 | 63 | includeAll(priority) { 64 | if (typeof priority === 'undefined') { 65 | priority = this.options.priority 66 | } 67 | 68 | this.filters.all = this._priority(priority) 69 | return this 70 | } 71 | 72 | resetFilters() { 73 | this.filters.all = -1 74 | this.filters.tags = {} 75 | return this 76 | } 77 | 78 | _hook() { 79 | if (this.options.fixLineFeeds) { 80 | const transform = this.stream.pipe(new Transform()) 81 | transform.on('data', data => { 82 | this.parser.parse(data) 83 | }) 84 | } else { 85 | this.stream.on('data', data => { 86 | this.parser.parse(data) 87 | }) 88 | } 89 | 90 | this.stream.on('error', err => { 91 | this.emit('error', err) 92 | }) 93 | 94 | this.stream.on('end', () => { 95 | this.emit('end') 96 | }) 97 | 98 | this.stream.on('finish', () => { 99 | this.emit('finish') 100 | }) 101 | 102 | this.parser.on('entry', entry => { 103 | if (this._filter(entry)) { 104 | this.emit('entry', entry) 105 | } 106 | }) 107 | 108 | this.parser.on('error', err => { 109 | this.emit('error', err) 110 | }) 111 | } 112 | 113 | _filter(entry) { 114 | const wanted = (entry.tag in this.filters.tags) 115 | ? this.filters.tags[entry.tag] 116 | : this.filters.all 117 | 118 | return entry.priority >= wanted 119 | } 120 | 121 | _priority(priority) { 122 | return typeof priority === 'number' ? priority : Priority.fromName(priority) 123 | } 124 | 125 | connect(stream) { 126 | this.stream = stream 127 | this._hook() 128 | return this 129 | } 130 | 131 | end() { 132 | this.stream.end() 133 | return this 134 | } 135 | } 136 | 137 | Reader.ANY = ANY 138 | 139 | module.exports = Reader 140 | -------------------------------------------------------------------------------- /lib/logcat/transform.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const stream = require('stream') 4 | 5 | class Transform extends stream.Transform { 6 | constructor(options) { 7 | super(options) 8 | this.savedR = null 9 | } 10 | 11 | // Sadly, the ADB shell is not very smart. It automatically converts every 12 | // 0x0a ('\n') it can find to 0x0d 0x0a ('\r\n'). This also applies to binary 13 | // content. We could get rid of this behavior by setting `stty raw`, but 14 | // unfortunately it's not available by default (you'd have to install busybox) 15 | // or something similar. On the up side, it really does do this for all line 16 | // feeds, so a simple transform works fine. 17 | _transform(chunk, encoding, done) { 18 | let lo = 0 19 | let hi = 0 20 | 21 | if (this.savedR) { 22 | if (chunk[0] !== 0x0a) { this.push(this.savedR) } 23 | this.savedR = null 24 | } 25 | 26 | const last = chunk.length - 1 27 | while (hi <= last) { 28 | if (chunk[hi] === 0x0d) { 29 | if (hi === last) { 30 | this.savedR = chunk.slice(last) 31 | break // Stop hi from incrementing, we want to skip the last byte. 32 | } else if (chunk[hi + 1] === 0x0a) { 33 | this.push(chunk.slice(lo, hi)) 34 | lo = hi + 1 35 | } 36 | } 37 | hi += 1 38 | } 39 | 40 | if (hi !== lo) { 41 | this.push(chunk.slice(lo, hi)) 42 | } 43 | 44 | done() 45 | } 46 | } 47 | 48 | module.exports = Transform 49 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adbkit-logcat", 3 | "version": "2.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "assertion-error": { 8 | "version": "1.0.2", 9 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", 10 | "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", 11 | "dev": true 12 | }, 13 | "balanced-match": { 14 | "version": "1.0.0", 15 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 16 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 17 | "dev": true 18 | }, 19 | "brace-expansion": { 20 | "version": "1.1.8", 21 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", 22 | "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", 23 | "dev": true, 24 | "requires": { 25 | "balanced-match": "1.0.0", 26 | "concat-map": "0.0.1" 27 | } 28 | }, 29 | "browser-stdout": { 30 | "version": "1.3.0", 31 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", 32 | "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", 33 | "dev": true 34 | }, 35 | "chai": { 36 | "version": "3.5.0", 37 | "resolved": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", 38 | "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", 39 | "dev": true, 40 | "requires": { 41 | "assertion-error": "1.0.2", 42 | "deep-eql": "0.1.3", 43 | "type-detect": "1.0.0" 44 | } 45 | }, 46 | "commander": { 47 | "version": "2.9.0", 48 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", 49 | "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", 50 | "dev": true, 51 | "requires": { 52 | "graceful-readlink": "1.0.1" 53 | } 54 | }, 55 | "concat-map": { 56 | "version": "0.0.1", 57 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 58 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 59 | "dev": true 60 | }, 61 | "debug": { 62 | "version": "2.6.8", 63 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", 64 | "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", 65 | "dev": true, 66 | "requires": { 67 | "ms": "2.0.0" 68 | } 69 | }, 70 | "deep-eql": { 71 | "version": "0.1.3", 72 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", 73 | "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", 74 | "dev": true, 75 | "requires": { 76 | "type-detect": "0.1.1" 77 | }, 78 | "dependencies": { 79 | "type-detect": { 80 | "version": "0.1.1", 81 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", 82 | "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", 83 | "dev": true 84 | } 85 | } 86 | }, 87 | "diff": { 88 | "version": "3.2.0", 89 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", 90 | "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", 91 | "dev": true 92 | }, 93 | "escape-string-regexp": { 94 | "version": "1.0.5", 95 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 96 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 97 | "dev": true 98 | }, 99 | "formatio": { 100 | "version": "1.1.1", 101 | "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.1.1.tgz", 102 | "integrity": "sha1-XtPM1jZVEJc4NGXZlhmRAOhhYek=", 103 | "dev": true, 104 | "requires": { 105 | "samsam": "1.1.2" 106 | } 107 | }, 108 | "fs.realpath": { 109 | "version": "1.0.0", 110 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 111 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 112 | "dev": true 113 | }, 114 | "glob": { 115 | "version": "7.1.1", 116 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", 117 | "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", 118 | "dev": true, 119 | "requires": { 120 | "fs.realpath": "1.0.0", 121 | "inflight": "1.0.6", 122 | "inherits": "2.0.3", 123 | "minimatch": "3.0.4", 124 | "once": "1.4.0", 125 | "path-is-absolute": "1.0.1" 126 | } 127 | }, 128 | "graceful-readlink": { 129 | "version": "1.0.1", 130 | "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", 131 | "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", 132 | "dev": true 133 | }, 134 | "growl": { 135 | "version": "1.9.2", 136 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", 137 | "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", 138 | "dev": true 139 | }, 140 | "has-flag": { 141 | "version": "1.0.0", 142 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", 143 | "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", 144 | "dev": true 145 | }, 146 | "he": { 147 | "version": "1.1.1", 148 | "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", 149 | "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", 150 | "dev": true 151 | }, 152 | "inflight": { 153 | "version": "1.0.6", 154 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 155 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 156 | "dev": true, 157 | "requires": { 158 | "once": "1.4.0", 159 | "wrappy": "1.0.2" 160 | } 161 | }, 162 | "inherits": { 163 | "version": "2.0.3", 164 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 165 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 166 | "dev": true 167 | }, 168 | "json3": { 169 | "version": "3.3.2", 170 | "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", 171 | "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", 172 | "dev": true 173 | }, 174 | "lodash._baseassign": { 175 | "version": "3.2.0", 176 | "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", 177 | "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", 178 | "dev": true, 179 | "requires": { 180 | "lodash._basecopy": "3.0.1", 181 | "lodash.keys": "3.1.2" 182 | } 183 | }, 184 | "lodash._basecopy": { 185 | "version": "3.0.1", 186 | "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", 187 | "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", 188 | "dev": true 189 | }, 190 | "lodash._basecreate": { 191 | "version": "3.0.3", 192 | "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", 193 | "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", 194 | "dev": true 195 | }, 196 | "lodash._getnative": { 197 | "version": "3.9.1", 198 | "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", 199 | "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", 200 | "dev": true 201 | }, 202 | "lodash._isiterateecall": { 203 | "version": "3.0.9", 204 | "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", 205 | "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", 206 | "dev": true 207 | }, 208 | "lodash.create": { 209 | "version": "3.1.1", 210 | "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", 211 | "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", 212 | "dev": true, 213 | "requires": { 214 | "lodash._baseassign": "3.2.0", 215 | "lodash._basecreate": "3.0.3", 216 | "lodash._isiterateecall": "3.0.9" 217 | } 218 | }, 219 | "lodash.isarguments": { 220 | "version": "3.1.0", 221 | "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", 222 | "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", 223 | "dev": true 224 | }, 225 | "lodash.isarray": { 226 | "version": "3.0.4", 227 | "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", 228 | "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", 229 | "dev": true 230 | }, 231 | "lodash.keys": { 232 | "version": "3.1.2", 233 | "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", 234 | "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", 235 | "dev": true, 236 | "requires": { 237 | "lodash._getnative": "3.9.1", 238 | "lodash.isarguments": "3.1.0", 239 | "lodash.isarray": "3.0.4" 240 | } 241 | }, 242 | "lolex": { 243 | "version": "1.3.2", 244 | "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.3.2.tgz", 245 | "integrity": "sha1-fD2mL/yzDw9agKJWbKJORdigHzE=", 246 | "dev": true 247 | }, 248 | "minimatch": { 249 | "version": "3.0.4", 250 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 251 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 252 | "dev": true, 253 | "requires": { 254 | "brace-expansion": "1.1.8" 255 | } 256 | }, 257 | "minimist": { 258 | "version": "0.0.8", 259 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 260 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 261 | "dev": true 262 | }, 263 | "mkdirp": { 264 | "version": "0.5.1", 265 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 266 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 267 | "dev": true, 268 | "requires": { 269 | "minimist": "0.0.8" 270 | } 271 | }, 272 | "mocha": { 273 | "version": "3.5.3", 274 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.3.tgz", 275 | "integrity": "sha512-/6na001MJWEtYxHOV1WLfsmR4YIynkUEhBwzsb+fk2qmQ3iqsi258l/Q2MWHJMImAcNpZ8DEdYAK72NHoIQ9Eg==", 276 | "dev": true, 277 | "requires": { 278 | "browser-stdout": "1.3.0", 279 | "commander": "2.9.0", 280 | "debug": "2.6.8", 281 | "diff": "3.2.0", 282 | "escape-string-regexp": "1.0.5", 283 | "glob": "7.1.1", 284 | "growl": "1.9.2", 285 | "he": "1.1.1", 286 | "json3": "3.3.2", 287 | "lodash.create": "3.1.1", 288 | "mkdirp": "0.5.1", 289 | "supports-color": "3.1.2" 290 | } 291 | }, 292 | "ms": { 293 | "version": "2.0.0", 294 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 295 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 296 | "dev": true 297 | }, 298 | "once": { 299 | "version": "1.4.0", 300 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 301 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 302 | "dev": true, 303 | "requires": { 304 | "wrappy": "1.0.2" 305 | } 306 | }, 307 | "path-is-absolute": { 308 | "version": "1.0.1", 309 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 310 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 311 | "dev": true 312 | }, 313 | "samsam": { 314 | "version": "1.1.2", 315 | "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.2.tgz", 316 | "integrity": "sha1-vsEf3IOp/aBjQBIQ5AF2wwJNFWc=", 317 | "dev": true 318 | }, 319 | "sinon": { 320 | "version": "1.17.7", 321 | "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.17.7.tgz", 322 | "integrity": "sha1-RUKk9JugxFwF6y6d2dID4rjv4L8=", 323 | "dev": true, 324 | "requires": { 325 | "formatio": "1.1.1", 326 | "lolex": "1.3.2", 327 | "samsam": "1.1.2", 328 | "util": "0.10.3" 329 | } 330 | }, 331 | "sinon-chai": { 332 | "version": "2.14.0", 333 | "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-2.14.0.tgz", 334 | "integrity": "sha512-9stIF1utB0ywNHNT7RgiXbdmen8QDCRsrTjw+G9TgKt1Yexjiv8TOWZ6WHsTPz57Yky3DIswZvEqX8fpuHNDtQ==", 335 | "dev": true 336 | }, 337 | "supports-color": { 338 | "version": "3.1.2", 339 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", 340 | "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", 341 | "dev": true, 342 | "requires": { 343 | "has-flag": "1.0.0" 344 | } 345 | }, 346 | "type-detect": { 347 | "version": "1.0.0", 348 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz", 349 | "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI=", 350 | "dev": true 351 | }, 352 | "util": { 353 | "version": "0.10.3", 354 | "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", 355 | "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", 356 | "dev": true, 357 | "requires": { 358 | "inherits": "2.0.1" 359 | }, 360 | "dependencies": { 361 | "inherits": { 362 | "version": "2.0.1", 363 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", 364 | "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", 365 | "dev": true 366 | } 367 | } 368 | }, 369 | "wrappy": { 370 | "version": "1.0.2", 371 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 372 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 373 | "dev": true 374 | } 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adbkit-logcat", 3 | "version": "2.0.1", 4 | "description": "A Node.js interface for working with Android's logcat output.", 5 | "keywords": [ 6 | "adb", 7 | "adbkit", 8 | "logcat" 9 | ], 10 | "bugs": { 11 | "url": "https://github.com/openstf/adbkit-logcat/issues" 12 | }, 13 | "license": "Apache-2.0", 14 | "author": { 15 | "name": "The OpenSTF Project", 16 | "email": "contact@openstf.io", 17 | "url": "https://openstf.io" 18 | }, 19 | "main": "./index", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/openstf/adbkit-logcat.git" 23 | }, 24 | "scripts": { 25 | "test": "mocha --recursive" 26 | }, 27 | "dependencies": {}, 28 | "devDependencies": { 29 | "chai": "^3.5.0", 30 | "mocha": "^3.0.2", 31 | "sinon": "^1.17.5", 32 | "sinon-chai": "^2.8.0" 33 | }, 34 | "engines": { 35 | "node": ">= 4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/fixtures/1-broken.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstf/adbkit-logcat/485b89defe9b14b50b873109fa39c3213051dd92/test/fixtures/1-broken.dat -------------------------------------------------------------------------------- /test/fixtures/1-crlf.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstf/adbkit-logcat/485b89defe9b14b50b873109fa39c3213051dd92/test/fixtures/1-crlf.dat -------------------------------------------------------------------------------- /test/fixtures/1-default.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstf/adbkit-logcat/485b89defe9b14b50b873109fa39c3213051dd92/test/fixtures/1-default.dat -------------------------------------------------------------------------------- /test/fixtures/3-default.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstf/adbkit-logcat/485b89defe9b14b50b873109fa39c3213051dd92/test/fixtures/3-default.dat -------------------------------------------------------------------------------- /test/logcat.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | const expect = require('chai').expect 4 | 5 | const Logcat = require('../') 6 | const Reader = require('../lib/logcat/reader') 7 | const Priority = require('../lib/logcat/priority') 8 | const MockDuplex = require('./mock/duplex') 9 | 10 | describe('Logcat', () => { 11 | describe('Reader', () => { 12 | it('should be exposed', done => { 13 | expect(Logcat.Reader).to.equal(Reader) 14 | done() 15 | }) 16 | }) 17 | 18 | describe('Priority', () => { 19 | it('should be exposed', done => { 20 | expect(Logcat.Priority).to.equal(Priority) 21 | done() 22 | }) 23 | }) 24 | 25 | describe('@readStream(stream, options)', () => { 26 | before(done => { 27 | this.duplex = new MockDuplex 28 | return done() 29 | }) 30 | 31 | it('should return a Reader instance', done => { 32 | const logcat = Logcat.readStream(this.duplex) 33 | expect(logcat).to.be.an.instanceOf(Reader) 34 | done() 35 | }) 36 | 37 | it('should pass stream to Reader', done => { 38 | const logcat = Logcat.readStream(this.duplex) 39 | expect(logcat.stream).to.equal(this.duplex) 40 | done() 41 | }) 42 | 43 | it('should pass options to Reader', done => { 44 | const options = {foo: 'bar'} 45 | const logcat = Logcat.readStream(this.duplex, options) 46 | expect(logcat.options.foo).to.equal('bar') 47 | done() 48 | }) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /test/logcat/entry.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | const expect = require('chai').expect 4 | 5 | const Entry = require('../../lib/logcat/entry') 6 | const BinaryParser = require('../../lib/logcat/parser/binary') 7 | 8 | describe('Entry', () => { 9 | it('should have a \'date\' property', done => { 10 | expect(new Entry()).to.have.property('date') 11 | done() 12 | }) 13 | 14 | it('should have a \'pid\' property', done => { 15 | expect(new Entry()).to.have.property('pid') 16 | done() 17 | }) 18 | 19 | it('should have a \'tid\' property', done => { 20 | expect(new Entry()).to.have.property('tid') 21 | done() 22 | }) 23 | 24 | it('should have a \'priority\' property', done => { 25 | expect(new Entry()).to.have.property('priority') 26 | done() 27 | }) 28 | 29 | it('should have a \'tag\' property', done => { 30 | expect(new Entry()).to.have.property('tag') 31 | done() 32 | }) 33 | 34 | it('should have a \'message\' property', done => { 35 | expect(new Entry()).to.have.property('message') 36 | done() 37 | }) 38 | 39 | describe('setDate(date)', () => { 40 | it('should set the \'date\' property', done => { 41 | const entry = new Entry() 42 | const date = new Date 43 | entry.setDate(date) 44 | expect(entry.date).to.equal(date) 45 | done() 46 | }) 47 | }) 48 | 49 | describe('setPid(pid)', () => { 50 | it('should set the \'pid\' property', done => { 51 | const entry = new Entry() 52 | entry.setPid(346) 53 | expect(entry.pid).to.equal(346) 54 | done() 55 | }) 56 | }) 57 | 58 | describe('setTid(tid)', () => { 59 | it('should set the \'tid\' property', done => { 60 | const entry = new Entry() 61 | entry.setTid(3278) 62 | expect(entry.tid).to.equal(3278) 63 | done() 64 | }) 65 | }) 66 | 67 | describe('setPriority(tid)', () => { 68 | it('should set the \'priority\' property', done => { 69 | const entry = new Entry() 70 | entry.setPriority('D') 71 | expect(entry.priority).to.equal('D') 72 | done() 73 | }) 74 | }) 75 | 76 | describe('setTag(tag)', () => { 77 | it('should set the \'tag\' property', done => { 78 | const entry = new Entry() 79 | entry.setTag('dalvikvm') 80 | expect(entry.tag).to.equal('dalvikvm') 81 | done() 82 | }) 83 | }) 84 | 85 | describe('setMessage(message)', () => { 86 | it('should set the \'message\' property', done => { 87 | const entry = new Entry() 88 | entry.setMessage('foo bar') 89 | expect(entry.message).to.equal('foo bar') 90 | done() 91 | }) 92 | }) 93 | 94 | describe('toBinary()', () => { 95 | it('should return a valid binary entry', done => { 96 | const entry = new Entry() 97 | entry.setDate(new Date()) 98 | entry.setPid(999) 99 | entry.setTid(888) 100 | entry.setPriority(6) 101 | entry.setTag('AAAAA') 102 | entry.setMessage('BBBBBB') 103 | const parser = new BinaryParser() 104 | parser.on('entry', parsed => { 105 | expect(JSON.stringify(parsed.date)).to.equal(JSON.stringify(entry.date)) 106 | expect(parsed.pid).to.equal(entry.pid) 107 | expect(parsed.tid).to.equal(entry.tid) 108 | expect(parsed.priority).to.equal(entry.priority) 109 | expect(parsed.tag).to.equal(entry.tag) 110 | expect(parsed.message).to.equal(entry.message) 111 | done() 112 | }) 113 | parser.parse(entry.toBinary()) 114 | }) 115 | }) 116 | }) 117 | -------------------------------------------------------------------------------- /test/logcat/parser/binary.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | const EventEmitter = require('events').EventEmitter 4 | const path = require('path') 5 | const fs = require('fs') 6 | const sinon = require('sinon') 7 | const expect = require('chai') 8 | .use(require('sinon-chai')) 9 | .expect 10 | 11 | const Priority = require('../../../lib/logcat/priority') 12 | const BinaryParser = require('../../../lib/logcat/parser/binary') 13 | 14 | const fixt1 = fs.readFileSync(path.join(__dirname, '../../fixtures/1-default.dat')) 15 | const fixt3 = fs.readFileSync(path.join(__dirname, '../../fixtures/3-default.dat')) 16 | const broken1 = fs.readFileSync(path.join(__dirname, '../../fixtures/1-broken.dat')) 17 | 18 | describe('Parser.Binary', () => { 19 | it('should implement EventEmitter', done => { 20 | const parser = new BinaryParser() 21 | expect(parser).to.be.an.instanceOf(EventEmitter) 22 | done() 23 | }) 24 | 25 | it('should emit \'drain\' when all data has been parsed', done => { 26 | const parser = new BinaryParser() 27 | parser.on('drain', done) 28 | parser.parse(new Buffer('')) 29 | }) 30 | 31 | it('should emit \'wait\' when waiting for more data', done => { 32 | const parser = new BinaryParser() 33 | parser.on('wait', done) 34 | parser.parse(new Buffer('foo')) 35 | }) 36 | 37 | it('should emit \'error\' if entry data cannot be parsed', done => { 38 | const parser = new BinaryParser() 39 | parser.on('error', err => { 40 | expect(err).to.be.an.instanceOf(Error) 41 | done() 42 | }) 43 | parser.parse(broken1) 44 | }) 45 | 46 | it('should emit \'entry\' when an entry is found', done => { 47 | const parser = new BinaryParser() 48 | parser.on('entry', entry => { 49 | expect(entry.date).to.be.an.instanceOf(Date) 50 | expect(entry.date.getFullYear()).to.equal(2013) 51 | expect(entry.date.getMonth()).to.equal(4) 52 | expect(entry.date.getDate()).to.equal(13) 53 | expect(entry.date.getHours()).to.equal(1) 54 | expect(entry.date.getMinutes()).to.equal(5) 55 | expect(entry.date.getSeconds()).to.equal(25) 56 | expect(entry.date.getMilliseconds()).to.equal(686) 57 | expect(entry.pid).to.equal(26642) 58 | expect(entry.tid).to.equal(26676) 59 | expect(entry.priority).to.equal(Priority.DEBUG) 60 | expect(entry.tag).to.equal('dalvikvm') 61 | expect(entry.message).to.equal('WAIT_FOR_CONCURRENT_GC blocked 15ms') 62 | done() 63 | }) 64 | parser.parse(fixt1) 65 | }) 66 | 67 | it('should parse an entry that arrives in multiple chunks', done => { 68 | const parser = new BinaryParser() 69 | parser.on('entry', entry => { 70 | expect(entry.message).to.equal('WAIT_FOR_CONCURRENT_GC blocked 15ms') 71 | done() 72 | }) 73 | parser.parse(fixt1.slice(0, 10)) 74 | parser.parse(fixt1.slice(10, 34)) 75 | parser.parse(fixt1.slice(34)) 76 | }) 77 | 78 | return it('should parse multiple entries in one chunk', done => { 79 | const parser = new BinaryParser() 80 | const entrySpy = sinon.spy() 81 | parser.on('entry', entrySpy) 82 | parser.on('drain', () => { 83 | expect(entrySpy).to.have.been.calledThrice 84 | done() 85 | }) 86 | parser.parse(fixt3) 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /test/logcat/priority.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | const expect = require('chai').expect 4 | 5 | const Priority = require('../../lib/logcat/priority') 6 | 7 | describe('Priority', () => { 8 | describe('@toName(value)', () => { 9 | it('should return the name of the priority', done => { 10 | expect(Priority.toName(Priority.UNKNOWN)).to.equal('UNKNOWN') 11 | expect(Priority.toName(Priority.DEFAULT)).to.equal('DEFAULT') 12 | expect(Priority.toName(Priority.VERBOSE)).to.equal('VERBOSE') 13 | expect(Priority.toName(Priority.DEBUG)).to.equal('DEBUG') 14 | expect(Priority.toName(Priority.INFO)).to.equal('INFO') 15 | expect(Priority.toName(Priority.WARN)).to.equal('WARN') 16 | expect(Priority.toName(Priority.ERROR)).to.equal('ERROR') 17 | expect(Priority.toName(Priority.FATAL)).to.equal('FATAL') 18 | expect(Priority.toName(Priority.SILENT)).to.equal('SILENT') 19 | done() 20 | }) 21 | 22 | it('should return undefined for unknown values', done => { 23 | expect(Priority.toName(-1)).to.be.undefined 24 | done() 25 | }) 26 | }) 27 | 28 | describe('@toLetter(value)', () => { 29 | it('should return the value of the priority', done => { 30 | expect(Priority.toLetter(Priority.UNKNOWN)).to.equal('?') 31 | expect(Priority.toLetter(Priority.DEFAULT)).to.equal('?') 32 | expect(Priority.toLetter(Priority.VERBOSE)).to.equal('V') 33 | expect(Priority.toLetter(Priority.DEBUG)).to.equal('D') 34 | expect(Priority.toLetter(Priority.INFO)).to.equal('I') 35 | expect(Priority.toLetter(Priority.WARN)).to.equal('W') 36 | expect(Priority.toLetter(Priority.ERROR)).to.equal('E') 37 | expect(Priority.toLetter(Priority.FATAL)).to.equal('F') 38 | expect(Priority.toLetter(Priority.SILENT)).to.equal('S') 39 | done() 40 | }) 41 | 42 | it('should return undefined for unknown values', done => { 43 | expect(Priority.toLetter(-1)).to.be.undefined 44 | done() 45 | }) 46 | }) 47 | 48 | describe('@fromLetter(letter)', () => { 49 | it('should return the value of the priority', done => { 50 | expect(Priority.fromLetter('?')).to.equal(Priority.UNKNOWN) 51 | expect(Priority.fromLetter('V')).to.equal(Priority.VERBOSE) 52 | expect(Priority.fromLetter('D')).to.equal(Priority.DEBUG) 53 | expect(Priority.fromLetter('I')).to.equal(Priority.INFO) 54 | expect(Priority.fromLetter('W')).to.equal(Priority.WARN) 55 | expect(Priority.fromLetter('E')).to.equal(Priority.ERROR) 56 | expect(Priority.fromLetter('F')).to.equal(Priority.FATAL) 57 | expect(Priority.fromLetter('S')).to.equal(Priority.SILENT) 58 | done() 59 | }) 60 | 61 | it('should ignore case', done => { 62 | expect(Priority.fromName('v')).to.equal(Priority.VERBOSE) 63 | done() 64 | }) 65 | 66 | it('should return undefined for unknown letters', done => { 67 | expect(Priority.fromLetter('.')).to.be.undefined 68 | done() 69 | }) 70 | }) 71 | 72 | describe('@fromName(name)', () => { 73 | it('should return the value of the priority', done => { 74 | expect(Priority.fromName('UNKNOWN')).to.equal(Priority.UNKNOWN) 75 | expect(Priority.fromName('VERBOSE')).to.equal(Priority.VERBOSE) 76 | expect(Priority.fromName('DEBUG')).to.equal(Priority.DEBUG) 77 | expect(Priority.fromName('INFO')).to.equal(Priority.INFO) 78 | expect(Priority.fromName('WARN')).to.equal(Priority.WARN) 79 | expect(Priority.fromName('ERROR')).to.equal(Priority.ERROR) 80 | expect(Priority.fromName('FATAL')).to.equal(Priority.FATAL) 81 | expect(Priority.fromName('SILENT')).to.equal(Priority.SILENT) 82 | done() 83 | }) 84 | 85 | it('should detect letters', done => { 86 | expect(Priority.fromName('V')).to.equal(Priority.VERBOSE) 87 | done() 88 | }) 89 | 90 | it('should ignore case', done => { 91 | expect(Priority.fromName('veRboSe')).to.equal(Priority.VERBOSE) 92 | done() 93 | }) 94 | 95 | it('should return undefined for unknown names', done => { 96 | expect(Priority.fromName('foo')).to.be.undefined 97 | done() 98 | }) 99 | }) 100 | }) 101 | -------------------------------------------------------------------------------- /test/logcat/reader.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | const sinon = require('sinon') 4 | const expect = require('chai') 5 | .use(require('sinon-chai')) 6 | .expect 7 | 8 | const Reader = require('../../lib/logcat/reader') 9 | const Entry = require('../../lib/logcat/entry') 10 | const MockDuplex = require('../mock/duplex') 11 | 12 | describe('Reader', () => { 13 | const mockEntry = (date, pid, tid, priority, tag, message) => { 14 | const entry = new Entry() 15 | entry.setDate(date) 16 | entry.setPid(pid) 17 | entry.setTid(tid) 18 | entry.setPriority(priority) 19 | entry.setTag(tag) 20 | entry.setMessage(message) 21 | return entry 22 | } 23 | 24 | it('should have a \'stream\' property', done => { 25 | const logcat = new Reader() 26 | expect(logcat).to.have.property('stream') 27 | done() 28 | }) 29 | 30 | it('should have an \'options\' property', done => { 31 | const logcat = new Reader() 32 | expect(logcat).to.have.property('options') 33 | done() 34 | }) 35 | 36 | describe('options', () => 37 | it('should be set via constructor', done => { 38 | const logcat = new Reader({bar: 'foo'}) 39 | expect(logcat.options.bar).to.equal('foo') 40 | done() 41 | }) 42 | ) 43 | 44 | describe('events', () => { 45 | it('should emit \'finish\' when underlying stream does', done => { 46 | const duplex = new MockDuplex() 47 | const logcat = new Reader().connect(duplex) 48 | logcat.on('finish', () => done()) 49 | duplex.end() 50 | }) 51 | 52 | it('should emit \'end\' when underlying stream does', done => { 53 | const duplex = new MockDuplex() 54 | const logcat = new Reader().connect(duplex) 55 | logcat.on('end', () => done()) 56 | duplex.causeRead('foo') 57 | duplex.causeEnd() 58 | }) 59 | 60 | it('should forward \'entry\' from parser', done => { 61 | const duplex = new MockDuplex() 62 | const logcat = new Reader().connect(duplex) 63 | logcat.on('entry', entry => { 64 | expect(entry).to.be.an.instanceOf(Entry) 65 | done() 66 | }) 67 | logcat.parser.emit('entry', new Entry) 68 | }) 69 | 70 | it('should forward \'error\' from parser', done => { 71 | const duplex = new MockDuplex() 72 | const logcat = new Reader().connect(duplex) 73 | logcat.on('error', err => { 74 | expect(err).to.be.an.instanceOf(Error) 75 | done() 76 | }) 77 | logcat.parser.emit('error', new Error('foo')) 78 | }) 79 | }) 80 | 81 | describe('exclude(tag)', () => { 82 | it('should be chainable', done => { 83 | const duplex = new MockDuplex() 84 | const logcat = new Reader().connect(duplex) 85 | expect(logcat.exclude('foo')).to.equal(logcat) 86 | done() 87 | }) 88 | 89 | it('should prevent entries with matching tag from being emitted', done => { 90 | const duplex = new MockDuplex() 91 | const logcat = new Reader().connect(duplex) 92 | logcat.exclude('foo') 93 | const entry1 = mockEntry(new Date(), 1, 2, 4, 'foo', 'bar') 94 | const entry2 = mockEntry(new Date(), 1, 2, 4, 'not foo', 'bar') 95 | duplex.causeRead(entry1.toBinary()) 96 | duplex.causeRead(entry2.toBinary()) 97 | duplex.causeEnd() 98 | const spy = sinon.spy() 99 | logcat.on('entry', spy) 100 | setImmediate(() => { 101 | expect(spy).to.have.been.calledOnce 102 | expect(spy).to.have.been.calledWith(entry2) 103 | done() 104 | }) 105 | }) 106 | 107 | it('should map to excludeAll() if tag is \'*\'', done => { 108 | const duplex = new MockDuplex() 109 | const logcat = new Reader().connect(duplex) 110 | const spy = sinon.spy(logcat, 'excludeAll') 111 | logcat.exclude('*') 112 | expect(spy).to.have.been.calledOnce 113 | done() 114 | }) 115 | }) 116 | 117 | describe('excludeAll()', () => { 118 | it('should be chainable', done => { 119 | const duplex = new MockDuplex() 120 | const logcat = new Reader().connect(duplex) 121 | expect(logcat.excludeAll()).to.equal(logcat) 122 | done() 123 | }) 124 | 125 | it('should prevent any entries from being emitted', done => { 126 | const duplex = new MockDuplex() 127 | const logcat = new Reader().connect(duplex) 128 | logcat.excludeAll() 129 | const entry = mockEntry(new Date(), 1, 2, 4, 'foo', 'bar') 130 | duplex.causeRead(entry.toBinary()) 131 | duplex.causeEnd() 132 | const spy = sinon.spy() 133 | logcat.on('entry', spy) 134 | setImmediate(() => { 135 | expect(spy).to.not.have.been.called 136 | done() 137 | }) 138 | }) 139 | }) 140 | 141 | describe('include(tag, priority)', () => { 142 | it('should be chainable', done => { 143 | const duplex = new MockDuplex() 144 | const logcat = new Reader().connect(duplex) 145 | expect(logcat.include('foo', 1)).to.equal(logcat) 146 | done() 147 | }) 148 | 149 | it('should prevent emit of matching entries with < priority', done => { 150 | const duplex = new MockDuplex() 151 | const logcat = new Reader().connect(duplex) 152 | logcat.include('foo', 5) 153 | const entry1 = mockEntry(new Date(), 1, 2, 4, 'foo', 'bar') 154 | duplex.causeRead(entry1.toBinary()) 155 | duplex.causeEnd() 156 | const spy = sinon.spy() 157 | logcat.on('entry', spy) 158 | setImmediate(() => { 159 | expect(spy).to.not.have.been.called 160 | done() 161 | }) 162 | }) 163 | 164 | it('should allow emit of matching entries with >= priority', done => { 165 | const duplex = new MockDuplex() 166 | const logcat = new Reader().connect(duplex) 167 | logcat.include('foo', 4) 168 | const entry1 = mockEntry(new Date(), 1, 2, 4, 'foo', 'bar') 169 | duplex.causeRead(entry1.toBinary()) 170 | duplex.causeEnd() 171 | const spy = sinon.spy() 172 | logcat.on('entry', spy) 173 | setImmediate(() => { 174 | expect(spy).to.have.been.calledOnce 175 | done() 176 | }) 177 | }) 178 | 179 | it('should map to includeAll(priority) if tag is \'*\'', done => { 180 | const duplex = new MockDuplex() 181 | const logcat = new Reader().connect(duplex) 182 | const spy = sinon.spy(logcat, 'includeAll') 183 | logcat.include('*', 4) 184 | expect(spy).to.have.been.calledOnce 185 | expect(spy).to.have.been.calledWith(4) 186 | done() 187 | }) 188 | }) 189 | 190 | describe('includeAll(priority)', () => { 191 | it('should be chainable', done => { 192 | const duplex = new MockDuplex() 193 | const logcat = new Reader().connect(duplex) 194 | expect(logcat.includeAll()).to.equal(logcat) 195 | done() 196 | }) 197 | 198 | it('should prevent emit of entries with < priority', done => { 199 | const duplex = new MockDuplex() 200 | const logcat = new Reader().connect(duplex) 201 | logcat.includeAll(5) 202 | const entry = mockEntry(new Date(), 1, 2, 4, 'foo', 'bar') 203 | duplex.causeRead(entry.toBinary()) 204 | duplex.causeEnd() 205 | const spy = sinon.spy() 206 | logcat.on('entry', spy) 207 | setImmediate(() => { 208 | expect(spy).to.not.have.been.called 209 | done() 210 | }) 211 | }) 212 | 213 | it('should allow emit of entries with >= priority', done => { 214 | const duplex = new MockDuplex() 215 | const logcat = new Reader().connect(duplex) 216 | logcat.includeAll(5) 217 | const entry = mockEntry(new Date(), 1, 2, 5, 'foo', 'bar') 218 | duplex.causeRead(entry.toBinary()) 219 | duplex.causeEnd() 220 | const spy = sinon.spy() 221 | logcat.on('entry', spy) 222 | setImmediate(() => { 223 | expect(spy).to.have.been.called 224 | done() 225 | }) 226 | }) 227 | 228 | it('should should override excludeAll()', done => { 229 | const duplex = new MockDuplex() 230 | const logcat = new Reader().connect(duplex) 231 | logcat.excludeAll() 232 | logcat.includeAll(5) 233 | const entry = mockEntry(new Date(), 1, 2, 5, 'foo', 'bar') 234 | duplex.causeRead(entry.toBinary()) 235 | duplex.causeEnd() 236 | const spy = sinon.spy() 237 | logcat.on('entry', spy) 238 | setImmediate(() => { 239 | expect(spy).to.have.been.called 240 | done() 241 | }) 242 | }) 243 | }) 244 | 245 | describe('resetFilters()', () => { 246 | it('should be chainable', done => { 247 | const duplex = new MockDuplex() 248 | const logcat = new Reader().connect(duplex) 249 | expect(logcat.resetFilters()).to.equal(logcat) 250 | done() 251 | }) 252 | 253 | it('should allow everything to pass again', done => { 254 | const duplex = new MockDuplex() 255 | const logcat = new Reader().connect(duplex) 256 | logcat.include('foo', 5) 257 | logcat.include('bar', 9) 258 | logcat.excludeAll() 259 | logcat.resetFilters() 260 | const entry = mockEntry(new Date(), 99, 99, 99, 'zup', 'bar') 261 | duplex.causeRead(entry.toBinary()) 262 | duplex.causeEnd() 263 | const spy = sinon.spy() 264 | logcat.on('entry', spy) 265 | setImmediate(() => { 266 | expect(spy).to.have.been.called 267 | done() 268 | }) 269 | }) 270 | }) 271 | 272 | describe('connect(stream)', () => { 273 | it('should set the \'stream\' property', done => { 274 | const duplex = new MockDuplex() 275 | const logcat = new Reader().connect(duplex) 276 | expect(logcat.stream).to.be.equal(duplex) 277 | done() 278 | }) 279 | 280 | it('should be chainable', done => { 281 | const duplex = new MockDuplex() 282 | const logcat = new Reader() 283 | expect(logcat.connect(duplex)).to.equal(logcat) 284 | done() 285 | }) 286 | }) 287 | 288 | describe('end()', () => { 289 | it('should be chainable', done => { 290 | const duplex = new MockDuplex() 291 | const logcat = new Reader().connect(duplex) 292 | expect(logcat.end()).to.equal(logcat) 293 | done() 294 | }) 295 | 296 | it('should end underlying stream', done => { 297 | const duplex = new MockDuplex() 298 | const logcat = new Reader().connect(duplex) 299 | logcat.on('finish', () => done()) 300 | logcat.end() 301 | }) 302 | }) 303 | }) 304 | -------------------------------------------------------------------------------- /test/logcat/transform.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-env mocha */ 4 | 5 | const stream = require('stream') 6 | const expect = require('chai') 7 | .use(require('sinon-chai')) 8 | .expect 9 | 10 | const Transform = require('../../lib/logcat/transform') 11 | const MockDuplex = require('../mock/duplex') 12 | 13 | describe('Transform', () => { 14 | it('should implement stream.Transform', done => { 15 | expect(new Transform()).to.be.an.instanceOf(stream.Transform) 16 | done() 17 | }) 18 | 19 | it('should not modify data that does not have 0x0d 0x0a in it', done => { 20 | const duplex = new MockDuplex() 21 | const transform = new Transform() 22 | 23 | transform.on('data', data => { 24 | expect(data.toString()).to.equal('foo') 25 | done() 26 | }) 27 | 28 | duplex.pipe(transform) 29 | duplex.causeRead('foo') 30 | duplex.causeEnd() 31 | }) 32 | 33 | it('should not remove 0x0d if not followed by 0x0a', done => { 34 | const duplex = new MockDuplex() 35 | const transform = new Transform() 36 | 37 | transform.on('data', data => { 38 | expect(data.length).to.equal(2) 39 | expect(data[0]).to.equal(0x0d) 40 | expect(data[1]).to.equal(0x05) 41 | done() 42 | }) 43 | 44 | duplex.pipe(transform) 45 | duplex.causeRead(new Buffer([0x0d, 0x05])) 46 | duplex.causeEnd() 47 | }) 48 | 49 | it('should remove 0x0d if followed by 0x0a', done => { 50 | const duplex = new MockDuplex() 51 | const transform = new Transform() 52 | 53 | transform.on('data', data => { 54 | expect(data.length).to.equal(2) 55 | expect(data[0]).to.equal(0x0a) 56 | expect(data[1]).to.equal(0x97) 57 | done() 58 | }) 59 | 60 | duplex.pipe(transform) 61 | duplex.causeRead(new Buffer([0x0d, 0x0a, 0x97])) 62 | duplex.causeEnd() 63 | }) 64 | 65 | it('should not push 0x0d if last in stream', done => { 66 | const duplex = new MockDuplex() 67 | const transform = new Transform() 68 | 69 | transform.on('data', data => { 70 | expect(data.length).to.equal(1) 71 | expect(data[0]).to.equal(0x62) 72 | done() 73 | }) 74 | 75 | duplex.pipe(transform) 76 | duplex.causeRead(new Buffer([0x62, 0x0d])) 77 | }) 78 | 79 | it('should push saved 0x0d if next chunk does not start with 0x0a', done => { 80 | const duplex = new MockDuplex() 81 | const transform = new Transform() 82 | 83 | let buffer = new Buffer('') 84 | transform.on('data', data => { 85 | buffer = Buffer.concat([buffer, data]) 86 | }) 87 | 88 | transform.on('end', () => { 89 | expect(buffer).to.have.length(3) 90 | expect(buffer[0]).to.equal(0x62) 91 | expect(buffer[1]).to.equal(0x0d) 92 | expect(buffer[2]).to.equal(0x37) 93 | done() 94 | }) 95 | 96 | duplex.pipe(transform) 97 | duplex.causeRead(new Buffer([0x62, 0x0d])) 98 | duplex.causeRead(new Buffer([0x37])) 99 | duplex.causeEnd() 100 | }) 101 | 102 | it('should remove saved 0x0d if next chunk starts with 0x0a', done => { 103 | const duplex = new MockDuplex() 104 | const transform = new Transform() 105 | 106 | let buffer = new Buffer('') 107 | transform.on('data', data => { 108 | buffer = Buffer.concat([buffer, data]) 109 | }) 110 | 111 | transform.on('end', () => { 112 | expect(buffer).to.have.length(2) 113 | expect(buffer[0]).to.equal(0x62) 114 | expect(buffer[1]).to.equal(0x0a) 115 | done() 116 | }) 117 | 118 | duplex.pipe(transform) 119 | duplex.causeRead(new Buffer([0x62, 0x0d])) 120 | duplex.causeRead(new Buffer([0x0a])) 121 | duplex.causeEnd() 122 | }) 123 | }) 124 | -------------------------------------------------------------------------------- /test/mock/duplex.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const stream = require('stream') 4 | 5 | class MockDuplex extends stream.Duplex { 6 | _read(/*size*/) { 7 | } 8 | 9 | _write(chunk, encoding, callback) { 10 | this.emit('write', chunk, encoding, callback) 11 | callback(null) 12 | } 13 | 14 | causeRead(chunk) { 15 | if (!Buffer.isBuffer(chunk)) { 16 | chunk = new Buffer(chunk) 17 | } 18 | 19 | this.push(chunk) 20 | } 21 | 22 | causeEnd() { 23 | this.push(null) 24 | } 25 | } 26 | 27 | module.exports = MockDuplex 28 | --------------------------------------------------------------------------------