Skip to content

Commit

Permalink
Include SourceReferences in message output
Browse files Browse the repository at this point in the history
Cucumber JVM can not reference files by URI. So it should use either java
methods or stack trace elements as a source reference instead.

For example:

```
{
  "hook": {
    "id": "a1839ec6-f75d-4029-9b75-4b8203d8b2e8",
    "sourceReference": {
      "javaMethod": {
        "className": "io.cucumber.compatibility.attachments.Attachments",
        "methodName": "before",
        "methodParameterTypes": [
          "io.cucumber.java.Scenario"
        ]
      }
    }
  }
}
```

See: cucumber/common#1119
Fixes: #2058
  • Loading branch information
mpkorstanje committed Jul 29, 2020
1 parent 239460b commit c564dd1
Show file tree
Hide file tree
Showing 12 changed files with 237 additions and 28 deletions.
Expand Up @@ -50,10 +50,10 @@ private static List<Matcher<?>> extractExpectedFields(GeneratedMessageV3 expecte
expected.add(hasEntry(is(fieldName), isA(expectedValue.getClass())));
break;

// exception: the CCK expects source references but java can not
// provide them
// exception: the CCK expects source references with URIs but
// Java can only provide method and stack trace references.
case "sourceReference":
expected.add(not(hasKey(is(fieldName))));
expected.add(hasKey(is(fieldName)));
break;

// exception: ids are not predictable
Expand Down
@@ -0,0 +1,53 @@
package io.cucumber.core.backend;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import static java.util.Objects.requireNonNull;

public final class JavaMethodReference implements SourceReference {

private final String className;
private final String methodName;
private final List<String> methodParameterTypes;

JavaMethodReference(Class<?> declaringClass, String methodName, Class<?>[] methodParameterTypes) {
this.className = requireNonNull(declaringClass).getName();
this.methodName = requireNonNull(methodName);
this.methodParameterTypes = new ArrayList<>(methodParameterTypes.length);
for (Class<?> parameterType : methodParameterTypes) {
this.methodParameterTypes.add(parameterType.getName());
}
}

public String className() {
return className;
}

public String methodName() {
return methodName;
}

public List<String> methodParameterTypes() {
return methodParameterTypes;
}

@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
JavaMethodReference that = (JavaMethodReference) o;
return className.equals(that.className) &&
methodName.equals(that.methodName) &&
methodParameterTypes.equals(that.methodParameterTypes);
}

@Override
public int hashCode() {
return Objects.hash(className, methodName, methodParameterTypes);
}

}
5 changes: 5 additions & 0 deletions core/src/main/java/io/cucumber/core/backend/Located.java
Expand Up @@ -2,6 +2,8 @@

import org.apiguardian.api.API;

import java.util.Optional;

@API(status = API.Status.STABLE)
public interface Located {

Expand Down Expand Up @@ -29,4 +31,7 @@ public interface Located {
*/
String getLocation();

default Optional<SourceReference> getSourceReference() {
return Optional.empty();
}
}
22 changes: 22 additions & 0 deletions core/src/main/java/io/cucumber/core/backend/SourceReference.java
@@ -0,0 +1,22 @@
package io.cucumber.core.backend;

import java.lang.reflect.Method;

public interface SourceReference {

static SourceReference fromMethod(Method method) {
return new JavaMethodReference(
method.getDeclaringClass(),
method.getName(),
method.getParameterTypes());
}

static SourceReference fromStackTraceElement(StackTraceElement stackTraceElement) {
return new StackTraceElementReference(
stackTraceElement.getClassName(),
stackTraceElement.getMethodName(),
stackTraceElement.getFileName(),
stackTraceElement.getLineNumber());
}

}
@@ -0,0 +1,56 @@
package io.cucumber.core.backend;

import java.util.Objects;
import java.util.Optional;

import static java.util.Objects.requireNonNull;

public class StackTraceElementReference implements SourceReference {

private final String className;
private final String methodName;
private final String fileName;
private final int lineNumber;

StackTraceElementReference(String className, String methodName, String fileName, int lineNumber) {
this.className = requireNonNull(className);
this.methodName = requireNonNull(methodName);
this.fileName = fileName;
this.lineNumber = lineNumber;
}

public String className() {
return className;
}

public String methodName() {
return methodName;
}

public Optional<String> fileName() {
return Optional.ofNullable(fileName);
}

public int lineNumber() {
return lineNumber;
}

@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
StackTraceElementReference that = (StackTraceElementReference) o;
return lineNumber == that.lineNumber &&
className.equals(that.className) &&
methodName.equals(that.methodName) &&
Objects.equals(fileName, that.fileName);
}

@Override
public int hashCode() {
return Objects.hash(className, methodName, fileName, lineNumber);
}

}
58 changes: 48 additions & 10 deletions core/src/main/java/io/cucumber/core/runner/CachingGlue.java
Expand Up @@ -7,8 +7,10 @@
import io.cucumber.core.backend.DocStringTypeDefinition;
import io.cucumber.core.backend.Glue;
import io.cucumber.core.backend.HookDefinition;
import io.cucumber.core.backend.JavaMethodReference;
import io.cucumber.core.backend.ParameterTypeDefinition;
import io.cucumber.core.backend.ScenarioScoped;
import io.cucumber.core.backend.StackTraceElementReference;
import io.cucumber.core.backend.StepDefinition;
import io.cucumber.core.eventbus.EventBus;
import io.cucumber.core.gherkin.Step;
Expand All @@ -24,6 +26,12 @@
import io.cucumber.datatable.TableCellByTypeTransformer;
import io.cucumber.datatable.TableEntryByTypeTransformer;
import io.cucumber.messages.Messages;
import io.cucumber.messages.Messages.Envelope;
import io.cucumber.messages.Messages.Hook;
import io.cucumber.messages.Messages.JavaStackTraceElement;
import io.cucumber.messages.Messages.Location;
import io.cucumber.messages.Messages.SourceReference;
import io.cucumber.messages.Messages.StepDefinition.Builder;
import io.cucumber.messages.Messages.StepDefinition.StepDefinitionPattern;
import io.cucumber.messages.Messages.StepDefinition.StepDefinitionPattern.StepDefinitionPatternType;
import io.cucumber.plugin.event.StepDefinedEvent;
Expand Down Expand Up @@ -259,10 +267,13 @@ private void emitParameterTypeDefined(ParameterType<?> parameterType) {
}

