rubyslim-unofficial 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/.gitignore +7 -0
  2. data/Gemfile +2 -0
  3. data/README.md +187 -0
  4. data/Rakefile +21 -0
  5. data/bin/rubyslim +10 -0
  6. data/lib/rubyslim/list_deserializer.rb +69 -0
  7. data/lib/rubyslim/list_executor.rb +12 -0
  8. data/lib/rubyslim/list_serializer.rb +45 -0
  9. data/lib/rubyslim/ruby_slim.rb +61 -0
  10. data/lib/rubyslim/slim_error.rb +3 -0
  11. data/lib/rubyslim/slim_helper_library.rb +23 -0
  12. data/lib/rubyslim/socket_service.rb +53 -0
  13. data/lib/rubyslim/statement.rb +79 -0
  14. data/lib/rubyslim/statement_executor.rb +174 -0
  15. data/lib/rubyslim/table_to_hash_converter.rb +32 -0
  16. data/lib/test_module/library_new.rb +13 -0
  17. data/lib/test_module/library_old.rb +12 -0
  18. data/lib/test_module/should_not_find_test_slim_in_here/test_slim.rb +7 -0
  19. data/lib/test_module/simple_script.rb +9 -0
  20. data/lib/test_module/test_chain.rb +5 -0
  21. data/lib/test_module/test_slim.rb +86 -0
  22. data/lib/test_module/test_slim_with_arguments.rb +23 -0
  23. data/lib/test_module/test_slim_with_no_sut.rb +5 -0
  24. data/rubyslim-unofficial.gemspec +25 -0
  25. data/spec/instance_creation_spec.rb +40 -0
  26. data/spec/it8f_spec.rb +8 -0
  27. data/spec/list_deserializer_spec.rb +66 -0
  28. data/spec/list_executor_spec.rb +187 -0
  29. data/spec/list_serialzer_spec.rb +38 -0
  30. data/spec/method_invocation_spec.rb +127 -0
  31. data/spec/slim_helper_library_spec.rb +80 -0
  32. data/spec/socket_service_spec.rb +98 -0
  33. data/spec/spec_helper.rb +6 -0
  34. data/spec/statement_executor_spec.rb +50 -0
  35. data/spec/statement_spec.rb +17 -0
  36. data/spec/table_to_hash_converter_spec.rb +31 -0
  37. metadata +132 -0
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ .rakeTasks
2
+ *.iml
3
+ *.ipr
4
+ *.iws
5
+ .DS_Store
6
+ .idea
7
+
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
data/README.md ADDED
@@ -0,0 +1,187 @@
1
+ Ruby Slim
2
+ =========
3
+
4
+ This package provides a SliM server implementing the [FitNesse](http://fitnesse.org)
5
+ [SliM protocol](http://fitnesse.org/FitNesse.UserGuide.SliM.SlimProtocol). It allows
6
+ you to write test fixtures in Ruby, and invoke them from a FitNesse test.
7
+
8
+
9
+ Fixture names
10
+ -------------
11
+
12
+ Rubyslim is very particular about how your modules and classes are named, and
13
+ how you import and use them in your FitNesse wiki:
14
+
15
+ * Fixture folder must be `lowercase_and_underscore`
16
+ * Fixture filenames must be `lowercase_and_underscore`
17
+ * Ruby module name must be the `CamelCase` version of fixture folder name
18
+ * Ruby class name must be the `CamelCase` version of the fixture file name
19
+
20
+ For example, this naming scheme is valid:
21
+
22
+ * Folder: `ruby_fix`
23
+ * Filename: `my_fixture.rb`
24
+ * Module: `RubyFix`
25
+ * Class: `MyFixture`
26
+
27
+ If you have `TwoWords` in CamelCase, then that would be `two_words` with
28
+ underscores. If you have only `oneword` in the lowercase version, then you must
29
+ have `Oneword` in the CamelCase version. If all of these naming conventions are
30
+ not exactly followed, you'll get mysterious errors like `Could not invoke
31
+ constructor` for your Slim tables.
32
+
33
+
34
+ Setup
35
+ -----
36
+
37
+ Put these commands in a parent of the Ruby test pages.
38
+
39
+ !define TEST_SYSTEM {slim}
40
+ !define TEST_RUNNER {rubyslim}
41
+ !define COMMAND_PATTERN {rubyslim}
42
+ !path your/ruby/fixtures
43
+
44
+ Paths can be relative. You should put the following in an appropriate SetUp page:
45
+
46
+ !|import|
47
+ |<ruby module of fixtures>|
48
+
49
+ You can have as many rows in this table as you like, one for each module that
50
+ contains fixtures. Note that this needs to be the *name* of the module as
51
+ written in the Ruby code, not the filename where the module is defined.
52
+
53
+ Ruby slim works a lot like Java slim. We tried to use ruby method naming
54
+ conventions. So if you put this in a table:
55
+
56
+ |SomeDecisionTable|
57
+ |input|get output?|
58
+ |1 |2 |
59
+
60
+ Then it will call the `set_input` and `get_output` functions of the
61
+ `SomeDecisionTable` class.
62
+
63
+ The `SomeDecisionTable` class would be written in a file called
64
+ `some_decision_table.rb`, like this (the file name must correspond to the class
65
+ name defined within, and the module name must match the one you are importing):
66
+
67
+ module MyModule
68
+ class SomeDecisionTable
69
+ def set_input(input)
70
+ @x = input
71
+ end
72
+
73
+ def get_output
74
+ @x
75
+ end
76
+ end
77
+ end
78
+
79
+ Note that there is no type information for the arguments of these functions, so
80
+ they will all be treated as strings. This is important to remember! All
81
+ arguments are strings. If you are expecting integers, you will have to convert
82
+ the strings to integers within your fixtures.
83
+
84
+
85
+ Hashes
86
+ ------
87
+
88
+ There is one exception to the above rule. If you pass a HashWidget in a table,
89
+ then the argument will be converted to a Hash.
90
+
91
+ Consider, for example, this fixtures class:
92
+
93
+ module TestModule
94
+ class TestSlimWithArguments
95
+ def initialize(arg)
96
+ @arg = arg
97
+ end
98
+
99
+ def arg
100
+ @arg
101
+ end
102
+
103
+ def name
104
+ @arg[:name]
105
+ end
106
+
107
+ def addr
108
+ @arg[:addr]
109
+ end
110
+
111
+ def set_arg(hash)
112
+ @arg = hash
113
+ end
114
+ end
115
+ end
116
+
117
+ This corresponds to the following tables.
118
+
119
+ |script|test slim with arguments|!{name:bob addr:here}|
120
+ |check|name|bob|
121
+ |check|addr|here|
122
+
123
+ |script|test slim with arguments|gunk|
124
+ |check|arg|gunk|
125
+ |set arg|!{name:bob addr:here}|
126
+ |check|name|bob|
127
+ |check|addr|here|
128
+
129
+ Note the use of the HashWidgets in the table cells. These get translated into
130
+ HTML, which RubySlim recognizes and converts to a standard ruby `Hash`.
131
+
132
+
133
+ System Under Test
134
+ -----------------
135
+
136
+ If a fixture has a `sut` method that returns an object, then if a method called
137
+ by a test is not found in the fixture itself, then if it exists in the object
138
+ returned by `sut` it will be called. For example:
139
+
140
+ !|script|my fixture|
141
+ |func|1|
142
+
143
+ class MySystem
144
+ def func(x)
145
+ #this is the function that will be called.
146
+ end
147
+ end
148
+
149
+ class MyFixture
150
+ attr_reader :sut
151
+
152
+ def initialize
153
+ @sut = MySystem.new
154
+ end
155
+ end
156
+
157
+ Since the fixture `MyFixture` does not have a function named `func`, but it
158
+ _does_ have a method named `sut`, RubySlim will try to call `func` on the
159
+ object returned by `sut`.
160
+
161
+
162
+ Library Fixtures
163
+ ----------------
164
+
165
+ Ruby Slim supports the `|Library|` feature of FitNesse. If you declare certain
166
+ classes to be libraries, then if a test calls a method, and the specified
167
+ fixture does not have it, and there is no specified `sut`, then the libraries
168
+ will be searched in reverse order (latest first). If the method is found, then
169
+ it is called.
170
+
171
+ For example:
172
+
173
+ |Library|
174
+ |echo fixture|
175
+
176
+ |script|
177
+ |check|echo|a|a|
178
+
179
+ class EchoFixture
180
+ def echo(x)
181
+ x
182
+ end
183
+ end
184
+
185
+ Here, even though no fixture was specified for the script table, since a
186
+ library was declared, functions will be called on it.
187
+
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ require 'rake'
2
+ #require 'rcov/rcovtask'
3
+ require 'spec/rake/spectask'
4
+
5
+ task :default => :spec
6
+
7
+ desc "Run all spec tests"
8
+ Spec::Rake::SpecTask.new(:spec) do |t|
9
+ t.spec_files = Dir.glob('spec/**/*_spec.rb')
10
+ t.spec_opts = ['--color', '--format specdoc']
11
+ end
12
+
13
+ desc "Run all spec tests and generate coverage report"
14
+ Spec::Rake::SpecTask.new(:rcov) do |t|
15
+ t.spec_files = Dir.glob('spec/**/*_spec.rb')
16
+ # RCov doesn't like this part for some reason
17
+ #t.spec_opts = ['--color', '--format specdoc']
18
+ t.rcov = true
19
+ t.rcov_opts = %w{--exclude osx\/objc,gems\/,spec\/,features\/,lib\/test_module\/}
20
+ end
21
+
data/bin/rubyslim ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubyslim/ruby_slim'
4
+ require 'jcode'
5
+ $KCODE="UTF8"
6
+
7
+ port = ARGV[0].to_i
8
+ rubySlim = RubySlim.new
9
+ rubySlim.run(port)
10
+
@@ -0,0 +1,69 @@
1
+ require 'jcode'
2
+
3
+ module ListDeserializer
4
+ class SyntaxError < Exception
5
+ end
6
+
7
+ # De-serialize the given string, and return a Ruby-native list.
8
+ # Raises a SyntaxError if the string is empty or badly-formatted.
9
+ def self.deserialize(string)
10
+ raise SyntaxError.new("Can't deserialize null") if string.nil?
11
+ raise SyntaxError.new("Can't deserialize empty string") if string.empty?
12
+ raise SyntaxError.new("Serialized list has no starting [") if string[0..0] != "["
13
+ raise SyntaxError.new("Serialized list has no ending ]") if string[-1..-1] != "]"
14
+ Deserializer.new(string).deserialize
15
+ end
16
+
17
+
18
+ class Deserializer
19
+ def initialize(string)
20
+ @string = string;
21
+ end
22
+
23
+ def deserialize
24
+ @pos = 1
25
+ @list = []
26
+ number_of_items = consume_length
27
+
28
+ # For each item in the list
29
+ number_of_items.times do
30
+ length_of_item = consume_length
31
+ item = @string[@pos...@pos+length_of_item]
32
+ length_in_bytes = length_of_item
33
+
34
+ until (item.jlength > length_of_item) do
35
+ length_in_bytes += 1
36
+ item = @string[@pos...@pos+length_in_bytes]
37
+ end
38
+
39
+ length_in_bytes -= 1
40
+ item = @string[@pos...@pos+length_in_bytes]
41
+
42
+ # Ensure the ':' list-termination character is found
43
+ term_char = @string[@pos+length_in_bytes,1]
44
+ if term_char != ':'
45
+ raise SyntaxError.new("List termination character ':' not found" +
46
+ " (got '#{term_char}' instead)")
47
+ end
48
+
49
+ @pos += length_in_bytes+1
50
+ begin
51
+ sublist = ListDeserializer.deserialize(item)
52
+ @list << sublist
53
+ rescue ListDeserializer::SyntaxError
54
+ @list << item
55
+ end
56
+ end
57
+ @list
58
+ end
59
+
60
+ # Consume the 6-digit length prefix, and return the
61
+ # length as an integer.
62
+ def consume_length
63
+ length = @string[@pos...@pos+6].to_i
64
+ @pos += 7
65
+ length
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,12 @@
1
+ require "rubyslim/statement"
2
+ require "rubyslim/statement_executor"
3
+
4
+ class ListExecutor
5
+ def initialize()
6
+ @executor = StatementExecutor.new
7
+ end
8
+
9
+ def execute(instructions)
10
+ instructions.collect {|instruction| Statement.execute(instruction, @executor)}
11
+ end
12
+ end
@@ -0,0 +1,45 @@
1
+ require 'jcode'
2
+
3
+ module ListSerializer
4
+ # Serialize a list according to the SliM protocol.
5
+ #
6
+ # Lists are enclosed in square-brackets '[...]'. Inside the opening
7
+ # bracket is a six-digit number indicating the length of the list
8
+ # (number of items), then a colon ':', then the serialization of each
9
+ # list item. For example:
10
+ #
11
+ # [] => "[000000:]"
12
+ # ["hello"] => "[000001:000005:hello:]"
13
+ # [1] => "[000001:000001:1:]"
14
+ #
15
+ # Strings are preceded by a six-digit sequence indicating their length:
16
+ #
17
+ # "" => "000000:"
18
+ # "hello" => "000005:hello"
19
+ # nil => "000004:null"
20
+ #
21
+ # See spec/list_serializer_spec.rb for more examples.
22
+ #
23
+ def self.serialize(list)
24
+ result = "["
25
+ result += length_string(list.length)
26
+
27
+ # Serialize each item in the list
28
+ list.each do |item|
29
+ item = "null" if item.nil?
30
+ item = serialize(item) if item.is_a?(Array)
31
+ item = item.to_s
32
+ result += length_string(item.jlength)
33
+ result += item + ":"
34
+ end
35
+
36
+ result += "]"
37
+ end
38
+
39
+
40
+ # Return the six-digit prefix for an element of the given length.
41
+ def self.length_string(length)
42
+ sprintf("%06d:",length)
43
+ end
44
+ end
45
+
@@ -0,0 +1,61 @@
1
+ require "rubyslim/socket_service"
2
+ require "rubyslim/list_deserializer"
3
+ require "rubyslim/list_serializer"
4
+ require "rubyslim/list_executor"
5
+
6
+ class RubySlim
7
+ def run(port)
8
+ @connected = true
9
+ @executor = ListExecutor.new
10
+ socket_service = SocketService.new()
11
+ socket_service.serve(port) do |socket|
12
+ serve_ruby_slim(socket)
13
+ end
14
+
15
+ while (@connected)
16
+ sleep(0.1)
17
+ end
18
+ end
19
+
20
+ # Read and execute instructions from the SliM socket, until a 'bye'
21
+ # instruction is reached. Each instruction is a list, serialized as a string,
22
+ # following the SliM protocol:
23
+ #
24
+ # length:command
25
+ #
26
+ # Where `length` is a 6-digit indicating the length in bytes of `command`,
27
+ # and `command` is a serialized list of instructions that may include any
28
+ # of the four standard instructions in the SliM protocol:
29
+ #
30
+ # Import: [<id>, import, <path>]
31
+ # Make: [<id>, make, <instance>, <class>, <arg>...]
32
+ # Call: [<id>, call, <instance>, <function>, <arg>...]
33
+ # CallAndAssign: [<id>, callAndAssign, <symbol>, <instance>, <function>, <arg>...]
34
+ #
35
+ # (from http://fitnesse.org/FitNesse.UserGuide.SliM.SlimProtocol)
36
+ #
37
+ def serve_ruby_slim(socket)
38
+ socket.puts("Slim -- V0.3");
39
+ said_bye = false
40
+
41
+ while !said_bye
42
+ length = socket.read(6).to_i # <length>
43
+ socket.read(1) # :
44
+ command = socket.read(length) # <command>
45
+
46
+ # Until a 'bye' command is received, deserialize the command, execute the
47
+ # instructions, and write a serialized response back to the socket.
48
+ if command.downcase != "bye"
49
+ instructions = ListDeserializer.deserialize(command);
50
+ results = @executor.execute(instructions)
51
+ response = ListSerializer.serialize(results);
52
+ socket.write(sprintf("%06d:%s", response.length, response))
53
+ socket.flush
54
+ else
55
+ said_bye = true
56
+ end
57
+ end
58
+ @connected = false
59
+ end
60
+ end
61
+
@@ -0,0 +1,3 @@
1
+ class SlimError < Exception
2
+
3
+ end
@@ -0,0 +1,23 @@
1
+ class SlimHelperLibrary
2
+ ACTOR_INSTANCE_NAME = "scriptTableActor"
3
+ attr_accessor :executor
4
+
5
+ def initialize(executor = nil)
6
+ @executor = executor
7
+ @fixtures = []
8
+ end
9
+
10
+ def get_fixture
11
+ executor.instance(ACTOR_INSTANCE_NAME)
12
+ end
13
+
14
+ def push_fixture
15
+ @fixtures << get_fixture
16
+ nil
17
+ end
18
+
19
+ def pop_fixture
20
+ executor.set_instance(ACTOR_INSTANCE_NAME, @fixtures.pop)
21
+ nil
22
+ end
23
+ end
@@ -0,0 +1,53 @@
1
+ require 'socket'
2
+ require 'thread'
3
+
4
+
5
+ class SocketService
6
+
7
+ attr_reader :closed
8
+
9
+ def initialize()
10
+ @ropeSocket = nil
11
+ @group = ThreadGroup.new
12
+ @serviceThread = nil
13
+ end
14
+
15
+ def serve(port, &action)
16
+ @closed = false
17
+ @action = action
18
+ @ropeSocket = TCPServer.open(port)
19
+ @serviceThread = Thread.start {serviceTask}
20
+ @group.add(@serviceThread)
21
+ end
22
+
23
+ def pendingSessions
24
+ @group.list.size - ((@serviceThread != nil) ? 1 : 0)
25
+ end
26
+
27
+ def serviceTask
28
+ while true
29
+ Thread.start(@ropeSocket.accept) do |s|
30
+ serverTask(s)
31
+ end
32
+ end
33
+ end
34
+
35
+ def serverTask(s)
36
+ @action.call(s)
37
+ s.close
38
+ end
39
+
40
+ def close
41
+ @serviceThread.kill
42
+ @serviceThread = nil
43
+ @ropeSocket.close
44
+ waitForServers
45
+ @closed = true
46
+ end
47
+
48
+ def waitForServers
49
+ @group.list.each do |t|
50
+ t.join
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,79 @@
1
+ require "rubyslim/statement_executor"
2
+
3
+ class Statement
4
+ EXCEPTION_TAG = "__EXCEPTION__:"
5
+ def self.execute(statement, executor)
6
+ Statement.new(statement).exec(executor)
7
+ end
8
+
9
+ def initialize(statement)
10
+ @statement = statement
11
+ end
12
+
13
+ def exec(executor)
14
+ @executor = executor
15
+ begin
16
+ case(operation)
17
+ when "make"
18
+ instance_name = get_word(2)
19
+ class_name = slim_to_ruby_class(get_word(3))
20
+ [id, @executor.create(instance_name, class_name, get_args(4))]
21
+ when "import"
22
+ @executor.add_module(slim_to_ruby_class(get_word(2)))
23
+ [id, "OK"]
24
+ when "call"
25
+ call_method_at_index(2)
26
+ when "callAndAssign"
27
+ result = call_method_at_index(3)
28
+ @executor.set_symbol(get_word(2), result[1])
29
+ result
30
+ else
31
+ [id, EXCEPTION_TAG + "message:<<INVALID_STATEMENT: #{@statement.inspect}.>>"]
32
+ end
33
+ rescue SlimError => e
34
+ [id, EXCEPTION_TAG + e.message]
35
+ rescue Exception => e
36
+ [id, EXCEPTION_TAG + e.message + "\n" + e.backtrace.join("\n")]
37
+ end
38
+
39
+ end
40
+
41
+ def call_method_at_index(index)
42
+ instance_name = get_word(index)
43
+ method_name = slim_to_ruby_method(get_word(index+1))
44
+ args = get_args(index+2)
45
+ [id, @executor.call(instance_name, method_name, *args)]
46
+ end
47
+
48
+ def slim_to_ruby_class(class_name)
49
+ parts = class_name.split(/\.|\:\:/)
50
+ converted = parts.collect {|part| part[0..0].upcase+part[1..-1]}
51
+ converted.join("::")
52
+ end
53
+
54
+ def slim_to_ruby_method(method_name)
55
+ value = method_name[0..0].downcase + method_name[1..-1]
56
+ value.gsub(/[A-Z]/) { |cap| "_#{cap.downcase}" }
57
+ end
58
+
59
+ def id
60
+ get_word(0)
61
+ end
62
+
63
+ def operation
64
+ get_word(1)
65
+ end
66
+
67
+ def get_word(index)
68
+ check_index(index)
69
+ @statement[index]
70
+ end
71
+
72
+ def get_args(index)
73
+ @statement[index..-1]
74
+ end
75
+
76
+ def check_index(index)
77
+ raise SlimError.new("message:<<MALFORMED_INSTRUCTION #{@statement.inspect}.>>") if index >= @statement.length
78
+ end
79
+ end