simplificator-fsm 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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