How to write an integration test for a loop? - ruby

I am having difficulty writing integration (no stubbing) tests for the following scenario: a process (rake task) that runs in a loop, emitting some values. Below is an approximation of the use case.
The test will succeed if I control-C it, but I would like it to catch the success condition and stop.
Anyone has some good suggestions? (stubbing/mocking are not good suggestions). I guess may be there is a way to instruct RSpec to stop a process after a matcher returns success?
describe 'rake reactor' do
it 'eventually returns 0.3' do
expect { Rake::Task['reactor'].execute }.to output(/^0\.3.*/).to_stdout
end
end
class Reactor
def initialize
#stop = false
end
def call
loop do
break if stop?
sleep random_interval
yield random_interval
end
end
def stop
#stop = true
end
def stop?
#stop == true
end
def random_interval
rand(0.1..0.4)
end
end
desc 'Start reactor'
task reactor: :environment do
reactor = Reactor.new
trap(:INT) do
reactor.stop
end
reactor.call { |m| p m }
end

A naïve way to handle it is to start a new thread and send INT from there after some predefined timeout:
before do
Thread.new do
sleep 0.5
Process.kill('INT', Process.pid)
end
end

Related

How can I make sure threads inside my class end after each rspec test?

I have a jruby class which contains a heartbeat that does something every certain number of seconds: (simplified code below)
class Client
def initialise
#interval = 30
#heartbeat = Thread.new do
begin
loop do
puts "heartbeat"
sleep #interval
end
rescue Exception => e
Thread.main.raise e
end
end
end
end
And I have a range of rspec tests that instantiate this class.
At the end of each test, I would expect the object to be destroyed, but the threads seem to remain.
At the moment I've fixed this with:
client.rb:
def kill
#heartbeat.kill
end
rspec:
after(:all) do
client.kill
end
Which seems to do the job - but this doesn't feel like the best way to do it.
What is the best way to approach this?
Using version jruby-9.1.10.0 & rspec 3.7.0
Edit:
As per http://ruby-doc.org/core-2.4.0/Thread.html I would expect the thread to normally terminate when the main thread does
In my tests I instantiate the client with
describe Client do
context 'bla' do
let(:client) do
described_class.new
end
it 'blas' do
end
end
end
You should replace after(:all) with after(:each).
Should be the correct syntax for what you want to do because after(:all) evaluates after all test cases have been run.

Rspec with thread

I have a class that start a Thread when initialize and I can push some actions in this thread from public methods :
class Engine
def initialize
#actions = []
self.start_thread()
end
def push_action(action)
start = #actions.empty?
#actions.push(action)
if start
#thread.run
end
end
protected
def start_thread
#thread = Thread.new do
loop do
if #actions.empty?
Thread.stop
end
#actions.each do |act|
# [...]
end
#actions.clear
sleep 1
end
end
end
end
I'd like to test this class with RSpec to check what happen when I pass some actions. But I don't know how to do that.
Thanks in advance
OK I've found a solution but it's quite dirty :
describe Engine do
describe "#push_action play" do
it "should do the play action" do
# Construct the Engine with a mock thread
mock_thread = double("Useless Thread")
allow(mock_thread).to receive(:run)
expect(Thread).to receive(:new).and_return(mock_thread)
engine = Engine.new
allow(engine).to receive(:sleep)
# Expect that the actual play action will be processed
expect(engine).to receive(:play)
# push the action in the actions' list
engine.push_action(:play)
# Stop the thread loop when the stop() method is called
expect(Thread).to receive(:stop).and_raise(StandardError)
# Manually call again the start_thread() protected method with a yielded thread
# in order to process the action
expect(Thread).to receive(:new).and_yield
expect {engine.send(:start_thread)}.to raise_error(StandardError)
end
end
end
If someone has a better solution I'd be very pleased :)

Monitoring thread variables in ruby

