simplificator-fsm 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2009 pascalbetz
1
+ Copyright (c) 2009 simplificator
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.rdoc CHANGED
@@ -1,7 +1,7 @@
1
1
  = fsm
2
2
 
3
- Description goes here.
3
+ FSM is a simple finite state machine
4
4
 
5
5
  == Copyright
6
6
 
7
- Copyright (c) 2009 pascalbetz. See LICENSE for details.
7
+ Copyright (c) 2009 simplificator GmbH. See LICENSE for details.
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  :major: 0
3
- :minor: 1
3
+ :minor: 2
4
4
  :patch: 0
data/lib/fsm.rb CHANGED
@@ -1,33 +1,32 @@
1
- require File.join(File.dirname(__FILE__), 'fsm', 'errors')
2
- require File.join(File.dirname(__FILE__), 'fsm', 'machine')
3
- require File.join(File.dirname(__FILE__), 'fsm', 'machine_config')
4
- require File.join(File.dirname(__FILE__), 'fsm', 'state')
5
- require File.join(File.dirname(__FILE__), 'fsm', 'transition')
6
- require File.join(File.dirname(__FILE__), 'fsm', 'executable')
1
+ %w[options errors machine state transition executable builder].each do |item|
2
+ require File.join(File.dirname(__FILE__), 'fsm', item)
3
+ end
7
4
 
8
5
  module FSM
9
6
  module ClassMethods
10
7
  def define_fsm(&block)
11
- raise "FSM is already defined. Call define_fsm only once" if Machine[self]
12
- machine = Machine.new(self)
13
- Machine[self] = machine
14
- config = MachineConfig.new(machine)
15
- config.process(&block)
16
- machine.post_process
8
+ raise 'FSM is already defined. Call define_fsm only once' if Machine[self]
9
+ builder = Builder.new(self)
10
+ Machine[self] = builder.process(&block)
11
+ self.instance_eval() do
12
+ alias_method "fsm_state_attribute", Machine[self].current_state_attribute_name
13
+ define_method(Machine[self].current_state_attribute_name) do
14
+ value = fsm_state_attribute
15
+ value ? value : Machine[self.class].initial_state_name
16
+ end
17
+ end
17
18
  end
18
-
19
-
20
19
  end
21
20
 
22
21
  module InstanceMethods
23
22
  #
24
23
  # Which states are reachable from the current state
25
24
  def reachable_state_names
26
- Machine[self.class].reachable_states.map() {|item| item.name}
25
+ Machine[self.class].reachable_states(self).map() {|item| item.name}
27
26
  end
28
27
 
29
28
  def available_transition_names
30
- Machine[self.class].available_transitions.map() {|item| item.name}
29
+ Machine[self.class].available_transitions(self).map() {|item| item.name}
31
30
  end
32
31
  end
33
32
 
@@ -0,0 +1,52 @@
1
+ module FSM
2
+ # Builder exposees 'only' (well there are some other methods exposed) the methods that are required to build the configuration
3
+ class Builder
4
+
5
+ # Blank Slate
6
+ instance_methods.each do |m|
7
+ undef_method m unless m == '__send__' || m == '__id__' || m == 'instance_eval'
8
+ end
9
+
10
+ # Create a new Builder which creates a Machine for the target_class
11
+ def initialize(target_class)
12
+ @target_class = target_class
13
+ @machine = Machine.new(target_class)
14
+ end
15
+
16
+ def process(&block)
17
+ raise ArgumentError.new('Block expected') unless block_given?
18
+ self.instance_eval(&block)
19
+ @machine
20
+ end
21
+
22
+ private
23
+ # Add a transition
24
+ # * name of the transition
25
+ # * from_name: name of the source state (symbol)
26
+ # * to_name: name of the target state (symbol)
27
+ # * options
28
+ #
29
+ def transition(name, from_name, to_name, options = {})
30
+ @machine.transition(name, from_name, to_name, options)
31
+ nil # do not expose FSM details
32
+ end
33
+
34
+ def state_attribute(name)
35
+ raise ArgumentError.new('Invalid attribute name') if name == nil
36
+ @machine.current_state_attribute_name = name
37
+ nil # do not expose FSM details
38
+ end
39
+
40
+ def initial(name)
41
+ @machine.initial_state_name = name
42
+ nil # do not expose FSM details
43
+ end
44
+
45
+ def state(name, options = {})
46
+ @machine.state(name, options)
47
+ nil # do not expose FSM details
48
+ end
49
+
50
+
51
+ end
52
+ end
@@ -2,24 +2,31 @@ module FSM
2
2
  #
