Skip to content

Commit add50ba

Browse files
committed
Fix negative zero in monitors
1 parent 4187f55 commit add50ba

File tree

5 files changed

+216
-8
lines changed

5 files changed

+216
-8
lines changed

src/engine/runtime.js

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const ScratchLinkWebSocket = require('../util/scratch-link-websocket');
2323
const FontManager = require('./tw-font-manager');
2424
const fetchWithTimeout = require('../util/fetch-with-timeout');
2525
const platform = require('./tw-platform.js');
26+
const {compareImmutableMaps, mergeMaps} = require('../util/tw-immutable-util');
2627

2728
// Virtual I/O devices.
2829
const Clock = require('../io/clock');
@@ -2577,7 +2578,7 @@ class Runtime extends EventEmitter {
25772578
this._refreshTargets = false;
25782579
}
25792580

2580-
if (!this._prevMonitorState.equals(this._monitorState)) {
2581+
if (!compareImmutableMaps(this._prevMonitorState, this._monitorState)) {
25812582
this.emit(Runtime.MONITORS_UPDATE, this._monitorState);
25822583
this._prevMonitorState = this._monitorState;
25832584
}
@@ -3112,13 +3113,7 @@ class Runtime extends EventEmitter {
31123113
const id = monitor.get('id');
31133114
if (this._monitorState.has(id)) {
31143115
this._monitorState =
3115-
// Use mergeWith here to prevent undefined values from overwriting existing ones
3116-
this._monitorState.set(id, this._monitorState.get(id).mergeWith((prev, next) => {
3117-
if (typeof next === 'undefined' || next === null) {
3118-
return prev;
3119-
}
3120-
return next;
3121-
}, monitor));
3116+
this._monitorState.set(id, mergeMaps(this._monitorState.get(id), monitor));
31223117
return true;
31233118
}
31243119
return false;

src/util/tw-immutable-util.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* @fileoverview Utilities for immutable.js objects.
3+
*/
4+
5+
/**
6+
* Determine if two maps have identical keys and content (Object.is equality)
7+
* Unlike the regular immutable.js comparison functions, this one considers 0 and -0 to be different.
8+
* @param {OrderedMap} a
9+
* @param {OrderedMap} b
10+
* @returns {boolean} true if a and b have the same keys and values
11+
*/
12+
const compareImmutableMaps = (a, b) => {
13+
if (a.size !== b.size) {
14+
return false;
15+
}
16+
17+
for (const key of a.keys()) {
18+
const aValue = a.get(key);
19+
const bValue = b.get(key);
20+
if (!Object.is(aValue, bValue)) {
21+
return false;
22+
}
23+
}
24+
25+
return true;
26+
};
27+
28+
/**
29+
* Merge map B into map A. Values of undefined or null in B will default to B.
30+
* Unlike the regular immutable.js comparison functions, this one considers 0 and -0 to be different.
31+
* @param {OrderedMap} a
32+
* @param {OrderedMap} b
33+
* @returns {OrderedMap}
34+
*/
35+
const mergeMaps = (a, b) => b
36+
.filter(value => value === 0)
37+
.merge(a)
38+
.mergeWith((prev, next) => {
39+
if (typeof next === 'undefined' || next === null) {
40+
return prev;
41+
}
42+
return next;
43+
}, b);
44+
45+
module.exports = {
46+
compareImmutableMaps,
47+
mergeMaps
48+
};

test/fixtures/tw-monitors.sb3

2.55 KB
Binary file not shown.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
const fs = require('node:fs');
2+
const path = require('node:path');
3+
const {test} = require('tap');
4+
const VM = require('../../src/virtual-machine');
5+
6+
test('monitor update', t => {
7+
const fixture = path.join(__dirname, '../fixtures/tw-monitors.sb3');
8+
const vm = new VM();
9+
vm.loadProject(fs.readFileSync(fixture)).then(() => {
10+
const variable = vm.runtime.getTargetForStage().lookupVariableByNameAndType('variable', '');
11+
const list = vm.runtime.getTargetForStage().lookupVariableByNameAndType('list', 'list');
12+
13+
const updates = [];
14+
vm.runtime.on('MONITORS_UPDATE', monitors => {
15+
const values = {};
16+
for (const monitor of monitors.values()) {
17+
const name = Object.values(monitor.get('params'))[0];
18+
const value = monitor.get('value');
19+
values[name] = value;
20+
}
21+
updates.push(values);
22+
});
23+
24+
// Baseline start
25+
updates.length = 0;
26+
vm.runtime._step();
27+
t.equal(updates.length, 1);
28+
t.equal(updates[0].variable, 0);
29+
t.same(updates[0].list, []);
30+
31+
// Change variable to 5
32+
updates.length = 0;
33+
variable.value = 5;
34+
vm.runtime._step();
35+
t.equal(updates.length, 1);
36+
t.equal(updates[0].variable, 5);
37+
t.same(updates[0].list, []);
38+
39+
// Change variable to -0
40+
updates.length = 0;
41+
variable.value = -0;
42+
vm.runtime._step();
43+
t.equal(updates.length, 1);
44+
t.ok(Object.is(updates[0].variable, -0));
45+
t.same(updates[0].list, []);
46+
47+
// Change variable to 0
48+
updates.length = 0;
49+
variable.value = 0;
50+
vm.runtime._step();
51+
t.equal(updates.length, 1);
52+
t.ok(Object.is(updates[0].variable, 0));
53+
t.same(updates[0].list, []);
54+
55+
// Replace list entirely
56+
updates.length = 0;
57+
list.value = [1, 2, 3];
58+
vm.runtime._step();
59+
t.equal(updates.length, 1);
60+
t.equal(updates[0].variable, 0);
61+
t.same(updates[0].list, [1, 2, 3]);
62+
63+
// Append to list
64+
updates.length = 0;
65+
list.value.push(4);
66+
list.value._monitorUpToDate = false;
67+
vm.runtime._step();
68+
t.equal(updates.length, 1);
69+
t.equal(updates[0].variable, 0);
70+
t.same(updates[0].list, [1, 2, 3, 4]);
71+
72+
t.end();
73+
});
74+
});

test/unit/tw_immutable_util.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
const {test} = require('tap');
2+
const {OrderedMap} = require('immutable');
3+
const {compareImmutableMaps, mergeMaps} = require('../../src/util/tw-immutable-util');
4+
5+
test('compareImmutableMaps', t => {
6+
t.ok(compareImmutableMaps(OrderedMap(), OrderedMap()));
7+
t.ok(compareImmutableMaps(OrderedMap({
8+
a: 'hello'
9+
}), OrderedMap({
10+
a: 'hello'
11+
})));
12+
t.ok(compareImmutableMaps(OrderedMap({
13+
what: 0,
14+
why: 'how'
15+
}), OrderedMap({
16+
why: 'how',
17+
what: 0
18+
})));
19+
20+
t.notOk(compareImmutableMaps(OrderedMap({
21+
a: 0
22+
}), OrderedMap({
23+
a: -0
24+
})));
25+
t.notOk(compareImmutableMaps(OrderedMap(), OrderedMap({
26+
a: 0
27+
})));
28+
t.notOk(compareImmutableMaps(OrderedMap({
29+
a: 0
30+
}), OrderedMap()));
31+
32+
const arr = [];
33+
t.ok(compareImmutableMaps(OrderedMap({
34+
arr
35+
}), OrderedMap({
36+
arr
37+
})));
38+
t.notOk(compareImmutableMaps(OrderedMap({
39+
arr: []
40+
}), OrderedMap({
41+
arr: []
42+
})));
43+
44+
t.end();
45+
});
46+
47+
test('mergeMaps', t => {
48+
t.ok(compareImmutableMaps(mergeMaps(
49+
OrderedMap({
50+
a: 'hello',
51+
b: 'bye',
52+
c: 'ok'
53+
}),
54+
OrderedMap({
55+
b: '!!',
56+
c: null,
57+
d: 'e',
58+
e: undefined
59+
})
60+
), OrderedMap({
61+
a: 'hello',
62+
b: '!!',
63+
c: 'ok',
64+
d: 'e',
65+
e: undefined
66+
})));
67+
68+
t.ok(compareImmutableMaps(mergeMaps(
69+
OrderedMap({
70+
a: 0
71+
}),
72+
OrderedMap({
73+
a: -0
74+
})
75+
), OrderedMap({
76+
a: -0
77+
})));
78+
79+
t.ok(compareImmutableMaps(mergeMaps(
80+
OrderedMap({
81+
a: -0
82+
}),
83+
OrderedMap({
84+
a: 0
85+
})
86+
), OrderedMap({
87+
a: 0
88+
})));
89+
90+
t.end();
91+
});

0 commit comments

Comments
 (0)