Archive for the ‘eclipse’ Category

Using Xtext for creating web.xml

Thursday, August 19th, 2010

A colleage of mine keeps saying: If 12 people within the company spend only 5 minutes a day on searching for something, this makes an hour a day, 5 hours a week, 20 a month, etc.
Now multiply that with your favorite hourly rate. To be sure that adds up to a lot of time and money that could be better spent. Further, imagine yourself going through endless xml files trying to spot some typo in an ID or fully qualified class name (adding frustration to the list). Depending on the situation, 5 minutes might even be very optimistic. Hopefully, this convinces you that this is not only some academic case but the reality.

web.xmls are one example. Not that I ever had to write one, but I was asked to implement an Xtext prototype for a more readable form with basic support for referencing classes and basic tests, whether given files actually exist. Of course the web.xml was to be generated from the model.

Not all possible concepts were to be supported for this show case. The goal was to support roughly what the petshop example required. The grammar was written from scratch, not importing existing meta models (e.g. generated from the xsd definition). We used the JvmType support that comes with Xtext 1.0.0 for referencing existing classes. Adding to Xtext’s out of the box suppport some validation rules for the referenced classes, some for searching the project for files, some adaption to the code completion for classes, some simple Xpand templates for generating the xml, a builder participant (thanks for the excellent code basis, Holger!) for generating it on saving the model. Done.

screenshot of the web.xml editor prototype

web.xml-Editor prototype

The time invested in this prototype was less than 3 days. Of course there is room for improvement in every direction. Add further concepts that are needed, more code completion (e.g. for parameters), starting the server on a given URL,… I don’t know what people having to write web.xmls dream of. Now, what is the point of this blog entry. Maybe just to make you think about what difference some investments in custom tooling could make in your company.

Basic lexer and parser tests for your Xtext grammar

Monday, August 2nd, 2010

If you spend some time in the TMF forum, you will notice that every once in a while there are questions of the type: ‘I want to define a terminal rule for SomeMoreOrLessComplicatedStuff’ or ‘I have defined a terminal rule, but I get the error mismatched input …’, ‘I want keyword X to be usable as a name for an entity’, etc.
This post is not about how to make the choice between using a terminal or a datatype rule or how to come up with good terminal rules (these are non-trivial topics). It is about (unit-) testing, whether the grammar reflects your intentions, that is checking whether xyz will actually cause the terminal rule you intended to fire (which often enough is not the case) and whether your datatype rules will successfully parse the strings you wrote them for. This post will not deal with value conversion.
In particular if your language is evolving and you introduce new keywords, terminal rules, it is important that you have a simple and quick way of validating if the changes did not break anything. Simply having one big sample model file that you open in the generated editor to see if there are syntax errors is hardly sufficient.
Xtext 1.0.0 comes with a nice entry point for writing your tests: AbstractXtextTests. If you check out the Xtext source code and look at the type hierarchy of this class, you will find plenty of code that might serve as inspiration for tests covering much of the language and editor infrastructure of an Xtext based project.

Our sample grammar looks as follows

grammar org.xtext.example.mydsl.MyDsl with org.eclipse.xtext.common.Terminals
generate myDsl "http://www.xtext.org/example/mydsl/MyDsl"

Model: (entities+=Entity)*;

Entity:'entity' name=ID (extends=[Entity|QualifiedName])? '{'
    (properties+=Property)*
'}';

Property: 'property' name=SPECIAL_ID; 

QualifiedName: ID('.'ID)*;
terminal SPECIAL_ID: ('A'..'Z')+('_'INT)?;

As a side note for those not seeing it right away: The terminal rules SPECIAL_ID and ID overlap. ABC fits both the ID and the SPECIAL_ID pattern, but SPECIAL_ID will win during tokenisation (lexing) as it has higher priority than ID (which is only imported). As a consequence you cannot name an entity ABC – the grammar requires an ID (ABC would thus be a syntax error). You cannot name it “entity” either, as that is a keyword in the grammar. If you wanted to allow that you would have to write name=(ID|SPECIAL_ID|’entity’) or introduce a corresponding datatype rule.

The following class could serve as the basic infrastructure for testing terminal and datatype rules:

