-
Notifications
You must be signed in to change notification settings - Fork 99
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add new guaranteed field inside a ScenarioState or a ProvidedScenario…
- Loading branch information
Showing
8 changed files
with
296 additions
and
64 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
9 changes: 9 additions & 0 deletions
9
...main/java/com/tngtech/jgiven/exception/JGivenMissingGuaranteedScenarioStateException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} | ||
} |
157 changes: 95 additions & 62 deletions
157
jgiven-core/src/main/java/com/tngtech/jgiven/impl/inject/ValueInjector.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
75 changes: 75 additions & 0 deletions
75
jgiven-core/src/test/java/com/tngtech/jgiven/impl/inject/ValueInjectorTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
51 changes: 51 additions & 0 deletions
51
jgiven-tests/src/main/java/com/tngtech/jgiven/tests/GuaranteedFieldRealTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() { | ||
} | ||
} | ||
} |
Oops, something went wrong.