rubyslim 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +16 -0
- data/README.md +187 -0
- data/Rakefile +21 -0
- data/bin/rubyslim +10 -0
- data/lib/rubyslim/list_deserializer.rb +67 -0
- data/lib/rubyslim/list_executor.rb +12 -0
- data/lib/rubyslim/list_serializer.rb +43 -0
- data/lib/rubyslim/ruby_slim.rb +61 -0
- data/lib/rubyslim/slim_error.rb +3 -0
- data/lib/rubyslim/slim_helper_library.rb +23 -0
- data/lib/rubyslim/socket_service.rb +53 -0
- data/lib/rubyslim/statement.rb +79 -0
- data/lib/rubyslim/statement_executor.rb +174 -0
- data/lib/rubyslim/table_to_hash_converter.rb +34 -0
- data/lib/test_module/library_new.rb +13 -0
- data/lib/test_module/library_old.rb +12 -0
- data/lib/test_module/should_not_find_test_slim_in_here/test_slim.rb +7 -0
- data/lib/test_module/simple_script.rb +9 -0
- data/lib/test_module/test_chain.rb +5 -0
- data/lib/test_module/test_slim.rb +86 -0
- data/lib/test_module/test_slim_with_arguments.rb +23 -0
- data/lib/test_module/test_slim_with_no_sut.rb +5 -0
- data/rubyslim.gemspec +21 -0
- data/spec/instance_creation_spec.rb +40 -0
- data/spec/it8f_spec.rb +9 -0
- data/spec/list_deserializer_spec.rb +67 -0
- data/spec/list_executor_spec.rb +187 -0
- data/spec/list_serialzer_spec.rb +39 -0
- data/spec/method_invocation_spec.rb +128 -0
- data/spec/slim_helper_library_spec.rb +80 -0
- data/spec/socket_service_spec.rb +98 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/statement_executor_spec.rb +50 -0
- data/spec/statement_spec.rb +17 -0
- data/spec/table_to_hash_converter_spec.rb +32 -0
- metadata +100 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
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,67 @@
|
|
1
|
+
module ListDeserializer
|
2
|
+
class SyntaxError < Exception
|
3
|
+
end
|
4
|
+
|
5
|
+
# De-serialize the given string, and return a Ruby-native list.
|
6
|
+
# Raises a SyntaxError if the string is empty or badly-formatted.
|
7
|
+
def self.deserialize(string)
|
8
|
+
raise SyntaxError.new("Can't deserialize null") if string.nil?
|
9
|
+
raise SyntaxError.new("Can't deserialize empty string") if string.empty?
|
10
|
+
raise SyntaxError.new("Serialized list has no starting [") if string[0..0] != "["
|
11
|
+
raise SyntaxError.new("Serialized list has no ending ]") if string[-1..-1] != "]"
|
12
|
+
Deserializer.new(string).deserialize
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
class Deserializer
|
17
|
+
def initialize(string)
|
18
|
+
@string = string;
|
19
|
+
end
|
20
|
+
|
21
|
+
def deserialize
|
22
|
+
@pos = 1
|
23
|
+
@list = []
|
24
|
+
number_of_items = consume_length
|
25
|
+
|
26
|
+
# For each item in the list
|
27
|
+
number_of_items.times do
|
28
|
+
length_of_item = consume_length
|
29
|
+
item = @string[@pos...@pos+length_of_item]
|
30
|
+
length_in_bytes = length_of_item
|
31
|
+
|
32
|
+
until (item.length > length_of_item) do
|
33
|
+
length_in_bytes += 1
|
34
|
+
item = @string[@pos...@pos+length_in_bytes]
|
35
|
+
end
|
36
|
+
|
37
|
+
length_in_bytes -= 1
|
38
|
+
item = @string[@pos...@pos+length_in_bytes]
|
39
|
+
|
40
|
+
# Ensure the ':' list-termination character is found
|
41
|
+
term_char = @string[@pos+length_in_bytes,1]
|
42
|
+
if term_char != ':'
|
43
|
+
raise SyntaxError.new("List termination character ':' not found" +
|
44
|
+
" (got '#{term_char}' instead)")
|
45
|
+
end
|
46
|
+
|
47
|
+
@pos += length_in_bytes+1
|
48
|
+
begin
|
49
|
+
sublist = ListDeserializer.deserialize(item)
|
50
|
+
@list << sublist
|
51
|
+
rescue ListDeserializer::SyntaxError
|
52
|
+
@list << item
|
53
|
+
end
|
54
|
+
end
|
55
|
+
@list
|
56
|
+
end
|
57
|
+
|
58
|
+
# Consume the 6-digit length prefix, and return the
|
59
|
+
# length as an integer.
|
60
|
+
def consume_length
|
61
|
+
length = @string[@pos...@pos+6].to_i
|
62
|
+
@pos += 7
|
63
|
+
length
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
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,43 @@
|
|
1
|
+
module ListSerializer
|
2
|
+
# Serialize a list according to the SliM protocol.
|
3
|
+
#
|
4
|
+
# Lists are enclosed in square-brackets '[...]'. Inside the opening
|
5
|
+
# bracket is a six-digit number indicating the length of the list
|
6
|
+
# (number of items), then a colon ':', then the serialization of each
|
7
|
+
# list item. For example:
|
8
|
+
#
|
9
|
+
# [] => "[000000:]"
|
10
|
+
# ["hello"] => "[000001:000005:hello:]"
|
11
|
+
# [1] => "[000001:000001:1:]"
|
12
|
+
#
|
13
|
+
# Strings are preceded by a six-digit sequence indicating their length:
|
14
|
+
#
|
15
|
+
# "" => "000000:"
|
16
|
+
# "hello" => "000005:hello"
|
17
|
+
# nil => "000004:null"
|
18
|
+
#
|
19
|
+
# See spec/list_serializer_spec.rb for more examples.
|
20
|
+
#
|
21
|
+
def self.serialize(list)
|
22
|
+
result = "["
|
23
|
+
result += length_string(list.length)
|
24
|
+
|
25
|
+
# Serialize each item in the list
|
26
|
+
list.each do |item|
|
27
|
+
item = "null" if item.nil?
|
28
|
+
item = serialize(item) if item.is_a?(Array)
|
29
|
+
item = item.to_s
|
30
|
+
result += length_string(item.length)
|
31
|
+
result += item + ":"
|
32
|
+
end
|
33
|
+
|
34
|
+
result += "]"
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
# Return the six-digit prefix for an element of the given length.
|
39
|
+
def self.length_string(length)
|
40
|
+
sprintf("%06d:",length)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
@@ -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
|
+
|