|
Post by trejkaz on May 5, 2020 2:05:53 GMT
I'm using cucumber-jvm...
I can't pass "Negating a tuple" because Gherkin itself refuses to acknowledge my set of rules:
"-a = tuple(-1.0, 2.0, -3.0, 4.0)" matches more than one step definition:
"-{word} = tuple\({real}, {real}, {real}, {real})" in TupleStepDefinitions.<init>(TupleStepDefinitions.kt:94)
"{word} = tuple\({real}, {real}, {real}, {real})" in TupleStepDefinitions.<init>(TupleStepDefinitions.kt:64)
This presumably occurs because the negative sign happens to be a valid character in a "{word}", but that sounds like a dumb character to allow at the start of a word, so I'm calling it a bug in their parser. I only mention it here in the hope that someone will at least provide some sympathy. XD
Whatever it is, it guarantees that I can't get my tests to pass without pulling a new negation symbol out of unicode to work around this.
|
|
|
Post by Jamis on May 7, 2020 15:42:51 GMT
I feel your pain. I had to work around some quirky Gherkin issues on some of my tests, too. ("The Quirky Gherkins" would make a great band name, btw.) My general strategy when I encountered Gherkin issues was to add new rules. For instance, in this case, instead of using {word}, I would create a new pattern called (e.g.) {variable} with explicit matching rules. Whether you can do this or not might depend on which implementation of Cucumber you're using... I was using the Ruby version. - Jamis
|
|
|
Post by trejkaz on May 8, 2020 22:49:22 GMT
That's exactly the path I'm going down. The next trap is that I've ended up with this in TupleStepDefinitions:
{var} * {var} = {var}
But then this in MatrixStepDefinitions:
{var} * {var} = {var}
I assume here I'll just decide what kind of names matrices can have and what kind tuples can have, but I'm annoyed at the lack of consistency - I usually like all uppercase vars for matrices, but then "identity_matrix" is randomly in lowercase which makes me sad.
|
|
|
Post by trejkaz on May 14, 2020 4:12:46 GMT
Well, that worked ........... for a while. Now I think I'm properly blocked? I'm up to: Scenario Outline: A ray intersects a cube Given c ← cube() And r ← ray(<origin>, <direction>) When xs ← local_intersect(c, r) Then xs.count = 2 And xs[0].t = <t1> And xs[1].t = <t2>
Given c ← cube() forces me to accept c as a shape_var... and then I get: "c.transform = identity_matrix" matches more than one step definition: "c.transform = {matrix_var}" in CameraStepDefinitions.<init>(CameraStepDefinitions.kt:53) "{shape_var}.transform = {matrix_var}" in ShapeStepDefinitions.<init>(ShapeStepDefinitions.kt:99)
Now I'm kind of at a loss. Short of editing the text of the scenario, which up to now I have been trying not to do.
|
|
|
Post by chrisd on May 17, 2020 12:11:09 GMT
Effectively, this is a bug in Cucumber, and you can use regular expressions instead of Cucumber expressions:
^s ← sphere\\(\\)$
|
|
|
Post by trejkaz on May 21, 2020 4:24:33 GMT
That I managed to get past previously. The current problem (as stated above) is more serious, because I have two steps which are both "c.transform = {matrix_var}", but "c" in one is supposed to be a Camera, while "c" in the other one is supposed to be a Cube. I can't see any way to distinguish those two cases as the only differing part of them is what matrix happens to be passed on the RHS. So at that point I more or less gave up on implementing the scenarios as written, and started rewriting the scenarios to remove ambiguity. Editing the scenarios felt like cheating, but given the situation I couldn't see an alternative. But I could see two ways to rewrite the scenarios to remove ambiguity: 1. Change the name of the variable (this is what I've done in my scenarios): - "When camera.transform = {matrix_var}"
- "When shape.transform = {matrix_var}"
2. Spell things out a bit more like a real scenario: - "When camera c has transform set to {matrix_var}"
- "When shape c has transform set to {matrix_var}"
But what I'm really curious about is whether there is a way to do this _without_ modifying the scenario. Do I extract a Transformable interface from both types and then refactor out all step definitions for setting/getting the transform into a TransformableStepDefinitions? Or is there another way to go about this? How did others approach this? (Incidentally, I found another workaround for that issue I posted on StackOverflow as well - I had some other scenario later on where I ended up wanting to reuse "cube\\(\\)", "sphere\\(\\)" so I turned it into a parameter type. That made it possible to put it into cucumber syntax and removed the need for the regex workaround in the step definition itself.)
|
|
|
Post by chrisd on May 29, 2020 5:40:08 GMT
Neat idea with defining the parameter types. I wish I had used them more.
First of all, due to the bugs like the one you mentioned on StackOverflow, I almost always used regular expressions. I created variables that contained my own regular expressions for words, numbers, etc.
I had a data store (called "data" in the code) that was injected into the step definition classes, and it stores instance variables of various object types. For a lot of the Step definitions, I made very generic Given/When/Then annotations that would match a common pattern, such as "objectName.propertyName ← double". The function then checks all possible object types to see if there is already an object with that name. It's a bit brute force. The default cases have "Unknown" assertion messages so I would get errors when a Given/When/Then matched but was not implemented. Here is one example.
@Given(wordPattern + "\\." + wordPattern + " ← " + doublePattern) public void objPropAssignDouble(String objectName, String propertyName, double value) { Material material = data.getMaterial(objectName);
if (material != null) { switch (propertyName) { case "ambient": material.setAmbient(value); return; case "diffuse": material.setDiffuse(value); return; case "specular": material.setSpecular(value); return; default: Assert.fail("Unknown material property " + propertyName); } }
Shape shape = data.getShape(objectName);
if (shape != null) { switch (propertyName) { case "minimum": if (shape instanceof TubeLike) ((TubeLike)shape).setMinimum(value); else Assert.fail("Shape property minimum not valid for object type " + shape.getClass().getName()); return; case "maximum": if (shape instanceof TubeLike) ((TubeLike)shape).setMaximum(value); else Assert.fail("Shape property maximum not valid for object type " + shape.getClass().getSimpleName()); return; default: Assert.fail("Unknown shape property " + propertyName); } }
Assert.fail("Unknown object name " + objectName); } Not sure if this is exactly what you were looking for, but I hope it helps.
|
|
|
Post by trejkaz on Jun 5, 2020 14:14:02 GMT
That's an interesting way to go about it. I had gone with the path of each property as a different given, like
Given("{shape_var}.transform ← {transform}") { shapeVar: String, transform: Matrix ->
shapes[shapeVar]!!.transform = value
}
Which makes each step much easier to understand but does run into issues if two things share the same name.
|
|
|
Post by chrisd on Jul 18, 2020 14:43:38 GMT
I ended up deciding my "interesting way to go about it" was overly complicated and too hard to debug. In light of that, I followed your example and converted everything over to less generic but far simpler Cucumber expressions (except a couple of instances that had to be regex due to limitations in Cucumber expressions). This is effectively letting the Given/When/Then expression determine what code to call, instead of having endless if/else case analysis. Additionally, generous helpings of custom ParameterType Attributes made expressions even simpler. When two objects of different types share the same attribute name, I reverted to using explicit case analysis. However, in practice there ended up being very few of those. Making a Transformable interface for that one method would work, but ventures too much into unnecessarily modifying production code design to make testing slightly easier. See Ruby on Rails creator DHH's article test induced design damage (since refuted by Robert Martin and Martin Fowler, but still a useful concept to consider). I think this kind of change would only be worthwhile if it were required to make testing POSSIBLE.
|
|