diff options
author | luke <luke@980ebf18-57e1-0310-9a29-db15c13687c0> | 2006-01-24 06:01:58 +0000 |
---|---|---|
committer | luke <luke@980ebf18-57e1-0310-9a29-db15c13687c0> | 2006-01-24 06:01:58 +0000 |
commit | ae2575b45de1e8f4c0ec956cebe0eed2bafbcf57 (patch) | |
tree | 9c2b7c839087c285c228374f525315e55c392a34 /lib/puppet/event-loop | |
parent | 18e8e74a2e3b4c5d092fc0aae38bbc5455d4db48 (diff) | |
download | puppet-ae2575b45de1e8f4c0ec956cebe0eed2bafbcf57.tar.gz puppet-ae2575b45de1e8f4c0ec956cebe0eed2bafbcf57.tar.xz puppet-ae2575b45de1e8f4c0ec956cebe0eed2bafbcf57.zip |
Adding the event-loop stuff to the repository and switching to using it. Also, breaking many classes out into their own class files.
git-svn-id: https://reductivelabs.com/svn/puppet/trunk@848 980ebf18-57e1-0310-9a29-db15c13687c0
Diffstat (limited to 'lib/puppet/event-loop')
-rw-r--r-- | lib/puppet/event-loop/better-definers.rb | 367 | ||||
-rw-r--r-- | lib/puppet/event-loop/event-loop.rb | 355 | ||||
-rw-r--r-- | lib/puppet/event-loop/signal-system.rb | 220 |
3 files changed, 942 insertions, 0 deletions
diff --git a/lib/puppet/event-loop/better-definers.rb b/lib/puppet/event-loop/better-definers.rb new file mode 100644 index 000000000..0af37da62 --- /dev/null +++ b/lib/puppet/event-loop/better-definers.rb @@ -0,0 +1,367 @@ +## better-definers.rb --- better attribute and method definers +# Copyright (C) 2005 Daniel Brockman + +# This program is free software; you can redistribute it +# and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation; +# either version 2 of the License, or (at your option) any +# later version. + +# This file is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty +# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public +# License along with this program; if not, write to the Free +# Software Foundation, 51 Franklin Street, Fifth Floor, +# Boston, MA 02110-1301, USA. + +class Symbol + def predicate? + to_s.include? "?" end + def imperative? + to_s.include? "!" end + def writer? + to_s.include? "=" end + + def punctuated? + predicate? or imperative? or writer? end + def without_punctuation + to_s.delete("?!=").to_sym end + + def predicate + without_punctuation.to_s + "?" end + def imperative + without_punctuation.to_s + "!" end + def writer + without_punctuation.to_s + "=" end +end + +class Hash + def collect! (&block) + replace Hash[*collect(&block).flatten] + end + + def flatten + to_a.flatten + end +end + +module Kernel + def returning (value) + yield value ; value + end +end + +class Module + def define_hard_aliases (name_pairs) + for new_aliases, existing_name in name_pairs do + new_aliases.kind_of? Array or new_aliases = [new_aliases] + for new_alias in new_aliases do + alias_method(new_alias, existing_name) + end + end + end + + def define_soft_aliases (name_pairs) + for new_aliases, existing_name in name_pairs do + new_aliases.kind_of? Array or new_aliases = [new_aliases] + for new_alias in new_aliases do + class_eval %{def #{new_alias}(*args, &block) + #{existing_name}(*args, &block) end} + end + end + end + + define_soft_aliases \ + :define_hard_alias => :define_hard_aliases, + :define_soft_alias => :define_soft_aliases + + # This method lets you define predicates like :foo?, + # which will be defined to return the value of @foo. + def define_readers (*names) + for name in names.map { |x| x.to_sym } do + if name.punctuated? + # There's no way to define an efficient reader whose + # name is different from the instance variable. + class_eval %{def #{name} ; @#{name.without_punctuation} end} + else + # Use `attr_reader' to define an efficient method. + attr_reader(name) + end + end + end + + def writer_defined? (name) + method_defined? name.to_sym.writer + end + + # If you pass a predicate symbol :foo? to this method, it'll first + # define a regular writer method :foo, without a question mark. + # Then it'll define an imperative writer method :foo! as a shorthand + # for setting the property to true. + def define_writers (*names, &body) + for name in names.map { |x| x.to_sym } do + if block_given? + define_method(name.writer, &body) + else + attr_writer(name.without_punctuation) + end + if name.predicate? + class_eval %{def #{name.imperative} + self.#{name.writer} true end} + end + end + end + + define_soft_aliases \ + :define_reader => :define_readers, + :define_writer => :define_writers + + # We don't need a singular alias for `define_accessors', + # because it always defines at least two methods. + + def define_accessors (*names) + define_readers(*names) + define_writers(*names) + end + + def define_opposite_readers (name_pairs) + name_pairs.collect! { |k, v| [k.to_sym, v.to_sym] } + for opposite_name, name in name_pairs do + define_reader(name) unless method_defined? name + class_eval %{def #{opposite_name} ; not #{name} end} + end + end + + def define_opposite_writers (name_pairs) + name_pairs.collect! { |k, v| [k.to_sym, v.to_sym] } + for opposite_name, name in name_pairs do + define_writer(name) unless writer_defined? name + class_eval %{def #{opposite_name.writer} x + self.#{name.writer} !x end} + class_eval %{def #{opposite_name.imperative} + self.#{name.writer} false end} + end + end + + define_soft_aliases \ + :define_opposite_reader => :define_opposite_readers, + :define_opposite_writer => :define_opposite_writers + + def define_opposite_accessors (name_pairs) + define_opposite_readers name_pairs + define_opposite_writers name_pairs + end + + def define_reader_with_opposite (name_pair, &body) + name, opposite_name = name_pair.flatten.collect { |x| x.to_sym } + define_method(name, &body) + define_opposite_reader(opposite_name => name) + end + + def define_writer_with_opposite (name_pair, &body) + name, opposite_name = name_pair.flatten.collect { |x| x.to_sym } + define_writer(name, &body) + define_opposite_writer(opposite_name => name) + end + + public :define_method + + def define_methods (*names, &body) + names.each { |name| define_method(name, &body) } + end + + def define_private_methods (*names, &body) + define_methods(*names, &body) + names.each { |name| private name } + end + + def define_protected_methods (*names, &body) + define_methods(*names, &body) + names.each { |name| protected name } + end + + def define_private_method (name, &body) + define_method(name, &body) + private name + end + + def define_protected_method (name, &body) + define_method(name, &body) + protected name + end +end + +class ImmutableAttributeError < StandardError + def initialize (attribute=nil, message=nil) + super message + @attribute = attribute + end + + define_accessors :attribute + + def to_s + if @attribute and @message + "cannot change the value of `#@attribute': #@message" + elsif @attribute + "cannot change the value of `#@attribute'" + elsif @message + "cannot change the value of attribute: #@message" + else + "cannot change the value of attribute" + end + end +end + +class Module + # Guard each of the specified attributes by replacing the writer + # method with a proxy that asks the supplied block before proceeding + # with the change. + # + # If it's okay to change the attribute, the block should return + # either nil or the symbol :mutable. If it isn't okay, the block + # should return a string saying why the attribute can't be changed. + # If you don't want to provide a reason, you can have the block + # return just the symbol :immutable. + def guard_writers(*names, &predicate) + for name in names.map { |x| x.to_sym } do + define_hard_alias("__unguarded_#{name.writer}" => name.writer) + define_method(name.writer) do |new_value| + case result = predicate.call + when :mutable, nil + __send__("__unguarded_#{name.writer}", new_value) + when :immutable + raise ImmutableAttributeError.new(name) + else + raise ImmutableAttributeError.new(name, result) + end + end + end + end + + def define_guarded_writers (*names, &block) + define_writers(*names) + guard_writers(*names, &block) + end + + define_soft_alias :guard_writer => :guard_writers + define_soft_alias :define_guarded_writer => :define_guarded_writers +end + +if __FILE__ == $0 + require "test/unit" + + class DefineAccessorsTest < Test::Unit::TestCase + def setup + @X = Class.new + @Y = Class.new @X + @x = @X.new + @y = @Y.new + end + + def test_define_hard_aliases + @X.define_method(:foo) { 123 } + @X.define_method(:baz) { 321 } + @X.define_hard_aliases :bar => :foo, :quux => :baz + assert_equal @x.foo, 123 + assert_equal @x.bar, 123 + assert_equal @y.foo, 123 + assert_equal @y.bar, 123 + assert_equal @x.baz, 321 + assert_equal @x.quux, 321 + assert_equal @y.baz, 321 + assert_equal @y.quux, 321 + @Y.define_method(:foo) { 456 } + assert_equal @y.foo, 456 + assert_equal @y.bar, 123 + @Y.define_method(:quux) { 654 } + assert_equal @y.baz, 321 + assert_equal @y.quux, 654 + end + + def test_define_soft_aliases + @X.define_method(:foo) { 123 } + @X.define_method(:baz) { 321 } + @X.define_soft_aliases :bar => :foo, :quux => :baz + assert_equal @x.foo, 123 + assert_equal @x.bar, 123 + assert_equal @y.foo, 123 + assert_equal @y.bar, 123 + assert_equal @x.baz, 321 + assert_equal @x.quux, 321 + assert_equal @y.baz, 321 + assert_equal @y.quux, 321 + @Y.define_method(:foo) { 456 } + assert_equal @y.foo, @y.bar, 456 + @Y.define_method(:quux) { 654 } + assert_equal @y.baz, 321 + assert_equal @y.quux, 654 + end + + def test_define_readers + @X.define_readers :foo, :bar + assert !@x.respond_to?(:foo=) + assert !@x.respond_to?(:bar=) + @x.instance_eval { @foo = 123 ; @bar = 456 } + assert_equal @x.foo, 123 + assert_equal @x.bar, 456 + @X.define_readers :baz?, :quux? + assert !@x.respond_to?(:baz=) + assert !@x.respond_to?(:quux=) + @x.instance_eval { @baz = false ; @quux = true } + assert !@x.baz? + assert @x.quux? + end + + def test_define_writers + assert !@X.writer_defined?(:foo) + assert !@X.writer_defined?(:bar) + @X.define_writers :foo, :bar + assert @X.writer_defined?(:foo) + assert @X.writer_defined?(:bar) + assert @X.writer_defined?(:foo=) + assert @X.writer_defined?(:bar=) + assert @X.writer_defined?(:foo?) + assert @X.writer_defined?(:bar?) + assert !@x.respond_to?(:foo) + assert !@x.respond_to?(:bar) + @x.foo = 123 + @x.bar = 456 + assert_equal @x.instance_eval { @foo }, 123 + assert_equal @x.instance_eval { @bar }, 456 + @X.define_writers :baz?, :quux? + assert !@x.respond_to?(:baz?) + assert !@x.respond_to?(:quux?) + @x.baz = true + @x.quux = false + assert_equal @x.instance_eval { @baz }, true + assert_equal @x.instance_eval { @quux }, false + end + + def test_define_accessors + @X.define_accessors :foo, :bar + @x.foo = 123 ; @x.bar = 456 + assert_equal @x.foo, 123 + assert_equal @x.bar, 456 + end + + def test_define_opposite_readers + @X.define_opposite_readers :foo? => :bar?, :baz? => :quux? + assert !@x.respond_to?(:foo=) + assert !@x.respond_to?(:bar=) + assert !@x.respond_to?(:baz=) + assert !@x.respond_to?(:quux=) + @x.instance_eval { @bar = true ; @quux = false } + assert !@x.foo? + assert @x.bar? + assert @x.baz? + assert !@x.quux? + end + + def test_define_opposite_writers + @X.define_opposite_writers :foo? => :bar?, :baz => :quux + end + end +end diff --git a/lib/puppet/event-loop/event-loop.rb b/lib/puppet/event-loop/event-loop.rb new file mode 100644 index 000000000..5d78844ef --- /dev/null +++ b/lib/puppet/event-loop/event-loop.rb @@ -0,0 +1,355 @@ +## event-loop.rb --- high-level IO multiplexer +# Copyright (C) 2005 Daniel Brockman + +# This program is free software; you can redistribute it +# and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation; +# either version 2 of the License, or (at your option) any +# later version. + +# This file is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty +# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public +# License along with this program; if not, write to the Free +# Software Foundation, 51 Franklin Street, Fifth Floor, +# Boston, MA 02110-1301, USA. + +require "puppet/event-loop/better-definers" +require "puppet/event-loop/signal-system" + +require "fcntl" + +class EventLoop + include SignalEmitter + + IO_STATES = [:readable, :writable, :exceptional] + + class << self + def default ; @default ||= new end + def default= x ; @default = x end + + def current + Thread.current["event-loop::current"] || default end + def current= x + Thread.current["event-loop::current"] = x end + + def with_current (new) + if current == new + yield + else + begin + old = self.current + self.current = new + yield + ensure + self.current = old + end + end + end + + def method_missing (name, *args, &block) + if current.respond_to? name + current.__send__(name, *args, &block) + else + super + end + end + end + + define_signals :before_sleep, :after_sleep + + def initialize + @running = false + @awake = false + @wakeup_time = nil + @timers = [] + + @io_arrays = [[], [], []] + @ios = Hash.new do |h, k| raise ArgumentError, + "invalid IO event: #{k}", caller(2) end + IO_STATES.each_with_index { |x, i| @ios[x] = @io_arrays[i] } + + @notify_src, @notify_snk = IO.pipe + + @notify_src.will_block = false + @notify_snk.will_block = false + + # Each time a byte is sent through the notification pipe + # we need to read it, or IO.select will keep returning. + monitor_io(@notify_src, :readable) + @notify_src.extend(Watchable) + @notify_src.on_readable do + begin + @notify_src.sysread(256) + rescue Errno::EAGAIN + # The pipe wasn't readable after all. + end + end + end + + define_opposite_accessors \ + :stopped? => :running?, + :sleeping? => :awake? + + def run + if block_given? + thread = Thread.new { run } + yield ; quit ; thread.join + else + running! + iterate while running? + end + ensure + quit + end + + def iterate (user_timeout=nil) + t1, t2 = user_timeout, max_timeout + timeout = t1 && t2 ? [t1, t2].min : t1 || t2 + select(timeout).zip(IO_STATES) do |ios, state| + ios.each { |x| x.signal(state) } if ios + end + end + + private + + def select (timeout) + @wakeup_time = timeout ? Time.now + timeout : nil + # puts "waiting: #{timeout} seconds" + signal :before_sleep ; sleeping! + IO.select(*@io_arrays + [timeout]) || [] + ensure + awake! ; signal :after_sleep + @timers.each { |x| x.sound_alarm if x.ready? } + end + + public + + def quit ; stopped! ; wake_up ; self end + + def monitoring_io? (io, event) + @ios[event].include? io end + def monitoring_timer? (timer) + @timers.include? timer end + + def monitor_io (io, *events) + for event in events do + unless monitoring_io?(io, event) + @ios[event] << io ; wake_up + end + end + end + + def monitor_timer (timer) + unless monitoring_timer? timer + @timers << timer + end + end + + def check_timer (timer) + wake_up if timer.end_time < @wakeup_time + end + + def ignore_io (io, *events) + events = IO_STATES if events.empty? + for event in events do + wake_up if @ios[event].delete(io) + end + end + + def ignore_timer (timer) + # Don't need to wake up for this. + @timers.delete(timer) + end + + def max_timeout + return nil if @timers.empty? + [@timers.collect { |x| x.time_left }.min, 0].max + end + + def wake_up + @notify_snk.write('.') if sleeping? + end +end + +class Symbol + def io_state? + EventLoop::IO_STATES.include? self + end +end + +module EventLoop::Watchable + include SignalEmitter + + define_signals :readable, :writable, :exceptional + + def monitor_events (*events) + EventLoop.monitor_io(self, *events) end + def ignore_events (*events) + EventLoop.ignore_io(self, *events) end + + define_soft_aliases \ + :monitor_event => :monitor_events, + :ignore_event => :ignore_events + + def close ; super + ignore_events end + def close_read ; super + ignore_event :readable end + def close_write ; super + ignore_event :writable end + + module Automatic + include EventLoop::Watchable + + def add_signal_handler (name, &handler) super + monitor_event(name) if name.io_state? + end + + def remove_signal_handler (name, handler) super + if @signal_handlers[name].empty? + ignore_event(name) if name.io_state? + end + end + end +end + +class IO + def on_readable &block + extend EventLoop::Watchable::Automatic + on_readable(&block) + end + + def on_writable &block + extend EventLoop::Watchable::Automatic + on_writable(&block) + end + + def on_exceptional &block + extend EventLoop::Watchable::Automatic + on_exceptional(&block) + end + + def will_block? + require "fcntl" + fcntl(Fcntl::F_GETFL, 0) & Fcntl::O_NONBLOCK == 0 + end + + def will_block= (wants_blocking) + require "fcntl" + flags = fcntl(Fcntl::F_GETFL, 0) + if wants_blocking + flags &= ~Fcntl::O_NONBLOCK + else + flags |= Fcntl::O_NONBLOCK + end + fcntl(Fcntl::F_SETFL, flags) + end +end + +class EventLoop::Timer + include SignalEmitter + + DEFAULT_INTERVAL = 0.0 + DEFAULT_TOLERANCE = 0.001 + + def initialize (options={}, &handler) + @running = false + @start_time = nil + + if options.kind_of? Numeric + options = { :interval => options } + end + + if options[:interval] + @interval = options[:interval].to_f + else + @interval = DEFAULT_INTERVAL + end + + if options[:tolerance] + @tolerance = options[:tolerance].to_f + elsif DEFAULT_TOLERANCE < @interval + @tolerance = DEFAULT_TOLERANCE + else + @tolerance = 0.0 + end + + @event_loop = options[:event_loop] || EventLoop.current + + if block_given? + add_signal_handler(:alarm, &handler) + start unless options[:start?] == false + else + start if options[:start?] + end + end + + define_readers :interval, :tolerance + define_signal :alarm + + def stopped? ; @start_time == nil end + def running? ; @start_time != nil end + + def interval= (new_interval) + old_interval = @interval + @interval = new_interval + if new_interval < old_interval + @event_loop.check_timer(self) + end + end + + def end_time + @start_time + @interval end + def time_left + end_time - Time.now end + def ready? + time_left <= @tolerance end + + def restart + @start_time = Time.now + end + + def sound_alarm + signal :alarm + restart if running? + end + + def start + @start_time = Time.now + @event_loop.monitor_timer(self) + end + + def stop + @start_time = nil + @event_loop.ignore_timer(self) + end +end + +if __FILE__ == $0 + require "test/unit" + + class TimerTest < Test::Unit::TestCase + def setup + @timer = EventLoop::Timer.new(:interval => 0.001) + end + + def test_timer + @timer.on_alarm do + puts "[#{@timer.time_left} seconds left after alarm]" + EventLoop.quit + end + 8.times do + t0 = Time.now + @timer.start ; EventLoop.run + t1 = Time.now + assert(t1 - t0 > @timer.interval - @timer.tolerance) + end + end + end +end + +## event-loop.rb ends here. diff --git a/lib/puppet/event-loop/signal-system.rb b/lib/puppet/event-loop/signal-system.rb new file mode 100644 index 000000000..f7fe9b52c --- /dev/null +++ b/lib/puppet/event-loop/signal-system.rb @@ -0,0 +1,220 @@ +## signal-system.rb --- simple intra-process signal system +# Copyright (C) 2005 Daniel Brockman + +# This program is free software; you can redistribute it +# and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation; +# either version 2 of the License, or (at your option) any +# later version. + +# This file is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty +# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public +# License along with this program; if not, write to the Free +# Software Foundation, 51 Franklin Street, Fifth Floor, +# Boston, MA 02110-1301, USA. + +require "puppet/event-loop/better-definers" + +module SignalEmitterModule + def self.extended (object) + if object.kind_of? Module and not object < SignalEmitter + if object.respond_to? :fcall + # This is the way to call private methods + # in Ruby 1.9 as of November 16. + object.fcall :include, SignalEmitter + else + object.__send__ :include, SignalEmitter + end + end + end + + def define_signal (name, slot=:before, &body) + # Can't use `define_method' and take a block pre-1.9. + class_eval %{ def on_#{name} &block + add_signal_handler(:#{name}, &block) end } + define_signal_handler(name, :before, &lambda {|*a|}) + define_signal_handler(name, :after, &lambda {|*a|}) + define_signal_handler(name, slot, &body) if block_given? + end + + def define_signals (*names, &body) + names.each { |x| define_signal(x, &body) } + end + + def define_signal_handler (name, slot=:before, &body) + case slot + when :before + define_protected_method "handle_#{name}", &body + when :after + define_protected_method "after_handle_#{name}", &body + else + raise ArgumentError, "invalid slot `#{slot.inspect}'; " + + "should be `:before' or `:after'", caller(1) + end + end +end + +# This is an old name for the same thing. +SignalEmitterClass = SignalEmitterModule + +module SignalEmitter + def self.included (includer) + if not includer.kind_of? SignalEmitterClass + includer.extend SignalEmitterClass + end + end + + def __maybe_initialize_signal_emitter + @signal_handlers ||= Hash.new { |h, k| h[k] = Array.new } + @allow_dynamic_signals ||= false + end + + define_accessors :allow_dynamic_signals? + + def add_signal_handler (name, &handler) + __maybe_initialize_signal_emitter + @signal_handlers[name] << handler + return handler + end + + define_soft_aliases [:on, :on_signal] => :add_signal_handler + + def remove_signal_handler (name, handler) + __maybe_initialize_signal_emitter + @signal_handlers[name].delete(handler) + end + + def __signal__ (name, *args, &block) + __maybe_initialize_signal_emitter + respond_to? "on_#{name}" or allow_dynamic_signals? or + fail "undefined signal `#{name}' for #{self}:#{self.class}" + __send__("handle_#{name}", *args, &block) if + respond_to? "handle_#{name}" + @signal_handlers[name].each { |x| x.call(*args, &block) } + __send__("after_handle_#{name}", *args, &block) if + respond_to? "after_handle_#{name}" + end + + define_soft_alias :signal => :__signal__ +end + +# This module is indended to be a convenience mixin to be used by +# classes whose objects need to observe foreign signals. That is, +# if you want to observe some signals coming from an object, *you* +# should mix in this module. +# +# You cannot use this module at two different places of the same +# inheritance chain to observe signals coming from the same object. +# +# XXX: This has not seen much use, and I'd like to provide a +# better solution for the problem in the future. +module SignalObserver + def __maybe_initialize_signal_observer + @observed_signals ||= Hash.new do |signals, object| + signals[object] = Hash.new do |handlers, name| + handlers[name] = Array.new + end + end + end + + def observe_signal (subject, name, &handler) + __maybe_initialize_signal_observer + @observed_signals[subject][name] << handler + subject.add_signal_handler(name, &handler) + end + + def map_signals (source, pairs={}) + pairs.each do |src_name, dst_name| + observe_signal(source, src_name) do |*args| + __signal__(dst_name, *args) + end + end + end + + def absorb_signals (subject, *names) + names.each do |name| + observe_signal(subject, name) do |*args| + __signal__(name, *args) + end + end + end + + define_soft_aliases \ + :map_signal => :map_signals, + :absorb_signal => :absorb_signals + + def ignore_signal (subject, name) + __maybe_initialize_signal_observer + __ignore_signal_1(subject, name) + @observed_signals.delete(subject) if + @observed_signals[subject].empty? + end + + def ignore_signals (subject, *names) + __maybe_initialize_signal_observer + names = @observed_signals[subject] if names.empty? + names.each { |x| __ignore_signal_1(subject, x) } + end + + private + + def __ignore_signal_1(subject, name) + @observed_signals[subject][name].each do |handler| + subject.remove_signal_handler(name, handler) end + @observed_signals[subject].delete(name) + end +end + +if __FILE__ == $0 + require "test/unit" + class SignalEmitterTest < Test::Unit::TestCase + class X + include SignalEmitter + define_signal :foo + end + + def setup + @x = X.new + end + + def test_on_signal + moomin = 0 + @x.on_signal(:foo) { moomin = 1 } + @x.signal :foo + assert moomin == 1 + end + + def test_on_foo + moomin = 0 + @x.on_foo { moomin = 1 } + @x.signal :foo + assert moomin == 1 + end + + def test_multiple_on_signal + moomin = 0 + @x.on_signal(:foo) { moomin += 1 } + @x.on_signal(:foo) { moomin += 2 } + @x.on_signal(:foo) { moomin += 4 } + @x.on_signal(:foo) { moomin += 8 } + @x.signal :foo + assert moomin == 15 + end + + def test_multiple_on_foo + moomin = 0 + @x.on_foo { moomin += 1 } + @x.on_foo { moomin += 2 } + @x.on_foo { moomin += 4 } + @x.on_foo { moomin += 8 } + @x.signal :foo + assert moomin == 15 + end + end +end + +## application-signals.rb ends here. |