public abstract class AbstractBasicLexerAndParserTest extends
    AbstractXtextTests {

  private Lexer lexer;
  private ITokenDefProvider tokenDefProvider;
  private IAntlrParser parser;

  protected Lexer getLexer() {
    return lexer;
  }

  protected ITokenDefProvider getTokenDefProvider() {
    return tokenDefProvider;
  }

  protected IAntlrParser getAntlrParser() {
    return parser;
  }

  @SuppressWarnings("rawtypes")
  abstract Class getStandaloneSetupClass();

  @SuppressWarnings("unchecked")
  @Override
  protected void setUp() throws Exception {
    super.setUp();
    with(getStandaloneSetupClass());
    lexer = get(Lexer.class);
    tokenDefProvider = get(ITokenDefProvider.class);
    parser = get(IAntlrParser.class);
  }

  /**
   * return the list of tokens created by the lexer from the given input
   * */
  protected List<Token> getTokens(String input) {
    CharStream stream = new ANTLRStringStream(input);
    getLexer().setCharStream(stream);
    XtextTokenStream tokenStream = new XtextTokenStream(getLexer(),
        getTokenDefProvider());
    @SuppressWarnings("unchecked")
    List<Token> tokens = tokenStream.getTokens();
    return tokens;
  }

  /**
   * return the name of the terminal rule for a given token
   * */
  protected String getTokenType(Token token) {
    return getTokenDefProvider().getTokenDefMap().get(token.getType());
  }

  /**
   * check whether an input is chopped into a list of expected token types
   * */
  protected void checkTokenisation(String input, String... expectedTokenTypes) {
    List<Token> tokens = getTokens(input);
    assertEquals(input, expectedTokenTypes.length, tokens.size());
    for (int i = 0; i < tokens.size(); i++) {
      Token token = tokens.get(i);
      assertEquals(input, expectedTokenTypes[i], getTokenType(token));
    }
  }

  /**
   * check that an input is not tokenised using a particular terminal rule
   * */
  protected void failTokenisation(String input, String unExpectedTokenType) {
    List<Token> tokens = getTokens(input);
    assertEquals(input, 1, tokens.size());
    Token token = tokens.get(0);
    assertNotSame(input, unExpectedTokenType, getTokenType(token));
  }

  /**
   * return the parse result for an input given a specific entry rule of the
   * grammar
   * */
  protected IParseResult getParseResult(String input, String entryRule) {
    return getAntlrParser().parse(entryRule, new StringReader(input));
  }

  /**
   * check that the input can be successfully parsed given a specific entry
   * rule of the grammar
   * */
  protected void checkParsing(String input, String entryRule) {
    IParseResult la = getParseResult(input, entryRule);
    assertEquals(input, 0, la.getParseErrors().size());
  }

  /**
   * check that the input cannot be successfully parsed given a specific entry
   * rule of the grammar
   * */
  protected void failParsing(String input, String entryRule) {
    IParseResult la = getParseResult(input, entryRule);
    assertNotSame(input, 0, la.getParseErrors().size());
  }

  /**
   * check that input is treated as a keyword by the grammar
   * */
  protected void checkKeyword(String input) {
    // the rule name for a keyword is usually
    // the keyword enclosed in single quotes
    String rule = new StringBuilder("'").append(input).append("'")
        .toString();
    checkTokenisation(input, rule);
  }

  /**
   * check that input is not treated as a keyword by the grammar
   * */
  protected void failKeyword(String keyword) {
    List<Token> tokens = getTokens(keyword);
    assertEquals(keyword, 1, tokens.size());
    String type = getTokenType(tokens.get(0));
    assertFalse(keyword, type.charAt(0) == '\'');
  }
}

And the following is an actual test class for the above grammar. It should give you an idea of how to use the abstract test class.

public class MyDslLexerAndParserTest extends AbstractBasicLexerAndParserTest {

  @SuppressWarnings("rawtypes")
  @Override
  Class getStandaloneSetupClass() {
    //here you should return the StandaloneSetup class
    //of the language you want to test
    return MyDslStandaloneSetup.class;
  }

