added test framework (#113)

* added test framework

* added toString on motors

* enabling logs
This commit is contained in:
Peli de Halleux 2017-12-19 07:07:50 -08:00 committed by GitHub
parent 60bf3a17d3
commit 9e427898ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 311 additions and 135 deletions

View File

@ -1,94 +1,47 @@
```typescript
let errors: string[] = [];
let tachoB = 0;
let tachoC = 0;
function assert(name: string, condition: boolean) {
if (!condition) {
errors.push(name)
brick.print("error:" + name, 0, 60)
}
}
function assertClose(name: string, expected: number, actual: number, tolerance = 5) {
const diff = Math.abs(expected - actual);
assert(name + ` ${expected}vs${actual}`, diff < tolerance);
}
function state(name: string, motor: motors.SingleMotor, line: number) {
brick.print(`${name}: ${motor.speed()}%,${motor.angle()}deg`, 0, 12 * line)
}
function test(name: string, f: () => void, check?: () => void) {
motors.stopAllMotors();
motors.largeB.clearCount()
motors.largeC.clearCount()
motors.largeB.setBrake(false)
motors.largeC.setBrake(false)
motors.largeB.setReversed(false);
motors.largeC.setReversed(false);
loops.pause(500);
tachoB = motors.largeB.angle()
tachoC = motors.largeC.angle()
brick.clearScreen()
brick.print(name, 0, 0)
state("B", motors.largeB, 1)
state("C", motors.largeC, 2)
f();
loops.pause(3000);
state("B", motors.largeB, 3)
state("C", motors.largeC, 4)
if (check)
check()
motors.stopAllMotors();
loops.pause(1000);
}
brick.buttonLeft.onEvent(ButtonEvent.Click, function () {
test("lgBC set speed 25", () => {
motors.largeBC.setSpeed(25)
}, () => {
assertClose("speedB", 25, motors.largeB.speed());
assertClose("speedC", 25, motors.largeC.speed());
});
tests.test("lgB set speed 10", () => {
motors.largeB.setSpeed(10);
loops.pause(100)
tests.assertClose("speedB", 10, motors.largeB.speed(), 2)
});
tests.test("lgB set speed 25 (reversed)", () => {
motors.largeB.setReversed(true)
motors.largeB.setSpeed(25)
loops.pause(100)
tests.assertClose("speedB", -25, motors.largeB.speed(), 2)
});
tests.test("lgBC set speed 5", () => {
motors.largeBC.setSpeed(5)
loops.pause(100)
tests.assertClose("speedB", 5, motors.largeB.speed(), 1);
tests.assertClose("speedC", 5, motors.largeC.speed(), 1);
});
tests.test("lgBC steer 50% 2x", () => {
motors.largeBC.setBrake(true)
motors.largeBC.steer(50, 50, 1, MoveUnit.Rotations)
loops.pause(1000)
tests.assertClose("largeB", 360, motors.largeB.angle(), 5)
motors.largeBC.setBrake(false)
})
brick.buttonEnter.onEvent(ButtonEvent.Click, function () {
test("lgB set speed 10", () => {
motors.largeB.setSpeed(10)
}, () => assertClose("speedB", 10, motors.largeB.speed()));
test("lgB set speed 25 (reversed)", () => {
motors.largeB.setReversed(true)
motors.largeB.setSpeed(25)
}, () => assertClose("speedB", -25, motors.largeB.speed()));
test("lgBC set speed 5", () => {
motors.largeBC.setSpeed(5)
}, () => {
assertClose("speedB", 5, motors.largeB.speed());
assertClose("speedC", 5, motors.largeC.speed());
});
test("lgBC steer 50% 2x", () => {
motors.largeBC.setBrake(true)
motors.largeBC.steer(50, 50, 2, MoveUnit.Rotations)
}, () => assertClose("largeB", 720, motors.largeB.angle()));
test("lgBC steer 50% 500deg", () => {
motors.largeBC.setBrake(true)
motors.largeBC.steer(50, 50, 500, MoveUnit.Degrees)
}, () => assertClose("largeB", 500, motors.largeB.angle()));
test("lgBC steer 50% 2s", () => {
motors.largeBC.setBrake(true)
motors.largeBC.steer(50, 50, 2000, MoveUnit.MilliSeconds)
})
test("lgBC tank 50% 720deg", () => {
motors.largeBC.setBrake(true)
motors.largeBC.tank(50, 50, 720, MoveUnit.Degrees)
}, () => assertClose("largeB", 720, motors.largeB.angle()));
brick.clearScreen()
brick.print(`${errors.length} errors`, 0, 0)
let l = 1;
for (const error of errors)
brick.print(`error: ${error}`, 0, l++ * 12)
tests.test("lgBC steer 50% 500deg", () => {
motors.largeBC.setBrake(true)
motors.largeBC.steer(50, 50, 135, MoveUnit.Degrees)
loops.pause(1000)
tests.assertClose("largeB", 135, motors.largeB.angle(), 5)
});
tests.test("lgBC steer 50% 2s", () => {
motors.largeBC.setBrake(true)
motors.largeBC.steer(50, 50, 500, MoveUnit.MilliSeconds)
loops.pause(1000)
})
tests.test("lgBC tank 50% 720deg", () => {
motors.largeBC.setBrake(true)
motors.largeBC.tank(50, 50, 180, MoveUnit.Degrees)
loops.pause(1000)
tests.assertClose("largeB", 180, motors.largeB.angle(), 5)
});
```
```package
tests
```

View File

@ -43,7 +43,7 @@
"brick.setStatusLight|param|pattern": "the lights pattern to use.",
"brick.showImage": "Shows an image on screen",
"brick.showImage|param|image": "image to draw",
"console": "Reading and writing data to the console output.\r\n\r\nReading and writing data to the console output.",
"console": "Reading and writing data to the console output.\n\nReading and writing data to the console output.",
"console.addListener": "Adds a listener for the log messages",
"console.log": "Write a line of text to the console output.",
"console.logValue": "Write a name:value pair as a line of text to the console output.",
@ -58,23 +58,24 @@
"control.raiseEvent": "Announce that an event happened to registered handlers.",
"control.raiseEvent|param|src": "ID of the Component that generated the event",
"control.raiseEvent|param|value": "Component specific code indicating the cause of the event.",
"motors.Motor.isReady": "Returns a value indicating if the motor is still running a previous command.",
"motors.Motor.move": "Moves the motor by a number of rotations, degress or seconds",
"motors.Motor.move|param|speed": "the speed from ``100`` full forward to ``-100`` full backward, eg: 50",
"motors.Motor.move|param|unit": "the meaning of the value",
"motors.Motor.move|param|value": "the move quantity, eg: 2",
"motors.Motor.pauseUntilReady": "Pauses the execution until the previous command finished.",
"motors.Motor.pauseUntilReady|param|timeOut": "optional maximum pausing time in milliseconds",
"motors.Motor.reset": "Resets the motor(s).",
"motors.Motor.setBrake": "Sets the automatic brake on or off when the motor is off",
"motors.Motor.setBrake|param|brake": "a value indicating if the motor should break when off",
"motors.Motor.setReversed": "Reverses the motor polarity",
"motors.Motor.setSpeed": "Sets the speed of the motor.",
"motors.Motor.setSpeed|param|speed": "the speed from ``100`` full forward to ``-100`` full backward, eg: 50",
"motors.Motor.stop": "Stops the motor(s).",
"motors.SingleMotor.angle": "Gets motor ration angle.",
"motors.SingleMotor.clearCount": "Clears the motor count",
"motors.SingleMotor.speed": "Gets motor actual speed.",
"motors.Motor.angle": "Gets motor ration angle.",
"motors.Motor.clearCount": "Clears the motor count",
"motors.Motor.speed": "Gets motor actual speed.",
"motors.Motor.toString": "Returns the status of the motor",
"motors.MotorBase.isReady": "Returns a value indicating if the motor is still running a previous command.",
"motors.MotorBase.move": "Moves the motor by a number of rotations, degress or seconds",
"motors.MotorBase.move|param|speed": "the speed from ``100`` full forward to ``-100`` full backward, eg: 50",
"motors.MotorBase.move|param|unit": "the meaning of the value",
"motors.MotorBase.move|param|value": "the move quantity, eg: 2",
"motors.MotorBase.pauseUntilReady": "Pauses the execution until the previous command finished.",
"motors.MotorBase.pauseUntilReady|param|timeOut": "optional maximum pausing time in milliseconds",
"motors.MotorBase.reset": "Resets the motor(s).",
"motors.MotorBase.setBrake": "Sets the automatic brake on or off when the motor is off",
"motors.MotorBase.setBrake|param|brake": "a value indicating if the motor should break when off",
"motors.MotorBase.setReversed": "Reverses the motor polarity",
"motors.MotorBase.setSpeed": "Sets the speed of the motor.",
"motors.MotorBase.setSpeed|param|speed": "the speed from ``100`` full forward to ``-100`` full backward, eg: 50",
"motors.MotorBase.stop": "Stops the motor(s).",
"motors.SynchedMotorPair.steer": "Turns the motor and the follower motor by a number of rotations",
"motors.SynchedMotorPair.steer|param|speed": "the speed from ``100`` full forward to ``-100`` full backward, eg: 50",
"motors.SynchedMotorPair.steer|param|steering": "the ratio of power sent to the follower motor, from ``-100`` to ``100``",
@ -84,7 +85,17 @@
"motors.SynchedMotorPair.tank|param|speedRight": "the speed on the right motor, eg: 50",
"motors.SynchedMotorPair.tank|param|unit": "@param speedLeft the speed on the left motor, eg: 50",
"motors.SynchedMotorPair.tank|param|value": "the amount of movement, eg: 2",
"motors.SynchedMotorPair.toString": "Returns the name(s) of the motor",
"motors.mkCmd": "Allocates a message buffer",
"motors.mkCmd|param|addSize": "required additional bytes",
"motors.mkCmd|param|cmd": "command id",
"motors.mkCmd|param|out": "ports",
"motors.readPWM": "Sends and receives a message from the motors device",
"motors.readPWM|param|buf": "message buffer",
"motors.resetAllMotors": "Resets all motors",
"motors.stopAllMotors": "Stops all motors",
"motors.writePWM": "Sends a command to the motors device",
"motors.writePWM|param|buf": "the command buffer",
"output.createBuffer": "Create a new zero-initialized buffer.",
"output.createBuffer|param|size": "number of bytes in the buffer",
"screen.clear": "Clear screen and reset font to normal.",

View File

@ -47,13 +47,13 @@
"console|block": "console",
"control.raiseEvent|block": "raise event|from %src|with value %value",
"control|block": "control",
"motors.Motor.move|block": "move `icons.motorLarge` %motor|for %value|%unit|at %speed|%",
"motors.Motor.pauseUntilReady|block": "%motor|pause until ready",
"motors.Motor.setBrake|block": "set `icons.motorLarge` %motor|brake %brake",
"motors.Motor.setReversed|block": "set `icons.motorLarge` %motor|reversed %reversed",
"motors.Motor.setSpeed|block": "set speed of `icons.motorLarge` %motor|to %speed|%",
"motors.SingleMotor.angle|block": "`icons.motorLarge` %motor|angle",
"motors.SingleMotor.speed|block": "`icons.motorLarge` %motor|speed",
"motors.Motor.angle|block": "`icons.motorLarge` %motor|angle",
"motors.Motor.speed|block": "`icons.motorLarge` %motor|speed",
"motors.MotorBase.move|block": "move `icons.motorLarge` %motor|for %value|%unit|at %speed|%",
"motors.MotorBase.pauseUntilReady|block": "%motor|pause until ready",
"motors.MotorBase.setBrake|block": "set `icons.motorLarge` %motor|brake %brake",
"motors.MotorBase.setReversed|block": "set `icons.motorLarge` %motor|reversed %reversed",
"motors.MotorBase.setSpeed|block": "set speed of `icons.motorLarge` %motor|to %speed|%",
"motors.SynchedMotorPair.steer|block": "steer %chassis|%steering|%|at speed %speed|%|by %value|%unit",
"motors.SynchedMotorPair.tank|block": "tank %chassis|left %speedLeft|%|right %speedRight|%|by %value|%unit",
"motors.largeAB|block": "large A+B",

View File

@ -53,32 +53,56 @@ namespace motors {
motorMM = control.mmap("/dev/lms_motor", MotorDataOff.Size * DAL.NUM_OUTPUTS, 0)
if (!motorMM) control.fail("no motor file")
resetMotors()
resetAllMotors()
let buf = output.createBuffer(1)
const buf = output.createBuffer(1)
buf[0] = DAL.opProgramStart
writePWM(buf)
}
function writePWM(buf: Buffer): void {
/**
* Sends a command to the motors device
* @param buf the command buffer
*/
//%
export function writePWM(buf: Buffer): void {
init()
pwmMM.write(buf)
}
function readPWM(buf: Buffer): number {
/**
* Sends and receives a message from the motors device
* @param buf message buffer
*/
//%
export function readPWM(buf: Buffer): number {
init()
return pwmMM.read(buf);
}
function mkCmd(out: Output, cmd: number, addSize: number) {
/**
* Allocates a message buffer
* @param out ports
* @param cmd command id
* @param addSize required additional bytes
*/
//%
export function mkCmd(out: Output, cmd: number, addSize: number) {
const b = output.createBuffer(2 + addSize)
b.setNumber(NumberFormat.UInt8LE, 0, cmd)
b.setNumber(NumberFormat.UInt8LE, 1, out)
return b
}
function resetMotors() {
reset(Output.ALL)
function outputToName(out: Output): string {
let r = "";
for (let i = 0; i < DAL.NUM_OUTPUTS; ++i) {
if (out & (1 << i)) {
if (r.length > 0) r += "+";
r += "ABCD"[i];
}
}
return r;
}
/**
@ -92,9 +116,18 @@ namespace motors {
writePWM(b)
}
/**
* Resets all motors
*/
//% group="Motion"
export function resetAllMotors() {
reset(Output.ALL)
}
//% fixedInstances
export class Motor extends control.Component {
export class MotorBase extends control.Component {
protected _port: Output;
protected _portName: string;
protected _brake: boolean;
private _initialized: boolean;
private _init: () => void;
@ -104,6 +137,7 @@ namespace motors {
constructor(port: Output, init: () => void, setSpeed: (speed: number) => void, move: (steps: boolean, stepsOrTime: number, speed: number) => void) {
super();
this._port = port;
this._portName = outputToName(this._port);
this._brake = false;
this._initialized = false;
this._init = init;
@ -244,7 +278,7 @@ namespace motors {
}
//% fixedInstances
export class SingleMotor extends Motor {
export class Motor extends MotorBase {
private _large: boolean;
constructor(port: Output, large: boolean) {
@ -322,34 +356,42 @@ namespace motors {
}
}
}
/**
* Returns the status of the motor
*/
//%
toString(): string {
return `${this._large ? "" : "M"}${this._portName} ${this.speed()}% ${this.angle()}>`;
}
}
//% whenUsed fixedInstance block="large A"
export const largeA = new SingleMotor(Output.A, true);
export const largeA = new Motor(Output.A, true);
//% whenUsed fixedInstance block="large B"
export const largeB = new SingleMotor(Output.B, true);
export const largeB = new Motor(Output.B, true);
//% whenUsed fixedInstance block="large C"
export const largeC = new SingleMotor(Output.C, true);
export const largeC = new Motor(Output.C, true);
//% whenUsed fixedInstance block="large D"
export const largeD = new SingleMotor(Output.D, true);
export const largeD = new Motor(Output.D, true);
//% whenUsed fixedInstance block="medium A"
export const mediumA = new SingleMotor(Output.A, false);
export const mediumA = new Motor(Output.A, false);
//% whenUsed fixedInstance block="medium B"
export const mediumB = new SingleMotor(Output.B, false);
export const mediumB = new Motor(Output.B, false);
//% whenUsed fixedInstance block="medium C"
export const mediumC = new SingleMotor(Output.C, false);
export const mediumC = new Motor(Output.C, false);
//% whenUsed fixedInstance block="medium D"
export const mediumD = new SingleMotor(Output.D, false);
export const mediumD = new Motor(Output.D, false);
//% fixedInstances
export class SynchedMotorPair extends Motor {
export class SynchedMotorPair extends MotorBase {
constructor(ports: Output) {
super(ports, () => this.__init(), (speed) => this.__setSpeed(speed), (steps, stepsOrTime, speed) => this.__move(steps, stepsOrTime, speed));
@ -458,6 +500,20 @@ namespace motors {
const steering = (speedRight * 100 / speedLeft) >> 0;
this.steer(speedLeft, steering, value, unit);
}
/**
* Returns the name(s) of the motor
*/
//%
toString(): string {
let r = outputToName(this._port);
for (let i = 0; i < DAL.NUM_OUTPUTS; ++i) {
if (this._port & (1 << i)) {
r += ` ${getMotorData(1 << i).actualSpeed}%`
}
}
return r;
}
}
//% whenUsed fixedInstance block="large B+C"

35
libs/tests/README.md Normal file
View File

@ -0,0 +1,35 @@
# tests
A unit test framework
## Defining tests
Tests are registered as event handlers. They will automatically run once ``on start`` is finished.
```blocks
tests.test("lgB set speed 10", () => {
motors.largeB.setSpeed(10);
loops.pause(100)
tests.assertClose("speedB", 10, motors.largeB.speed(), 2)
});
```
## Assertions
The library has various asserts that will register fault. Note that since exceptions are not available, assertion failure **do not** stop the program execution.
* **assert** checks a boolean condition
```blocks
tests.assert("speed positive", motors.largeB.speed() > 0)
```
* **assert close** checks that a numberical value is within a particular range
```blocks
tests.assertClose("speed", motors.largeB.speed(), 10, 2)
```
```package
tests
```

View File

@ -0,0 +1,9 @@
{
"tests": "Unit tests framework",
"tests.assert": "Checks a boolean condition",
"tests.assertClose": "Checks that 2 values are close to each other",
"tests.assertClose|param|actual": "what the value was",
"tests.assertClose|param|expected": "what the value should be",
"tests.assertClose|param|tolerance": "the acceptable error margin",
"tests.test": "Registers a test to run"
}

View File

@ -0,0 +1,6 @@
{
"tests.assert|block": "assert %message|%condition",
"tests.test|block": "test %name",
"tests|block": "tests",
"{id:category}Tests": "Tests"
}

14
libs/tests/pxt.json Normal file
View File

@ -0,0 +1,14 @@
{
"name": "tests",
"description": "A unit test library",
"files": [
"README.md",
"tests.ts"
],
"testFiles": [
],
"public": true,
"dependencies": {
"core": "file:../core"
}
}

89
libs/tests/tests.ts Normal file
View File

@ -0,0 +1,89 @@
/**
* Unit tests framework
*/
//% weight=100 color=#0fbc11 icon=""
namespace tests {
class Test {
name: string;
handler: () => void;
errors: string[];
constructor(name: string, handler: () => void) {
this.name = name;
this.handler = handler;
this.errors = [];
}
reset() {
motors.stopAllMotors();
motors.resetAllMotors();
}
run() {
// clear state
this.reset();
console.log(`# ${this.name}`)
this.handler()
// ensure clean state after test
this.reset();
}
}
let _tests: Test[] = undefined;
let _currentTest: Test = undefined;
function run() {
if (!_tests) return;
const start = control.millis();
console.sendToScreen();
console.log(`${_tests.length} tests`)
for (let i = 0; i < _tests.length; ++i) {
const t = _currentTest = _tests[i];
t.run();
_currentTest = undefined;
}
console.log(`${_tests.map(t => t.errors.length).reduce((p, c) => p + c, 0)} X, ${Math.ceil((control.millis() - start) / 1000)}s`)
}
/**
* Registers a test to run
*/
//% blockId=testtest block="test %name"
export function test(name: string, handler: () => void): void {
if (!name || !handler) return;
if (!_tests) {
_tests = [];
control.runInBackground(function () {
// should run after on start
loops.pause(100)
run()
})
}
_tests.push(new Test(name, handler));
}
/**
* Checks a boolean condition
*/
//% blockId=testAssert block="assert %message|%condition"
export function assert(message: string, condition: boolean) {
if (!condition) {
console.log(` X ${message || ''}`)
if (_currentTest)
_currentTest.errors.push(message);
}
}
/**
* Checks that 2 values are close to each other
* @param expected what the value should be
* @param actual what the value was
* @param tolerance the acceptable error margin
*/
export function assertClose(name: string, expected: number, actual: number, tolerance: number) {
assert(`${name} ${expected} != ${actual} +-${tolerance}`, Math.abs(expected - actual) <= tolerance);
}
}

View File

@ -15,7 +15,8 @@
"libs/ultrasonic-sensor",
"libs/infrared-sensor",
"libs/gyro-sensor",
"libs/ev3"
"libs/ev3",
"libs/tests"
],
"simulator": {
"autoRun": true,
@ -52,8 +53,10 @@
"serial": {
"vendorId": "0x0694",
"productId": "0x0005",
"rawHID": true
},
"rawHID": true,
"useEditor": true,
"log": true
},
"runtime": {
"mathBlocks": true,
"loopsBlocks": true,