1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/testing/xpcshell/node-http2/node_modules/http2-protocol/test/stream.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,415 @@ 1.4 +var expect = require('chai').expect; 1.5 +var util = require('./util'); 1.6 + 1.7 +var stream = require('../lib/stream'); 1.8 +var Stream = stream.Stream; 1.9 + 1.10 +function createStream() { 1.11 + var stream = new Stream(util.log); 1.12 + stream.upstream._window = Infinity; 1.13 + return stream; 1.14 +} 1.15 + 1.16 +// Execute a list of commands and assertions 1.17 +var recorded_events = ['state', 'error', 'window_update', 'headers', 'promise']; 1.18 +function execute_sequence(stream, sequence, done) { 1.19 + if (!done) { 1.20 + done = sequence; 1.21 + sequence = stream; 1.22 + stream = createStream(); 1.23 + } 1.24 + 1.25 + var outgoing_frames = []; 1.26 + 1.27 + var emit = stream.emit, events = []; 1.28 + stream.emit = function(name) { 1.29 + if (recorded_events.indexOf(name) !== -1) { 1.30 + events.push({ name: name, data: Array.prototype.slice.call(arguments, 1) }); 1.31 + } 1.32 + return emit.apply(this, arguments); 1.33 + }; 1.34 + 1.35 + var commands = [], checks = []; 1.36 + sequence.forEach(function(step) { 1.37 + if ('method' in step || 'incoming' in step || 'outgoing' in step || 'wait' in step || 'set_state' in step) { 1.38 + commands.push(step); 1.39 + } 1.40 + 1.41 + if ('outgoing' in step || 'event' in step || 'active' in step) { 1.42 + checks.push(step); 1.43 + } 1.44 + }); 1.45 + 1.46 + var activeCount = 0; 1.47 + function count_change(change) { 1.48 + activeCount += change; 1.49 + } 1.50 + 1.51 + function execute(callback) { 1.52 + var command = commands.shift(); 1.53 + if (command) { 1.54 + if ('method' in command) { 1.55 + var value = stream[command.method.name].apply(stream, command.method.arguments); 1.56 + if (command.method.ret) { 1.57 + command.method.ret(value); 1.58 + } 1.59 + execute(callback); 1.60 + } else if ('incoming' in command) { 1.61 + command.incoming.count_change = count_change; 1.62 + stream.upstream.write(command.incoming); 1.63 + execute(callback); 1.64 + } else if ('outgoing' in command) { 1.65 + outgoing_frames.push(stream.upstream.read()); 1.66 + execute(callback); 1.67 + } else if ('set_state' in command) { 1.68 + stream.state = command.set_state; 1.69 + execute(callback); 1.70 + } else if ('wait' in command) { 1.71 + setTimeout(execute.bind(null, callback), command.wait); 1.72 + } else { 1.73 + throw new Error('Invalid command', command); 1.74 + } 1.75 + } else { 1.76 + setTimeout(callback, 5); 1.77 + } 1.78 + } 1.79 + 1.80 + function check() { 1.81 + checks.forEach(function(check) { 1.82 + if ('outgoing' in check) { 1.83 + var frame = outgoing_frames.shift(); 1.84 + for (var key in check.outgoing) { 1.85 + expect(frame).to.have.property(key).that.deep.equals(check.outgoing[key]); 1.86 + } 1.87 + count_change(frame.count_change); 1.88 + } else if ('event' in check) { 1.89 + var event = events.shift(); 1.90 + expect(event.name).to.be.equal(check.event.name); 1.91 + check.event.data.forEach(function(data, index) { 1.92 + expect(event.data[index]).to.deep.equal(data); 1.93 + }); 1.94 + } else if ('active' in check) { 1.95 + expect(activeCount).to.be.equal(check.active); 1.96 + } else { 1.97 + throw new Error('Invalid check', check); 1.98 + } 1.99 + }); 1.100 + done(); 1.101 + } 1.102 + 1.103 + setImmediate(execute.bind(null, check)); 1.104 +} 1.105 + 1.106 +var example_frames = [ 1.107 + { type: 'PRIORITY', flags: {}, priority: 1 }, 1.108 + { type: 'WINDOW_UPDATE', flags: {}, settings: {} }, 1.109 + { type: 'RST_STREAM', flags: {}, error: 'CANCEL' }, 1.110 + { type: 'HEADERS', flags: {}, headers: {}, priority: undefined }, 1.111 + { type: 'DATA', flags: {}, data: new Buffer(5) }, 1.112 + { type: 'PUSH_PROMISE', flags: {}, headers: {}, promised_stream: new Stream(util.log) } 1.113 +]; 1.114 + 1.115 +var invalid_incoming_frames = { 1.116 + IDLE: [ 1.117 + { type: 'DATA', flags: {}, data: new Buffer(5) }, 1.118 + { type: 'PRIORITY', flags: {}, priority: 1 }, 1.119 + { type: 'WINDOW_UPDATE', flags: {}, settings: {} }, 1.120 + { type: 'PUSH_PROMISE', flags: {}, headers: {} }, 1.121 + { type: 'RST_STREAM', flags: {}, error: 'CANCEL' } 1.122 + ], 1.123 + RESERVED_LOCAL: [ 1.124 + { type: 'DATA', flags: {}, data: new Buffer(5) }, 1.125 + { type: 'HEADERS', flags: {}, headers: {}, priority: undefined }, 1.126 + { type: 'PUSH_PROMISE', flags: {}, headers: {} }, 1.127 + { type: 'WINDOW_UPDATE', flags: {}, settings: {} } 1.128 + ], 1.129 + RESERVED_REMOTE: [ 1.130 + { type: 'DATA', flags: {}, data: new Buffer(5) }, 1.131 + { type: 'PRIORITY', flags: {}, priority: 1 }, 1.132 + { type: 'PUSH_PROMISE', flags: {}, headers: {} }, 1.133 + { type: 'WINDOW_UPDATE', flags: {}, settings: {} } 1.134 + ], 1.135 + OPEN: [ 1.136 + ], 1.137 + HALF_CLOSED_LOCAL: [ 1.138 + ], 1.139 + HALF_CLOSED_REMOTE: [ 1.140 + { type: 'DATA', flags: {}, data: new Buffer(5) }, 1.141 + { type: 'HEADERS', flags: {}, headers: {}, priority: undefined }, 1.142 + { type: 'PUSH_PROMISE', flags: {}, headers: {} } 1.143 + ] 1.144 +}; 1.145 + 1.146 +var invalid_outgoing_frames = { 1.147 + IDLE: [ 1.148 + { type: 'DATA', flags: {}, data: new Buffer(5) }, 1.149 + { type: 'PRIORITY', flags: {}, priority: 1 }, 1.150 + { type: 'WINDOW_UPDATE', flags: {}, settings: {} }, 1.151 + { type: 'PUSH_PROMISE', flags: {}, headers: {} } 1.152 + ], 1.153 + RESERVED_LOCAL: [ 1.154 + { type: 'DATA', flags: {}, data: new Buffer(5) }, 1.155 + { type: 'PRIORITY', flags: {}, priority: 1 }, 1.156 + { type: 'PUSH_PROMISE', flags: {}, headers: {} }, 1.157 + { type: 'WINDOW_UPDATE', flags: {}, settings: {} } 1.158 + ], 1.159 + RESERVED_REMOTE: [ 1.160 + { type: 'DATA', flags: {}, data: new Buffer(5) }, 1.161 + { type: 'HEADERS', flags: {}, headers: {}, priority: undefined }, 1.162 + { type: 'PUSH_PROMISE', flags: {}, headers: {} }, 1.163 + { type: 'WINDOW_UPDATE', flags: {}, settings: {} } 1.164 + ], 1.165 + OPEN: [ 1.166 + ], 1.167 + HALF_CLOSED_LOCAL: [ 1.168 + { type: 'DATA', flags: {}, data: new Buffer(5) }, 1.169 + { type: 'HEADERS', flags: {}, headers: {}, priority: undefined }, 1.170 + { type: 'PUSH_PROMISE', flags: {}, headers: {} } 1.171 + ], 1.172 + HALF_CLOSED_REMOTE: [ 1.173 + ], 1.174 + CLOSED: [ 1.175 + { type: 'PRIORITY', flags: {}, priority: 1 }, 1.176 + { type: 'WINDOW_UPDATE', flags: {}, settings: {} }, 1.177 + { type: 'HEADERS', flags: {}, headers: {}, priority: undefined }, 1.178 + { type: 'DATA', flags: {}, data: new Buffer(5) }, 1.179 + { type: 'PUSH_PROMISE', flags: {}, headers: {}, promised_stream: new Stream(util.log) } 1.180 + ] 1.181 +}; 1.182 + 1.183 +describe('stream.js', function() { 1.184 + describe('Stream class', function() { 1.185 + describe('._transition(sending, frame) method', function() { 1.186 + it('should emit error, and answer RST_STREAM for invalid incoming frames', function() { 1.187 + Object.keys(invalid_incoming_frames).forEach(function(state) { 1.188 + invalid_incoming_frames[state].forEach(function(invalid_frame) { 1.189 + var stream = createStream(); 1.190 + stream.state = state; 1.191 + expect(stream._transition.bind(stream, false, invalid_frame)).to.throw('Uncaught, unspecified "error" event.'); 1.192 + }); 1.193 + }); 1.194 + 1.195 + // CLOSED state as a result of incoming END_STREAM (or RST_STREAM) 1.196 + var stream = createStream(); 1.197 + stream.headers({}); 1.198 + stream.end(); 1.199 + stream.upstream.write({ type: 'HEADERS', headers:{}, flags: { END_STREAM: true }, count_change: util.noop }); 1.200 + example_frames.forEach(function(invalid_frame) { 1.201 + invalid_frame.count_change = util.noop; 1.202 + expect(stream._transition.bind(stream, false, invalid_frame)).to.throw('Uncaught, unspecified "error" event.'); 1.203 + }); 1.204 + 1.205 + // CLOSED state as a result of outgoing END_STREAM 1.206 + var stream = createStream(); 1.207 + stream.upstream.write({ type: 'HEADERS', headers:{}, flags: { END_STREAM: true }, count_change: util.noop }); 1.208 + stream.headers({}); 1.209 + stream.end(); 1.210 + example_frames.slice(3).forEach(function(invalid_frame) { 1.211 + invalid_frame.count_change = util.noop; 1.212 + expect(stream._transition.bind(stream, false, invalid_frame)).to.throw('Uncaught, unspecified "error" event.'); 1.213 + }); 1.214 + }); 1.215 + it('should throw exception for invalid outgoing frames', function() { 1.216 + Object.keys(invalid_outgoing_frames).forEach(function(state) { 1.217 + invalid_outgoing_frames[state].forEach(function(invalid_frame) { 1.218 + var stream = createStream(); 1.219 + stream.state = state; 1.220 + expect(stream._transition.bind(stream, true, invalid_frame)).to.throw(Error); 1.221 + }); 1.222 + }); 1.223 + }); 1.224 + it('should close the stream when there\'s an incoming or outgoing RST_STREAM', function() { 1.225 + [ 1.226 + 'RESERVED_LOCAL', 1.227 + 'RESERVED_REMOTE', 1.228 + 'OPEN', 1.229 + 'HALF_CLOSED_LOCAL', 1.230 + 'HALF_CLOSED_REMOTE' 1.231 + ].forEach(function(state) { 1.232 + [true, false].forEach(function(sending) { 1.233 + var stream = createStream(); 1.234 + stream.state = state; 1.235 + stream._transition(sending, { type: 'RST_STREAM', flags: {} }); 1.236 + expect(stream.state).to.be.equal('CLOSED'); 1.237 + }); 1.238 + }); 1.239 + }); 1.240 + it('should ignore any incoming frame after sending reset', function() { 1.241 + var stream = createStream(); 1.242 + stream.reset(); 1.243 + example_frames.forEach(stream._transition.bind(stream, false)); 1.244 + }); 1.245 + it('should ignore certain incoming frames after closing the stream with END_STREAM', function() { 1.246 + var stream = createStream(); 1.247 + stream.upstream.write({ type: 'HEADERS', flags: { END_STREAM: true }, headers:{} }); 1.248 + stream.headers({}); 1.249 + stream.end(); 1.250 + example_frames.slice(0,3).forEach(function(frame) { 1.251 + frame.count_change = util.noop; 1.252 + stream._transition(false, frame); 1.253 + }); 1.254 + }); 1.255 + }); 1.256 + }); 1.257 + describe('test scenario', function() { 1.258 + describe('sending request', function() { 1.259 + it('should trigger the appropriate state transitions and outgoing frames', function(done) { 1.260 + execute_sequence([ 1.261 + { method : { name: 'headers', arguments: [{ ':path': '/' }] } }, 1.262 + { outgoing: { type: 'HEADERS', flags: { }, headers: { ':path': '/' } } }, 1.263 + { event : { name: 'state', data: ['OPEN'] } }, 1.264 + 1.265 + { wait : 5 }, 1.266 + { method : { name: 'end', arguments: [] } }, 1.267 + { event : { name: 'state', data: ['HALF_CLOSED_LOCAL'] } }, 1.268 + { outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: new Buffer(0) } }, 1.269 + 1.270 + { wait : 10 }, 1.271 + { incoming: { type: 'HEADERS', flags: { }, headers: { ':status': 200 } } }, 1.272 + { incoming: { type: 'DATA' , flags: { END_STREAM: true }, data: new Buffer(5) } }, 1.273 + { event : { name: 'headers', data: [{ ':status': 200 }] } }, 1.274 + { event : { name: 'state', data: ['CLOSED'] } }, 1.275 + 1.276 + { active : 0 } 1.277 + ], done); 1.278 + }); 1.279 + }); 1.280 + describe('answering request', function() { 1.281 + it('should trigger the appropriate state transitions and outgoing frames', function(done) { 1.282 + var payload = new Buffer(5); 1.283 + execute_sequence([ 1.284 + { incoming: { type: 'HEADERS', flags: { }, headers: { ':path': '/' } } }, 1.285 + { event : { name: 'state', data: ['OPEN'] } }, 1.286 + { event : { name: 'headers', data: [{ ':path': '/' }] } }, 1.287 + 1.288 + { wait : 5 }, 1.289 + { incoming: { type: 'DATA', flags: { }, data: new Buffer(5) } }, 1.290 + { incoming: { type: 'DATA', flags: { END_STREAM: true }, data: new Buffer(10) } }, 1.291 + { event : { name: 'state', data: ['HALF_CLOSED_REMOTE'] } }, 1.292 + 1.293 + { wait : 5 }, 1.294 + { method : { name: 'headers', arguments: [{ ':status': 200 }] } }, 1.295 + { outgoing: { type: 'HEADERS', flags: { }, headers: { ':status': 200 } } }, 1.296 + 1.297 + { wait : 5 }, 1.298 + { method : { name: 'end', arguments: [payload] } }, 1.299 + { outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: payload } }, 1.300 + { event : { name: 'state', data: ['CLOSED'] } }, 1.301 + 1.302 + { active : 0 } 1.303 + ], done); 1.304 + }); 1.305 + }); 1.306 + describe('sending push stream', function() { 1.307 + it('should trigger the appropriate state transitions and outgoing frames', function(done) { 1.308 + var payload = new Buffer(5); 1.309 + var pushStream; 1.310 + 1.311 + execute_sequence([ 1.312 + // receiving request 1.313 + { incoming: { type: 'HEADERS', flags: { END_STREAM: true }, headers: { ':path': '/' } } }, 1.314 + { event : { name: 'state', data: ['OPEN'] } }, 1.315 + { event : { name: 'state', data: ['HALF_CLOSED_REMOTE'] } }, 1.316 + { event : { name: 'headers', data: [{ ':path': '/' }] } }, 1.317 + 1.318 + // sending response headers 1.319 + { wait : 5 }, 1.320 + { method : { name: 'headers', arguments: [{ ':status': '200' }] } }, 1.321 + { outgoing: { type: 'HEADERS', flags: { }, headers: { ':status': '200' } } }, 1.322 + 1.323 + // sending push promise 1.324 + { method : { name: 'promise', arguments: [{ ':path': '/' }], ret: function(str) { pushStream = str; } } }, 1.325 + { outgoing: { type: 'PUSH_PROMISE', flags: { }, headers: { ':path': '/' } } }, 1.326 + 1.327 + // sending response data 1.328 + { method : { name: 'end', arguments: [payload] } }, 1.329 + { outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: payload } }, 1.330 + { event : { name: 'state', data: ['CLOSED'] } }, 1.331 + 1.332 + { active : 0 } 1.333 + ], function() { 1.334 + // initial state of the promised stream 1.335 + expect(pushStream.state).to.equal('RESERVED_LOCAL'); 1.336 + 1.337 + execute_sequence(pushStream, [ 1.338 + // push headers 1.339 + { wait : 5 }, 1.340 + { method : { name: 'headers', arguments: [{ ':status': '200' }] } }, 1.341 + { outgoing: { type: 'HEADERS', flags: { }, headers: { ':status': '200' } } }, 1.342 + { event : { name: 'state', data: ['HALF_CLOSED_REMOTE'] } }, 1.343 + 1.344 + // push data 1.345 + { method : { name: 'end', arguments: [payload] } }, 1.346 + { outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: payload } }, 1.347 + { event : { name: 'state', data: ['CLOSED'] } }, 1.348 + 1.349 + { active : 1 } 1.350 + ], done); 1.351 + }); 1.352 + }); 1.353 + }); 1.354 + describe('receiving push stream', function() { 1.355 + it('should trigger the appropriate state transitions and outgoing frames', function(done) { 1.356 + var payload = new Buffer(5); 1.357 + var original_stream = createStream(); 1.358 + var promised_stream = createStream(); 1.359 + 1.360 + done = util.callNTimes(2, done); 1.361 + 1.362 + execute_sequence(original_stream, [ 1.363 + // sending request headers 1.364 + { method : { name: 'headers', arguments: [{ ':path': '/' }] } }, 1.365 + { method : { name: 'end', arguments: [] } }, 1.366 + { outgoing: { type: 'HEADERS', flags: { END_STREAM: true }, headers: { ':path': '/' } } }, 1.367 + { event : { name: 'state', data: ['OPEN'] } }, 1.368 + { event : { name: 'state', data: ['HALF_CLOSED_LOCAL'] } }, 1.369 + 1.370 + // receiving response headers 1.371 + { wait : 10 }, 1.372 + { incoming: { type: 'HEADERS', flags: { }, headers: { ':status': 200 } } }, 1.373 + { event : { name: 'headers', data: [{ ':status': 200 }] } }, 1.374 + 1.375 + // receiving push promise 1.376 + { incoming: { type: 'PUSH_PROMISE', flags: { }, headers: { ':path': '/2.html' }, promised_stream: promised_stream } }, 1.377 + { event : { name: 'promise', data: [promised_stream, { ':path': '/2.html' }] } }, 1.378 + 1.379 + // receiving response data 1.380 + { incoming: { type: 'DATA' , flags: { END_STREAM: true }, data: payload } }, 1.381 + { event : { name: 'state', data: ['CLOSED'] } }, 1.382 + 1.383 + { active : 0 } 1.384 + ], done); 1.385 + 1.386 + execute_sequence(promised_stream, [ 1.387 + // initial state of the promised stream 1.388 + { event : { name: 'state', data: ['RESERVED_REMOTE'] } }, 1.389 + 1.390 + // push headers 1.391 + { wait : 10 }, 1.392 + { incoming: { type: 'HEADERS', flags: { END_STREAM: false }, headers: { ':status': 200 } } }, 1.393 + { event : { name: 'state', data: ['HALF_CLOSED_LOCAL'] } }, 1.394 + { event : { name: 'headers', data: [{ ':status': 200 }] } }, 1.395 + 1.396 + // push data 1.397 + { incoming: { type: 'DATA', flags: { END_STREAM: true }, data: payload } }, 1.398 + { event : { name: 'state', data: ['CLOSED'] } }, 1.399 + 1.400 + { active : 0 } 1.401 + ], done); 1.402 + }); 1.403 + }); 1.404 + }); 1.405 + 1.406 + describe('bunyan formatter', function() { 1.407 + describe('`s`', function() { 1.408 + var format = stream.serializers.s; 1.409 + it('should assign a unique ID to each frame', function() { 1.410 + var stream1 = createStream(); 1.411 + var stream2 = createStream(); 1.412 + expect(format(stream1)).to.be.equal(format(stream1)); 1.413 + expect(format(stream2)).to.be.equal(format(stream2)); 1.414 + expect(format(stream1)).to.not.be.equal(format(stream2)); 1.415 + }); 1.416 + }); 1.417 + }); 1.418 +});