transcendental-lisp/fitnesse/FitNesseRoot/FitNesse/UserGuide/WritingAcceptanceTests/SliM/SlimProtocol/content.txt

157 lines
17 KiB
Plaintext

!2 The Slim Protocol, version 0.5
!-FitNesse-! communicates with the Slim Server via !-StdIn and StdOut-! (''[0.5]'' default) or via a socket (see >PortManagement). When you hit the ''Test'' button, !-FitNesse-! starts up a !-SlimServer-! process by issuing the appropriate command line. !-FitNesse-! then sends a list of instructions to the !-SlimServer-!, and expects a list of responses back. The instructions are things like ''call function f(a,b,c)'' or ''make instance of class X with arguments p,q,r''. The responses are simply the values that are returned by the instructions.
{{{
instruction list
+----------+ o---> +------------+ +----------+ +-----+
| FitNesse |---[socket]-->| SlimServer |----->| Fixtures |------>| SUT |
+----------+ <---o +------------+ +----------+ +-----+
response list
}}}
Each instruction in the list is itself a list of strings. Here is a typical instruction list:{{{
[
[id_0, make, instance, fixture, argument],
[id_1, call, instance, f, 3],
]}}}The first instruction in this list tells the !-SlimServer-! to create an instance of a class named ''fixture'' using the constructor argument ''argument'', and register the newly created instance under the name ''instance''. The next instruction causes the function ''f'' to be called on the instance ''instance'', passing the value ''3''. The first column in each instruction is simply an instruction id.
The response to this instruction list might look like this:{{{
[
[id_0, OK],
[id_1, 9]
]
}}}Again, each element of the response list is itself a list of strings. The first string in each response is the id of the instruction being responded to. The second is the response value. In this case the construction in instruction ''id_0'' was successful, and the call to function ''f'' with value ''3'' in instruction ''id_1'' returned a ''9''.
That's pretty much it. Lists of instructions go out. Lists of responses come back. Typically the instructions for an entire test page will be sent in one large list, yielding one large response list.
There is no type information in the instructions. Each instruction is a list of strings. Each response is a list of strings. Strings and lists are the only two types in the entire protocol. It is up to the !-SlimExecutor-! to find the functions and constructors that match the instructions, and to do the necessary type conversion.
!3 The Data
The !-SlimServer-! maintains five pieces of data that are operated on by the instructions that it processes.
* A list of class search path items.
* A dictionary of created objects. Each object is addressed by an instance name string.
* A dictionary of symbol values. Each symbol value is addressed by a symbol name string.
* ''[0.1]'' '''Library Instances:''' A stack of library objects.
* ''[0.3]'' '''Actors:''' A stack of actor objects.
!3 The Instructions
There are five instructions in the Slim protocol. import, make, call, callAndAssign, and ''[0.4]'' assign. That's all.
!4 Import
[''<id>'', import, ''<path>'']
This instruction causes the <path> to be added to the list of class search path items. In java <path> gets added to the ''CLASSPATH''. In ''.NET'', the <path> is a namespace. You can send as many imports as you like. The Slim system will use all imported paths to find fixture classes. This instruction returns ''OK''.
!4 Make
[''<id>'', make, ''<instance>'', ''<class>'', ''<arg>...'']
This instruction causes slim to search for a class named ''<class>'' using the list of class search path items. ''<class>'' can also be fully qualified. If found slim looks for a constructor with the right number of arguments. If found, the ''<arg>'' strings are converted to the appropriate types, and the constructor is called. The newly created instance is added to the dictionary of created objects with the name ''<instance>'', and the instruction returns ''OK''.
''[0.1]'' '''Library Instances:''' If a ''make'' instruction creates an object with an instance name that begins with !style_code(library) then that instance is pushed on the stack of library objects. These objects endure for the entire duration of the !-SlimServer-! execution.
''[0.2]'' '''Fixture Chaining:''' Symbols can be used in the ''Make'' command to represent a class name. If the ''<class>'' argument of the ''Make'' command contains '$' characters, then Slim should replace any symbols that have been created by previous ''callAndAssign'' commands. This allows !-FitNesse-! to compose fixture names from symbols set by fixtures, and therefore enables fixture chaining.
''[0.3]'' '''Symbol Copy:''' If ''<class>'' consists entirely of a single symbol name prefixed with $, and the item from the dictionary of symbol values with the symbol name is not a string, then the item is added to the dictionary of created objects with the name ''<instance>''. The ''<arg>'' strings are ignored and no constructor is called.
!4 Call
[''<id>'', call,''<instance>'',''<function>'',''<arg>...'']
This instruction causes slim to find a function named ''<function>'' in the class of the object from the dictionary of created objects with the name ''<instance>''. The function must have the same number of arguments as the instruction. If found, each argument in the instruction is converted to the appropriate type, and then the function is called on the instance. The ''<arg>'' strings may contain symbols (see below) which will be substituted before the type conversion is done. If the function returns a value, it is converted to a string and returned. Otherwise the instruction returns the string: ''/__VOID__/''.
''[0.1]'' '''System Under Test:''' Each fixture may have a way to declare a particular object to be the ''System Under Test''. In java this is done with the @SystemUnderTest attribute. In !-RubySlim-! the object is accessed using the ''sut'' method. In .NET, the fixture implements the !-DomainAdapter-! interface. Whatever mechanism is used, if a the method specified by a ''Call'' or ''!-CallAndAssign-!'' is not found on the specified instance, then if there is a ''System Under Test'' object specified, and the method exists on that object, then it will be called.
''[0.1]'' '''Library Instances:''' If a method specified by a ''Call'' or ''!-CallAndAssign-!'' is not found on either the specified instance, or on the ''System Under Test'' then the stack of library objects is searched, starting at the top (latest). If the method is found, it is called.
''[0.3]'' '''Symbol As Object:''' If an ''<arg>'' consists entirely of a single symbol name prefixed with $, then the item from the dictionary of symbol values with the symbol name is used directly as an argument in the function call.
!4 !-CallAndAssign-!
[''<id>'', callAndAssign, ''<symbol>'', ''<instance>'', ''<function>'', ''<arg>...'']
This instruction is identical to ''call'' except that the return value is saved in the dictionary of symbol values with the name ''<symbol>''. Symbol names may only contain letters.
!4 ''[0.4]'' !-Assign-!
[''<id>'', assign, ''<symbol>'', ''<value>'']
This instruction is used to set symbols on the SUT. The ''<value>'' is saved in the dictionary of symbol values with the name ''<symbol>''. This instruction returns ''OK''.
!3 Symbols
That last one was probably puzzling. Symbol values are strings or objects (or null values) that are kept in a dictionary. The ''callAndAssign'' instruction is the only thing that can create a symbol. Symbols are used in in the ''<arg>'' strings of the ''make'', ''call'', and ''callAndAssign'' instructions. If one of those ''<arg>'' strings contains a $ followed by a symbol name (as in $V), and if the symbol has been assigned, then that string will be replaced by the value of the symbol. What this means is that the !-FitNesse-! side can tell Slim to remember a value in a symbol, and then to use that value later.
''[0.3]'' '''Symbol As Object:''' If the symbol is replaced within a string context, it will be converted to a string. If only the $ followed by a symbol name is given and an object is stored for that symbol, then the object will be used.
!3 Actors
''[0.3]'' The stack of library objects should be initialized with an instance of a class with the following 3 methods:
* getFixture(): returns the object from the dictionary of created objects named "scriptTableActor". Throws an exception if no object exists.
* pushFixture(): pushes the object from the dictionary of created objects named "scriptTableActor" on to the stack of actor objects. Throws an exception if no object exists.
* popFixture(): pops an object from the stack of actor objects and adds it to the dictionary of created objects with the name "scriptTableActor". Throws an exception if the stack is empty.
To support symbol assignments in Decision Tables which are implemented with a Scenario function is required in a Library:
* cloneSymbol
!3 Strings and Lists
As we will see, slim views a list as a special kind of string. Therefore functions can take and return lists as well as strings. The lists must be lists of strings, but since a list is a special kind of string, lists of lists of lists of ... are possible. The Slim executor will convert back and forth between these forms as needed.
A string is encoded as six or more digits followed by a colon, followed by the characters of the string. The six+ digits are the number of characters in the string, not including the digits themselves. Thus, the empty string is "000000:". This length encoding scheme is used in other places so we'll use the token ''<length>'' to mean six digits followed by a colon.
If a string is ''<null>'', then the four character string ''null'' will replace it.
A list is encoded as a string that begins with a '[', followed by a ''<length>'' specifying the number of items in the list. This is followed by that many strings, each terminated by a colon, and then finally a ']' Thus, this list: ''[hello,world]'' is encoded as the following string:{{{000035:[000002:000005:hello:000005:world:]}}}Take careful note of all the colons and counts. Colons are terminators not separators.
As you can see, each item of a list is a string. But since a string can encode a list, each item of a list can be another list. So we can have very deep recursive definitions.
''[0.4]'' For versions older than 0.4, the length part of an encoded string was exactly six digits. For version 0.4 the size can grow beyond 6 digits for really long messages.
!3 Slim Server
So when you send a list of instructions, what you are really sending is a string. When you receive a list of responses, what you are really receiving is a string. So the high level protocol of Slim is just strings. It looks like this:
1 !-FitNesse-! invokes the Slim Server via a command line. One of the command line arguments is the port number of the socket to listen on. If the port number is 1 than the communication happens via !-StdIn and Stdout-!. For any other port number the !-Slim Server-! opens that socket and start listening. !-FitNesse-! connects to that socket.
1 Slim Server responds to the connect request with the string "Slim !----! ''V<version>''\n", where ''<version>'' is the version number of the slim ''protocol''. If this protocol ever changes, ''that'' version number will change. This is the only string that is ever sent without the ''<length>'' encoding. It is terminated by the '''\n''' instead. Every other message that slim sends will be prefixed by a ''<length>'' in ''!style_red(bytes)'', followed by a colon. !style_red(NOTE:) Every other length in this document is in UTF-8 ''characters''. This one length is in bytes.
1 !-FitNesse-! sends a list of instructions encoded as a string of course.
1 Slim Server sends a list of responses similarly encoded.
1 3 and 4 repeat until !-FitNesse-! sends a ''bye'' directive. This is simply the string ''bye'' properly encoded with ''<length>''. e.g. "000003:bye".
1 Slim Server shuts down.
!3 !-StdOut and StdErr-! from the SUT
Everything written to !-StdOut and StdErr-! by the SUT is added by !-FitNesse-! to the Execution Log and stored with the test results.
''[0.5]'' In case the port number is 1 the communication is happening via !-StdIn and StdOut-!. As !-StdOut-! is in use for testing everything written to !-StdOut-! by the SUT is tunneled via !-StdErr-!
* the Slim Server is prefixing every line from !-StdOut-! with "SOUT :". In case a single message spans multiple lines all following lines will be prefixed with "SOUT.:".
* the Slim Server is prefixing every line from !-StdErr-! with "SERR :". In case a single message spans multiple lines all following lines will be prefixed with "SERR.:".
* both types of messages are then send via !-StdErr-!
* !-FitNesse-! splits incoming text again into stdout and stderr and adds it to the appropriate section in the Execution Log
* The above prefixes are never visible for the !-FitNesse-! user
* if !-FitNesse-! receives text without such a prefix it adds the text as it is to the stdout section from the Execution Log
!3 Exceptions
Sometimes a function or a constructor will throw an exception in response to a ''make'', ''call'', or ''callAndAssign'' instruction. When this happens, the response value for that instruction will be: "__EXCEPTION__:''<exception string>''". The ''<exception string>'' ought to be a stack trace or some other relevant debugging information. If you want a nice yellow message to appear in one of the SLIM tables, then somewhere in the ''<<exception string>>'' put ''message:<<'' in front of the message and ''>>'' after it. e.g. !style_code(message:<<can't find constructor>>)
!4 Standard exception messages
There are some standard exception messages that every Slim implementation should create.
| COULD_NOT_INVOKE_CONSTRUCTOR ''<some class>'' | Where ''<some class>'' is the name of the class whose constructor cannot be invoked. |
| NO_METHOD_IN_CLASS ''<some method>'' ''<some class>'' | Where ''<some method>'' is the name of the missing method. |
| NO_CONSTRUCTOR ''<some class>'' | Where ''<some class>'' is the name of the class that is missing the constructor. |
| NO_CONVERTER_FOR_ARGUMENT_NUMBER ''<argument type>'' | Where ''<argument type>'' is the class that has no corresponding converter. |
| NO_INSTANCE ''<instance name>'' | Where ''<instance name>'' is the name of the missing instance. |
| NO_CLASS ''<some class>'' | Where ''<some class>'' is the class that could not be found. |
| MALFORMED_INSTRUCTION [''instruction list''] | Where ''instruction list'' is a comma separated list of the instruction strings. |
| TIMED_OUT ''<n>'' | The instruction timed out after ''<n>'' seconds. To enforce timeouts, set the ''-s xx'' flag (via the ''slim.flags'' variable) |
!4 Aborting a Test
If a fixture throws an exception with a ''class'' name that contains "!style_code(!-StopTest-!)", then Slim should stop executing the instructions in the current batch, and return immediately. The response for this type of exception should be "!style_code(__EXCEPTION__:ABORT_SLIM_TEST:)" which may have an optional suffix of: "!style_code(message:<<''reason''>>)".
To abort the test suite (stop execution directly), throw an exception with a class name that contains "!style_code(!-StopSuite-!)". The syntax is identical to !-StopTest-!.
!3 Type Conversions
The only types in the instructions and responses are lists and strings, and since the leaves of the lists must eventually be strings, all we really have to worry about are strings. But we don't want to restrict our fixtures to use only Strings. So Slim comes with some standard type converters that allow fixtures to take more convenient data types.
!see DataTypes.
''[0.1]'' '''Hashes:''' !style_note(Optional) If one of the method arguments int a ''Make'', ''Call'', or ''!-CallAndAssign-!'' matches the "hash" format, then it should be converted into a dictionary, or a hash, or some convenient form for the fixture authors. In Java they are converted into Maps. In Ruby they are converted into Hashes. Other languages may use other structures. The "hash" format is the format produced by the [[Hash Widget][<UserGuide.MarkupHashTable]], and is simply the HTML for a table with two columns and ''n'' rows. !style_code(<table><tr><td>name</td><td>value</td></tr>...</table>).
!3 Conclusion
That's pretty much it. If you want to port Slim to a new platform, I suggest you look at the code in the fitnesse.slim package. Pay special attention to the !-ListSerializer-! and !-ListDeserializer-! classes. Also check out the logic in Statement and !-StatementExecutor-! classes. The unit tests ought to be expecially educational. You should be able to build equivalent unit tests without much fuss. Finally, take a look at the unit tests in fitnesse.responders.run.slimResponder. These should all still run with your new port (although you'll have to replace the command line that invokes the Slim Server).