Skip to content

Commit 3ebb2cf

Browse files
committed
chore(tests2png): add first version of PNG diagram generator
Build Jasmine helper that detects tests tagged with asDiagram() and runs a painter script to create a PNG marble diagram out of them. First attempt at resolving #697.
1 parent 836b140 commit 3ebb2cf

16 files changed

+295
-16
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ The build and test structure is fairly primitive at the moment. There are variou
7171
- build_docs: generates API documentation from `dist/es6` to `dist/docs`
7272
- build_cover: runs `istanbul` code coverage against test cases
7373
- test: runs tests with `jasmine`, must have built prior to running.
74+
- tests2png: generates PNG marble diagrams from test cases. You must have `imagemagick`, `graphicsmagick`, and `ghostscript` installed locally.
7475

7576
### Example
7677

img/buffer.png

47.9 KB
Loading

img/bufferCount(3,2).png

47.4 KB
Loading

img/bufferTime(100).png

37.6 KB
Loading

img/bufferToggle.png

46.2 KB
Loading

img/delay.png

19.4 KB
Loading

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"cover": "istanbul cover -x \"*-spec.js index.js *-helper.js spec/helpers/*\" ./node_modules/jasmine/bin/jasmine.js && npm run cover_remapping",
3030
"cover_remapping": "remap-istanbul -i coverage/coverage.json -o coverage/coverage-remapped.json && remap-istanbul -i coverage/coverage.json -o coverage/coverage-remapped.lcov -t lcovonly && remap-istanbul -i coverage/coverage.json -o coverage/coverage-remapped -t html",
3131
"test": "jasmine",
32+
"tests2png": "JASMINE_CONFIG_PATH=spec/support/tests2png.json jasmine",
3233
"watch": "watch \"echo triggering build && npm run build_test && echo build completed\" src -d -u -w=15",
3334
"perf": "protractor protractor.conf.js",
3435
"perf_micro": "node ./perf/micro/index.js",
@@ -88,6 +89,7 @@
8889
"fs-extra": "0.24.0",
8990
"ghooks": "0.3.2",
9091
"glob": "5.0.14",
92+
"gm": "^1.21.1",
9193
"google-closure-compiler": "20151015.0.0",
9294
"http-server": "0.8.0",
9395
"istanbul": "0.3.22",
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000;
2+
3+
var Rx = require('../../../dist/cjs/Rx.KitchenSink');
4+
var marbleHelpers = require('../marble-testing');
5+
var painter = require('./painter');
6+
7+
global.rxTestScheduler = null;
8+
global.cold = marbleHelpers.cold;
9+
global.hot = marbleHelpers.hot;
10+
global.expectObservable = marbleHelpers.expectObservable;
11+
global.expectSubscriptions = marbleHelpers.expectSubscriptions;
12+
13+
var glit = global.it;
14+
15+
global.it = function (description, specFn, timeout) { };
16+
17+
global.it.asDiagram = function asDiagram(operatorLabel) {
18+
return function specFnWithPainter(description, specFn) {
19+
if (specFn.length === 0) {
20+
glit(description, function () {
21+
var outputStream;
22+
global.rxTestScheduler = new Rx.TestScheduler(function (actual) {
23+
if (Array.isArray(actual) && typeof actual[0].frame === 'number') {
24+
outputStream = actual;
25+
}
26+
return true;
27+
});
28+
specFn();
29+
var inputStreams = global.rxTestScheduler.hotObservables
30+
.map(function (hot) { return hot.messages; })
31+
.slice();
32+
global.rxTestScheduler.flush();
33+
painter(inputStreams, operatorLabel, outputStream);
34+
console.log('Painted img/' + operatorLabel + '.png');
35+
});
36+
} else {
37+
throw new Error('Cannot generate PNG marble diagram for async test ' + description);
38+
}
39+
};
40+
};
41+
42+
beforeEach(function () {
43+
jasmine.addMatchers({
44+
toDeepEqual: function (util, customEqualityTesters) {
45+
return {
46+
compare: function (actual, expected) {
47+
return { pass: true };
48+
}
49+
};
50+
}
51+
});
52+
});
53+
54+
afterEach(function () {
55+
global.rxTestScheduler = null;
56+
});
57+
58+
(function () {
59+
var objectTypes = {
60+
'boolean': false,
61+
'function': true,
62+
'object': true,
63+
'number': false,
64+
'string': false,
65+
'undefined': false
66+
};
67+
68+
Object.defineProperty(Error.prototype, 'toJSON', {
69+
value: function () {
70+
var alt = {};
71+
72+
Object.getOwnPropertyNames(this).forEach(function (key) {
73+
if (key !== 'stack') {
74+
alt[key] = this[key];
75+
}
76+
}, this);
77+
return alt;
78+
},
79+
configurable: true
80+
});
81+
82+
var _root = (objectTypes[typeof self] && self) || (objectTypes[typeof window] && window);
83+
84+
var freeExports = objectTypes[typeof exports] && exports && !exports.nodeType && exports;
85+
var freeModule = objectTypes[typeof module] && module && !module.nodeType && module;
86+
var freeGlobal = objectTypes[typeof global] && global;
87+
88+
if (freeGlobal && (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal)) {
89+
_root = freeGlobal;
90+
}
91+
92+
global.__root__ = _root;
93+
})();
94+
95+
global.lowerCaseO = function lowerCaseO() {
96+
var values = [].slice.apply(arguments);
97+
98+
var o = {
99+
subscribe: function (observer) {
100+
values.forEach(function (v) {
101+
observer.next(v);
102+
});
103+
observer.complete();
104+
}
105+
};
106+
107+
o[Symbol.observable] = function () {
108+
return this;
109+
};
110+
111+
return o;
112+
};
113+