I'm building a task runner where each task is built from a number of commands:
def run
begin
#Validating task params
set_progress "Validating params", "Validating params: #{#params}"
validate_params
#task_info["steps"].each do |step|
#log.info "Running command: #{step["description"]}"
set_progress step["description"]
command = Command.factory #params, step, #signature, #log
timeout = General.in_seconds step["timeout"]
command_res = Timeout.timeout(timeout) do
command.execute
end
end
set_progress "Completed"
rescue Exception => exception
#log.error exception.message + "\nBACK TRACE:\n" + exception.backtrace.join("\n")
set_progress #progress, "Failed, check logs. exception: #{exception.message}"
end
end
Now the command is ran by "command.execute", and there is a field inside the command class which is called "current_status" which i would like to monitor each X seconds and check the command status in order to update the user about the command status, how can i do it ? i probably need to run the command in a separate thread and then monitor it, but how can i monitor it ?
a quick and dirty methodology which might contain syntax errors :P
class JobManager
def initialize
#threads =[]
end
def registered_jobs
#registered_jobs ||= [Job1.new, Job2.new]
end
def start_jobs
registered_jobs.each {|j| #threads << Thread.new { j.run } }
end
def statuses?
registered_jobs.collect {|j| j.status? }
end
end
Usage:
manager = JobManager.new
manager.start_jobs
# elsewhere
manager.statuses? # returns [Job1.status?, Job2.status?]
This is the sort of idiom I'd use in my code. It's important to be aware that the status variables are subject to race conditions if they are not properly guarded against concurrent modification and access.

How can I terminate a SupervisionGroup?

I am implementing a simple program in Celluloid that ideally will run a few actors in parallel, each of which will compute something, and then send its result back to a main actor, whose job is simply to aggregate results.
Following this FAQ, I introduced a SupervisionGroup, like this:
module Shuffling
class AggregatorActor
include Celluloid
def initialize(shufflers)
#shufflerset = shufflers
#results = {}
end
def add_result(result)
#results.merge! result
#shufflerset = #shufflerset - result.keys
if #shufflerset.empty?
self.output
self.terminate
end
end
def output
puts #results
end
end
class EvalActor
include Celluloid
def initialize(shufflerClass)
#shuffler = shufflerClass.new
self.async.runEvaluation
end
def runEvaluation
# computation here, which yields result
Celluloid::Actor[:aggregator].async.add_result(result)
self.terminate
end
end
class ShufflerSupervisionGroup < Celluloid::SupervisionGroup
shufflers = [RubyShuffler, PileShuffle, VariablePileShuffle, VariablePileShuffleHuman].to_set
supervise AggregatorActor, as: :aggregator, args: [shufflers.map { |sh| sh.new.name }]
shufflers.each do |shuffler|
supervise EvalActor, as: shuffler.name.to_sym, args: [shuffler]
end
end
ShufflerSupervisionGroup.run
end
I terminate the EvalActors after they're done, and I also terminate the AggregatorActor when all of the workers are done.
However, the supervision thread stays alive and keeps the main thread alive. The program never terminates.
If I send .run! to the group, then the main thread terminates right after it, and nothing works.
What can I do to terminate the group (or, in group terminology, finalize, I suppose) after the AggregatorActor terminates?
What I did after all, is change the AggregatorActor to have a wait_for_results:
class AggregatorActor
include Celluloid
def initialize(shufflers)
#shufflerset = shufflers
#results = {}
end
def wait_for_results
sleep 5 while not #shufflerset.empty?
self.output
self.terminate
end
def add_result(result)
#results.merge! result
#shufflerset = #shufflerset - result.keys
puts "Results for #{result.keys.inspect} recorded, remaining: #{#shufflerset.inspect}"
end
def output
puts #results
end
end
And then I got rid of the SupervisionGroup (since I didn't need supervision, ie rerunning of actors that failed), and I used it like this:
shufflers = [RubyShuffler, PileShuffle, VariablePileShuffle, VariablePileShuffleHuman, RiffleShuffle].to_set
Celluloid::Actor[:aggregator] = AggregatorActor.new(shufflers.map { |sh| sh.new.name })
shufflers.each do |shuffler|
Celluloid::Actor[shuffler.name.to_sym] = EvalActor.new shuffler
end
Celluloid::Actor[:aggregator].wait_for_results
That doesn't feel very clean, it would be nice if there was a cleaner way, but at least this works.

How to rspec threaded code?

Starting using rspec I have difficulties trying to test threaded code.
Here is a simplicfication of a code founded, and I made it cause i need a Queue with Timeout capabilities
require "thread"
class TimeoutQueue
def initialize
#lock = Mutex.new
#items = []
#new_item = ConditionVariable.new
end
def push(obj)
#lock.synchronize do
#items.push(obj)
#new_item.signal
end
end
def pop(timeout = :never)
timeout += Time.now unless timeout == :never
#lock.synchronize do
loop do
time_left = timeout == :never ? nil : timeout - Time.now
if #items.empty? and time_left.to_f >= 0
#new_item.wait(#lock, time_left)
end
return #items.shift unless #items.empty?
next if timeout == :never or timeout > Time.now
return nil
end
end
end
alias_method :<<, :push
end
But I can't find a way to test it using rspec. Is there any effective documentation on testing threaded code? Any gem that can helps me?
I'm a bit blocked, thanks in advance
When unit-testing we don't want any non-deterministic behavior to affect our tests, so when testing threading we should not run anything in parallel.
Instead, we should isolate our code, and simulate the cases we want to test, by stubbing #lock, #new_item, and perhaps even Time.now (to be more readable I've taken the liberty to imagine you also have attr_reader :lock, :new_item):
it 'should signal after push' do
allow(subject.lock).to receive(:synchronize).and_yield
expect(subject.new_item).to receive(:signal)
subject.push('object')
expect(subject.items).to include('object')
end
it 'should time out if taken to long to enter synchronize loop' do
#now = Time.now
allow(Time).to receive(:now).and_return(#now, #now + 10.seconds)
allow(subject.items).to receive(:empty?).and_return true
allow(subject.lock).to receive(:synchronize).and_yield
expect(subject.new_item).to_not receive(:wait)
expect(subject.pop(5.seconds)).to be_nil
end
etc...

Resources