3
3
  # Execute an action specified by either String, Sylbol or Proc.
4
4
  # Symbol and String represent methods which are called on the target object, Proc will get executed
5
- # and receives at least the target as parameter
5
+ # and receives at least the target as parameter. If others parameters are passed then they'll get forwarded as well.
6
6
  class Executable
7
7
  # Create a new Executable
8
8
  # if args is true, then arguments are passed on to the target method or the Proc, if false nothing
9
9
  # will get passed
10
- def initialize(thing, args = false)
10
+ def initialize(thing)
11
+ raise ArgumentError.new("Unknown thing #{thing}") unless thing
11
12
  @thing = thing
12
- @has_arguments = args
13
13
  end
14
14
 
15
15
  # execute this executable on the given target
16
- def execute(target, args)
16
+ def execute(target, *args)
17
17
  case @thing
18
18
  when String, Symbol:
19
- @has_arguments ? target.send(@thing, *args) : target.send(@thing)
19
+ if (args.length > 0)
20
+ target.send(@thing, *args)
21
+ else
22
+ target.send(@thing)
23
+ end
20
24
  when Proc:
21
- @has_arguments ? @thing.call(target, *args) : @thing.call(target)
22
- when Nil:
25
+ if (args.length > 0)
26
+ @thing.call(target, *args)
27
+ else
28
+ @thing.call(target)
29
+ end
23
30
  else
24
31
  raise "Unknown Thing #{@thing}"
25
32
  end
data/lib/fsm/machine.rb CHANGED
@@ -1,11 +1,11 @@
1
1
  module FSM
2
2
  class Machine
3
- attr_accessor(:current_state, :states, :state_attribute)
3
+ attr_accessor(:initial_state_name, :current_state_attribute_name, :states)
4
4
 
5
- def initialize(klass)
6
- @klass = klass
5
+ def initialize(target_class)
6
+ @target_class = target_class
7
7
  self.states = {}
8
- self.state_attribute = :state
8
+ self.current_state_attribute_name = :state
9
9
  end
10
10
 
11
11
  def self.[](includer)
@@ -19,76 +19,69 @@ module FSM
19
19
  def state(name, options = {})
20
20
  raise "State is already defined: '#{name}'" if self.states[name]
21
21
  self.states[name] = State.new(name, options)
22
- initial(name) unless self.current_state
23
- nil
22
+ self.initial_state_name=(name) unless self.initial_state_name
24
23
  end
25
24
 
26
- def initial(name)
27
- self.current_state = self.states[name]
28
- raise UnknownState.new("Unknown state '#{name}'. Define states first with state(name)") unless self.current_state
29
- nil
25
+ def initial_state_name=(value)
26
+ raise UnknownState.new("Unknown state '#{value}'. Define states first with state(name)") unless self.states[value]
27
+ @initial_state_name = value
30
28
  end
31
29
 
32
30
  def transition(name, from_name, to_name, options = {})
33
31
  raise ArgumentError.new("name, from_name and to_name are required") if name.nil? || from_name.nil? || to_name.nil?
34
32
  raise UnknownState.new("Unknown source state '#{from}'") unless self.states[from_name]
35
33
  raise UnknownState.new("Unknown target state '#{to}'") unless self.states[to_name]
36
- define_transition_method(name, to_name)
34
+
37
35
  from_state = self.states[from_name]
38
36
  to_state = self.states[to_name]
37
+
39
38
  transition = Transition.new(name, from_state, to_state, options)
40
- from_state.transitions[to_state.name] = transition
41
- nil
39
+ from_state.add_transition(transition)
40
+
41
+ define_transition_method(name, to_name)
42
+
42
43
  end
