Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new guaranteed field inside a ScenarioState or a ProvidedScenarioState #700

Merged
merged 12 commits into from
Aug 3, 2021
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# v1.1
##New features
## New features
* The lifecycle method annotations `@BeforeStage` and `@AfterStage` have an option to make the associated methods be invoked repeatedly if the stage class is invoked multiple times. Note that the duration of a stage is denoted by the change of the stage class, thus multiple invocations (no matter the form) on the same stage class count as one stage.
* The field annotations `@ScenarioState` and `@ProvidedScenarioState` can be set to be `guaranteed`. This ensures that the stage that is declaring such field must operate on it during execution and after the stage finishes the field is ensured to be initialized.

##Fixed Issues
## Fixed Issues
* Refurbished the jgiven-scala example project [#619](https://github.com/TNG/JGiven/pull/619) (thanks to seblm)
* Updated most dependencies in the project
* Enabled printing of nested stage [#366](https://github.com/TNG/JGiven/pull/619) (thanks to laurinvesely)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@
@Target( ElementType.FIELD )
public @interface ProvidedScenarioState {
Resolution resolution() default Resolution.AUTO;

boolean guaranteed() default false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ public enum Resolution {
* @since 0.14.0
*/
boolean required() default false;

boolean guaranteed() default false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.tngtech.jgiven.exception;

import java.lang.reflect.Field;

public class JGivenMissingGuaranteedScenarioStateException extends RuntimeException {
public JGivenMissingGuaranteedScenarioStateException(Field field) {
super("The field " + field.getName() + " is guaranteed but the stage has not initialized it");
}
}
Original file line number Diff line number Diff line change
@@ -1,139 +1,172 @@
package com.tngtech.jgiven.impl.inject;

import static java.util.stream.Collectors.toList;

import com.google.common.collect.Maps;
import com.tngtech.jgiven.annotation.ExpectedScenarioState;
import com.tngtech.jgiven.annotation.ProvidedScenarioState;
import com.tngtech.jgiven.annotation.ScenarioState;
import com.tngtech.jgiven.annotation.ScenarioState.Resolution;
import com.tngtech.jgiven.exception.AmbiguousResolutionException;
import com.tngtech.jgiven.exception.JGivenInjectionException;
import com.tngtech.jgiven.exception.JGivenMissingGuaranteedScenarioStateException;
import com.tngtech.jgiven.exception.JGivenMissingRequiredScenarioStateException;
import com.tngtech.jgiven.impl.util.FieldCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static java.util.stream.Collectors.toList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Used by Scenario to inject and read values from objects.
*/
public class ValueInjector {
private static final Logger log = LoggerFactory.getLogger( ValueInjector.class );

private final ValueInjectorState state = new ValueInjectorState();

private static final Logger log = LoggerFactory.getLogger(ValueInjector.class);
/**
* Caches all classes that have been already validated for ambiguous resolution.
* This avoids duplicate validations of the same class.
*/
private static final ConcurrentHashMap<Class<?>, Boolean> validatedClasses = new ConcurrentHashMap<>();
private final ValueInjectorState state = new ValueInjectorState();

/**
* @throws AmbiguousResolutionException when multiple fields with the same resolution exist in the given object
*/
@SuppressWarnings( "unchecked" )
public void validateFields( Object object ) {
if( validatedClasses.get( object.getClass() ) == Boolean.TRUE ) {
@SuppressWarnings("unchecked")
public void validateFields(Object object) {
if (validatedClasses.get(object.getClass()) == Boolean.TRUE) {
return;
}

Map<Object, Field> resolvedFields = Maps.newHashMap();

for( ScenarioStateField field : getScenarioFields( object ) ) {
field.getField().setAccessible( true );
for (ScenarioStateField field : getScenarioFields(object)) {
field.getField().setAccessible(true);
Resolution resolution = field.getResolution();
Object key = null;
if( resolution == Resolution.NAME ) {
if (resolution == Resolution.NAME) {
key = field.getField().getName();
} else {
key = field.getField().getType();
}
if( resolvedFields.containsKey( key ) ) {
Field existingField = resolvedFields.get( key );
throw new AmbiguousResolutionException( "Ambiguous fields with same " + resolution + " detected. Field 1: " +
existingField + ", field 2: " + field.getField() );
if (resolvedFields.containsKey(key)) {
Field existingField = resolvedFields.get(key);
throw new AmbiguousResolutionException("Ambiguous fields with same " + resolution + " detected. Field 1: "
+ existingField + ", field 2: " + field.getField());
}
resolvedFields.put( key, field.getField() );
resolvedFields.put(key, field.getField());
}

validatedClasses.put( object.getClass(), Boolean.TRUE );
validatedClasses.put(object.getClass(), Boolean.TRUE);
}

private List<ScenarioStateField> getScenarioFields( Object object ) {
@SuppressWarnings( "unchecked" )
private List<ScenarioStateField> getScenarioFields(Object object) {
@SuppressWarnings("unchecked")
List<Field> scenarioFields = FieldCache
.get( object.getClass() )
.getFieldsWithAnnotation( ScenarioState.class, ProvidedScenarioState.class, ExpectedScenarioState.class );
.get(object.getClass())
.getFieldsWithAnnotation(ScenarioState.class, ProvidedScenarioState.class, ExpectedScenarioState.class);

return scenarioFields.stream()
.map( ScenarioStateField.fromField )
.collect( toList() );
.map(ScenarioStateField.fromField)
.collect(toList());
}

@SuppressWarnings( "unchecked" )
public void readValues( Object object ) {
validateFields( object );
for( ScenarioStateField field : getScenarioFields( object ) ) {
/**
* @throws JGivenMissingGuaranteedScenarioStateException in case a field is guaranteed
* and is not initialized by the finishing stage
*/
@SuppressWarnings("unchecked")
public void readValues(Object object) {
validateFields(object);
checkGuaranteedStatesAreInitialized(object);

for (ScenarioStateField field : getScenarioFields(object)) {
try {
Object value = field.getField().get( object );
updateValue( field, value );
log.debug( "Reading value {} from field {}", value, field.getField() );
} catch( IllegalAccessException e ) {
throw new RuntimeException( "Error while reading field " + field.getField(), e );
Object value = field.getField().get(object);
updateValue(field, value);
log.debug("Reading value {} from field {}", value, field.getField());
} catch (IllegalAccessException e) {
throw new RuntimeException("Error while reading field " + field.getField(), e);
}
}
}

/**
*
* @throws JGivenMissingRequiredScenarioStateException in case a field requires
* a value and the value is not present
* a value and the value is not present
*/
@SuppressWarnings( "unchecked" )
public void updateValues( Object object ) {
validateFields( object );
for( ScenarioStateField field : getScenarioFields( object ) ) {
Object value = getValue( field );
@SuppressWarnings("unchecked")
public void updateValues(Object object) {
validateFields(object);
for (ScenarioStateField field : getScenarioFields(object)) {
Object value = getValue(field);

if( value != null ) {
if (value != null) {
try {
field.getField().set( object, value );
} catch( IllegalAccessException e ) {
throw new RuntimeException( "Error while updating field " + field.getField(), e );
field.getField().set(object, value);
} catch (IllegalAccessException e) {
throw new RuntimeException("Error while updating field " + field.getField(), e);
}

log.debug( "Setting field {} to value {}", field.getField(), value );
} else if( field.isRequired() ) {
throw new JGivenMissingRequiredScenarioStateException( field.getField() );
log.debug("Setting field {} to value {}", field.getField(), value);
} else if (field.isRequired()) {
throw new JGivenMissingRequiredScenarioStateException(field.getField());
}
}
}

public <T> void injectValueByType( Class<T> clazz, T value ) {
state.updateValueByType( clazz, value );
public <T> void injectValueByType(Class<T> clazz, T value) {
state.updateValueByType(clazz, value);
}

public <T> void injectValueByName( String name, T value ) {
state.updateValueByName( name, value );
public <T> void injectValueByName(String name, T value) {
state.updateValueByName(name, value);
}

private void updateValue( ScenarioStateField field, Object value ) {
if( field.getResolution() == Resolution.NAME ) {
state.updateValueByName( field.getField().getName(), value );
private void updateValue(ScenarioStateField field, Object value) {
if (field.getResolution() == Resolution.NAME) {
state.updateValueByName(field.getField().getName(), value);
} else {
state.updateValueByType( field.getField().getType(), value );
state.updateValueByType(field.getField().getType(), value);
}
}

private Object getValue( ScenarioStateField field ) {
if( field.getResolution() == Resolution.NAME ) {
return state.getValueByName( field.getField().getName() );
private Object getValue(ScenarioStateField field) {
if (field.getResolution() == Resolution.NAME) {
return state.getValueByName(field.getField().getName());
}

return state.getValueByType( field.getField().getType() );
return state.getValueByType(field.getField().getType());
}

private void checkGuaranteedStatesAreInitialized(Object instance) {
for (Field field: FieldCache.get(instance.getClass())
.getFieldsWithAnnotation(ProvidedScenarioState.class, ScenarioState.class)) {
if (field.isAnnotationPresent(ProvidedScenarioState.class)) {
if (field.getAnnotation(ProvidedScenarioState.class).guaranteed()) {
checkInitialized(instance, field);
}
}
if (field.isAnnotationPresent(ScenarioState.class)) {
if (field.getAnnotation(ScenarioState.class).guaranteed()) {
checkInitialized(instance, field);
}
}
}
}

private void checkInitialized(Object instance, Field field) {
Object value = null;
try {
value = field.get(instance);
} catch (IllegalAccessException e) {
throw new JGivenInjectionException("The guaranteed field inside the scenario state cannot be accessed",
e);
}
if (value == null) {
throw new JGivenMissingGuaranteedScenarioStateException(field);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.tngtech.jgiven.impl.inject;

import static org.assertj.core.api.Assertions.assertThat;

import com.tngtech.jgiven.annotation.ExpectedScenarioState;
import com.tngtech.jgiven.annotation.ProvidedScenarioState;
import com.tngtech.jgiven.annotation.ScenarioState;
import com.tngtech.jgiven.exception.JGivenMissingGuaranteedScenarioStateException;
import com.tngtech.jgiven.exception.JGivenMissingRequiredScenarioStateException;
import org.junit.Test;

public class ValueInjectorTest {
private ValueInjector injector = new ValueInjector();
andru47 marked this conversation as resolved.
Show resolved Hide resolved

@Test(expected = JGivenMissingGuaranteedScenarioStateException.class)
public void null_provided_field_throws_exception() {
andru47 marked this conversation as resolved.
Show resolved Hide resolved
FakeStage stageObject = new FakeStage(null, null, "");

injector.readValues(stageObject);
}

@Test(expected = JGivenMissingGuaranteedScenarioStateException.class)
public void null_state_field_throws_exception() throws Throwable {
FakeStage stageObject = new FakeStage("", null, null);

injector.readValues(stageObject);
}

@Test
public void initialized_fields_do_not_interrupt_execution() {
FakeStage stageObject = new FakeStage("", null, "");

injector.readValues(stageObject);
}

@Test(expected = JGivenMissingRequiredScenarioStateException.class)
public void null_expected_field_throws_exception() {
FakeStage stageObject = new FakeStage(null, null, "");

injector.updateValues(stageObject);
}

@Test(expected = JGivenMissingRequiredScenarioStateException.class)
public void null_expected_state_field_throws_exception() {
FakeStage stageObject = new FakeStage("", "", null);

injector.updateValues(stageObject);
}

@Test
public void initialized_expected_fields_do_not_interrupt_execution() {
FakeStage stageObject = new FakeStage("", "", "");

injector.readValues(stageObject); //update field value in cache
injector.injectValueByName("providedExpectedString", "Test");
injector.updateValues(stageObject);

assertThat(stageObject.providedExpectedString).isEqualTo("Test");
}

private class FakeStage {
@ProvidedScenarioState(guaranteed = true)
String providedObject;
@ExpectedScenarioState(required = true)
String providedExpectedString;
@ScenarioState(guaranteed = true, required = true)
String stateObject;

public FakeStage(String providedObject, String providedExpectedString, String stateObject) {
this.providedObject = providedObject;
this.providedExpectedString = providedExpectedString;
this.stateObject = stateObject;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.tngtech.jgiven.tests;

import com.tngtech.jgiven.Stage;
import com.tngtech.jgiven.annotation.BeforeStage;
import com.tngtech.jgiven.annotation.ProvidedScenarioState;
import com.tngtech.jgiven.junit.ScenarioTest;
import org.junit.Test;

public class GuaranteedFieldRealTest extends ScenarioTest<GuaranteedFieldRealTest.RealGiven, GuaranteedFieldRealTest.RealWhen, GuaranteedFieldRealTest.RealThen> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for that name :)


@Test
public void a_sample_test() {
given().a_sample_uninitialized_stage();
when().I_do_something();
then().I_did_something();
}

@Test
public void a_sample_initialized_test() {
given().a_sample_initialized_stage();
when().I_do_something();
then().I_did_something();
}

public static class RealGiven extends Stage<RealGiven> {
@ProvidedScenarioState(guaranteed = true)
Object guaranteedObject = null;

public void a_sample_uninitialized_stage() {
}

public void a_sample_initialized_stage() {
this.guaranteedObject = "I'm initialized";
}
}

public static class RealThen extends Stage<RealGiven> {
public void I_did_something() {
}
}

public static class RealWhen extends Stage<RealGiven> {
@BeforeStage
public void beforeSetup() throws ClassNotFoundException {
throw new ClassNotFoundException("Not a JGiven exception");
}

public void I_do_something() {
}
}
}
Loading