Introducing
Epoxy
A flexible test framework
Red Hat QA meet-up
Camping Baldovec, Czech Republic
June 2013
Nikolai Kondrashov
Sr. QA Engineer at Red Hat IDM QE.
Formerly Sr. System Software Engineer
at NVIDIA Tegra graphics team.
Open Source developer.
Founder of DIGImend project - improving
Linux support for generic graphics tablets.
EPOXY
Bash test framework
An alternative to BeakerLib
3 KLOC in Bash, Lua and Python
Nearing first release, already useful
Personal spare-time project
a definition
— all verify an assertion
in place of "assertion verification"
any of "suite", "test phase", or "test"
Why not BeakerLib?
Limits assertion structuring
Produces noisy output
Can't verify a subset of assertions
Ignores setup/teardown failures
Limited structuring
"suite", "phase", "assertion"
or integrate by external means
Three is not enough
#!/bin/bash
# Phase
rlPhaseStartTest "defaults"
# Assertion
rlRun client_sudo_user_requires_auth 0 "defaults_without"
# Assertion
rlRun client_sudo_user_is_allowed 0 "defaults_with"
rlPhaseEnd
# Phase
rlPhaseStartTest "config"
# Assertion
rlRun client_sudo_user_is_allowed 0 "config_all_options"
rlPhaseEnd
How about something more complex?
Noisy output
script output while running
result messages as a workaround
and spot failures and their causes
Spot the failure
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :: [ LOG ] :: simple_access_001: where simple_allow_groups = simple group1 :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: spawn ssh -o StrictHostKeyChecking=no -l simple userB localhost simple userB@localhost's password: Last login: Thu Jun 27 10:19:34 2013 from localhost.localdomain [simple userB@client-rhel6 ~]$ :: [ PASS ] :: Authentication successfull, as expected spawn ssh -o StrictHostKeyChecking=no -l simple userE localhost simple userE@localhost's password: Permission denied, please try again. simple userE@localhost's password: :: [ FAIL ] :: ERROR: Authentiation failed, not as expected Jun 27 10:38:16 client-rhel6 sshd[484]: pam_sss(sshd:account): Access denied for user simple userD: 6 (Permission denied) :: [ PASS ] :: Running 'cat /var/log/secure | grep "pam_sss(sshd:account): Access denied for user simple[ ]userD: 6 (Permission denied)"' :: [ PASS ] :: Authentication successfull connection closed, as expected :: [ PASS ] :: Running '> /var/log/secure' Jun 27 10:38:22 client-rhel6 sshd[508]: pam_sss(sshd:account): Access denied for user simple userF: 6 (Permission denied) :: [ PASS ] :: Running 'cat /var/log/secure | grep "pam_sss(sshd:account): Access denied for user simple[ ]userF: 6 (Permission denied)"' :: [ PASS ] :: Authentication successfull connection closed, as expected
Monolithic suites
a few tests, skipping others
edit/test loop becomes painfully slow
which leads to mistakes
Go make a coffee
# time SERVER="server.sss-test.test" CLIENT="client-rhel6.sss-test.test" make :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :: [ LOG ] :: defaults :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :: [ PASS ] :: defaults_without :: [ PASS ] :: defaults_with . . . :: [ PASS ] :: attrs_command_paranoid_non_equal2_match_negative :: [ PASS ] :: attrs_command_paranoid_non_equal2_mismatch :: [ 18:34:04 ] :: JOURNAL XML: /var/tmp/beakerlib-y2ii2jZ/journal.xml :: [ 18:34:04 ] :: JOURNAL TXT: /var/tmp/beakerlib-y2ii2jZ/journal.txt result_server not set, assuming developer mode. Setting client-rhel6.sss-test.test to state DONE real 10m28.666s user 0m10.454s sys 0m8.929s
10 minutes
Got milk?
Setup/teardown neglect
a setup/teardown command failed
complicates debugging
command status, as "set -e" is not supported
Wait, what?
:: [ FAIL ] :: Running 'cat /var/log/secure | grep "pam_sss(sshd:account): Access denied for user simple[ ]userC: 6 (Permission denied)"' (Expected 0, got 1)
50 lines back...
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :: [ LOG ] :: simple_group_Setup :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ldap_bind: Invalid credentials (49) :: [ FAIL ] :: Running 'ldapmodify -x -h server.sss-test.test -D "cn=Manager,dc=example,dc=com" -w Secret124 -a -f /tmp/setup.ldif' (Expected 0, got 49) :: [ 10:43:23 ] :: Server Setup: :: [ 10:43:23 ] :: simple group1 => simple UserA simple UserB simple Group3 :: [ 10:43:23 ] :: simple Group2 => simple UserC simple UserD :: [ 10:43:23 ] :: simple Group3 => simple UserE :: [ 10:43:23 ] :: simple UserF (belongs to no group/its own group) :: [ 10:43:23 ] ::
More definitions
"suite" and "test"
code and other suites and tests
if all its sub-suites and tests pass
How does it look?
#!/bin/bash
# Add Epoxy module directory to PATH
eval "`ep_env || echo exit 1`"
# Source epoxy modules
. ep.sh
# Initialize the suite shell, process command line arguments
ep_suite_init "$@"
# Setup
declare TMP_DIR="`mktemp -d`"
# Add a teardown command
ep_teardown_push rm -Rf "$TMP_DIR"
# More setup and teardown
pushd "$TMP_DIR" >/dev/null
ep_teardown_push eval 'popd >/dev/null'
But where's the meat?
That was only the beginning
# Run a test
ep_test "touch_present" command -v "test"
# Run another test
ep_test "rm_present" command -v "rm"
# Run a sub-suite
ep_suite_begin "file"; (
ep_suite_init
# Sub-suite teardown
ep_teardown_push rm -f "file1" "file2"
# A test
ep_test "create" touch "file1"
# Setup
touch "file2"
# Another test
ep_test "remove" rm "file2"
); ep_suite_end
So Why Epoxy instead?
Produces terse, clear output
Assertion nesting
and referred to by path names
as an executable, a shell function, or a subshell
and splitting arbitrarily large suites.
It takes all kinds
function test_function()
{
true
}
function suite_function()
{
ep_test "executable_test" true
ep_test_sh "function_test" test_function
}
ep_suite_begin "subshell_suite"; (
ep_suite_init
ep_suite_sh "function_suite" suite_function
ep_test_begin "subshell_test"; (
ep_test_init
true
); ep_test_end
); ep_suite_end
and combines them
#!/bin/bash
eval "`ep_env || echo exit 1`"
. ep.sh
ep_suite_init "$@"
declare depth="${EP_SUITE_ARGS[0]-0}"
declare i
if ((depth >= 3)); then
exit
fi
for ((i = 1; i <= 3; i++)); do
ep_suite "suite$i" "$0" -- "$((depth + 1))"
done
Into recursion!
CLEAR OUTPUT
lines are clearly separated
outputting one assertion per line
conversion to BeakerLib log format
Eat'em raw
$ ./example -r STRUCT BEGIN '' STRUCT BEGIN '/file' STRUCT BEGIN '/file/create' STRUCT END '/file/create' PASSED STRUCT BEGIN '/file/remove' STRUCT END '/file/remove' PASSED STRUCT BEGIN '/file/stress' STRUCT BEGIN '/file/stress/1' OUTPUT Creating 1 STRUCT END '/file/stress/1' PASSED STRUCT BEGIN '/file/stress/2' OUTPUT Creating 2 STRUCT END '/file/stress/2' PASSED STRUCT BEGIN '/file/stress/3' OUTPUT Creating 3 STRUCT END '/file/stress/3' PASSED OUTPUT Removing 1 OUTPUT Removing 2 OUTPUT Removing 3 STRUCT END '/file/stress' PASSED STRUCT END '/file' PASSED STRUCT END '' PASSED
Or cook'em
$ ./example /file/create PASSED /file/remove PASSED /file/stress/1 PASSED /file/stress/2 PASSED /file/stress/3 PASSED /file/stress PASSED /file PASSED PASSED
Suppress THem
$ ./example -f-b2 /file/create PASSED /file/remove PASSED /file/stress PASSED /file PASSED PASSED
Or convert them
$ ./example -r | ep_log_beakerlib :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :: [ LOG ] :: /file :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :: [ PASS ] :: /file/create :: [ PASS ] :: /file/remove :: [ PASS ] :: /file/stress :: [ 17:05:37 ] :: JOURNAL XML: /var/tmp/beakerlib-XpmJyEb/journal.xml :: [ 17:05:37 ] :: JOURNAL TXT: /var/tmp/beakerlib-XpmJyEb/journal.txt
Test selection
matching sub-suites/tests to run
command line or environment variables
speeds up development
Cut to THE CHASE
$ ./example_long /file/create PASSED /file/remove PASSED /file/stress/1 PASSED /file/stress/2 PASSED /file/stress/3 PASSED /file/stress/4 PASSED /file/stress/5 PASSED /file/stress PASSED /file PASSED /dir/create PASSED /dir/remove PASSED /dir/stress/1 PASSED /dir/stress/2 PASSED /dir/stress/3 PASSED /dir/stress/4 PASSED /dir/stress/5 PASSED /dir/stress PASSED /dir PASSED PASSED
$ ./example_long /file/create /dir/remove /file/create PASSED /file PASSED /dir/remove PASSED /dir PASSED PASSED
$ ./example_long /file/@(create|remove|stress/[123]) /file/create PASSED /file/remove PASSED /file/stress/1 PASSED /file/stress/2 PASSED /file/stress/3 PASSED /file/stress PASSED /file PASSED PASSED
Setup/teardown CARE
All code runs with "set -e"
setup/teardown code
stops and proceeds to teardown
the whole suite stack terminates
No chance for another error
ep_test dusk true
ep_test bar true
ep_test vampires true
shotgun
ep_test dawn true
/dusk PASSED
/bar PASSED
/vampires PASSED
ERRORED "./dusk_dawn: line 10: shotgun: command not found"
Don't panic
ep_suite_begin Earth; ( ep_suite_init ep_suite_begin Arthur; ( ep_suite_init ep_teardown_push "lifting ray" ep_test bulldozers true touch earth ep_test bar true ep_test beer true ep_test vogons true ); ep_suite_end
ep_suite_begin Ford; ( ep_suite_init ep_test towel true ); ep_suite_end ); ep_suite_end
do
panic!
/Earth/Arthur/bulldozers PASSED /Earth/Arthur/bar PASSED /Earth/Arthur/beer PASSED /Earth/Arthur/vogons PASSED /Earth/Arthur PANICKED "ep_teardown.sh: line 55: lifting ray: command not found" /Earth PANICKED PANICKED
Why else Epoxy?
assertions
assertion verification results
attached to any assertion
What does it do already?
Tests itself (1.3 KLOC)
Tests Carton (2.3 KLOC)
Why not Epoxy?
deeper understanding of Bash
sometimes non-obvious
— implement as a library on top
What's still missing?
TCMS (Nitrate) integration
Suite/test code consistency verification
Minor fixes
Extensive testing
What's next?
Where to get?
How to help?
so work on it is approved and
can proceed faster
Thank you!
Epoxy - a flexible test framework
By Nikolai Kondrashov
Epoxy - a flexible test framework
- 7,239