Skip to content

Commit

Permalink
Fixed complex type writer and added tests. (#1)
Browse files Browse the repository at this point in the history
* Fixed complex type writer and added tests.

* [Core] Add explicit error message to ComplexTypeWriter
 When dealing with nested complex types, the ComplexTypeWriter would produce
 unbalanced and misaligned tables.

 This could be resolved by adding  an `@XStreamConverter` to the complex field
 but this was not obvious. By adding an explicit exception this is resolved.

 For ease of use this exception is not thrown when acomplex field is empty or
 not included in the table .
  • Loading branch information
mpkorstanje authored and sagacity committed Jun 19, 2017
1 parent fd25cdc commit 9fcf26a
Show file tree
Hide file tree
Showing 4 changed files with 303 additions and 10 deletions.
@@ -0,0 +1,35 @@
package cucumber.runtime.table;

import java.util.regex.Pattern;

public class PascalCaseStringConverter implements StringConverter {

private static final String WHITESPACE = " ";
private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+");

@Override
public String map(String string) {
String[] splitted = normalizeSpace(string).split(WHITESPACE);
for (int i = 0; i < splitted.length; i++) {
splitted[i] = capitalize(splitted[i]);
}
return join(splitted);
}

private String join(String[] splitted) {
StringBuilder sb = new StringBuilder();
for (String s : splitted) {
sb.append(s);
}
return sb.toString();
}

private String normalizeSpace(String originalHeaderName) {
return WHITESPACE_PATTERN.matcher(originalHeaderName.trim()).replaceAll(WHITESPACE);
}

private String capitalize(String string) {
return new StringBuilder(string.length()).append(Character.toTitleCase(string.charAt(0))).append(string.substring(1)).toString();
}

}
73 changes: 63 additions & 10 deletions core/src/main/java/cucumber/runtime/xstream/ComplexTypeWriter.java
@@ -1,20 +1,25 @@
package cucumber.runtime.xstream; package cucumber.runtime.xstream;



import cucumber.deps.com.thoughtworks.xstream.annotations.XStreamConverter;
import cucumber.runtime.CucumberException;
import cucumber.runtime.table.CamelCaseStringConverter; import cucumber.runtime.table.CamelCaseStringConverter;
import cucumber.runtime.table.PascalCaseStringConverter;


import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Stack;


import static java.util.Arrays.asList; import static java.util.Arrays.asList;


public class ComplexTypeWriter extends CellWriter { public class ComplexTypeWriter extends CellWriter {
private final List<String> columnNames; private final List<String> columnNames;
private Map<String, String> fields = new LinkedHashMap<String, String>(); private final Map<String, String> fields = new LinkedHashMap<String, String>();
private String currentKey; private final Stack<String> currentKey = new Stack<String>();

private int nodeDepth = 0;


public ComplexTypeWriter(List<String> columnNames) { public ComplexTypeWriter(List<String> columnNames) {
this.columnNames = columnNames; this.columnNames = columnNames;
Expand Down Expand Up @@ -48,10 +53,10 @@ public List<String> getValues() {


@Override @Override
public void startNode(String name) { public void startNode(String name) {
if (nodeDepth == 1) { currentKey.push(name);
currentKey = name; if (currentKey.size() == 2) {
fields.put(name, "");
} }
nodeDepth++;
} }


@Override @Override
Expand All @@ -60,13 +65,26 @@ public void addAttribute(String name, String value) {


@Override @Override
public void setValue(String value) { public void setValue(String value) {
fields.put(currentKey, value == null ? "" : value); // Add all simple types at level 2. nodeDepth 1 is the root node.
if(currentKey.size() < 2){
return;
}

if (currentKey.size() == 2) {
fields.put(currentKey.peek(), value == null ? "" : value);
return;
}

final String clazz = currentKey.get(0);
final String field = currentKey.get(1);
if ((columnNames.isEmpty() || columnNames.contains(field))) {
throw createMissingConverterException(clazz, field);
}
} }


@Override @Override
public void endNode() { public void endNode() {
nodeDepth--; currentKey.pop();
currentKey = null;
} }


@Override @Override
Expand All @@ -78,4 +96,39 @@ public void flush() {
public void close() { public void close() {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }

private static CucumberException createMissingConverterException(String clazz, String field) {
PascalCaseStringConverter converter = new PascalCaseStringConverter();
return new CucumberException(String.format(
"Don't know how to convert \"%s.%s\" into a table entry.\n" +
"Either exclude %s from the table by selecting the fields to include:\n" +
"\n" +
"DataTable.create(entries, \"Field\", \"Other Field\")\n" +
"\n" +
"Or try writing your own converter:\n" +
"\n" +
"@%s(%sConverter.class)\n" +
"%s %s;\n",
clazz,
field,
field,
XStreamConverter.class.getName(),
converter.map(field),
modifierAndTypeOfField(clazz, field),
field
));
}

private static String modifierAndTypeOfField(String clazz, String fieldName) {
try {
Field field = Class.forName(clazz).getDeclaredField(fieldName);
String simpleTypeName = field.getType().getSimpleName();
String modifiers = Modifier.toString(field.getModifiers());
return modifiers + " " + simpleTypeName;
} catch (NoSuchFieldException e) {
return "private Object";
} catch (ClassNotFoundException e) {
return "private Object";
}
}
} }
140 changes: 140 additions & 0 deletions core/src/test/java/cucumber/runtime/table/TableConverterTest.java
Expand Up @@ -2,20 +2,25 @@


import cucumber.api.DataTable; import cucumber.api.DataTable;
import cucumber.deps.com.thoughtworks.xstream.annotations.XStreamConverter; import cucumber.deps.com.thoughtworks.xstream.annotations.XStreamConverter;
import cucumber.deps.com.thoughtworks.xstream.converters.SingleValueConverter;
import cucumber.deps.com.thoughtworks.xstream.converters.javabean.JavaBeanConverter; import cucumber.deps.com.thoughtworks.xstream.converters.javabean.JavaBeanConverter;
import cucumber.runtime.CucumberException;
import cucumber.runtime.ParameterInfo; import cucumber.runtime.ParameterInfo;
import org.junit.Test; import org.junit.Test;


import java.util.Arrays; import java.util.Arrays;
import java.util.Calendar; import java.util.Calendar;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;


import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;


public class TableConverterTest { public class TableConverterTest {


Expand Down Expand Up @@ -184,6 +189,141 @@ public void converts_to_list_of_java_bean_and_almost_back() {
assertEquals(" | birthDate | deathCal |\n | 1957-05-10 | 1979-02-02 |\n", table.toTable(converted).toString()); assertEquals(" | birthDate | deathCal |\n | 1957-05-10 | 1979-02-02 |\n", table.toTable(converted).toString());
} }


public static class BlogBean {
private String author;
private List<String> tags;
private String post;

public String getPost() {
return post;
}

public void setPost(String post) {
this.post = post;
}

public String getAuthor() {
return author;
}

public void setAuthor(String author) {
this.author = author;
}

public List<String> getTags() {
return tags;
}

public void setTags(List<String> tags) {
this.tags = tags;
}
}

@Test
public void throws_cucumber_exception_for_complex_types() {
BlogBean blog = new BlogBean();
blog.setAuthor("Tom Scott");
blog.setTags(asList("Language", "Linguistics", " Mycenaean Greek"));
blog.setPost("Linear B is a syllabic script that was used for writing Mycenaean Greek...");
try {
DataTable.create(Collections.singletonList(blog));
fail();
} catch (CucumberException expected) {
assertEquals("" +
"Don't know how to convert \"cucumber.runtime.table.TableConverterTest$BlogBean.tags\" into a table entry.\n" +
"Either exclude tags from the table by selecting the fields to include:\n" +
"\n" +
"DataTable.create(entries, \"Field\", \"Other Field\")\n" +
"\n" +
"Or try writing your own converter:\n" +
"\n" +
"@cucumber.deps.com.thoughtworks.xstream.annotations.XStreamConverter(TagsConverter.class)\n" +
"private List tags;\n",
expected.getMessage());
}
}

@Test
public void converts_empty_complex_types_and_almost_back() {
DataTable table = TableParser.parse("" +
"|Author |Tags |Post |\n" +
"|Tom Scott| |Linear B is a...|\n", PARAMETER_INFO);
List<BlogBean> converted = table.asList(BlogBean.class);
BlogBean blog = converted.get(0);
assertEquals("Tom Scott", blog.getAuthor());
assertEquals(emptyList(), blog.getTags());
assertEquals("Linear B is a...", blog.getPost());
assertEquals("" +
" | author | tags | post |\n" +
" | Tom Scott | | Linear B is a... |\n",
table.toTable(converted).toString());
}

public static class AnnotatedBlogBean {
private String author;
@XStreamConverter(TagsConverter.class)
private List<String> tags;
private String post;

public String getPost() {
return post;
}

public void setPost(String post) {
this.post = post;
}

public String getAuthor() {
return author;
}

public void setAuthor(String author) {
this.author = author;
}

public List<String> getTags() {
return tags;
}

public void setTags(List<String> tags) {
this.tags = tags;
}
}

public static class TagsConverter implements SingleValueConverter {

@Override
public String toString(Object o) {
return o.toString().replace("[", "").replace("]", "");
}

@Override
public Object fromString(String s) {
return asList(s.split(", "));
}

@Override
public boolean canConvert(Class type) {
return List.class.isAssignableFrom(type);
}
}

@Test
public void converts_annotated_complex_types_and_almost_back() {
DataTable table = TableParser.parse("" +
"|Author |Tags |Post |\n" +
"|Tom Scott|Language, Linguistics, Mycenaean Greek|Linear B is a...|\n", PARAMETER_INFO);
List<AnnotatedBlogBean> converted = table.asList(AnnotatedBlogBean.class);
AnnotatedBlogBean blog = converted.get(0);
assertEquals("Tom Scott", blog.getAuthor());
assertEquals(asList("Language", "Linguistics", "Mycenaean Greek"), blog.getTags());
assertEquals("Linear B is a...", blog.getPost());
assertEquals("" +
" | author | tags | post |\n" +
" | Tom Scott | Language, Linguistics, Mycenaean Greek | Linear B is a... |\n",
table.toTable(converted).toString());
}

@Test @Test
public void converts_to_list_of_map_of_date() { public void converts_to_list_of_map_of_date() {
DataTable table = TableParser.parse("|Birth Date|Death Cal|\n|1957-05-10|1979-02-02|\n", PARAMETER_INFO); DataTable table = TableParser.parse("|Birth Date|Death Cal|\n|1957-05-10|1979-02-02|\n", PARAMETER_INFO);
Expand Down

0 comments on commit 9fcf26a

Please sign in to comment.