43
44
 
44
- def attribute(name)
45
- self.state_attribute = name
45
+ def self.get_current_state_name(target)
46
+ value = target.send(Machine[target.class].current_state_attribute_name)
47
+ (value && value.is_a?(String)) ? value.intern : value
46
48
  end
47
49
 
48
- def reachable_states()
49
- self.states.map do |name, state|
50
- state.to_states if state == current_state
51
- end.flatten.compact
50
+ def self.set_current_state_name(target, value)
51
+ target.send("#{Machine[target.class].current_state_attribute_name}=", value)
52
52
  end
53
+
53
54
 
54
- def available_transitions()
55
- current_state.transitions.values
55
+ def reachable_states(target)
56
+ reachables = []
57
+ current_state_name = Machine.get_current_state_name(target)
58
+ self.states.map do |name, state|
59
+ reachables += state.to_states if state.name == current_state_name
60
+ end
61
+ reachables
56
62
  end
57
63
 
58
-
59
- def post_process
60
- define_state_attribute_methods(self.state_attribute)
64
+ def available_transitions(target)
65
+ self.states[Machine.get_current_state_name(target)].transitions.values
61
66
  end
62
67
 
63
-
64
-
65
68
  private
66
69
 
67
- def define_state_attribute_methods(name)
68
- @klass.instance_eval do
69
- define_method("#{name}") do
70
- Machine[self.class].current_state.name
71
- end
72
-
73
- define_method("#{name}=") do |value|
74
- Machine[self.class].current_state = Machine[self.class].states[value]
75
- raise("Unknown State #{value}") unless Machine[self.class].current_state
76
- end
77
- end
78
- end
79
-
80
70
  def define_transition_method(name, to_name)
81
- @klass.instance_eval do
71
+ @target_class.instance_eval do
82
72
  define_method(name) do |*args|
83
- from_state = Machine[self.class].current_state
84
- to_state = Machine[self.class].states[to_name]
73
+ machine = Machine[self.class]
74
+ from_name = Machine.get_current_state_name(self)
75
+ from_state = machine.states[from_name]
76
+ to_state = machine.states[to_name]
85
77
  transition = from_state.transitions[to_name]
86
- raise InvalidStateTransition.new("No transition defined from #{from_state.name} -> #{to_state.name}") unless transition
78
+ raise InvalidStateTransition.new("No transition defined from #{from_name} -> #{to_name}") unless transition
87
79
 
88
80
  from_state.exit(self)
89
81
  transition.fire_event(self, args)
90
82
  to_state.enter(self)
91
- Machine[self.class].current_state = to_state
83
+ Machine.set_current_state_name(self, to_name)
84
+ true # at the moment always return true ... as soon as we have guards or thelike this could be false as well
92
85
  end
93
86
  end
94
87
  end
@@ -0,0 +1,17 @@
1
+ module FSM
2
+ module Options
3
+ module InstanceMethods
4
+ def assert_options(options, optional_keys = {}, mandatory_keys = {})
5
+ keys_processed = []
6
+ mandatory_keys.each do |key|
7
+ raise ArgumentError.new("Mandatory Key #{key} is missing") unless options.keys.include?(key)
8
+ keys_processed << key
9
+ end
10
+ options.keys.each do |key|
11
+ raise ArgumentError.new("Unsupported key #{key}") unless optional_keys.include?(key) || mandatory_keys.include?(key)
12
+ end
13
+ end
14
+ end
15
+
16
+ end
17
+ end
data/lib/fsm/state.rb CHANGED
@@ -3,6 +3,7 @@ module FSM
3
3
  # A State has a name and a list of outgoing transitions.
4
4
  #
5
5
  class State
6
+ include FSM::Options::InstanceMethods
6
7
  attr_reader(:name, :transitions)
7
8
 
8
9
  # name: a symbol which identifies this state
@@ -10,28 +11,38 @@ module FSM
10
11
  #  * :enter : a symbol or string or Proc
11
12
  #  * :exit : a symbol or string or Proc
12
13
  def initialize(name, options = {})