  //for convenience, define constants for the
  //rule names in your grammar
  //the names of terminal rules will be capitalised
  //and "RULE_" will be appended to the front
  private static final String ID="RULE_ID";
  private static final String SPECIAL_ID="RULE_SPECIAL_ID";
  private static final String INT="RULE_INT";
  private static final String WS="RULE_WS";
  private static final String SL_COMMENT="RULE_SL_COMMENT";

  private static final String FQN="QualifiedName";

  public void testID(){
    checkTokenisation("a", ID);
    checkTokenisation("abc", ID);
    checkTokenisation("abc123", ID);
    checkTokenisation("abc_123", ID);
    checkTokenisation("^entity", ID);

    //fail as entity is a keyword
    failTokenisation("entity", ID);
    //fail as A is a SPECIAL_ID
    failTokenisation("A", ID);
  }

  public void testSpecialID(){
    checkTokenisation("A", SPECIAL_ID);
    checkTokenisation("ABC", SPECIAL_ID);
    checkTokenisation("ABC_123", SPECIAL_ID);

    //fail as underscore is missing
    failTokenisation("ABC123", SPECIAL_ID);
  }

  public void testSLCOMMENT(){
    checkTokenisation("//comment", SL_COMMENT);
    checkTokenisation("//comment\n", SL_COMMENT);
    checkTokenisation("// comment \t\t comment\r\n", SL_COMMENT);
  }

  public void testKeywords(){
    checkKeyword("entity");
    checkKeyword("property");
    checkKeyword(".");

    //Entity is not a keyword
    failKeyword("Entity");
  }

  public void testTokenSequences(){
    checkTokenisation("123 abc", INT, WS, ID);
    checkTokenisation("123 \t//comment\n abc", INT, WS, SL_COMMENT,WS,ID);

    //note that no white space is necessary!
    checkTokenisation("123abc", INT, ID);
  }

  public void testQualifiedName(){
    checkParsing("abc.d", FQN);
    //note that white spaces and comments are hidden
    //so they are allowed within qualified names
    //if you don't want that, you have to start the rule
    //definition with QualifiedName hidden():
    //thereby making all tokens visible to the rule
    checkParsing("abc   .   d", FQN);
    checkParsing("abc /*comment*/  .\t\n//comment\n  d", FQN);

    //fail as ABC is a SPECIAL_ID
    failParsing("ABC.d", FQN);
  }

  //this test has nothing to do with the blog post topic
  //but it illustrates a simple way for unit testing your
  //language, querying the instantiated model
  public void testModel(){
    Model m;
    try {
      //missing names
      m=(Model) getModelAndExpect("entity{}entity{}", EXPECT_ERRORS);
      int entityCount=m.getEntities().size();
      assertEquals(2, entityCount);

      m=(Model)getModelAndExpect("entity name{}", 0);
      String name=m.getEntities().get(0).getName();
      assertEquals("name", name);

      //name must be ID not SPECIAL_ID
      m=(Model)getModelAndExpect("entity NAME{property p:PNAME}", EXPECT_ERRORS);
      int propertyCount=m.getEntities().get(0).getProperties().size();
      assertEquals(1, propertyCount);

    } catch (Exception e) {}
  }
}

Now, what do you think will be the token types created for 123ABC_123ab //comment? If you think “nothing simpler than that”, add the corresponding test and if you were wrong, improve the fail messages in the abstract class, so that they are more helpful for finding your mistake.

modellbasiertes schönes Testen mit Xtext und Fit

Tuesday, June 15th, 2010

Ich bin gerade unter Anderem dabei einen Showcase vorzubereiten, in dem es darum geht, Anwendungen mit Fit/FitNesse zu testen.
Speziell geht es dabei um eine Webanwendung die als Software-Under-Test mit Black-Box Tests getestet werden soll.
FitNesse in Kombination mit Selenium eignet sich für diese Art des Testens sehr gut. Der wikibasierte Editor im FitNesse ist allerdings unzureichend für diejenigen die sich gern eigene Editoren z.B. mit Xtext bauen.

Und so habe ich mir eben einen Testeditor und einen Oberflächenbeschreibungseditor gebaut. Damit kann ich sehr schnell Oberflächen modellieren und diese Modelle mit Testmodellen verbinden. Neben dem sehr angenehmen Schreiben der Testfälle und Testsuiten habe ich auch die Möglichkeit beispielsweise Testüberdeckungsmaße für Oberflächenelemente zu berechnen. Auch die möglichen Fixtures und ihre Kommandos kann ich darin modellieren und nutzen.