spec/helpers/tests2png/painter.js

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
var gm = require('gm');
2+
3+
var CANVAS_WIDTH = 1280;
4+
var canvasHeight;
5+
var CANVAS_PADDING = 20;
6+
var OBSERVABLE_HEIGHT = 200;
7+
var OPERATOR_HEIGHT = 140;
8+
var ARROW_HEAD_SIZE = 18;
9+
var OBSERVABLE_END_PADDING = 4 * ARROW_HEAD_SIZE;
10+
var MARBLE_RADIUS = 32;
11+
var SIN_45 = 0.707106;
12+
13+
function getMaxFrame() {
14+
var argsLength = arguments.length;
15+
var max = 0;
16+
for (var i = 0; i < argsLength; i++) {
17+
var messagesLen = arguments[i].length;
18+
for (var j = 0; j < messagesLen; j++) {
19+
if (arguments[i][j].frame > max) {
20+
max = arguments[i][j].frame;
21+
}
22+
}
23+
}
24+
return max;
25+
}
26+
27+
function drawObservableArrow(out, y) {
28+
out = out.stroke('#000000', 3);
29+
out = out.drawLine(
30+
CANVAS_PADDING,
31+
y,
32+
CANVAS_WIDTH - CANVAS_PADDING,
33+
y);
34+
out = out.drawLine(
35+
CANVAS_WIDTH - CANVAS_PADDING,
36+
y,
37+
CANVAS_WIDTH - CANVAS_PADDING - ARROW_HEAD_SIZE * 2,
38+
y - ARROW_HEAD_SIZE);
39+
out = out.drawLine(
40+
CANVAS_WIDTH - CANVAS_PADDING,
41+
y,
42+
CANVAS_WIDTH - CANVAS_PADDING - ARROW_HEAD_SIZE * 2,
43+
y + ARROW_HEAD_SIZE);
44+
return out;
45+
}
46+
47+
function drawObservableMessages(out, maxFrame, messages, y) {
48+
var messagesWidth = (CANVAS_WIDTH - 2 * CANVAS_PADDING - OBSERVABLE_END_PADDING);
49+
messages.forEach(function (message) {
50+
var x = messagesWidth * (message.frame / maxFrame);
51+
switch (message.notification.kind) {
52+
case 'N':
53+
out = out.stroke('#000000', 3);
54+
out = out.fill('#82D736');
55+
out = out.drawEllipse(x, y, MARBLE_RADIUS, MARBLE_RADIUS, 0, 360);
56+
57+
out = out.strokeWidth(-1);
58+
out = out.fill('#000000');
59+
out = out.font('helvetica', 28);
60+
out = out.draw(
61+
'translate ' + (x - CANVAS_WIDTH * 0.5) + ',' + (y - canvasHeight * 0.5),
62+
'gravity Center',
63+
'text 0,0',
64+
String('"'+message.notification.value+'"'));
65+
break;
66+
67+
case 'E':
68+
out = out.stroke('#000000', 3);
69+
out = out.drawLine(
70+
x - MARBLE_RADIUS * SIN_45, y - MARBLE_RADIUS * SIN_45,
71+
x + MARBLE_RADIUS * SIN_45, y + MARBLE_RADIUS * SIN_45);
72+
out = out.drawLine(
73+
x + MARBLE_RADIUS * SIN_45, y - MARBLE_RADIUS * SIN_45,
74+
x - MARBLE_RADIUS * SIN_45, y + MARBLE_RADIUS * SIN_45);
75+
break;
76+
77+
case 'C':
78+
out = out.stroke('#000000', 3);
79+
out = out.drawLine(x, y - MARBLE_RADIUS, x, y + MARBLE_RADIUS);
80+
break;
81+
}
82+
});
83+
return out;
84+
}
85+
86+
function drawInputObservable(out, testMessages, index, maxFrame) {
87+
var y = OBSERVABLE_HEIGHT * (index + 0.5);
88+
out = drawObservableArrow(out, y);
89+
out = drawObservableMessages(out, maxFrame, testMessages, y);
90+
return out;
91+
}
92+
93+
function drawOutputObservable(out, testMessages, numInputs, maxFrame) {
94+
var y = (numInputs + 0.5) * OBSERVABLE_HEIGHT + OPERATOR_HEIGHT;
95+
out = drawObservableArrow(out, y);
96+
out = drawObservableMessages(out, maxFrame, testMessages, y);
97+
return out;
98+
}
99+
100+
function drawOperator(out, label, numInputs) {
101+
var y = numInputs * OBSERVABLE_HEIGHT + OPERATOR_HEIGHT * 0.5;
102+
out = out.stroke('#000000', 3);
103+
out = out.fill('#FFFFFF00');
104+
out = out.drawRectangle(
105+
CANVAS_PADDING, y - OPERATOR_HEIGHT * 0.5,
106+
CANVAS_WIDTH - CANVAS_PADDING, y + OPERATOR_HEIGHT * 0.5);
107+
out = out.strokeWidth(-1);
108+
out = out.fill('#000000');
109+
out = out.font('helvetica', 54);
110+
out = out.draw(
111+
'translate 0,' + (y - canvasHeight * 0.5),
112+
'gravity Center',
113+
'text 0,0',
114+
String('"' + label + '"'));
115+
return out;
116+
}
117+
118+
module.exports = function painter(inputStreams, operatorLabel, outputStream) {
119+
var maxFrame = getMaxFrame(inputStreams, outputStream);
120+
canvasHeight =
121+
inputStreams.length * OBSERVABLE_HEIGHT +
122+
OPERATOR_HEIGHT +
123+
OBSERVABLE_HEIGHT;
124+
125+
var out;
126+
out = gm(CANVAS_WIDTH, canvasHeight, '#ffffff');
127+
inputStreams.forEach(function (inputTestMessages, index) {
128+
out = drawInputObservable(out, inputTestMessages, index, maxFrame);
129+
});
130+
out = drawOperator(out, operatorLabel, inputStreams.length);
131+
out = drawOutputObservable(out, outputStream, inputStreams.length, maxFrame);
132+
133+
out.write('./img/' + operatorLabel + '.png', function (err) {
134+
if (err) {
135+
return console.error(arguments);
136+
}
137+
//console.log(this.outname + ' created :: ' + arguments[3]);
138+
});
139+
};