14
+ raise ArgumentError.new('Name is required') unless name
15
+ assert_options(options, [:enter, :exit])
13
16
  @name = name
14
- @enter = Executable.new options[:enter]
15
- @exit = Executable.new options[:exit]
17
+ @enter = Executable.new options[:enter] if options.has_key?(:enter)
18
+ @exit = Executable.new options[:exit] if options.has_key?(:exit)
16
19
  @transitions = {}
17
20
  end
18
21
 
19
22
  # Called when this state is entered
20
23
  def enter(target)
21
- @enter.execute(target, nil)
24
+ @enter.execute(target) if @enter
25
+ nil
22
26
  end
23
27
  # Called when this state is exited
24
28
  def exit(target)
25
- @exit.execute(target, nil)
29
+ @exit.execute(target) if @exit
30
+ nil
26
31
  end
27
32
 
33
+ def add_transition(transition)
34
+ raise ArgumentError.new("#{self} already has a transition to '#{transition.name}'") if @transitions.has_key?(transition.name)
35
+ raise ArgumentError.new("the transition '#{transition.name}' is already defined") if @transitions.detect() {|to_name, tr| transition.name == tr.name}
36
+ @transitions[transition.to.name] = transition
37
+ end
38
+
28
39
  # All states that are reachable form this state by one hop
29
40
  def to_states
30
- self.transitions.map { |to_name, transition| transition.to}
41
+ @transitions.map { |to_name, transition| transition.to}
31
42
  end
32
43
 
33
44
  def to_s
34
- "State '#{self.name}' (#{self.object_id})"
45
+ "State '#{self.name}'"
35
46
  end
36
47
 
37
48
  end
@@ -1,16 +1,20 @@
1
1
  module FSM
2
2
  class Transition
3
+ include FSM::Options::InstanceMethods
3
4
  attr_accessor(:name, :from, :to, :event)
4
5
  def initialize(name, from, to, options = {})
6
+ raise ArgumentError.new("name, from and to are required but were '#{name}', '#{from}' and '#{to}'") unless name && from && to
7
+ assert_options(options, [:event])
5
8
  self.name = name
6
9
  self.from = from
7
10
  self.to = to
8
- self.event = Executable.new options[:event], true
11
+ self.event = Executable.new options[:event] if options.has_key?(:event)
9
12
  end
10
13
 
11
14
  def fire_event(target, args)
12
- self.event.execute(target, args)
15
+ self.event.execute(target, *args) if self.event
13
16
  end
17
+
14
18
  def to_s
15
19
  "Transition from #{self.from.name} -> #{self.to.name} with event #{self.event}"
16
20
  end
