rubyslim 0.1.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 (38) hide show
  1. data/.gitignore +7 -0
  2. data/Gemfile +3 -0
  3. data/Gemfile.lock +16 -0
  4. data/README.md +187 -0
  5. data/Rakefile +21 -0
  6. data/bin/rubyslim +10 -0
  7. data/lib/rubyslim/list_deserializer.rb +67 -0
  8. data/lib/rubyslim/list_executor.rb +12 -0
  9. data/lib/rubyslim/list_serializer.rb +43 -0
  10. data/lib/rubyslim/ruby_slim.rb +61 -0
  11. data/lib/rubyslim/slim_error.rb +3 -0
  12. data/lib/rubyslim/slim_helper_library.rb +23 -0
  13. data/lib/rubyslim/socket_service.rb +53 -0
  14. data/lib/rubyslim/statement.rb +79 -0
  15. data/lib/rubyslim/statement_executor.rb +174 -0
  16. data/lib/rubyslim/table_to_hash_converter.rb +34 -0
  17. data/lib/test_module/library_new.rb +13 -0
  18. data/lib/test_module/library_old.rb +12 -0
  19. data/lib/test_module/should_not_find_test_slim_in_here/test_slim.rb +7 -0
  20. data/lib/test_module/simple_script.rb +9 -0
  21. data/lib/test_module/test_chain.rb +5 -0
  22. data/lib/test_module/test_slim.rb +86 -0
  23. data/lib/test_module/test_slim_with_arguments.rb +23 -0
  24. data/lib/test_module/test_slim_with_no_sut.rb +5 -0
  25. data/rubyslim.gemspec +21 -0
  26. data/spec/instance_creation_spec.rb +40 -0
  27. data/spec/it8f_spec.rb +9 -0
  28. data/spec/list_deserializer_spec.rb +67 -0
  29. data/spec/list_executor_spec.rb +187 -0
  30. data/spec/list_serialzer_spec.rb +39 -0
  31. data/spec/method_invocation_spec.rb +128 -0
  32. data/spec/slim_helper_library_spec.rb +80 -0
  33. data/spec/socket_service_spec.rb +98 -0
  34. data/spec/spec_helper.rb +2 -0
  35. data/spec/statement_executor_spec.rb +50 -0
  36. data/spec/statement_spec.rb +17 -0
  37. data/spec/table_to_hash_converter_spec.rb +32 -0
  38. metadata +100 -0
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ .rakeTasks
2
+ *.iml
3
+ *.ipr
4
+ *.iws
5
+ .DS_Store
6
+ .idea
7
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://www.rubygems.org"
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,16 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rubyslim (0.1.1)
5
+
6
+ GEM
7
+ remote: http://www.rubygems.org/
8
+ specs:
9
+ rspec (1.3.2)
10
+
11
+ PLATFORMS
12
+ ruby
13
+
14
+ DEPENDENCIES
15
+ rspec (~> 1.3.0)
16
+ rubyslim!
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
+ $:.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib/"))
4
+
5
+ require 'rubyslim/ruby_slim'
6
+
7
+ port = ARGV[0].to_i
8
+ rubySlim = RubySlim.new
9
+ rubySlim.run(port)
10
+
@@ -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
+
@@ -0,0 +1,3 @@
1
+ class SlimError < Exception
2
+
3
+ end