Several advanced users have expressed interest to process the test suite generated by Spec Explorer in a different way from the built-in test code generation. The Spec Explorer object model allows to do that. This article provides an introduction to its basic usage.
Whenever Spec Explorer performs an exploration – including a machine which generates test cases – the result is stored in a file ending in ‘.seexpl’. This file is in fact a zipped archive of several XML files representing the result of exploration. The content of this file is displayed under Visual Studio in the well-known graphical form, as shown below – from the far -- for the TestSuite machine of the SMB2 project which comes as a sample with Spec Explorer:
In addition to displaying this file in Visual Studio, it can be also processed with a user program – which we are going to demonstrate in this article.
To this end, the assembly Microsoft.SpecExplorer.ObjectModel contains all you need. Let’s use this to develop a little program which prints some statistics about exploration, the captured requirements, and the action sequences. To replay what we do, create a console application under Visual Studio and add the reference to the above assembly:
The object model is contained in the namespace Microsoft.SpecExplorer.ObjectModel:
using System;using System.Collections.Generic;using System.Linq;using System.Text;using Microsoft.SpecExplorer.ObjectModel;namespace TransitionSystemDemo{ class Program { ...
The top-level object representing an exploration result is called a transition system. This stems from the fact that it actually represents the exploration as a set of states, and transitions between those states labeled with actions. The transition system is loaded by a class called TransitionSystemLoader. Our main method can therefore look as below:
static void Main(string[] args){ if (args.Length != 1) { Console.WriteLine("usage: transitionsystemdemo <pathtoseexpl>"); Environment.Exit(1); } try { var loader = new ExplorationResultLoader(args[0]); TransitionSystem tsys = loader.LoadTransitionSystem(); Process(tsys); } catch (Exception e) { Console.WriteLine("failed: " + e.Message); Environment.Exit(2); }}
The method Process will be defined later. Let us first examine how to print statistics about the transition system, a method which is called from within Process. We use a bit of LINQish magic to compute the ordered list of all requirements captured or assumed to be captured by the transition system:
static void PrintStatistics(TransitionSystem tsys){ Console.WriteLine("{0} states ({1} initial), {2} steps", tsys.States.Length, tsys.InitialStates.Length, tsys.Transitions.Length); var captured = tsys.Transitions.Aggregate( new string[0] as IEnumerable<string>, (cs, tr) => cs.Concat(tr.CapturedRequirements).Concat(tr.AssumeCapturedRequirements)) .Distinct() .OrderBy(x => x); Console.WriteLine("captured: "); foreach (var req in captured) Console.WriteLine(" " + req);}
The transition system contains three central properties which build the hooks for traversing it:
Running this part of the program on the exploration result file of the TestSuite machine in the SMB2 project results in output as below:
This part was easy. The more tricky part is to compute sequences (which would correspond to test cases) from the transition system. The challenges are:
The code resulting from this design is below:
static void PrintPatterns(TransitionSystem tsys){ foreach (var stateLabel in tsys.InitialStates) { Console.WriteLine("Starting in state {0}:", stateLabel); Console.WriteLine(ToPattern(tsys, new HashSet<State>(), GetState(tsys, stateLabel))); }}static string ToPattern(TransitionSystem tsys, HashSet<State> visiting, State state){ if (state.RepresentativeState != null) // skip intermediate state return ToPattern(tsys, visiting, GetState(tsys, state.RepresentativeState)); if (visiting.Contains(state)) // Stop at cycle return ""; visiting.Add(state); // compute successor var successors = GetSuccessors(tsys, state).Select(tr => ToPattern(tsys, visiting, tr)).ToArray(); visiting.Remove(state); if (successors.Length == 0) // end state return ""; else if (successors.Length == 1) return successors[0]; else return "(" + string.Join("|\r\n", successors) + ")";}static string ToPattern(TransitionSystem tsys, HashSet<State> visiting, Transition tr){ var continuation = ToPattern(tsys, visiting, GetState(tsys, tr.Target)); if (continuation != "") return tr.Action.Text + ";\r\n" + continuation; else return tr.Action.Text;}static State GetState(TransitionSystem tsys, string stateLabel){ return tsys.States.First(state => state.Label == stateLabel);}static IEnumerable<Transition> GetSuccessors(TransitionSystem tsys, State state){ return tsys.Transitions.Where(tr => tr.Source == state.Label);}
We will illustrate the result again using the example of the TestSuite exploration result from the SMB2 project. The result for one of the proper sequence cases looks as below:
This concludes this little demo. There are a number of features of the transition system which you can exploit which we haven’t shown. Among the coolest is probably is that all information about actions and action parameters can be converted on-the-fly into reflection information and LINQ expressions, allowing one to write a test executor directly based on the transition system. I may come back with an example for this in a later post.
You can find the full text of the demo program in the window below:
using System;using System.Collections.Generic;using System.Linq;using System.Text;using Microsoft.SpecExplorer.ObjectModel;namespace TransitionSystemDemo{ class Program { static void Main(string[] args) { if (args.Length != 1) { Console.WriteLine("usage: transitionsystemdemo <pathtoseexpl>"); Environment.Exit(1); } try { var loader = new ExplorationResultLoader(args[0]); TransitionSystem tsys = loader.LoadTransitionSystem(); Process(tsys); } catch (Exception e) { Console.WriteLine("failed: " + e.Message); Environment.Exit(2); } } static void Process(TransitionSystem tsys) { PrintStatistics(tsys); PrintPatterns(tsys); } static void PrintStatistics(TransitionSystem tsys) { Console.WriteLine("{0} states ({1} initial), {2} steps", tsys.States.Length, tsys.InitialStates.Length, tsys.Transitions.Length); var captured = tsys.Transitions.Aggregate( new string[0] as IEnumerable<string>, (cs, tr) => cs.Concat(tr.CapturedRequirements).Concat(tr.AssumeCapturedRequirements)) .Distinct() .OrderBy(x => x); Console.WriteLine("captured: "); foreach (var req in captured) Console.WriteLine(" " + req); } static void PrintPatterns(TransitionSystem tsys) { foreach (var stateLabel in tsys.InitialStates) { Console.WriteLine("Starting in state {0}:", stateLabel); Console.WriteLine(ToPattern(tsys, new HashSet<State>(), GetState(tsys, stateLabel))); } } static string ToPattern(TransitionSystem tsys, HashSet<State> visiting, State state) { if (state.RepresentativeState != null) // skip intermediate state return ToPattern(tsys, visiting, GetState(tsys, state.RepresentativeState)); if (visiting.Contains(state)) // Stop at cycle return ""; visiting.Add(state); // compute successor var successors = GetSuccessors(tsys, state).Select(tr => ToPattern(tsys, visiting, tr)).ToArray(); visiting.Remove(state); if (successors.Length == 0) // end state return ""; else if (successors.Length == 1) return successors[0]; else return "(" + string.Join("|\r\n", successors) + ")"; } static string ToPattern(TransitionSystem tsys, HashSet<State> visiting, Transition tr) { var continuation = ToPattern(tsys, visiting, GetState(tsys, tr.Target)); if (continuation != "") return tr.Action.Text + ";\r\n" + continuation; else return tr.Action.Text; } static State GetState(TransitionSystem tsys, string stateLabel) { return tsys.States.First(state => state.Label == stateLabel); } static IEnumerable<Transition> GetSuccessors(TransitionSystem tsys, State state) { return tsys.Transitions.Where(tr => tr.Source == state.Label); } }}