Skip to content

Commit

Permalink
chore(tests2png): add support for combine/concat/zip-All operators
Browse files Browse the repository at this point in the history
Improve tests2png/painter.js to support rendering ghost streams: reference to an inner Observable
not yet subscribed, belonging to a higher-order Observable. Operators concatAll, combineAll, and
zipAll match these cases, where we need to render both the not-yet-subscribed inner Observable
reference and its corresponding now-subscribed Observable. This commit helps generate more PNG
marble diagrams, and adds a few tests.
  • Loading branch information
staltz authored and kwonoj committed Jan 11, 2016
1 parent 5a2014c commit 9d5f3c2
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 26 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"benchmark": "1.0.0",
"benchpress": "2.0.0-alpha.37.2",
"browserify": "11.0.0",
"color": "^0.11.1",
"colors": "1.1.2",
"commitizen": "2.4.4",
"coveralls": "2.11.4",
Expand Down
110 changes: 93 additions & 17 deletions spec/helpers/tests2png/painter.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*eslint-disable no-param-reassign, no-use-before-define*/
var gm = require('gm');
var _ = require('lodash');
var Color = require('color');

var CANVAS_WIDTH = 1280;
var canvasHeight;
Expand All @@ -10,13 +11,20 @@ var OPERATOR_HEIGHT = 140;
var ARROW_HEAD_SIZE = 18;
var OBSERVABLE_END_PADDING = 5 * ARROW_HEAD_SIZE;
var MARBLE_RADIUS = 32;
var COMPLETE_HEIGHT = MARBLE_RADIUS;
var TALLER_COMPLETE_HEIGHT = 1.8 * MARBLE_RADIUS;
var SIN_45 = 0.707106;
var NESTED_STREAM_ANGLE = 18; // degrees
var TO_RAD = (Math.PI / 180);
var MESSAGES_WIDTH = (CANVAS_WIDTH - 2 * CANVAS_PADDING - OBSERVABLE_END_PADDING);
var BLACK_COLOR = '#101010';
var COLORS = ['#3EA1CB', '#FFCB46', '#FF6946', '#82D736'];
var SPECIAL_COLOR = '#1010F0';
var MESSAGE_OVERLAP_HEIGHT = TALLER_COMPLETE_HEIGHT;

function colorToGhostColor(hex) {
return Color(hex).mix(Color('white')).hexString();
}