private void emitHook(CoreHookDefinition hook) {
Hook.Builder hookDefinitionBuilder = Hook.newBuilder()
.setId(hook.getId().toString())
.setTagExpression(hook.getTagExpression());
hook.getDefinitionLocation()
.ifPresent(reference -> hookDefinitionBuilder.setSourceReference(createSourceReference(reference)));
bus.send(Messages.Envelope.newBuilder()
.setHook(Messages.Hook.newBuilder()
.setId(hook.getId().toString())
.setTagExpression(hook.getTagExpression()))
.setHook(hookDefinitionBuilder)
.build());
}

Expand All @@ -272,16 +283,43 @@ private void emitStepDefined(CoreStepDefinition stepDefinition) {
new io.cucumber.plugin.event.StepDefinition(
stepDefinition.getStepDefinition().getLocation(),
stepDefinition.getExpression().getSource())));
bus.send(Messages.Envelope.newBuilder()
.setStepDefinition(
Messages.StepDefinition.newBuilder()
.setId(stepDefinition.getId().toString())
.setPattern(StepDefinitionPattern.newBuilder()
.setSource(stepDefinition.getExpression().getSource())
.setType(getExpressionType(stepDefinition))))

Builder stepDefinitionBuilder = Messages.StepDefinition.newBuilder()
.setId(stepDefinition.getId().toString())
.setPattern(StepDefinitionPattern.newBuilder()
.setSource(stepDefinition.getExpression().getSource())
.setType(getExpressionType(stepDefinition)));
stepDefinition.getDefinitionLocation()
.ifPresent(reference -> stepDefinitionBuilder.setSourceReference(createSourceReference(reference)));
bus.send(Envelope.newBuilder()
.setStepDefinition(stepDefinitionBuilder)
.build());
}

private SourceReference.Builder createSourceReference(io.cucumber.core.backend.SourceReference reference) {
SourceReference.Builder sourceReferenceBuilder = SourceReference.newBuilder();
if (reference instanceof JavaMethodReference) {
JavaMethodReference methodReference = (JavaMethodReference) reference;
sourceReferenceBuilder.setJavaMethod(Messages.JavaMethod.newBuilder()
.setClassName(methodReference.className())
.setMethodName(methodReference.methodName())
.addAllMethodParameterTypes(methodReference.methodParameterTypes()));
}

if (reference instanceof StackTraceElementReference) {
StackTraceElementReference stackReference = (StackTraceElementReference) reference;
JavaStackTraceElement.Builder stackTraceElementBuilder = JavaStackTraceElement.newBuilder()
.setClassName(stackReference.className())
.setMethodName(stackReference.methodName());
stackReference.fileName().ifPresent(stackTraceElementBuilder::setFileName);
sourceReferenceBuilder
.setJavaStackTraceElement(stackTraceElementBuilder)
.setLocation(Location.newBuilder()
.setLine(stackReference.lineNumber()));
}
return sourceReferenceBuilder;
}

