The Slim Protocol
+----------+ o---> +------------+ +----------+ +-----+
| FitNesse |---[socket]-->| SlimServer |----->| Fixtures |------>| SUT |
+----------+ <---o +------------+ +----------+ +-----+
FitNesse communicates with Slim over 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.
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:
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 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 search path for fixtures. In java <path> gets added to the CLASSPATH. In .NET, the <path> would name a dll. 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> in the imported search paths. <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 create instance is registered under the name <instance>, and the instruction returns OK.
- Call: [<id>,call,<instance>,<function>,<arg>...] This instruction causes slim to find a function named <function> in the class of the object registered under <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. If the function returns a value, it is converted to a string and returned. Otherwise the instruction returns the string: /__VOID__/.
- CallAndAssign: [<id>, callAndAssign, <symbol>, <instance>, <function>, <arg>...] This instruction is identical to call except that the return value is saved in a symbol named <symbol>. That symbol is kept in a dictionary in the SLIM system. Symbol names may only contain letters.
SymbolsThat last one was probably puzzling. Symbols are just strings 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.
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 instructor 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.
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).
Add Child Page to SlimProtocol