Skip to content

Commit

Permalink
Expand test cases for the sensor class, and fix median value handling
Browse files Browse the repository at this point in the history
Restructure analog sensor shape test to check that all and only expected properties and methods exist
Verify that expected default property values are set
Check that data events are emitted at the expected time tick
Check that median processing give the expected results and emitted events
  • Loading branch information
mMerlin committed Jun 30, 2015
1 parent 835f1ff commit 6f2dfef
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 56 deletions.
23 changes: 12 additions & 11 deletions lib/sensor.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ function Sensor(opts) {
var value, last, min, max, samples;

value = null;
last = null;
min = 1023;
max = 0;
last = -min;
samples = [];

Board.Component.call(
Expand All @@ -69,14 +69,14 @@ function Sensor(opts) {

// Read event loop
this.io[opts.type + "Read"](this.pin, function(data) {
// Update the instance-local `last` value.
last = value;

// Update the instance-local `value` value.
// This is shared by the accessors defined below.
value = data;

samples.push(value);
// Only append to the samples when noise filtering can/will be used
if (opts.type !== "digital") {
samples.push(value);
}
}.bind(this));

// Throttle
Expand All @@ -94,6 +94,8 @@ function Sensor(opts) {
aliases.change.forEach(function(change) {
this.emit(change, err, value);
}, this);
// Update the instance-local `last` value.
last = value;
}
return;
}
Expand Down Expand Up @@ -126,12 +128,15 @@ function Sensor(opts) {
high = last + this.threshold;

// Includes all aliases
// Prevent events from firing if the latest value is the same as
// the last value to trigger a change event
// Prevent events from firing if the latest value is the same as (within
// the threshold of) the last value to trigger a change event
if (median < low || median > high) {
aliases.change.forEach(function(change) {
this.emit(change, err, median);
}, this);
// Update the instance-local `last` value (only) when a new change event
// has been emitted. For comparison in the next interval
last = median;
}


Expand All @@ -152,10 +157,6 @@ function Sensor(opts) {
}
}

// Store this media value for comparison
// in next interval
last = median;

// Reset samples
samples.length = 0;
}.bind(this), this.freq);
Expand Down
207 changes: 162 additions & 45 deletions test/sensor.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,43 +19,35 @@ exports["Sensor - Analog"] = {
board: board
});

this.proto = [{
name: "scale"
}, {
name: "scaleTo"
}, {
name: "booleanAt"
}, {
name: "within"
}];

this.instance = [{
name: "id"
}, {
name: "pin"
}, {
name: "mode"
}, {
name: "freq"
}, {
name: "range"
}, {
name: "threshold"
}, {
name: "isScaled"
}, {
name: "raw"
}, {
name: "analog"
}, {
name: "constrained"
}, {
name: "boolean"
}, {
name: "scaled"
}, {
name: "value"
}, ];
this.defFreq = 25;

this.methods = [
"constructor",
"within",
"scale",
"scaleTo",
"booleanAt"
];

this.members = {
id: { type: "object" },
pin: { type: "number" },
mode: { type: "number" },
freq: { type: "number" },
range: { type: "object" },
threshold: { type: "number" },
isScaled: { type: "boolean" },
raw: { type: "object" }, // defined property that returns var inited to null
analog: { type: "object" }, // defined property
constrained: { type: "object" }, // defined property
boolean: { type: "boolean" }, // defined property always true or false
scaled: { type: "object" }, // defined property
value: { type: "object" }, // defined property

board: { type: "object" },
io: { type: "object" },
limit: { type: "object" } // null initial value
};

done();
},
Expand All @@ -67,15 +59,40 @@ exports["Sensor - Analog"] = {
},

