rubyslim-unofficial 0.0.1

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.
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