spec/operators/buffer-spec.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ var Rx = require('../../dist/cjs/Rx');
33
var Observable = Rx.Observable;
44

55
describe('Observable.prototype.buffer()', function () {
6+
it.asDiagram('buffer')('should emit buffers that close and reopen', function () {
7+
var a = hot('-a-b-c-d-e-f-g-h-i-|');
8+
var b = hot('-----1-----2-----3-|');
9+
var expected = '-----x-----y-----z-|';
10+
var expectedValues = {
11+
x: ['a','b','c'],
12+
y: ['d','e','f'],
13+
z: ['g','h','i']
14+
};
15+
expectObservable(a.buffer(b)).toBe(expected, expectedValues);
16+
});
17+
618
it('should work with empty and empty selector', function () {
719
var a = Observable.empty();
820
var b = Observable.empty();
@@ -45,18 +57,6 @@ describe('Observable.prototype.buffer()', function () {
4557
expectObservable(a.buffer(b)).toBe(expected);
4658
});
4759

48-
it('should emit buffers that close and reopen', function () {
49-
var a = hot('-a-b-c-d-e-f-g-h-i-|');
50-
var b = hot('-----1-----2-----3-|');
51-
var expected = '-----x-----y-----z-|';
52-
var expectedValues = {
53-
x: ['a','b','c'],
54-
y: ['d','e','f'],
55-
z: ['g','h','i']
56-
};
57-
expectObservable(a.buffer(b)).toBe(expected, expectedValues);
58-
});
59-
6060
it('should work with non-empty and throw selector', function () {
6161
var a = hot('---^--a--');
6262
var b = Observable.throw(new Error('too bad'));

0 commit comments

Comments
 (0)