The Slim Protocol
instruction list +----------+ o---> +------------+ +----------+ +-----+ | FitNesse |---[socket]-->| SlimServer |----->| Fixtures |------>| SUT | +----------+ <---o +------------+ +----------+ +-----+ response list
FitNesse communicates with Slim over a socket (see >Port Management). 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.
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.
The DataThe 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.
The InstructionsThere are four instructions in the Slim protocol. import, make, call, and callAndAssign. That's all.
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.
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 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 $, then the item from the dictionary of symbol values with the symbol name is added to the dictionary of created objects with the name <instance>. The <arg> strings are ignored and no constructor is called.
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.
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.
SymbolsThat 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.
[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.
Strings and ListsAs 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 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.
You might think that 6 digits is plenty; but the last few decades have shown us that a megabyte isn't even close to infinite. So you'll want to keep that in mind.
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:
- 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. FitNesse opens that socket.
- Slim Server responds 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 bytes, followed by a colon. NOTE: Every other length in this document is in UTF-8 characters. This one length is in bytes.
- FitNesse sends a list of instructions encoded as a string of course.
- Slim Server sends a list of responses similarly encoded.
- 3 and 4 repeat until FitNesse sends a bye directive. This is simply the string bye properly encoded with <length>. e.g. "000003:bye".
- Slim Server shuts down.
ExceptionsSometimes 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. message:<<can't find constructor>>
Standard exception messagesThere 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.|
Aborting a TestIf a fixture throws an exception with a class name that contains "StopTest", then Slim should stop executing the instructions in the current batch, and return immediately. The response for this type of exception should be "__EXCEPTION__:ABORT_SLIM_TEST:" which may have an optional suffix of: "message:<<reason>>".
Type ConversionsThe 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: Data Types.
[0.1] Hashes: 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, and is simply the HTML for a table with two columns and n rows. <table><tr><td>name</td><td>value</td></tr>...</table>.
ConclusionThat'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).
Keyboard Shortcuts ?