cukeup nyc ian dees on testing embedded systems with cucumber
DESCRIPTION
Testing Embedded Systems With Cucumber Cucumber isn't just for web apps. You can test just about anything with it—including embedded systems! In this talk, we'll look at several different facets of driving hardware from Cucumber, including: Connecting to an Arduino using a custom serial protocol Taking advantage of the TCP stack when it's available Building the Cucumber wire protocol into your device What to do when you can't modify the app under test Driving more fully-featured devices such as the BeagleBone or RaspberryPi By the end of the presentation, you'll have a handle on what the various options are for testing embedded devices, and which tradeoffs will apply to your system.TRANSCRIPT
Testing Embedded Systems With Cucumber
Ian Dees • @undeesCukeUp! NYC 2013
Plenty of Ruby, but...
There will be C
There will be C++
There will be C#
http://pragprog.com/titles/dhwcr
discount code:CucumberIanDees
The Embedded Continuum
almost acomputer
chip andsome ROM
Simple devicesNo Ruby, no Cucumber
Gray CodeA simple system to test
Feature: Gray Code Scenario Outline: Counter When I press the button Then the LEDs should read "<leds>"
Examples: | leds | | ..O | | .OO | | .O. | | OO. | | OOO | | O.O | | O.. | | ... |
Arduino
void loop() { button.update(); bool buttonPressed = button.risingEdge();
if (buttonPressed) { counter = (counter + 1) % ENTRIES; updateLeds(); }
delay(50);}
Drive code directly
void loop() { button.update(); bool buttonPressed = button.risingEdge();
if (buttonPressed) { counter = (counter + 1) % ENTRIES; updateLeds(); }
delay(50);}
void loop() { button.update(); bool buttonPressed = button.risingEdge();
if (buttonPressed) { counter = (counter + 1) % ENTRIES; updateLeds(); }
delay(50);}
static bool isFakeButtonPressed;
class Bounce {public: // ...
bool risingEdge() { bool result = isFakeButtonPressed; isFakeButtonPressed = false; return result; }};
extern "C" void press() { isFakeButtonPressed = true;}
const char* leds() { static char buf[LEDS + 1] = {0};
for (int i = 0; i < LEDS; ++i) { buf[i] = STATES[counter][i] == HIGH ? 'O' : '.'; }
return buf;}
require 'ffi'
module Arduino extend FFI::Library ffi_lib 'graycode'
attach_function :press, [], :void attach_function :leds, [], :string
attach_function :setup, [], :void attach_function :loop, [], :voidend
When /^I press the button$/ do Arduino.press Arduino.loopend
Then /^the LEDs should read "(.*?)"$/ do |leds|
expect(Arduino.leds).to eq(leds)end
Cucumber Wire Protocol
Cucumber-CPPhttps://github.com/cucumber/cucumber-cpp
host: localhostport: 3902
features/step_definitions/cucumber.wire
When /^I press the button$/ do Arduino.press Arduino.loopend
Then /^the LEDs should read "(.*?)"$/ do |expected| expect(Arduino.leds).to eq(expected)end
WHEN("^I press the button$") { press(); loop();}
THEN("^the LEDs should read \"(.*?)\"$") { REGEX_PARAM(string, expected); BOOST_CHECK_EQUAL(leds(), expected);}
Bespoke wire server
http://www.2600.com/code/212/listener.c
listen/accept/read/write
while(fgets(buf,sizeof buf,rStream)) { respond_to_cucumber(wStream, buf);}
step_matchesinvoke
Two messages
Then the LEDs should read "..O"
⬇["step_matches", {"name_to_match": "the LEDs should read"}]
⬇["success", [{"id":"1", "args":[{"val":"..O", "pos":"22"}]}]]
json spirithttp://www.codeproject.com/KB/recipes/
JSON_Spirit.aspx
extern "C" void respond_to_cucumber( FILE* stream, const char* message) { string s = message; Value v; read(s, v);
Array& a = v.get_array(); string type = a[0].get_str();
// handle Cucumber message types
report_success(stream);}
if (type == "step_matches") { string name = step_name(v);
if (name == "I press the button") { report_success(stream); return; } else if (...)
// ...
}}
if (type == "step_matches") { string name = step_name(v);
if (name == "I press the button") { report_success(stream); return; } else if (...)
// ...
}}
if (type == "step_matches") { string name = step_name(v);
if (...) { // ...
} else if (name.find("the LEDs") == 0) const int START = 22; string leds = name.substr(START, 3); report_match(leds, START, stream); return; }}
Then the LEDs should read "..O"
⬇["invoke", {"id":"1", "args":["..O"]}]
⬇["success", []]
if (type == "invoke") { string id = step_id(v);
if (id == "0") { press(); loop(); } else if (id == "1") {
// ...
} } }
if (type == "invoke") { string id = step_id(v);
if (id == "0") { press(); loop(); } else if (id == "1") {
// ...
} } }
if (type == "invoke") { string id = step_id(v);
if (id == "0") { // ...
} else if (id == "1") { string expected = step_leds(v); if (expected != leds()) { report_failure("LEDs", stream); return; } } }
https://github.com/hparra/ruby-serialport
Serial
void loop() { button.update(); bool buttonPressed = button.risingEdge();
if (buttonPressed) { counter = (counter + 1) % ENTRIES; updateLeds(); }
delay(50);}
void loop() { button.update(); bool buttonPressed = button.risingEdge();
if ( buttonPressed ) { counter = (counter + 1) % ENTRIES; updateLeds();
delay(50);}
void loop() { button.update(); bool buttonPressed = button.risingEdge();
int command = (Serial.available() > 0 ? Serial.read() : -1);
if (isIncrement(buttonPressed, command)) { counter = (counter + 1) % ENTRIES; updateLeds(); } else if (isQuery(command)) { Serial.write(leds()); Serial.write('\n'); }
delay(50);}
void loop() { button.update(); bool buttonPressed = button.risingEdge();
int command = (Serial.available() > 0 ? Serial.read() : -1);
if (isIncrement(buttonPressed, command)) { counter = (counter + 1) % ENTRIES; updateLeds(); } else if (isQuery(command)) { Serial.write(leds()); Serial.write('\n'); }
delay(50);}
void loop() { button.update(); bool buttonPressed = button.risingEdge();
int command = (Serial.available() > 0 ? Serial.read() : -1);
if (isIncrement(buttonPressed, command)) { counter = (counter + 1) % ENTRIES; updateLeds(); } else if (isQuery(command)) { Serial.write(leds()); Serial.write('\n'); }
delay(50);}
void loop() { button.update(); bool buttonPressed = button.risingEdge();
int command = (Serial.available() > 0 ? Serial.read() : -1);
if (isIncrement(buttonPressed, command)) { counter = (counter + 1) % ENTRIES; updateLeds(); } else if (isQuery(command)) { Serial.write(leds()); Serial.write('\n'); }
delay(50);}
require 'serialport'
module Arduino @@port = SerialPort.open 2, 9600 at_exit { @@port.close }
def self.press @@port.write '+' end
def self.leds @@port.write '?' @@port.read.strip endend
Ruby, C#, SpecFlow, Cucumber
Almost a computer
Feature: Calculator
Scenario: Add two numbers When I multiply 2 and 3 Then I should get 6
Run Cucumber directly
rsync -av --delete . remote1:test_path
ssh remote1 'cd test_path && cucumber'
Drive the GUI
TestStack Whitehttps://github.com/TestStack/White
namespace Calc.Spec{ [Binding] public class CalculatorSteps { private Window window;
[Before] public void Before() { Application application = Application.Launch("calc.exe"); window = application.GetWindow( "Calculator", InitializeOption.NoCache); } // ... }}
namespace Calc.Spec{ [Binding] public class CalculatorSteps { private Window window;
[Before] public void Before() { Application application = Application.Launch("calc.exe"); window = application.GetWindow( "Calculator", InitializeOption.NoCache); } // ... }}
namespace Calc.Spec{ [Binding] public class CalculatorSteps { private Window window;
[Before] public void Before() { Application application = Application.Launch("calc.exe"); window = application.GetWindow( "Calculator", InitializeOption.NoCache); } // ... }}
[When(@"I multiply (.*) and (.*)")]public void WhenIMultiply(string a, string b){ window.Keyboard.Enter(a + "*" + b + "=");}
[Then(@"I should get (.*)")]public void ThenIShouldGet(string expected){ window.Get<Label>(expected);}
Give your app an API
Mongoose web server
int main(){ struct mg_context *ctx = mg_start(); mg_set_option(ctx, "ports", "33333");
mg_set_uri_callback(ctx, "/", &show_index, 0); mg_set_uri_callback(ctx, "/multiply", &multiply, 0); mg_set_uri_callback(ctx, "/result", &get_result, 0);
getchar(); mg_stop(ctx);
return 0;}
int main(){ struct mg_context *ctx = mg_start(); mg_set_option(ctx, "ports", "33333");
mg_set_uri_callback(ctx, "/", &show_index, 0); mg_set_uri_callback(ctx, "/multiply", &multiply, 0); mg_set_uri_callback(ctx, "/result", &get_result, 0);
getchar(); mg_stop(ctx);
return 0;}
static void multiply( struct mg_connection *conn, const struct mg_request_info *request_info, void *user_data){ char *ap = mg_get_var(conn, "multiplier"); char *bp = mg_get_var(conn, "multiplicand");
int a = atol(ap); int b = atol(bp);
result = a * b;
mg_printf(conn, "HTTP/1.1 200 OK\r\n\Content-Type: text/plain\r\n\r\n");
mg_free(multiplier_s); mg_free(multiplicand_s);}
static void multiply( struct mg_connection *conn, const struct mg_request_info *request_info, void *user_data){ char *ap = mg_get_var(conn, "multiplier"); char *bp = mg_get_var(conn, "multiplicand");
int a = atol(ap); int b = atol(bp);
result = a * b;
mg_printf(conn, "HTTP/1.1 200 OK\r\n\Content-Type: text/plain\r\n\r\n");
mg_free(multiplier_s); mg_free(multiplicand_s);}
static void multiply( struct mg_connection *conn, const struct mg_request_info *request_info, void *user_data){ char *ap = mg_get_var(conn, "multiplier"); char *bp = mg_get_var(conn, "multiplicand");
int a = atol(ap); int b = atol(bp);
result = a * b;
mg_printf(conn, "HTTP/1.1 200 OK\r\n\Content-Type: text/plain\r\n\r\n");
mg_free(multiplier_s); mg_free(multiplicand_s);}
static void multiply( struct mg_connection *conn, const struct mg_request_info *request_info, void *user_data){ char *ap = mg_get_var(conn, "multiplier"); char *bp = mg_get_var(conn, "multiplicand");
int a = atol(ap); int b = atol(bp);
result = a * b;
mg_printf(conn, "HTTP/1.1 200 OK\r\n\Content-Type: text/plain\r\n\r\n");
mg_free(multiplier_s); mg_free(multiplicand_s);}
static void get_result( struct mg_connection *conn, const struct mg_request_info *request_info, void *user_data){ mg_printf(conn, "HTTP/1.1 200 OK\r\n\Content-Type: text/plain\r\n\r\n%d", result);}
https://github.com/jnunemaker/httparty
HTTParty
When(/^I multiply (\d+) and (\d+)$/) do |a, b| Calculator.multiply a, bend
Then(/^I should get (\d+)$/) do |n| expect(Calculator.result).to eq(n.to_i)end
require 'httparty'
class Calculator include HTTParty base_uri 'localhost:33333'
def self.multiply(a, b) get "/multiply?multiplier=#{a}\&multiplicand=#{b}" end
def self.result get("/result").to_i endend
(because tl;dr is passé)
In summary:
Have source? Device Technique
yes simple Direct
yes simple Serial
yes mediumCucumber Wire
Protocol
yes powerful Custom TCP
no powerful GUI
Thank youhttps://github.com/undees/cukeup