Showing posts with label cross-stub. Show all posts
Showing posts with label cross-stub. Show all posts

Wednesday, July 21, 2010

Stubbing only the nth instance's method, should or shouldn't ?

A few days back, cross-stub version 0.2.0 has been released, with this release, cross process instance method stubbing is now possible. While doing review of zan's work, we've discussed the possibility of having more control on which exact instance the stub should be applied to, as illustrated below:
1
2
Time.xstub(:day, :instance => 1)    # applies to ONLY 1st Time instance
Time.xstub(:day, :instance => 2..3) # applies to ONLY 2nd ~ 3rd Time instances

Initially, i was quite happy to have brought up this. But after some thinking, i'm convinced that this a LOUSY idea, as it requires the user to have intimate knowledge of how many times a class's instance have been created, which can be tedious (if not impossible) if the instance method is provided by ruby core/std libs, or popular gems. It also results in extremely brittle tests, as nth can easily beconme xth, or zth ...

I think the issue is very fundamental, it goes down to how the code is written. Given the following example:
1
2
3
4
5
6
7
8
9
10
11
12
13
  class Papa
def free_today?
# Any papa is free on weekend but not weekday
!Time.now.strftime('%w').between?(0,5)
end
end

class Mama
def free_today?
# Any mama is free on weekday but not weekend
Time.now.strftime('%w').between?(0,5)
end
end

If i were to apply stubbing for Time#strftime, i would affect both Papa#free_today? & Mama#free_today?. Taking one step back, if we to affect only Papa#free_today?, we need to consider the context where the Time#strftime stub is-to-be applied, & do the following revision:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Papa

def free_today?
!today_dow.between?(0,5)
end

def today_dow
Time.now.strftime('%w')
end

end

class Mama

def free_today?
today_dow.between?(0,5)
end

def today_dow
Time.now.strftime('%w')
end

end

Therefore, by stubbing Papa#today_dow, we won't affect Mama#free_today?. Moreover, taking this approach gives us the chance to do some cleaning up:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Parent
def today_dow
Time.now.strftime('%w')
end
end

class Papa < Parent
def free_today?
!today_dow.between?(0,5)
end
end

class Mama < Parent
def free_today?
today_dow.between?(0,5)
end
end

Imagine if we go the other approach of doing nth instance's method stubbing, though feasible, the test will be extremely brittle (nightmarish maintenance), as nth can easily be changed to xth, or zth.

CONCLUSION: No, we don't need better control over nth instance's method stubbing, cross-stub's instance method stubbing is going to remain as it is. If u need it, drop me a mail with an example to justify the need :]

Sunday, July 18, 2010

Tidying up specs suite with otaku

In the specs suite of cross-stub, i've built a custom echo server using this approach to support testing of stubbing in another process. Nothing wrong with it, except that it is not so fun & clean. Anyway, over the weekend, i've extracted out the echo server code, and put it inside a new & fun project otaku.

Here's my tidied up specs (which serve as a good example usage of otaku):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
shared 'has other process setup' do

behaves_like 'has standard setup'

before do
# Don't start the service if it has already been started.
$service_started ||= (
Otaku.start do |data|

# Pull in all the helpers, __FILE__ magically converts to path of this file,
# even after otaku's internal marshalling/unmarshalling.
require File.join(File.dirname(__FILE__), '..', 'includes')

# Some processing ... using helpers & class definitions pulled in by the above
# require statement.
store_type, method_call_args = data.match(/^(.*?)\/(.*)$/)[1..2]
CrossStub.refresh(cache_store($prev_store_type)) if $prev_store_type
CrossStub.refresh(cache_store($prev_store_type = store_type))

# Actually capturing of error is not necessary, cos Otaku captures any error &
# wrap it up in Otaku::DataProcessError.
do_local_method_call(method_call_args) rescue $!.message
end

true
)
end

end

at_exit do
# Stops otaku if it has been started, only this process is abt to exit.
$service_started && Otaku.stop
end

Thursday, July 15, 2010

Building a simple server/client with eventmachine

In writing cross-stub, i need to write specs to ensure cross process stubbing indeed work as expected. This is achieved by building a simple server/client using eventmachine, here's how echo_server.rb looks like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
require 'rubygems'
require 'eventmachine'
require 'logger'
require 'base64'

module EchoServer

ADDRESS = '127.0.0.1'
PORT = 10999
LOG_FILE = '/tmp/echo_server.log'
INIT_WAIT_TIME = 2 # may need to increase this depending on how slow is ur machine

class << self

def log(*msg)
(@logger ||= Logger.new(LOG_FILE)) << [msg, ""].flatten.join("\n")
end

def cleanup
@logger.close
end

def start(other_process=false)
unless other_process
print 'Starting echo service ... '
@process = IO.popen("ruby #{__FILE__}",'r')
sleep INIT_WAIT_TIME
puts 'done (pid#%s)' % @process.pid
else
log 'Echo service (pid#%s) listening at %s:%s' % [Process.pid, ADDRESS, PORT]
EventMachine::run { EventMachine::start_server(ADDRESS, PORT, EM) }
end
end

def stop
Process.kill('SIGHUP', @process.pid) if @process
end

end

private

module EM

def receive_data(data)
log 'Received client data: %s' % data.inspect
result = process_data(data)
log 'Processed to yield result: %s' % result.inspect
send_data(Base64.encode64(Marshal.dump(result)))
end

