Skip to content

Commit

Permalink
Add new guaranteed field inside a ScenarioState or a ProvidedScenario…
Browse files Browse the repository at this point in the history
…State (#700)

Add new guaranteed field inside ScenarioState and ProvidedScenarioState.
Fixes #268
  • Loading branch information
andru47 authored Aug 3, 2021
1 parent 555196f commit 9a59242
Show file tree
Hide file tree
Showing 8 changed files with 296 additions and 64 deletions.
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();

@Test(expected = JGivenMissingGuaranteedScenarioStateException.class)
public void null_provided_field_throws_exception() {
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> {

@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

0 comments on commit 9a59242

Please sign in to comment.