I’ve been working on some more spikes around my Fluent Fitnesse effort mentioned a couple of days ago. One of the things this is designed to be is a roundtrip tool – so Fitnesse pages get converted to code, and vice versa. This way the business people still use Fitnesse for all of their work, and the coders can build and maintain it in code.
The spike tonight was the basic parsing of a page. I had some ideas of what I wanted to do, but I hadn’t settled on a strategy. So I did what I always do when I’m stuck – I write a test:
[Test]
public void AllLinesStartingWithBangNotInATableShouldBePlacedInTheHeaderBuffer()
{
string expected = @"!contents -R2 -g -p -f -h
!define COMMAND_PATTERN {%m %p}
!define TEST_RUNNER {dotnet2\FitServer.exe}
!path C:\Users\Cory\Documents\Visual Studio 2008\Projects\FluentFitnesse\DivisionFixtures\bin\Debug\DivisionFixtures.dll
";
string actual = HeaderParser.Parse(SAMPLE_PAGE);
Assert.AreEqual(expected, actual);
}
Seemed reasonable enough. I knew in my head this wouldn’t be perfect – definitions can happen throughout the page, and this kind of shoves them all into one. But I knew that would come out later. So I wrote some code to make it pass:
public class HeaderParser
{
public static string Parse(string page)
{
StringBuilder output = new StringBuilder();
foreach (string line in page.Split(new char[] { '\r','\n' }, StringSplitOptions.RemoveEmptyEntries))
{
if (line.StartsWith("!"))
{
if (line.IndexOf("|") > 0)
continue;
output.AppendLine(line);
}
}
return output.ToString();
}
}
Again, not perfect. For example, if you haven’t used Fitnesse, why does it matter if line.IndexOf("|") is greater than 0? (It’s because you can tell the wiki not to process WikiWords inside a table by prefixing the table row with a bang (!)). But I was on a mission – because I knew that this whole thing wouldn’t last. So I wrote my next test:
[Test]
public void CommentTableWithOneLineShouldConvertToFluentFitnesseCommentClass()
{
string commentTable = @"|Comment|
|Module1.DivisionDoFixture|";
string expected = @"using(FluentFitnesse.Comment.BoundTable)" + System.Environment.NewLine
+ "{" + System.Environment.NewLine
+ "FluentFitnesse.Comment.Call.Comment(\"Module1.DivisionDoFixture\");" + System.Environment.NewLine
+ "}" + System.Environment.NewLine;
string actual = CommentParser.Parse(commentTable);
Assert.AreEqual(expected, actual);
}
}
This deals with |Comment| tables in Fitnesse, which won’t be tied to a class. And the code to make this pass? This nasty thing:
public static string Parse(string page)
{
StringBuilder classOutput = new StringBuilder();
bool inCommentTable = false;
foreach (string line in page.Split(new char[] { ‘\r’,’\n’ }, StringSplitOptions.RemoveEmptyEntries))
{
if (line.StartsWith("|Comment|"))
{
inCommentTable = true;
classOutput.AppendLine("using(FluentFitnesse.Comment.BoundTable)");
classOutput.AppendLine("{");
}
else if (inCommentTable)
{
if (line.Trim().Length == 0)
{
classOutput.AppendLine("}");
inCommentTable = false;
}
else
{
string contents = line.Replace(‘|’, ‘ ‘);
classOutput.AppendLine("FluentFitnesse.Comment.Call.Comment(\"" + contents.Trim() + "\");");
}
}
}
if (inCommentTable)
{
classOutput.AppendLine("}");
}
return classOutput.ToString();
}
My spidey-pattern senses were tingling, but I wanted one more test:
[Test]
public void TextNotInATableShouldBeConvertedToFluentFitnesseTextCall()
{
string page = "This is some text";
string expected = "FluentFitnesse.Text.Call.Text(\"This is some text\");";
string actual = PageTextParser.Parse(page);
Assert.AreEqual(expected, actual);
}
And the code?
public static string Parse(string page)
{
return "FluentFitnesse.Text.Call.Text(\"" + page + "\");";
}
Ah! Now that’s more like it. Since text can be anywhere in the page, it has to stay in the same place because otherwise it will be all jumbled. For the header I knew it wasn’t as important, because, after all, it is header information. But page text not in a table means that I have to preserve the line locations. Which means that my initial though of parsing the pages and spitting out whole sections of code is off – I instead need to parse line by line and pass
to a more Chain-based approach to let handlers deal with what to output next.
I’m sure that had I taken time to write and sketch I would have come to the same conclusion, but having the natural feel of the tests and writing code is something that is so, well, satisfying.