def process_data(data)
# do some meaningful processing to yield result
result = '~ processed %s ~ ' % data
end

def log(*msg)
EchoServer.log(*msg)
end

end

end

if $0 == __FILE__
begin
EchoServer.start(true)
rescue
EchoServer.log "#{$!.inspect}\n"
ensure
EchoServer.cleanup
end
end

And echo_client.rb:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# Keeping things DRY, avoid repeated typing of requiring statements &
# server specific info (eg. address & port)
require File.join(File.dirname(__FILE__), 'echo_server')

module EchoClient

ADDRESS = EchoServer::ADDRESS
PORT = EchoServer::PORT

class << self
def get(data)
EventMachine::run do
EventMachine::connect(ADDRESS, PORT, EM).execute(data) do |data|
@result = Marshal.load(Base64.decode64(data))
end
end
@result
end
end

private

module EM

def receive_data(data)
@callback.call(data)
EventMachine::stop_event_loop # ends loop & resumes program flow
end

def execute(method, &callback)
@callback = callback
send_data(method)
end

end

end

And finally the example usage:
1
2
3
4
5
6
7
8
9
10
require File.join(File.dirname(__FILE__), 'echo_server')
require File.join(File.dirname(__FILE__), 'echo_client')

EchoServer.start

10.times do |i|
puts EchoClient.get(i)
end

EchoServer.stop

Personally, i prefer placing the server & client code inside a single file echo_service.rb, since the client code is pretty coupled with the server code. One gotcha i've encountered while working with this server/client thingy is that error in the server hangs the client, that's why having the server log file is really useful in debugging.

Marshal/Unmarshal problem with Jruby

While working on cross-stub today, ran into a marshal/unmarshal problem with jruby, caused by binary string encoding issue. Anyway, let's start by taking a look at some code:
1
2
3
4
5
6
time1 = Time.now
time2 = Marshal.load(
%|#{Marshal.dump(time1).gsub('|','\|')}|
)

puts time1 == time2

Here's what i got when running the above in different rubies:

#1. The expected output of Ruby 1.8.7 & Ruby 1.9.1 & Ree 1.8.7
# >> true


#2. The unexpected output of Jruby-1.5.1
# >> (eval):1:in `_load': marshaled time format differ (TypeError)

Did quite abit of research, either my googling skill sucks, or nobody has the same problem as me ... yet eventually found something at stackoverflow, which lights up the bulb, & here's the amended code that works for all the above-mentioned rubies:
1
2
3
4
5
6
7
8
9
require 'base64'

time1 = Time.now
time2 = Marshal.load(
Base64.decode64(
%|#{Base64.encode64(Marshal.dump(time1)).gsub('|','\|')}|
))

puts time1 == time2

Wednesday, July 14, 2010

Minimalist ... even more minimal than BasicObject

In cross-stub, it is possible to declare stubs using a proc:
1
2
3
4
5
  Someone.xstub do
def say(something)
'saying "%s"' % something
end
end

Implementation wise, i need a way to find out the methods delared within the proc in order to do further processing. The most straight-forward way is to have a bare minimum object, after instance_eval of the proc, do a diff of the original set of instance methods & the set of methods for this manipulated object. The following illustrates the concept:
1
2
(minimalist = Minimalist.new).instance_eval(&block)
minimalist.methods - Minimalist.instance_methods

Well, i've in fact considered using BasicObject, but then, it doesn't appear to be as basic as i hope it should it. Thus i decided to handroll my own Minimalist solution:
1
2
3
4
5
class Minimalist
alias_method :__instance_eval__, :instance_eval
alias_method :__methods__, :methods
instance_methods.each{|meth| undef_method(meth) if meth.to_s !~ /^__.*?__$/ }
end

As u can see (in order to prevent any methods clash with user-defined-to-be-stubbed-methods) what i've done is just:

#1. renaming instance_eval & methods to the more cryptic __instance_eval__ & __methods__, and
#2. undefining all methods, except those with prepending & appending __

It works well in ruby-1.8.7, but then, running the above in ruby-1.9.1 displays the following warning:

warning: undefining `object_id' may cause serious problem

With the solution suggested in this discussion, Minimalist is revised to:
1
2
3
4
5
6
7
class Minimalist
alias_method :__instance_eval__, :instance_eval
alias_method :__methods__, :methods
$VERBOSE = nil
instance_methods.each{|meth| undef_method(meth) if meth.to_s !~ /^__.*?__$/ }
$VERBOSE = true
end

Now the warning doesn't show. Yet in ruby-1.8.7, i got plenty of eval warnings:

(eval):1: warning: discarding old say
(eval):1: warning: discarding old say
(eval):1: warning: method redefined; discarding old bang

Hmmm ... undaunted, i got this final version of Minimalist:
1
2
3
4
5
6
7
class Minimalist
alias_method :__instance_eval__, :instance_eval
alias_method :__methods__, :methods
orig_verbosity, $VERBOSE = $VERBOSE, nil
instance_methods.each{|meth| undef_method(meth) if meth.to_s !~ /^__.*?__$/ }
$VERBOSE = orig_verbosity
end

Yet for jruby-1.5.1, it yields:

warning: `instance_eval' should not be aliased

The final final final (i promise) Minimalist becomes:
1
2
3
4
5
class Minimalist
orig_verbosity, $VERBOSE = $VERBOSE, nil
# (method aliasing & removal stays here)
$VERBOSE = orig_verbosity
end

Labels