@@ -0,0 +1,33 @@
1
+ require 'test_helper'
2
+
3
+ class ExecutableTest < Test::Unit::TestCase
4
+ context 'Should execute method without arguments' do
5
+ should 'with symbol' do
6
+ assert_equal 4, FSM::Executable.new(:length).execute('1234')
7
+ end
8
+ should 'with string' do
9
+ assert_equal 4, FSM::Executable.new('length').execute('1234')
10
+ end
11
+ should 'with proc' do
12
+ assert_equal 4, FSM::Executable.new(lambda { |target| target.length }).execute('1234')
13
+ end
14
+ end
15
+
16
+ context 'Should execute method with arguments' do
17
+ should 'with symbol' do
18
+ assert_equal 'firstlast', FSM::Executable.new(:+).execute('first', 'last')
19
+ end
20
+ should 'with string' do
21
+ assert_equal 'firstlast', FSM::Executable.new('+').execute('first', 'last')
22
+ end
23
+
24
+ should 'with proc varargs' do
25
+ assert_equal 'some things are good', FSM::Executable.new(lambda {|target, *args| args.join(' ') }).execute(:foo, 'some', 'things', 'are', 'good')
26
+ end
27
+
28
+ should 'with proc' do
29
+ assert_equal 'some things', FSM::Executable.new(lambda {|target, a, b| "#{a} #{b}" }).execute(:foo, 'some', 'things')
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,67 @@
1
+ require 'test_helper'
2
+
3
+ class Invoice
4
+ attr_accessor(:state, :amount)
5
+ include FSM
6
+ define_fsm do
7
+ state_attribute(:state)
8
+ state(:open)
9
+ state(:paid)
10
+ state(:refunded)
11
+
12
+ transition(:pay, :open, :paid, :event => :event_paid)
13
+ transition(:refund, :paid, :refunded)
14
+ end
15
+
16
+
17
+ def initialize(state = :open, amount = 1000)
18
+ self.state = state
19
+ self.amount = amount
20
+ end
21
+
22
+ private
23
+ def event_paid(amount_paid)
24
+ raise "Not enough paid" unless amount_paid == self.amount
25
+ self.amount -= amount_paid
26
+ end
27
+ end
28
+
29
+ class InvoiceTest < Test::Unit::TestCase
30
+ context 'Invoice' do
31
+
32
+ should 'Initial State is the first state defined unless no initial() call was made' do
33
+ invoice = Invoice.new
34
+ assert_equal(:open, invoice.state)
35
+ end
36
+
37
+ should 'Accept an initial state from outside' do
38
+ invoice = Invoice.new(:paid)
39
+ assert_equal(:paid, invoice.state)
40
+ end
41
+
42
+ should 'Trasition to paid and then refunded' do
43
+ invoice = Invoice.new
44
+ assert_equal(:open, invoice.state)
45
+
46
+ invoice.pay(1000)
47
+ assert_equal(:paid, invoice.state)
48
+
49
+ invoice.refund
50
+ assert_equal(:refunded, invoice.state)
51
+ end
52
+
53
+ should 'Raise on illegal transition' do
54
+ invoice = Invoice.new
55
+ assert_raise(FSM::InvalidStateTransition) do
56
+ invoice.refund
57
+ end
58
+ end
59
+
60
+ should 'Pass the arguments to the event handler' do
61
+ invoice = Invoice.new
62
+ invoice.pay(1000)
63
+ assert_equal(0, invoice.amount)
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,37 @@
1
+ require 'test_helper'
2
+
3
+ class OptionsTest < Test::Unit::TestCase
4
+ include FSM::Options::InstanceMethods
5
+ context 'assert_options' do
6
+ should 'allow empty options' do
7
+ assert_options({})
8
+ end
9
+ should('throw on unknown key') do
10
+ assert_raise(ArgumentError) do
11
+ assert_options({:foo => 12}, [], [])
12
+ end
13
+ assert_raise(ArgumentError) do
14
+ assert_options({:foo => 12}, [:optional], [])
15
+ end
16
+ assert_raise(ArgumentError) do
17
+ assert_options({:foo => 12}, [], [:mandatory])
18
+ end
19
+ assert_raise(ArgumentError) do
20
+ assert_options({:foo => 12}, [:optional], [:mandatory])
21
+ end
22
+ end
23
+ should('find missong mandatory options') do
24
+ assert_raise(ArgumentError) do
25
+ assert_options({}, [], [:foo])
26
+ end
27
+ assert_raise(ArgumentError) do
28
+ assert_options({:foo => 12}, [], [:bar])
29
+ end
30
+ assert_raise(ArgumentError) do
31
+ assert_options({:foo => 12}, [:bar], [:bar])
32
+ end
33
+ assert_options({:foo => 42}, [], [:foo])
34
+ assert_options({:foo => 42}, [:foo], [:foo])
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,21 @@
1
+ require 'test_helper'
2
+
3
+ class StateTest < Test::Unit::TestCase
4
+ context 'Initializer' do
5
+ should 'require name' do
6
+ assert_raise(ArgumentError) do
7
+ FSM::State.new(nil, nil, nil)
8
+ end
9
+
10
+ FSM::State.new('bla')
11
+ end
12
+
13
+ should 'allow only valid options' do
14
+ assert_raise(ArgumentError) do
15
+ FSM::State.new('bla', :foo => 12)
16
+ end
17
+ FSM::State.new('bla', :enter => :some)
18
+ FSM::State.new('bla', :exit => :some)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,38 @@
1
+ require 'test_helper'
2
+
3
+ class TransitionTest < Test::Unit::TestCase
4
+ context 'Initializer' do
5
+ should 'require name, from and to' do
6
+ assert_raise(ArgumentError) do
7
+ FSM::Transition.new(nil, nil, nil)
8
+ end
9
+ assert_raise(ArgumentError) do
10
+ FSM::Transition.new('bla', nil, nil)
11
+ end
12
+ assert_raise(ArgumentError) do
13
+ FSM::Transition.new(nil, 'bli', nil)
14
+ end
15
+ assert_raise(ArgumentError) do
16
+ FSM::Transition.new(nil, nil, 'blo')
17
+ end
18
+ assert_raise(ArgumentError) do
19
+ FSM::Transition.new('bli', 'bli', nil)
20
+ end
21
+ assert_raise(ArgumentError) do
22
+ FSM::Transition.new(nil, 'blo', 'blo')
23
+ end
24
+ assert_raise(ArgumentError) do
25
+ FSM::Transition.new('blo', nil, 'bli')
26
+ end
27
+
28
+ FSM::Transition.new('bla', 'bli', 'blo')
29
+ end
30
+
31
+ should 'allow only valid options' do
32
+ assert_raise(ArgumentError) do
33
+ FSM::Transition.new('bla', 'bli', 'blo', :foo => 12)
34
+ end
35
+ FSM::Transition.new('bla', 'bli', 'blo', :event => :some)
36
+ end
37
+ end
38
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simplificator-fsm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - simplificator
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-05-14 00:00:00 -07:00
12
+ date: 2009-05-18 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -28,14 +28,19 @@ files:
28
28
  - Rakefile
