// Copyright (c) 2017 John Elm // // This software is released under the MIT License. // https://opensource.org/licenses/MIT import org.spockframework.runtime.extension.* import org.spockframework.runtime.model.* import java.lang.annotation.* import java.util.stream.* @Retention( RetentionPolicy.RUNTIME ) @Target( ElementType.METHOD ) @ExtensionAnnotation( SequenceTweakingIterationExtension ) @interface TweakSequence { String sequenceParameterName() default "sequence" String tweaksParameterName() default "tweaks" } @Retention( RetentionPolicy.RUNTIME ) @Target( ElementType.METHOD ) @interface TweaksSequence {} // methods with this annotation are added to the tweakOperations map // TODO consider enforcing that the methods are static // TODO consider using BiFunctions instead to enforce , , signature class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtension { private final static PARAMETER_NAME_RANGE = "range" @Override void visitFeatureAnnotation( TweakSequence annotation, FeatureInfo feature ) { def parameterNames = feature.getParameterNames() def sequenceParameterName = annotation.sequenceParameterName() def tweaksParameterName = annotation.tweaksParameterName() int parameterIndexToFiddleWith = parameterNames.indexOf( sequenceParameterName ) def dataValues = null feature.addIterationInterceptor( new IMethodInterceptor() { @Override void intercept( IMethodInvocation invocation ) throws Throwable { IterationInfo iteration = invocation.getIteration() dataValues = iteration.getDataValues() // capture the data values via closure invocation.proceed() } } ) feature.getFeatureMethod().addInterceptor( new IMethodInterceptor() { @Override void intercept( IMethodInvocation invocation ) throws Throwable { // now manipulate the data def inputSequence = dataValues[ parameterIndexToFiddleWith ] def tweaks // TODO support things like valueReplacements, indexExclusions (?) def tweakOperations = [ indexReplacements: { List inputList, theMap -> if ( !( theMap instanceof Map ) ) { throw new RuntimeException( "indexReplacements must be a map of indexes and replacements (encountered: $theMap)" ) } theMap.each { position, value -> inputList[ position ] = value } return inputList }, valueExclusions : { List inputList, valuesToExclude -> inputList.removeAll( valuesToExclude ) // or return inputList.filter{ i -> !valuesToExclude.contains( i ) } return inputList } ] def specInstance = invocation.sharedInstance List sequenceTweakingMethods = specInstance. getClass().getMethods().toList().stream() .filter { m -> !!m.getAnnotationsByType( TweaksSequence.class ).length }.toArray() sequenceTweakingMethods.forEach( { method -> tweakOperations[ method.getName() ] = { List inputList, arg -> return method.invoke( specInstance, inputList, arg ) } } ) def getDataForParameterName = { parameterName -> if ( inputSequence instanceof Map && !!inputSequence[ parameterName ] ) { return inputSequence[ parameterName ] } if ( parameterNames.contains( parameterName ) ) { return dataValues[ parameterNames.indexOf( parameterName ) ] } if ( !!tweaks ) { return tweaks[ parameterName ]; } return null } tweaks = getDataForParameterName( tweaksParameterName ) // if the parameter specified by the annotation is a map (potentially containing tweaks also), // get the sequence out of the map. henceforth the subject sequence is newSequence def newSequence = inputSequence if ( inputSequence instanceof Map ) { if ( !!inputSequence[ sequenceParameterName ] ) { if ( !!inputSequence[ PARAMETER_NAME_RANGE ] ) { throw new RuntimeException( "either '$sequenceParameterName' or 'range' must be provided, not both" ) } newSequence = inputSequence[ sequenceParameterName ] // List literal } else if ( !!inputSequence[ PARAMETER_NAME_RANGE ] ) { //TODO convert this into a function and genericize the tweakOperations map to handle data generation, i.e. random def range = inputSequence[ PARAMETER_NAME_RANGE ] def start = range.start ?: 0, end = range.end ?: 0, step = range.step ?: 1, repeat = range.repeat ?: 1 // disappointed that groovy doesn't support destructuring assignment from maps :-( def stepFilterFunction = { i -> ( i - start ) % step == 0 } def orderingFunction = { i -> i } if ( start > end ) { orderingFunction = { i -> end - i + start } ( start, end ) = [ end, start ] } newSequence = IntStream.rangeClosed( start, end ) .map( orderingFunction ) .filter( stepFilterFunction ) .boxed().collect( Collectors.toList() ) * repeat } else { throw new RuntimeException( "the specified parameter '$sequenceParameterName' is a Map, so one of '$sequenceParameterName' or 'range' attributes must be provided." ) } } if ( !( newSequence instanceof List ) ) { throw new RuntimeException( "the derived '$sequenceParameterName' data parameter must be a List (found: $newSequence)" ) } tweakOperations.each { operationName, operation -> def operationData = getDataForParameterName( operationName ) if ( !!operationData ) { // if manipulations have been specified but the specified parameter name (table column) // doesn't exist, throw an exception. if ( parameterIndexToFiddleWith < 0 ) { throw new RuntimeException( "data parameter $sequenceParameterName does not exist" ) } newSequence = operation( newSequence, operationData ) } } // TODO now add a new #sequenceTweakDescription parameter (the sequence's expression before sequence generation & tweaks), // for use in the Feature method name // per the bottom of http://spockframework.org/spock/docs/1.1-rc-4/extensions.html#_injecting_method_parameters invocation.arguments[ parameterIndexToFiddleWith ] = newSequence.toArray() as int[] invocation.proceed() } } ) } }