private StepDefinitionPatternType getExpressionType(CoreStepDefinition stepDefinition) {
Class<? extends Expression> expressionType = stepDefinition.getExpression().getExpressionType();
if (expressionType.isAssignableFrom(RegularExpression.class)) {
Expand Down
Expand Up @@ -2,12 +2,14 @@

import io.cucumber.core.backend.HookDefinition;
import io.cucumber.core.backend.ScenarioScoped;
import io.cucumber.core.backend.SourceReference;
import io.cucumber.core.backend.TestCaseState;
import io.cucumber.tagexpressions.Expression;
import io.cucumber.tagexpressions.TagExpressionException;
import io.cucumber.tagexpressions.TagExpressionParser;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

import static java.util.Objects.requireNonNull;
Expand Down Expand Up @@ -84,4 +86,7 @@ public void dispose() {

}

Optional<SourceReference> getDefinitionLocation() {
return delegate.getSourceReference();
}
}
Expand Up @@ -3,6 +3,7 @@
import io.cucumber.core.backend.CucumberBackendException;
import io.cucumber.core.backend.CucumberInvocationTargetException;
import io.cucumber.core.backend.ParameterInfo;
import io.cucumber.core.backend.SourceReference;
import io.cucumber.core.backend.StepDefinition;
import io.cucumber.core.gherkin.Step;
import io.cucumber.core.stepexpression.Argument;
Expand All @@ -11,6 +12,7 @@

import java.lang.reflect.Type;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

import static java.util.Objects.requireNonNull;
Expand Down Expand Up @@ -84,4 +86,8 @@ public String getLocation() {
return stepDefinition.getLocation();
}

Optional<SourceReference> getDefinitionLocation() {
return stepDefinition.getSourceReference();
}

}
Expand Up @@ -506,7 +506,7 @@ void emits_a_meta_message() {
.run();

Messages.Meta meta = messages.get(0).getMeta();
assertThat(meta.getProtocolVersion(), matchesPattern("\\d+\\.\\d+\\.\\d+"));
assertThat(meta.getProtocolVersion(), matchesPattern("\\d+\\.\\d+\\.\\d+(-RC\\d+)?(-SNAPSHOT)?"));
assertThat(meta.getImplementation().getName(), is("cucumber-jvm"));
assertThat(meta.getImplementation().getVersion(), matchesPattern("\\d+\\.\\d+\\.\\d+(-RC\\d+)?(-SNAPSHOT)?"));
assertThat(meta.getOs().getName(), matchesPattern(".+"));
Expand Down
11 changes: 11 additions & 0 deletions java/src/main/java/io/cucumber/java/AbstractGlueDefinition.java
Expand Up @@ -2,9 +2,11 @@

import io.cucumber.core.backend.Located;
import io.cucumber.core.backend.Lookup;
import io.cucumber.core.backend.SourceReference;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Optional;

import static java.util.Objects.requireNonNull;

Expand All @@ -13,6 +15,7 @@ abstract class AbstractGlueDefinition implements Located {
protected final Method method;
private final Lookup lookup;
private String fullFormat;
private SourceReference sourceReference;

AbstractGlueDefinition(Method method, Lookup lookup) {
this.method = requireNonNull(method);
Expand Down Expand Up @@ -44,4 +47,12 @@ final Object invokeMethod(Object... args) {
return Invoker.invoke(this, lookup.getInstance(method.getDeclaringClass()), method, args);
}

@Override
public Optional<SourceReference> getSourceReference() {
if (sourceReference == null) {
sourceReference = SourceReference.fromMethod(this.method);
}
return Optional.of(sourceReference);
}

}
12 changes: 12 additions & 0 deletions java8/src/main/java/io/cucumber/java8/AbstractGlueDefinition.java
Expand Up @@ -2,12 +2,15 @@

import io.cucumber.core.backend.Located;
import io.cucumber.core.backend.ScenarioScoped;
import io.cucumber.core.backend.SourceReference;
import net.jodah.typetools.TypeResolver;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import static io.cucumber.core.backend.SourceReference.fromStackTraceElement;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;

Expand All @@ -16,6 +19,7 @@ abstract class AbstractGlueDefinition implements ScenarioScoped, Located {
private Object body;
final Method method;
final StackTraceElement location;
SourceReference sourceReference;

AbstractGlueDefinition(Object body, StackTraceElement location) {
this.body = requireNonNull(body);
Expand Down Expand Up @@ -59,6 +63,14 @@ public final boolean isDefinedAt(StackTraceElement stackTraceElement) {
return location.getFileName() != null && location.getFileName().equals(stackTraceElement.getFileName());
}

@Override
public Optional<SourceReference> getSourceReference() {
if (sourceReference == null) {
sourceReference = fromStackTraceElement(location);
}
return Optional.of(sourceReference);
}

Class<?>[] resolveRawArguments(Class<?> bodyClass, Class<?> body) {
Class<?>[] rawArguments = TypeResolver.resolveRawArguments(bodyClass, body);
for (Class<?> aClass : rawArguments) {
Expand Down

0 comments on commit c564dd1

Please sign in to comment.