Ursprünglich hieß der Vortrag modellbasiertes Testen. Da modellbasiert aber nicht mein Ziel, sondern mein Weg ist, habe ich den Vortrag kurzerhand umbenannt in schönes Testen. ;)

Und letzte Woche waren nun 2 eclipse Democamps hier in der Nähe. Zuerst hatten wir zusammen mit der TU Dresden ein Democamp in den hochmodernen Räumen der TU. Einen Tag später durfte ich nochmal ran beim Democamp in Jena im Intershop-Tower. Der Ausblick aus der 29. Etage ist immer wieder faszinierend.

Anbei noch 2 Fotos vom Dresdner Democamp, bei dem fast 80 Teilnehmer anwesend waren.

rund 80 Interessierte lauschen den spannenden Vorträgen kurze Pause während der Vorträge

Multilingual Eclipse help using Docbook

Tuesday, June 8th, 2010

Based on Robert’s blog on creating an Eclipse Help plugin, I want to extend that example to support multiple languages. By default, Eclipse seems already to interpret toc.xml, contexts.xml (for contexts defined using the contexts extension point) and the html-files from an nl/de, nl/fr,… directory within the plugin if the corresponding locale is set in the Eclipse start parameters.

So the idea is to adapt the sample to allow docbook sources for all wanted languages within the plugin. There are other objectives that I want to address here:

  • some support for keeping help in synch for all languages
  • try to support some basic context help

In this post, I will not present many snippets but only briefly describe the intentions of the sample help project (download here).
You’ll have to copy the docbook-xml-4.5- and docbook-xsl-files to the corresponing directories in the project as described in Robert’s blog. Note that you don’t need the modified eclipse33.xsl file. For our purposes, we further extended that file and it is included in the docbook-directory and automatically used. There you also find the build script. I have limited experience with xslt and ant (not to say none), so there is much room for improvement.

The docbook directory should contain one subdirectory per language named corresponding to the intended locale (the sample has one for German and one for English). The script assumes that for each language the root xml file is called manual.xml. After first running the script (which probably fails) a run-configuration is created. In the JRE-tab, choose “run in the same JRE as workspace” as described in Robert’s blog. Afterwards, the build should be successful.

If you don’t change the parameters, a contexts.xml will be generated. It basically contains all ids you define in the docbook sources along with a link to their location in the generated html file. Also a ContextConstants.java is generated which contains java-constants for all those ids. Why that? The idea is to simplify a basic context sensitive help. Usually you don’t write a help plugin for its own purpose but for documenting some other set of plugins.

Assuming, you have defined a view in some plugin (and it implements Adaptable) you can use the following code to get a link to the documentation for that view in the dynamic help (please make getInternalHelpContext in HelpUtil.java a public method…)

public Object getAdapter(Class adapter) {
  if (IContextProvider.class.equals(adapter)) {
    return HelpUtil.getInternalHelpContext(
       APxHelpUtil.ConstantForContextThatShouldBeUsedForTheView);
    }
  ...
}

When the dynamic help view is open this should cause a see-also-link to the documentation to be displayed.

The generated constants make sure that the context you call actually exists (otherwise you get a compile error, as the constant is not known). But they also help you to keep the help for different languages in synch. Ideally, the structure of the help is the same for all languages and only the texts differ. So for every language you would use the same ids for chapters, sections etc. If sturcture and ids are indeed the same, the generated ContextConstants will be identical for all languages. If not, they differ and you have feedback that something is wrong.

There are a couple of things missing. If there are broken links in the docbook files (i.e. pointers to non-existent ids), the build process does not fail. The generated html files will contain ???. It would be cool if there was some automatic report for that. Also, the help will not work if toc.xml or contexts.xml contain a & as in &auml;. For some characters, there is a replace statement in the build.xml but it would be nice, if the “correct” characters were already generated. Anyway, I have tried to provide helpful comments in the build script so that you should get an idea what is going on and have hints for making your own adaptions. Note that contexts.xml and plugin.xml will “always” be overwritten, so if you want to define your ones, you have to adapt the script and the transformations accordingly.