shape: function(test) {
test.expect(this.proto.length + this.instance.length);
var propsActual, propsExpected, methodsActual;
propsActual = Object.getOwnPropertyNames(this.sensor);
propsExpected = Object.getOwnPropertyNames(this.members);
methodsActual = Object.getOwnPropertyNames(Object.getPrototypeOf(this.sensor));

test.expect(9 + 3 * (this.methods.length + propsExpected.length));

this.proto.forEach(function(method) {
test.equal(typeof this.sensor[method.name], "function");
// Verify that all of the expected prototype functions and properties exist for the instance
this.methods.forEach(function(proto) {
test.ok(methodsActual.includes(proto), "missing '" + proto + "' sensor prototype method");
}, this);
propsExpected.forEach(function(property) {
test.ok(propsActual.includes(property), "missing '" + property + "' sensor instance property");
}, this);

this.instance.forEach(function(property) {
test.notEqual(typeof this.sensor[property.name], "undefined");
// Make sure that all of the existing instance properties and prototype methods are actually expected, and the correct datatype
propsActual.forEach(function(property) {
test.ok(propsActual.includes(property), "found unexpected " + property + " sensor instance member");
test.strictEqual(typeof this.sensor[property], this.members[property].type, "Unexected datatype found for '" + property + "' property");
}, this);
methodsActual.forEach(function(proto) {
test.ok(this.methods.includes(proto), "found unexpected '" + proto + "' sensor prototype method");
test.strictEqual(typeof this.sensor[proto], "function", "Unexected datatype found for '" + proto + "' method");
}, this);

test.strictEqual(this.sensor.board, board, "Expected to be the mock board");
test.strictEqual(this.sensor.io, board.io, "Expected to be the same io as the mock board");
test.strictEqual(this.sensor.mode, this.sensor.io.MODES.ANALOG, "Expected to be analog mode");
test.strictEqual(this.sensor.freq, this.defFreq, "defaulted freq value expected to be " + this.defFreq + ", found '" + this.sensor.freq + "'");
test.ok(Array.isArray(this.sensor.range) && this.sensor.range.length === 2, "range property should be a 2 element array");
test.ok(this.sensor.range[0] === 0 && this.sensor.range[1] === 1023, "defaulted range should be [0, 1023]");
test.strictEqual(this.sensor.limit, null, "defaulted limit should be null");
test.strictEqual(this.sensor.threshold, 1, "defaulted threshold should be 1");
test.strictEqual(this.sensor.isScaled, false, "defaulted isScaled flag should be false");

test.done();
},
Expand All @@ -89,11 +106,111 @@ exports["Sensor - Analog"] = {
},

data: function(test) {
var spy = sinon.spy();
test.expect(1);
var tickAccum, tickDelta, spy = sinon.spy();
test.expect(7);

this.sensor.on("data", spy);
this.clock.tick(25);
tickAccum = 0;
test.ok(!spy.called, "tick " + tickAccum + ": data event handler should not be called until tick " + this.defFreq);
tickDelta = this.defFreq - 1;
this.clock.tick(tickDelta);
tickAccum += tickDelta;
test.ok(!spy.called, "tick " + tickAccum + ": data event handler should not be called until tick " + this.defFreq);
tickDelta = 1;
this.clock.tick(tickDelta);
tickAccum += tickDelta;
test.ok(spy.called, "tick " + tickAccum + ": data event handler should have been called at tick " + this.defFreq);
test.ok(spy.calledOnce);
tickDelta = this.defFreq - 1;
this.clock.tick(tickDelta);
tickAccum += tickDelta;
test.ok(spy.calledOnce, "tick" + tickAccum + ": data event handler should not be called again until tick " + (this.defFreq * 2));
test.ok(spy.calledOnce);
tickDelta = 1;
this.clock.tick(tickDelta);
tickAccum += tickDelta;
test.ok(spy.calledTwice, "tick " + tickAccum + ": data event handler should be called again at tick " + (this.defFreq * 2));

test.done();
},

filtered: function(test) {
var callback = this.analogRead.args[0][1],
dataSpy = sinon.spy(),
chgSpy = sinon.spy(),
tickDelta, tickAccum, spyCall;
test.expect(20);

this.sensor.on("data", dataSpy);
this.sensor.on("change", chgSpy);
tickAccum = 0;
callback(100);
tickDelta = 1;
this.clock.tick(tickDelta);
tickAccum += tickDelta;
callback(102);
this.clock.tick(tickDelta);
tickAccum += tickDelta;
callback(101);
this.clock.tick(tickDelta);
tickAccum += tickDelta;
callback(103);
tickDelta = this.defFreq - tickAccum - 1;
this.clock.tick(tickDelta);
tickAccum += tickDelta;
callback(104);
test.ok(!dataSpy.called, "tick " + tickAccum + ": data event handler should not be called until tick " + this.defFreq);
test.ok(!chgSpy.called, "tick " + tickAccum + ": change event handler should not be called until tick " + this.defFreq);
tickDelta = 1;
this.clock.tick(tickDelta);
tickAccum += tickDelta;

test.ok(dataSpy.calledOnce, "tick " + tickAccum + ": data event handler should be called at tick " + this.defFreq);
test.ok(chgSpy.calledOnce, "tick " + tickAccum + ": change event handler should be called at tick " + this.defFreq);
spyCall = dataSpy.getCall(0);
test.strictEqual(spyCall.args[0], null, "data event err argument expected to be null");
test.strictEqual(spyCall.args[1], 102, "data event value expected to be the median (102) value");
test.ok(spyCall.calledOn(this.sensor), "data event 'this' parameter expected to be source sensor object");
spyCall = chgSpy.getCall(0);
test.strictEqual(spyCall.args[0], null, "change event err argument expected to be null");
test.strictEqual(spyCall.args[1], 102, "change event value expected to be the median (102) value");
test.ok(spyCall.calledOn(this.sensor), "change event 'this' parameter expected to be source sensor object");

test.strictEqual(this.sensor.raw, 104, "sensor raw property expected to be the last value read (injected)");
test.strictEqual(this.sensor.value, 104, "sensor value property expected to be the last value read (injected)");

// Check for non-integer median value (when even number of data points)
tickAccum = 0;
tickDelta = 1;
this.clock.tick(tickDelta);
tickAccum += tickDelta;
callback(102);
this.clock.tick(tickDelta);
tickAccum += tickDelta;
callback(106);
this.clock.tick(tickDelta);
tickAccum += tickDelta;
callback(101);
tickDelta = this.defFreq - tickAccum - 1;
this.clock.tick(tickDelta);
tickAccum += tickDelta + this.defFreq;
callback(103);
test.ok(dataSpy.calledOnce, "tick " + tickAccum + ": data event handler should not be called again until tick " + this.defFreq * 2);

tickDelta = 1;
this.clock.tick(tickDelta);
tickAccum += tickDelta;
test.ok(dataSpy.calledTwice, "tick " + tickAccum + ": data event handler should be called again at tick " + this.defFreq * 2);
test.ok(chgSpy.calledOnce, "tick " + tickAccum + ": change event handler should not be called yet: data points do not exceed change threshold");

spyCall = dataSpy.getCall(1);
test.strictEqual(spyCall.args[0], null, "data event err argument expected to be null");
test.strictEqual(spyCall.args[1], 102.5, "data event value expected to be the median (102.5) value");
test.ok(spyCall.calledOn(this.sensor), "data event 'this' parameter expected to be source sensor object");

test.strictEqual(this.sensor.raw, 103, "sensor raw property expected to be the last value read (injected)");
test.strictEqual(this.sensor.value, 103, "sensor value property expected to be the last value read (injected)");

test.done();
},

Expand Down

0 comments on commit 6f2dfef

Please sign in to comment.