// 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 ) //Specify the extension class that backs this annotation @ExtensionAnnotation( SequenceTweakingIterationExtension ) @interface TweakSequence { //Accept a string value with the name of the feature method parameter we're going to meddle with String sequenceParameterName() default "sequence" // TODO rename to sequenceParameterName String tweaksParameterName() default "tweaks" } class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtension { private final static PARAMETER_NAME_RANGE = "range" @Override void visitFeatureAnnotation( TweakSequence annotation, FeatureInfo feature ) { def parameterNames = feature.getParameterNames() def tweaksParameterName = annotation.tweaksParameterName( ) def sequenceParameterName = annotation.sequenceParameterName() 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 = [ //TODO allow addition of these tweaks from the specification indexReplacements: { theMap, List inputList -> 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 : { valuesToExclude, List inputList -> inputList.removeAll( valuesToExclude ) // or return inputList.filter{ i -> !valuesToExclude.contains( i ) } return inputList } ] def getDataForParameterName = { parameterName -> 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 ] } else if ( !!inputSequence[ PARAMETER_NAME_RANGE ] ) { 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 '$tweaksParameterName' or 'range' attributes must be provided." ) } } if ( !( newSequence instanceof List ) ) { throw new RuntimeException( "the derived 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( operationData, newSequence ) } } // TODO now add a new #sequenceTweakDescription parameter, per the bottomof // http://spockframework.org/spock/docs/1.1-rc-4/extensions.html#_injecting_method_parameters invocation.arguments[ parameterIndexToFiddleWith ] = newSequence.toArray() as int[] invocation.proceed() } } ) } }