testing/xpcshell/node-http2/node_modules/http2-protocol/test/stream.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 var expect = require('chai').expect;
michael@0 2 var util = require('./util');
michael@0 3
michael@0 4 var stream = require('../lib/stream');
michael@0 5 var Stream = stream.Stream;
michael@0 6
michael@0 7 function createStream() {
michael@0 8 var stream = new Stream(util.log);
michael@0 9 stream.upstream._window = Infinity;
michael@0 10 return stream;
michael@0 11 }
michael@0 12
michael@0 13 // Execute a list of commands and assertions
michael@0 14 var recorded_events = ['state', 'error', 'window_update', 'headers', 'promise'];
michael@0 15 function execute_sequence(stream, sequence, done) {
michael@0 16 if (!done) {
michael@0 17 done = sequence;
michael@0 18 sequence = stream;
michael@0 19 stream = createStream();
michael@0 20 }
michael@0 21
michael@0 22 var outgoing_frames = [];
michael@0 23
michael@0 24 var emit = stream.emit, events = [];
michael@0 25 stream.emit = function(name) {
michael@0 26 if (recorded_events.indexOf(name) !== -1) {
michael@0 27 events.push({ name: name, data: Array.prototype.slice.call(arguments, 1) });
michael@0 28 }
michael@0 29 return emit.apply(this, arguments);
michael@0 30 };
michael@0 31
michael@0 32 var commands = [], checks = [];
michael@0 33 sequence.forEach(function(step) {
michael@0 34 if ('method' in step || 'incoming' in step || 'outgoing' in step || 'wait' in step || 'set_state' in step) {
michael@0 35 commands.push(step);
michael@0 36 }
michael@0 37
michael@0 38 if ('outgoing' in step || 'event' in step || 'active' in step) {
michael@0 39 checks.push(step);
michael@0 40 }
michael@0 41 });
michael@0 42
michael@0 43 var activeCount = 0;
michael@0 44 function count_change(change) {
michael@0 45 activeCount += change;
michael@0 46 }
michael@0 47
michael@0 48 function execute(callback) {
michael@0 49 var command = commands.shift();
michael@0 50 if (command) {
michael@0 51 if ('method' in command) {
michael@0 52 var value = stream[command.method.name].apply(stream, command.method.arguments);
michael@0 53 if (command.method.ret) {
michael@0 54 command.method.ret(value);
michael@0 55 }
michael@0 56 execute(callback);
michael@0 57 } else if ('incoming' in command) {
michael@0 58 command.incoming.count_change = count_change;
michael@0 59 stream.upstream.write(command.incoming);
michael@0 60 execute(callback);
michael@0 61 } else if ('outgoing' in command) {
michael@0 62 outgoing_frames.push(stream.upstream.read());
michael@0 63 execute(callback);
michael@0 64 } else if ('set_state' in command) {
michael@0 65 stream.state = command.set_state;
michael@0 66 execute(callback);
michael@0 67 } else if ('wait' in command) {
michael@0 68 setTimeout(execute.bind(null, callback), command.wait);
michael@0 69 } else {
michael@0 70 throw new Error('Invalid command', command);
michael@0 71 }
michael@0 72 } else {
michael@0 73 setTimeout(callback, 5);
michael@0 74 }
michael@0 75 }
michael@0 76
michael@0 77 function check() {
michael@0 78 checks.forEach(function(check) {
michael@0 79 if ('outgoing' in check) {
michael@0 80 var frame = outgoing_frames.shift();
michael@0 81 for (var key in check.outgoing) {
michael@0 82 expect(frame).to.have.property(key).that.deep.equals(check.outgoing[key]);
michael@0 83 }
michael@0 84 count_change(frame.count_change);
michael@0 85 } else if ('event' in check) {
michael@0 86 var event = events.shift();
michael@0 87 expect(event.name).to.be.equal(check.event.name);
michael@0 88 check.event.data.forEach(function(data, index) {
michael@0 89 expect(event.data[index]).to.deep.equal(data);
michael@0 90 });
michael@0 91 } else if ('active' in check) {
michael@0 92 expect(activeCount).to.be.equal(check.active);
michael@0 93 } else {
michael@0 94 throw new Error('Invalid check', check);
michael@0 95 }
michael@0 96 });
michael@0 97 done();
michael@0 98 }
michael@0 99
michael@0 100 setImmediate(execute.bind(null, check));
michael@0 101 }
michael@0 102
michael@0 103 var example_frames = [
michael@0 104 { type: 'PRIORITY', flags: {}, priority: 1 },
michael@0 105 { type: 'WINDOW_UPDATE', flags: {}, settings: {} },
michael@0 106 { type: 'RST_STREAM', flags: {}, error: 'CANCEL' },
michael@0 107 { type: 'HEADERS', flags: {}, headers: {}, priority: undefined },
michael@0 108 { type: 'DATA', flags: {}, data: new Buffer(5) },
michael@0 109 { type: 'PUSH_PROMISE', flags: {}, headers: {}, promised_stream: new Stream(util.log) }
michael@0 110 ];
michael@0 111
michael@0 112 var invalid_incoming_frames = {
michael@0 113 IDLE: [
michael@0 114 { type: 'DATA', flags: {}, data: new Buffer(5) },
michael@0 115 { type: 'PRIORITY', flags: {}, priority: 1 },
michael@0 116 { type: 'WINDOW_UPDATE', flags: {}, settings: {} },
michael@0 117 { type: 'PUSH_PROMISE', flags: {}, headers: {} },
michael@0 118 { type: 'RST_STREAM', flags: {}, error: 'CANCEL' }
michael@0 119 ],
michael@0 120 RESERVED_LOCAL: [
michael@0 121 { type: 'DATA', flags: {}, data: new Buffer(5) },
michael@0 122 { type: 'HEADERS', flags: {}, headers: {}, priority: undefined },
michael@0 123 { type: 'PUSH_PROMISE', flags: {}, headers: {} },
michael@0 124 { type: 'WINDOW_UPDATE', flags: {}, settings: {} }
michael@0 125 ],
michael@0 126 RESERVED_REMOTE: [
michael@0 127 { type: 'DATA', flags: {}, data: new Buffer(5) },
michael@0 128 { type: 'PRIORITY', flags: {}, priority: 1 },
michael@0 129 { type: 'PUSH_PROMISE', flags: {}, headers: {} },
michael@0 130 { type: 'WINDOW_UPDATE', flags: {}, settings: {} }
michael@0 131 ],
michael@0 132 OPEN: [
michael@0 133 ],
michael@0 134 HALF_CLOSED_LOCAL: [
michael@0 135 ],
michael@0 136 HALF_CLOSED_REMOTE: [
michael@0 137 { type: 'DATA', flags: {}, data: new Buffer(5) },
michael@0 138 { type: 'HEADERS', flags: {}, headers: {}, priority: undefined },
michael@0 139 { type: 'PUSH_PROMISE', flags: {}, headers: {} }
michael@0 140 ]
michael@0 141 };
michael@0 142
michael@0 143 var invalid_outgoing_frames = {
michael@0 144 IDLE: [
michael@0 145 { type: 'DATA', flags: {}, data: new Buffer(5) },
michael@0 146 { type: 'PRIORITY', flags: {}, priority: 1 },
michael@0 147 { type: 'WINDOW_UPDATE', flags: {}, settings: {} },
michael@0 148 { type: 'PUSH_PROMISE', flags: {}, headers: {} }
michael@0 149 ],
michael@0 150 RESERVED_LOCAL: [
michael@0 151 { type: 'DATA', flags: {}, data: new Buffer(5) },
michael@0 152 { type: 'PRIORITY', flags: {}, priority: 1 },
michael@0 153 { type: 'PUSH_PROMISE', flags: {}, headers: {} },
michael@0 154 { type: 'WINDOW_UPDATE', flags: {}, settings: {} }
michael@0 155 ],
michael@0 156 RESERVED_REMOTE: [
michael@0 157 { type: 'DATA', flags: {}, data: new Buffer(5) },
michael@0 158 { type: 'HEADERS', flags: {}, headers: {}, priority: undefined },
michael@0 159 { type: 'PUSH_PROMISE', flags: {}, headers: {} },
michael@0 160 { type: 'WINDOW_UPDATE', flags: {}, settings: {} }
michael@0 161 ],
michael@0 162 OPEN: [
michael@0 163 ],
michael@0 164 HALF_CLOSED_LOCAL: [
michael@0 165 { type: 'DATA', flags: {}, data: new Buffer(5) },
michael@0 166 { type: 'HEADERS', flags: {}, headers: {}, priority: undefined },
michael@0 167 { type: 'PUSH_PROMISE', flags: {}, headers: {} }
michael@0 168 ],
michael@0 169 HALF_CLOSED_REMOTE: [
michael@0 170 ],
michael@0 171 CLOSED: [
michael@0 172 { type: 'PRIORITY', flags: {}, priority: 1 },
michael@0 173 { type: 'WINDOW_UPDATE', flags: {}, settings: {} },
michael@0 174 { type: 'HEADERS', flags: {}, headers: {}, priority: undefined },
michael@0 175 { type: 'DATA', flags: {}, data: new Buffer(5) },
michael@0 176 { type: 'PUSH_PROMISE', flags: {}, headers: {}, promised_stream: new Stream(util.log) }
michael@0 177 ]
michael@0 178 };
michael@0 179
michael@0 180 describe('stream.js', function() {
michael@0 181 describe('Stream class', function() {
michael@0 182 describe('._transition(sending, frame) method', function() {
michael@0 183 it('should emit error, and answer RST_STREAM for invalid incoming frames', function() {
michael@0 184 Object.keys(invalid_incoming_frames).forEach(function(state) {
michael@0 185 invalid_incoming_frames[state].forEach(function(invalid_frame) {
michael@0 186 var stream = createStream();
michael@0 187 stream.state = state;
michael@0 188 expect(stream._transition.bind(stream, false, invalid_frame)).to.throw('Uncaught, unspecified "error" event.');
michael@0 189 });
michael@0 190 });
michael@0 191
michael@0 192 // CLOSED state as a result of incoming END_STREAM (or RST_STREAM)
michael@0 193 var stream = createStream();
michael@0 194 stream.headers({});
michael@0 195 stream.end();
michael@0 196 stream.upstream.write({ type: 'HEADERS', headers:{}, flags: { END_STREAM: true }, count_change: util.noop });
michael@0 197 example_frames.forEach(function(invalid_frame) {
michael@0 198 invalid_frame.count_change = util.noop;
michael@0 199 expect(stream._transition.bind(stream, false, invalid_frame)).to.throw('Uncaught, unspecified "error" event.');
michael@0 200 });
michael@0 201
michael@0 202 // CLOSED state as a result of outgoing END_STREAM
michael@0 203 var stream = createStream();
michael@0 204 stream.upstream.write({ type: 'HEADERS', headers:{}, flags: { END_STREAM: true }, count_change: util.noop });
michael@0 205 stream.headers({});
michael@0 206 stream.end();
michael@0 207 example_frames.slice(3).forEach(function(invalid_frame) {
michael@0 208 invalid_frame.count_change = util.noop;
michael@0 209 expect(stream._transition.bind(stream, false, invalid_frame)).to.throw('Uncaught, unspecified "error" event.');
michael@0 210 });
michael@0 211 });
michael@0 212 it('should throw exception for invalid outgoing frames', function() {
michael@0 213 Object.keys(invalid_outgoing_frames).forEach(function(state) {
michael@0 214 invalid_outgoing_frames[state].forEach(function(invalid_frame) {
michael@0 215 var stream = createStream();
michael@0 216 stream.state = state;
michael@0 217 expect(stream._transition.bind(stream, true, invalid_frame)).to.throw(Error);
michael@0 218 });
michael@0 219 });
michael@0 220 });
michael@0 221 it('should close the stream when there\'s an incoming or outgoing RST_STREAM', function() {
michael@0 222 [
michael@0 223 'RESERVED_LOCAL',
michael@0 224 'RESERVED_REMOTE',
michael@0 225 'OPEN',
michael@0 226 'HALF_CLOSED_LOCAL',
michael@0 227 'HALF_CLOSED_REMOTE'
michael@0 228 ].forEach(function(state) {
michael@0 229 [true, false].forEach(function(sending) {
michael@0 230 var stream = createStream();
michael@0 231 stream.state = state;
michael@0 232 stream._transition(sending, { type: 'RST_STREAM', flags: {} });
michael@0 233 expect(stream.state).to.be.equal('CLOSED');
michael@0 234 });
michael@0 235 });
michael@0 236 });
michael@0 237 it('should ignore any incoming frame after sending reset', function() {
michael@0 238 var stream = createStream();
michael@0 239 stream.reset();
michael@0 240 example_frames.forEach(stream._transition.bind(stream, false));
michael@0 241 });
michael@0 242 it('should ignore certain incoming frames after closing the stream with END_STREAM', function() {
michael@0 243 var stream = createStream();
michael@0 244 stream.upstream.write({ type: 'HEADERS', flags: { END_STREAM: true }, headers:{} });
michael@0 245 stream.headers({});
michael@0 246 stream.end();
michael@0 247 example_frames.slice(0,3).forEach(function(frame) {
michael@0 248 frame.count_change = util.noop;
michael@0 249 stream._transition(false, frame);
michael@0 250 });
michael@0 251 });
michael@0 252 });
michael@0 253 });
michael@0 254 describe('test scenario', function() {
michael@0 255 describe('sending request', function() {
michael@0 256 it('should trigger the appropriate state transitions and outgoing frames', function(done) {
michael@0 257 execute_sequence([
michael@0 258 { method : { name: 'headers', arguments: [{ ':path': '/' }] } },
michael@0 259 { outgoing: { type: 'HEADERS', flags: { }, headers: { ':path': '/' } } },
michael@0 260 { event : { name: 'state', data: ['OPEN'] } },
michael@0 261
michael@0 262 { wait : 5 },
michael@0 263 { method : { name: 'end', arguments: [] } },
michael@0 264 { event : { name: 'state', data: ['HALF_CLOSED_LOCAL'] } },
michael@0 265 { outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: new Buffer(0) } },
michael@0 266
michael@0 267 { wait : 10 },
michael@0 268 { incoming: { type: 'HEADERS', flags: { }, headers: { ':status': 200 } } },
michael@0 269 { incoming: { type: 'DATA' , flags: { END_STREAM: true }, data: new Buffer(5) } },
michael@0 270 { event : { name: 'headers', data: [{ ':status': 200 }] } },
michael@0 271 { event : { name: 'state', data: ['CLOSED'] } },
michael@0 272
michael@0 273 { active : 0 }
michael@0 274 ], done);
michael@0 275 });
michael@0 276 });
michael@0 277 describe('answering request', function() {
michael@0 278 it('should trigger the appropriate state transitions and outgoing frames', function(done) {
michael@0 279 var payload = new Buffer(5);
michael@0 280 execute_sequence([
michael@0 281 { incoming: { type: 'HEADERS', flags: { }, headers: { ':path': '/' } } },
michael@0 282 { event : { name: 'state', data: ['OPEN'] } },
michael@0 283 { event : { name: 'headers', data: [{ ':path': '/' }] } },
michael@0 284
michael@0 285 { wait : 5 },
michael@0 286 { incoming: { type: 'DATA', flags: { }, data: new Buffer(5) } },
michael@0 287 { incoming: { type: 'DATA', flags: { END_STREAM: true }, data: new Buffer(10) } },
michael@0 288 { event : { name: 'state', data: ['HALF_CLOSED_REMOTE'] } },
michael@0 289
michael@0 290 { wait : 5 },
michael@0 291 { method : { name: 'headers', arguments: [{ ':status': 200 }] } },
michael@0 292 { outgoing: { type: 'HEADERS', flags: { }, headers: { ':status': 200 } } },
michael@0 293
michael@0 294 { wait : 5 },
michael@0 295 { method : { name: 'end', arguments: [payload] } },
michael@0 296 { outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: payload } },
michael@0 297 { event : { name: 'state', data: ['CLOSED'] } },
michael@0 298
michael@0 299 { active : 0 }
michael@0 300 ], done);
michael@0 301 });
michael@0 302 });
michael@0 303 describe('sending push stream', function() {
michael@0 304 it('should trigger the appropriate state transitions and outgoing frames', function(done) {
michael@0 305 var payload = new Buffer(5);
michael@0 306 var pushStream;
michael@0 307
michael@0 308 execute_sequence([
michael@0 309 // receiving request
michael@0 310 { incoming: { type: 'HEADERS', flags: { END_STREAM: true }, headers: { ':path': '/' } } },
michael@0 311 { event : { name: 'state', data: ['OPEN'] } },
michael@0 312 { event : { name: 'state', data: ['HALF_CLOSED_REMOTE'] } },
michael@0 313 { event : { name: 'headers', data: [{ ':path': '/' }] } },
michael@0 314
michael@0 315 // sending response headers
michael@0 316 { wait : 5 },
michael@0 317 { method : { name: 'headers', arguments: [{ ':status': '200' }] } },
michael@0 318 { outgoing: { type: 'HEADERS', flags: { }, headers: { ':status': '200' } } },
michael@0 319
michael@0 320 // sending push promise
michael@0 321 { method : { name: 'promise', arguments: [{ ':path': '/' }], ret: function(str) { pushStream = str; } } },
michael@0 322 { outgoing: { type: 'PUSH_PROMISE', flags: { }, headers: { ':path': '/' } } },
michael@0 323
michael@0 324 // sending response data
michael@0 325 { method : { name: 'end', arguments: [payload] } },
michael@0 326 { outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: payload } },
michael@0 327 { event : { name: 'state', data: ['CLOSED'] } },
michael@0 328
michael@0 329 { active : 0 }
michael@0 330 ], function() {
michael@0 331 // initial state of the promised stream
michael@0 332 expect(pushStream.state).to.equal('RESERVED_LOCAL');
michael@0 333
michael@0 334 execute_sequence(pushStream, [
michael@0 335 // push headers
michael@0 336 { wait : 5 },
michael@0 337 { method : { name: 'headers', arguments: [{ ':status': '200' }] } },
michael@0 338 { outgoing: { type: 'HEADERS', flags: { }, headers: { ':status': '200' } } },
michael@0 339 { event : { name: 'state', data: ['HALF_CLOSED_REMOTE'] } },
michael@0 340
michael@0 341 // push data
michael@0 342 { method : { name: 'end', arguments: [payload] } },
michael@0 343 { outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: payload } },
michael@0 344 { event : { name: 'state', data: ['CLOSED'] } },
michael@0 345
michael@0 346 { active : 1 }
michael@0 347 ], done);
michael@0 348 });
michael@0 349 });
michael@0 350 });
michael@0 351 describe('receiving push stream', function() {
michael@0 352 it('should trigger the appropriate state transitions and outgoing frames', function(done) {
michael@0 353 var payload = new Buffer(5);
michael@0 354 var original_stream = createStream();
michael@0 355 var promised_stream = createStream();
michael@0 356
michael@0 357 done = util.callNTimes(2, done);
michael@0 358
michael@0 359 execute_sequence(original_stream, [
michael@0 360 // sending request headers
michael@0 361 { method : { name: 'headers', arguments: [{ ':path': '/' }] } },
michael@0 362 { method : { name: 'end', arguments: [] } },
michael@0 363 { outgoing: { type: 'HEADERS', flags: { END_STREAM: true }, headers: { ':path': '/' } } },
michael@0 364 { event : { name: 'state', data: ['OPEN'] } },
michael@0 365 { event : { name: 'state', data: ['HALF_CLOSED_LOCAL'] } },
michael@0 366
michael@0 367 // receiving response headers
michael@0 368 { wait : 10 },
michael@0 369 { incoming: { type: 'HEADERS', flags: { }, headers: { ':status': 200 } } },
michael@0 370 { event : { name: 'headers', data: [{ ':status': 200 }] } },
michael@0 371
michael@0 372 // receiving push promise
michael@0 373 { incoming: { type: 'PUSH_PROMISE', flags: { }, headers: { ':path': '/2.html' }, promised_stream: promised_stream } },
michael@0 374 { event : { name: 'promise', data: [promised_stream, { ':path': '/2.html' }] } },
michael@0 375
michael@0 376 // receiving response data
michael@0 377 { incoming: { type: 'DATA' , flags: { END_STREAM: true }, data: payload } },
michael@0 378 { event : { name: 'state', data: ['CLOSED'] } },
michael@0 379
michael@0 380 { active : 0 }
michael@0 381 ], done);
michael@0 382
michael@0 383 execute_sequence(promised_stream, [
michael@0 384 // initial state of the promised stream
michael@0 385 { event : { name: 'state', data: ['RESERVED_REMOTE'] } },
michael@0 386
michael@0 387 // push headers
michael@0 388 { wait : 10 },
michael@0 389 { incoming: { type: 'HEADERS', flags: { END_STREAM: false }, headers: { ':status': 200 } } },
michael@0 390 { event : { name: 'state', data: ['HALF_CLOSED_LOCAL'] } },
michael@0 391 { event : { name: 'headers', data: [{ ':status': 200 }] } },
michael@0 392
michael@0 393 // push data
michael@0 394 { incoming: { type: 'DATA', flags: { END_STREAM: true }, data: payload } },
michael@0 395 { event : { name: 'state', data: ['CLOSED'] } },
michael@0 396
michael@0 397 { active : 0 }
michael@0 398 ], done);
michael@0 399 });
michael@0 400 });
michael@0 401 });
michael@0 402
michael@0 403 describe('bunyan formatter', function() {
michael@0 404 describe('`s`', function() {
michael@0 405 var format = stream.serializers.s;
michael@0 406 it('should assign a unique ID to each frame', function() {
michael@0 407 var stream1 = createStream();
michael@0 408 var stream2 = createStream();
michael@0 409 expect(format(stream1)).to.be.equal(format(stream1));
michael@0 410 expect(format(stream2)).to.be.equal(format(stream2));
michael@0 411 expect(format(stream1)).to.not.be.equal(format(stream2));
michael@0 412 });
michael@0 413 });
michael@0 414 });
michael@0 415 });

mercurial