The Slim Protocol, version 0.5


FitNesse communicates with the Slim Server via StdIn and StdOut ([0.5] default) or via 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.

             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.

The Data

The SlimServer maintains five pieces of data that are operated on by the instructions that it processes.


The Instructions

There are five instructions in the Slim protocol. import, make, call, callAndAssign, and [0.4] assign. 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 $, 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.

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.

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.

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

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.

Actors

[0.3] The stack of library objects should be initialized with an instance of a class with the following 3 methods:


To support symbol assignments in Decision Tables which are implemented with a Scenario function is required in a Library:


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, represented as UTF-16 code units. The six+ digits are the number of UTF-16 code units 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.

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.
  2. 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 bytes, followed by a colon. NOTE: Every other length in this document is in UTF-16 code units. This one length is in bytes.
  3. FitNesse sends a list of instructions encoded as a string of course.
  4. Slim Server sends a list of responses similarly encoded.
  5. 3 and 4 repeat until FitNesse sends a bye directive. This is simply the string bye properly encoded with <length>. e.g. "000003:bye".
  6. Slim Server shuts down.

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

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. message:<<can't find constructor>>

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)

Aborting a Test

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

To abort the test suite (stop execution directly), throw an exception with a class name that contains "StopSuite". The syntax is identical to StopTest.

Ignoring Test

If a fixture throws an exception with a class name that contains "IgnoreScriptTest", then Slim should ignore and skip the instructions in the rest of that script table. The response for this type of exception should be "__EXCEPTION__:IGNORE_SCRIPT_TEST:" which may have an optional suffix of: "message:<<reason>>".

Ignoring All Tests

If a fixture throws an exception with a class name that contains "IgnoreAllTests", then Slim should ignore and skip all the instructions in the rest of that test. The response for this type of exception should be "__EXCEPTION__:IGNORE_ALL_TESTS:" which may have an optional suffix of: "message:<<reason>>".

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

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