function getMaxFrame(allStreams) {
var allStreamsLen = allStreams.length;
Expand Down Expand Up @@ -102,13 +110,28 @@ function measureNestedStreamHeight(maxFrame, streamData) {
return measureInclination(startX, endX, NESTED_STREAM_ANGLE);
}

function amountPriorOverlaps(message, messageIndex, otherMessages) {
return otherMessages.reduce(function (acc, otherMessage, otherIndex) {
if (otherIndex < messageIndex
&& otherMessage.frame === message.frame
&& message.notification.kind === 'N'
&& otherMessage.notification.kind === 'N') {
return acc + 1;
}
return acc;
}, 0);
}

function measureStreamHeight(maxFrame) {
return function measureStreamHeightWithMaxFrame(streamData) {
var maxMessageHeight = streamData.messages
.map(function (message) {
return isNestedStreamData(message) ?
measureNestedStreamHeight(maxFrame, message.notification.value) + OBSERVABLE_HEIGHT * 0.25 :
var messages = streamData.messages;
var maxMessageHeight = messages
.map(function (msg, index) {
var height = isNestedStreamData(msg) ?
measureNestedStreamHeight(maxFrame, msg.notification.value) + OBSERVABLE_HEIGHT * 0.25 :
OBSERVABLE_HEIGHT * 0.5;
var overlapHeightBonus = amountPriorOverlaps(msg, index, messages) * MESSAGE_OVERLAP_HEIGHT;
return height + overlapHeightBonus;
})
.reduce(function (acc, curr) {
return curr > acc ? curr : acc;
Expand All @@ -126,6 +149,9 @@ function drawObservableArrow(out, maxFrame, y, angle, streamData, isSpecial) {
if (isSpecial) {
outlineColor = SPECIAL_COLOR;
}
if (streamData.isGhost) {
outlineColor = colorToGhostColor(outlineColor);
}
out = out.stroke(outlineColor, 3);
var inclination = measureInclination(startX, endX, angle);
out = out.drawLine(startX, y, endX, y + inclination);
Expand Down Expand Up @@ -155,13 +181,18 @@ function stringifyContent(content) {
return String('"' + string + '"');
}

function drawMarble(out, x, y, inclination, content, isSpecial) {
function drawMarble(out, x, y, inclination, content, isSpecial, isGhost) {
var fillColor = stringToColor(stringifyContent(content));
var outlineColor = BLACK_COLOR;
if (isSpecial) {
outlineColor = SPECIAL_COLOR;
}
if (isGhost) {
outlineColor = colorToGhostColor(outlineColor);
fillColor = colorToGhostColor(fillColor);
}
out = out.stroke(outlineColor, 3);
out = out.fill(stringToColor(stringifyContent(content)));
out = out.fill(fillColor);
out = out.drawEllipse(x, y + inclination, MARBLE_RADIUS, MARBLE_RADIUS, 0, 360);

out = out.strokeWidth(-1);
Expand All @@ -175,12 +206,15 @@ function drawMarble(out, x, y, inclination, content, isSpecial) {
return out;
}

function drawError(out, x, y, startX, angle, isSpecial) {
function drawError(out, x, y, startX, angle, isSpecial, isGhost) {
var inclination = measureInclination(startX, x, angle);
var outlineColor = BLACK_COLOR;
if (isSpecial) {
outlineColor = SPECIAL_COLOR;
}
if (isGhost) {
outlineColor = colorToGhostColor(outlineColor);
}
out = out.stroke(outlineColor, 3);
out = out.draw(
'translate', String(x) + ',' + String(y + inclination),
Expand All @@ -194,7 +228,7 @@ function drawError(out, x, y, startX, angle, isSpecial) {
return out;
}

function drawComplete(out, x, y, maxFrame, angle, streamData, isSpecial) {
function drawComplete(out, x, y, maxFrame, angle, streamData, isSpecial, isGhost) {
var startX = CANVAS_PADDING +
MESSAGES_WIDTH * (streamData.subscription.start / maxFrame);
var isOverlapping = streamData.messages.some(function (msg) {
Expand All @@ -206,8 +240,11 @@ function drawComplete(out, x, y, maxFrame, angle, streamData, isSpecial) {
if (isSpecial) {
outlineColor = SPECIAL_COLOR;
}
if (isGhost) {
outlineColor = colorToGhostColor(outlineColor);
}
var inclination = measureInclination(startX, x, angle);
var radius = isOverlapping ? 1.8 * MARBLE_RADIUS : MARBLE_RADIUS;
var radius = isOverlapping ? TALLER_COMPLETE_HEIGHT : COMPLETE_HEIGHT;
out = out.stroke(outlineColor, 3);
out = out.draw(
'translate', String(x) + ',' + String(y + inclination),
Expand All @@ -225,29 +262,32 @@ function drawNestedObservable(out, maxFrame, y, streamData) {
return out;
}

function drawObservableMessages(out, maxFrame, y, angle, streamData, isSpecial) {
function drawObservableMessages(out, maxFrame, baseY, angle, streamData, isSpecial) {
var startX = CANVAS_PADDING +
MESSAGES_WIDTH * (streamData.subscription.start / maxFrame);
var messages = streamData.messages;

streamData.messages.slice().reverse().forEach(function (message) {
messages.slice().reverse().forEach(function (message, reversedIndex) {
if (message.frame < 0) { // ignore messages with negative frames
return;
}
var index = messages.length - reversedIndex - 1;
var x = startX + MESSAGES_WIDTH * (message.frame / maxFrame);
if (x - MARBLE_RADIUS < 0) { // out of screen, on the left
x += MARBLE_RADIUS;
}
var y = baseY + amountPriorOverlaps(message, index, messages) * MESSAGE_OVERLAP_HEIGHT;
var inclination = measureInclination(startX, x, angle);
switch (message.notification.kind) {
case 'N':
if (isNestedStreamData(message)) {
out = drawNestedObservable(out, maxFrame, y, message.notification.value);
} else {
out = drawMarble(out, x, y, inclination, message.notification.value, isSpecial);
out = drawMarble(out, x, y, inclination, message.notification.value, isSpecial, streamData.isGhost);
}
break;
case 'E': out = drawError(out, x, y, startX, angle, isSpecial); break;
case 'C': out = drawComplete(out, x, y, maxFrame, angle, streamData, isSpecial); break;
case 'E': out = drawError(out, x, y, startX, angle, isSpecial, streamData.isGhost); break;
case 'C': out = drawComplete(out, x, y, maxFrame, angle, streamData, isSpecial, streamData.isGhost); break;
default: break;
}
});
Expand Down Expand Up @@ -279,8 +319,8 @@ function drawOperator(out, label, y) {
return out;
}

function sanitizeHigherOrderInputStreams(inputStreams, outputStreams) {
// Remove cold inputStreams which are already nested in some higher order stream
// Remove cold inputStreams which are already nested in some higher order stream
function removeDuplicateInputs(inputStreams, outputStreams) {
return inputStreams.filter(function (inputStream) {
return !inputStreams.concat(outputStreams).some(function (otherStream) {
return otherStream.messages.some(function (msg) {
Expand All @@ -301,9 +341,45 @@ function sanitizeHigherOrderInputStreams(inputStreams, outputStreams) {
});
}

// For every inner stream in a higher order stream, create its ghost version
// A ghost stream is a reference to an Observable that has not yet executed,
// and is painted as a semi-transparent stream.
function addGhostInnerInputs(inputStreams) {
for (var i = 0; i < inputStreams.length; i++) {
var inputStream = inputStreams[i];
for (var j = 0; j < inputStream.messages.length; j++) {
var message = inputStream.messages[j];
if (isNestedStreamData(message) && typeof message.isGhost !== 'boolean') {
var referenceTime = message.frame;
var subscriptionTime = message.notification.value.subscription.start;
if (referenceTime !== subscriptionTime) {
message.isGhost = false;
message.notification.value.isGhost = false;
message.frame = subscriptionTime;

var ghost = _.cloneDeep(message);
ghost.isGhost = true;
ghost.notification.value.isGhost = true;
ghost.frame = referenceTime;
ghost.notification.value.subscription.start = referenceTime;
ghost.notification.value.subscription.end -= subscriptionTime - referenceTime;
inputStream.messages.push(ghost);
}
}
}
}
return inputStreams;
}

function sanitizeHigherOrderInputStreams(inputStreams, outputStreams) {
var newInputStreams = removeDuplicateInputs(inputStreams, outputStreams);
newInputStreams = addGhostInnerInputs(newInputStreams);
return newInputStreams;
}

module.exports = function painter(inputStreams, operatorLabel, outputStreams, filename) {
var maxFrame = getMaxFrame(inputStreams.concat(outputStreams));
inputStreams = sanitizeHigherOrderInputStreams(inputStreams, outputStreams);
var maxFrame = getMaxFrame(inputStreams.concat(outputStreams));
var allStreamsHeight = inputStreams.concat(outputStreams)
.map(measureStreamHeight(maxFrame))
.reduce(function (x, y) { return x + y; }, 0);
Expand Down
11 changes: 11 additions & 0 deletions spec/operators/combineAll-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ var Observable = Rx.Observable;
var queueScheduler = Rx.Scheduler.queue;

describe('Observable.prototype.combineAll()', function () {
it.asDiagram('combineAll')('should combine events from two observables', function () {
var x = cold( '-a-----b---|');
var y = cold( '--1-2-| ');
var outer = hot('-x----y--------| ', { x: x, y: y });
var expected = '-----------------A-B--C---|';

var result = outer.combineAll(function (a, b) { return String(a) + String(b); });

expectObservable(result).toBe(expected, { A: 'a1', B: 'a2', C: 'b2' });
});

it('should work with two nevers', function () {
var e1 = cold( '-');
var e1subs = '^';
Expand Down
21 changes: 12 additions & 9 deletions spec/operators/concatAll-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ var Observable = Rx.Observable;
var Promise = require('promise');

describe('Observable.prototype.concatAll()', function () {
it.asDiagram('concatAll')('should concat an observable of observables', function () {
var x = cold( '----a------b------| ');
var y = cold( '---c-d---| ');
var z = cold( '---e--f-|');
var outer = hot('-x---y----z------| ', { x: x, y: y, z: z });
var expected = '-----a------b---------c-d------e--f-|';

var result = outer.concatAll();

expectObservable(result).toBe(expected);
});

it('should concat sources from promise', function (done) {
var sources = Rx.Observable.fromArray([
new Promise(function (res) { res(0); }),
Expand Down Expand Up @@ -63,15 +75,6 @@ describe('Observable.prototype.concatAll()', function () {
expectObservable(e1.concatAll()).toBe(expected);
});

it('should concat a hot observable of observables', function () {
var x = cold( 'a---b---c---|');
var y = cold( 'd---e---f---|');
var e1 = hot('--x--y--|', { x: x, y: y });
var expected = '--a---b---c---d---e---f---|';

expectObservable(e1.concatAll()).toBe(expected);
});

it('should concat merging a hot observable of non-overlapped observables', function () {
var values = {
x: cold( 'a-b---------|'),
Expand Down
11 changes: 11 additions & 0 deletions spec/operators/zipAll-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ var Observable = Rx.Observable;
var queueScheduler = Rx.Scheduler.queue;

describe('Observable.prototype.zipAll', function () {
it.asDiagram('zipAll')('should combine paired events from two observables', function () {
var x = cold( '-a-----b-|');
var y = cold( '--1-2-----');
var outer = hot('-x----y--------| ', { x: x, y: y });
var expected = '-----------------A----B-|';

var result = outer.zipAll(function (a, b) { return String(a) + String(b); });

expectObservable(result).toBe(expected, { A: 'a1', B: 'b2' });
});

it('should combine two observables', function () {
var a = hot('---1---2---3---');
var asubs = '^';
Expand Down

0 comments on commit 9d5f3c2

Please sign in to comment.