I have been playing with all major ruby IDEs, but without any doubt JetBrains RubyMine is the best ! Period.
Unfortunately they only provide 1 month free subscription 😦
I have been playing with all major ruby IDEs, but without any doubt JetBrains RubyMine is the best ! Period.
Unfortunately they only provide 1 month free subscription 😦
When you are running all your Cucumber feature scripts from Rake, often you would want to change the default execution order. Currently, cucumber loads all the feature files in ‘features/’ directory in alphabetical order and executes them.
This can be very annoying, specially when you want to execute certain critical features run before running the other not so critical features. Cucumber does not have any provision for feature ordering. But with some minor hacks, you can achieve this.
1st, you need to give each of your features a certain ‘weight’ in a yml file:
features/vcc_admin_create_test.feature: 80 features/transferred_acd_call.feature: 9 features/supervisor_create_test.feature: 8 features/super_force_signout_test.feature: 7 features/simple_queue_call_test.feature: 6 features/queue_delivered_count_test.feature : 67 features/queue_create_test.feature : 13 features/queue_call_treatment_test.feature : 12 features/email_status_monitor_test.feature : 11 features/email_forward.feature : 10 features/dynamic_and_static_number_test.feature : 9 features/agent_profile_change_test.feature : 8 features/agent_email_receiver_test.feature : 7 features/agent_create_test.feature : 6 features/agent_acd_state_test.feature : 5 features/agent_acd_call_duration_test.feature : 4 features/agent_acd_call_count_test.feature : 3 features/admin_call_distribution_policy_test.feature : 2 features/CC_create_test.feature : 1
next, update your env.rb file for adding a hook:
# Overrides the method +method_name+ in +obj+ with the passed block
def override_method(obj, method_name, &block)
# Get the singleton class/eigenclass for 'obj'
klass = class <<obj; self; end
# Undefine the old method (using 'send' since 'undef_method' is protected)
klass.send(:undef_method, method_name)
# Create the new method
klass.send(:define_method, method_name, block)
end
def get_weight(x)
weights = YAML::load(File.open('order.yml')) #this is expensive but is done only on rake start
weight = weights[x]
if weight == nil or weight == ''
weight = '100' #this is because we want to run the newly added tests first.
end
return weight.to_i()
end
def sort_according_to_weights(features)
return features.sort { |x,y| get_weight(y) <=> get_weight(x)}
end
AfterConfiguration do |configuration|
puts "\n\n\n ***** config: #{configuration} "
featurefiles = configuration.feature_files
override_method(configuration, :feature_files) {
puts "overriding the old featurefiles according to their weight"
sorted_files = sort_according_to_weights(featurefiles);
sorted_files
}
puts "\n\n *************************** features will be executed in following order: \n"
for i in configuration.feature_files
puts "#{i} : weight: #{get_weight(i)}"
end
end
After Cucumber loads the feature files from the directory, it calls “AfterConfiguration” before it starts executing the features. Sadly the configuration.feature_files is not an array which you can just sort. Its a method ! But with ruby being a dynamic language, you can push new method definition into the configuration instance so that it returns the features according to the order you specified !
Sipper is a good framework for doing functional automation of any sip call flow. Cucumber is a great tool for writing your functional test cases in plain text and execute them. Cucumber is mostly used for doing web automation with other tools like Watir. But now a days, VOIP meets Web now and then, And you are left with the challenge of integrating your web automation with your sip automation. For example, you want to automate “When a call comes to my sip phone, I want to see a javascript popup in my screen”
Sipper comes with its testing/assertion/validation framework. First things first, we need to drop them. We are only interested into the Sip Stack and Call abstraction framework provided by sipper. We will do the assertion from our Cucumber framework.
First of all, you need to be able to run sipper in standalone mode rather than using the ‘rake’ provided by sipper framework. You need to bootstrap sipper framework from your ruby code. You can do that with the following code (usually you would want to put it in your support/env.rb configuration file for cucumber) :
require 'sipper'
require 'sipper_configurator'</code>
cf = File.join("C:/dev/projects/myproject/qa/sipper_tests/cucumber_tests/", "config", "sipper.cfg")
SipperConfigurator.load_yaml_file(cf)
@sipper = SIP::Sipper.new()
@sipper.start
Now that you have started sipper, you can register any controller in it from your cucumber steps. Lets say we have a step like this:
Then I start customer's phone
You would want to start the controller that ‘REGISTER’ for the customer’s phone and wait for any incoming call.
Then /^I start customer's phone$/ do
puts "Going to start Controller: CallReceiverCustomerController"
@customer_controller = @sipper.start_named_controller("CallReceiverCustomerController")
sleep 3
end
I have extended the standard sipper controller so that I can call it and control it from any ruby application. Basically this will act as a SIP phone and provide me the nice API’s
require 'sip_test_driver_controller'
# NOTE: Extending Core Sipper controller to provide programmable API to simple RUBY client. The ruby client
# can directly call this controller to control the call flows.
class CommonCallController < SIP::SipTestDriverController
# change the directive below to true to enable transaction usage.
# If you do that then make sure that your controller is also
# transaction aware. i.e does not try send ACK to non-2xx responses,
# does not send 100 Trying response etc.
transaction_usage :use_transactions=>false
# change the directive below to true to start after loading.
start_on_load false
def initialize
logd('CallReceiverController: Controller created')
clear_states()
end
def clear_states()
@registered = false
@current_session = nil
@call_ended = false
@got_invite=false
end
def specified_transport
log " specified_transport #{get_local_sipper_ip()} : #{get_local_sipper_port()}"
[get_local_sipper_ip(),get_local_sipper_port()]
end
def start
if required_registration
register()
end
return self
end
def register()
log "doing registration with " + get_line_port
session = create_udp_session(SipperConfigurator[:DefaultRIP], SipperConfigurator[:DefaultRP])
r = session.create_initial_request("REGISTER", "sip:" + SipperConfigurator[:BroadworksDomain] ,
:from=> "sip:"+ get_line_port, :to=>"sip:"+ get_line_port,
:contact=> "sip:" + get_line_port_without_domain + "@"+ get_local_sipper_ip() +":"+get_local_sipper_port().to_s(),
:expires=>"300",
:cseq=>"1 REGISTER",
:p_session_record=>"msg-info")
r.contact.q="0.9"
session = create_udp_session(SipperConfigurator[:DefaultRIP], SipperConfigurator[:DefaultRP])
session.send(r)
@current_session = session
log "---------session: #{session}"
end
def required_registration
true
end
def on_success_res_for_register(s)
@current_session = s
log "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ on_success_res_for_register"
@registered = true
s.invalidate(true);
end
def on_success_res(s)
@current_session = s
log '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ on_success_res ->'
@registered = true
s.invalidate(true);
end
def on_failure_res(session)
sleep 10
log "on_failure_res error code: #{session.iresponse.code}"
puts "\n -----------------------------**************** #{caller}"
if session.iresponse.code == 401
r = session.create_request_with_response_to_challenge(session.iresponse.www_authenticate, false,get_auth_id, get_auth_password)
session.send r
session[:auth] = r.authorization
end
end
def is_ringing()
return @got_invite
end
#for the first invite it will send 180, for all other re-invite, it will send 200
def on_invite(session)
if !session['invite']
log "got invite------------"
@current_session = session
log 'got call from Queue '
session.respond_with(100)
session.respond_with(180)
session['invite']=1
@got_invite=true
else
log "got re invite------------"
log 'CallReceiverController: got re-invite'
if @accept_call
session.respond_with(200)
end
end
end
def make_call(uri)
log "make_call : #{uri}"
session = create_udp_session('10.0.0.27', 5060)
session.request_with('INVITE', uri)
#@current_session = session
end
def accept_call()
log "accept_call #{@current_session}"
@accept_call=true
@current_session.respond_with(200)
end
def end_call()
log "ending call #{@current_session}"
if @current_session
log 'sending bye'
@current_session.rp = SipperConfigurator[:DefaultRP]
@current_session.request_with('BYE')
end
end
def on_bye(session)
log "*******************got bye"
@call_ended = true
session.respond_with(200)
session.invalidate(true)
end
def is_call_ended()
return @call_ended
end
def last_session_state
@current_session.last_state
end
def get_local_sipper_port
SipperConfigurator[:LocalSipperPort][4]
end
protected :get_local_sipper_port
def get_local_sipper_ip
SipperConfigurator[:LocalSipperIP]
end
protected :get_local_sipper_ip
def get_line_port
SipperConfigurator[:AgentLinePort]
end
protected :get_line_port
def controller_name
self.class.name
end
def get_line_port_without_domain
SipperConfigurator[:AgentLinePortWithoutDomain].to_s
end
protected :get_line_port_without_domain
def get_auth_password
SipperConfigurator[:CustomerAuthenticationPassword]
end
protected :get_auth_password
def get_auth_id
SipperConfigurator[:CustomerAuthenticationUserId]
end
protected :get_auth_id
def log(argument)
puts controller_name + " : " + argument
end
end
Our CallReceiverCustomerController is a simple subclass of the CommonCallController to provide the SIP Registration Details so that it can register to any sip provider (in our case Broadworks).
require 'common_call_controller'
class CallReceiverCustomerController < CommonCallController
def initialize
puts "initialize"
super()
log 'CallReceiverControllerAgent: Controller created'
end
def get_local_sipper_port
SipperConfigurator[:LocalSipperPort][6]
end
protected :get_local_sipper_port
def get_local_sipper_ip
SipperConfigurator[:LocalSipperIP]
end
protected :get_local_sipper_ip
def get_line_port
SipperConfigurator[:AgentLinePort]
end
protected :get_line_port
def controller_name
self.class.name
end
def get_line_port_without_domain
SipperConfigurator[:AgentLinePortWithoutDomain].to_s
end
protected :get_line_port_without_domain
def get_auth_password
SipperConfigurator[:AgentAuthenticationPassword]
end
protected :get_auth_password
def get_auth_id
SipperConfigurator[:AgentAuthenticationUserId]
end
protected :get_auth_id
end
Now the rest is simple, you can add cucumber steps for like this :
Then customer Should get Alerting on his phone
Then customer waits for 10 seconds
Then customer accepts the phone
Then customer waits for 20 seconds
Then customer hangups the phone
and the steps definitions like this:
Then /^customer accepts the Call$/ do
puts "Agent is accepting the Call"
@customer_controller.accept_call()
end
Then /^customer hangs up the Call$/ do
puts "Agent is hanging up the Call"
@customer_controller.end_call()
end
Then /^customer should get alerting on his phone$/ do
i = 0
#wait for the ringing event to come
while ! @customer_controller.is_ringing() && i < 10
sleep 1
i += 1
end
if !@customer_controller.is_ringing()
raise "Agent didn't get any incoming call"
end
end
There you go. The beauty of this approach is that now, even your product owner can write test steps without having any clue of SIP / SIPPER. This is just plain english!
The "CommonCallController" I wrote is neither perfect not elegant. It uses instance variables to store the call state which can get wild for unexpected call flows. But It can easily be modified to suit your need. For me, it does its job for simple call flows.
There are plenty of documents on the web about how to use cucumber with Watir to automate webapplication testing..so I am not gonna talk about that.
What else can you do in a dull Saturday evening 😉
-module (sorting).
-compile (export_all).
merge_sort([]) -> [];
merge_sort(L) ->
{L1 , L2} = split(L),
merge(merge_sort(L1),merge_sort(L2)).
merge(X , Y) ->
merge(X,Y,[]).
merge([], [] , X) -> X;
merge([] , S2, X) -> X ++ S2;
merge(S1 , [], X) -> X ++ S1;
merge([H1 | T1] , [H2 | T2], X) when H1 >= H2 ->
merge( [H1 | T1] , T2 , append(X,[H2])); %tail recursion
merge([H1 | T1] , [H2 | T2], X) ->
merge( T1 , [H2 | T2], append(X,[H1])). %tail recursion
append(X , Y) ->
X ++ Y. % appending two list ! how cool!
split(L) ->
split(L , {[] , []}).
split([] , {X , Y}) -> { X , Y };
split([H|T] , {X , Y}) ->
split( T , { Y , append(X , [H]) }). %tail recursion