29
29
  - VERSION.yml
30
30
  - lib/fsm.rb
31
+ - lib/fsm/builder.rb
31
32
  - lib/fsm/errors.rb
32
33
  - lib/fsm/executable.rb
33
34
  - lib/fsm/machine.rb
34
- - lib/fsm/machine_config.rb
35
+ - lib/fsm/options.rb
35
36
  - lib/fsm/state.rb
36
37
  - lib/fsm/transition.rb
37
- - test/fsm_test.rb
38
+ - test/executable_test.rb
39
+ - test/invoice_sample_test.rb
40
+ - test/options_test.rb
41
+ - test/state_test.rb
38
42
  - test/test_helper.rb
43
+ - test/transition_test.rb
39
44
  has_rdoc: true
40
45
  homepage: http://github.com/simplificator/fsm
41
46
  post_install_message:
@@ -63,5 +68,9 @@ signing_key:
63
68
  specification_version: 3
64
69
  summary: A simple finite state machine (FSM) gem.
65
70
  test_files:
66
- - test/fsm_test.rb
71
+ - test/executable_test.rb
72
+ - test/invoice_sample_test.rb
73
+ - test/options_test.rb
74
+ - test/state_test.rb
67
75
  - test/test_helper.rb
76
+ - test/transition_test.rb
@@ -1,21 +0,0 @@
1
- module FSM
2
- class MachineConfig
3
- CONFIG_METHODS = %w[state transition initial attribute]
4
- instance_methods.each do |m|
5
- undef_method m unless m == '__send__' || m == '__id__' || m == 'instance_eval'
6
- end
7
-
8
- def initialize(target)
9
- @target = target
10
- end
11
-
12
- def process(&block)
13
- instance_eval(&block)
14
- end
15
-
16
- def method_missing(sym, *args, &block)
17
- raise "Unknown config method '#{sym}'. Only #{CONFIG_METHODS.map() {|item| "'#{item}'" }.join(', ')} are known" unless CONFIG_METHODS.include?(sym.to_s)
18
- @target.__send__(sym, *args, &block)
19
- end
20
- end
21
- end
data/test/fsm_test.rb DELETED
@@ -1,7 +0,0 @@
1
- require 'test_helper'
2
-
3
- class FsmTest < Test::Unit::TestCase
4
- should "probably rename this file and start testing for real" do
5
- flunk "hey buddy, you should probably rename this file and start testing for real"
6
- end
7
- end