Last active
December 14, 2017 20:07
-
-
Save johnelm/c8c207dfe7ed42b0f5ecccf0990eb2d3 to your computer and use it in GitHub Desktop.
Revisions
-
johnelm revised this gist
Dec 14, 2017 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -195,7 +195,7 @@ Now let's review everything that's available with the extension: | Tweak / Name | what it does | How specified | Contents | |----------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------| | `sequence` <br/>overridden via the `sequenceParameterName` annotation attribute. | specifies the (pre-tweak) sequence, typically as a List literal or an expression resulting in a List | * as a data column <br/> or <br/>* as a map attribute, within the sequence column | List, e.g.<br/> `[ 1, 2, 3 ]`<br/> or<br/> `[ sequence: [ 1, 2, 3 ] ]` | | `range` | generates a range of values | as a map, within the sequence column | Map with attributes: `start`, `end`, `step`, `repeat`:<br/> `[ range: [ start: 1, end: 3 ] ]` | | `tweaks` <br/>overridden via the `tweaksParameterName` annotation attribute. | specifies a map of tweaks as `tweakName:data` mappings | as a data column | a Map with zero or more tweak names and the data used by the tweak (e.g. List, Map, or Number, etc) | | `indexReplacements` | overwrites values at positions corresponding with mapping's key, with the mapping's value | * As a data column <br/> or <br/> * as a map, within the sequence column or the tweaks column | Map of `location:value` mappings:<br/> `[ 1:99, 3: 0 ]` | | `valueExclusions` | removes all occurrences of specified values from the sequence | * As a data column <br/> or <br/>* as a map, within the sequence column or the tweaks column | List of values to exclude from the sequence:<br/> `[ 1, 3, 5 ]` | -
johnelm revised this gist
Dec 14, 2017 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -253,7 +253,7 @@ When you're generating very large sequences, you probably want to avoid includin Watch out for precendence of tweak operations. You might expect them to be performed in the order you wrote them in, but they'll actually be applied in the order of their addition to the `tweakOperations` map under the covers in the extension. Any added `@TweaksSequence` tweaks are also added to this map, so they'll be performed last. I had a gas doing this. I got to play with creating Annotations, and using Groovy was especially cool. Aside from a bit of build and deploy scripting long ago, I hadn't used it.. but it seemed very natural and familiar from my recent years in ES6+ and Node.js. For example, the extension relies heavily on Closures. Groovy is like a perfect marriage of Java with the dynamic freedom I've grown accustomed to from using JS. Much easier to debug too! TODOs: -
johnelm revised this gist
Dec 14, 2017 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -158,7 +158,7 @@ There are a few ways to use the tweaks from the extension.. To demonstrate, let ``` In the first row, we see that if the table includes an `indexReplacements` data column of `position:value` mappings, the replacements are performed like before: the element at the index of the map's key is replaced by its value. In the second row, we see that if we're using `range` to generate the sequence, we can include tweaks within the same map as the range. If we only need them for one or two rows, we are spared the trouble of adding columns out to the table. > Rows where the indexReplacements value is an empty map are intended to depict the convenience and tradeoffs of leaving the column out of the table altogether. In other words, for those rows, pretend the column isn't there. :-) -
johnelm revised this gist
Dec 14, 2017 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -156,7 +156,7 @@ There are a few ways to use the tweaks from the extension.. To demonstrate, let [ 9, 2, 0, 4 ] | [ sequence: [ 1, 2, 3, 4 ], indexReplacements: [ 0: 9, 2: 0 ] ] | [ : ] ``` In the first row, we see that if the table includes an `indexReplacements` data column of `position:value` mappings, the replacements are performed like before: the element at the index of the map's key is replaced by its value. In the second row, we see that if we're using `range` to generate the sequence, we can include tweaks within the same map as the range. If we only need them for one or two rows, we are spared the trouble of adding column out of the table. -
johnelm revised this gist
Dec 14, 2017 . 1 changed file with 0 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -143,7 +143,6 @@ The `step` and `repeat` attributes do exactly what their names imply. Further, > The absolute value for `step` is effectively used. Negation has no effect, but stepping works the same for ascending and descending ranges. ### Ways to use tweaks There are a few ways to use the tweaks from the extension.. To demonstrate, let's bring back the `indexReplacements` utility I was using earlier. Once again, all three rows in the example below are equivalent. -
johnelm revised this gist
Dec 14, 2017 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -1,4 +1,4 @@ # Sequence Tweaking Spock Extension *Quick and easy generation of int[] sequences for Codility and Leetcode algorithm challenge test cases* Recently I've been catching up on modern Java.. I haven't used it on the job since Java 5 came out. I've been practicing algorithms on [Codility](http://www.codility.com) and [Leetcode](http://www.leetcode.com). -
johnelm revised this gist
Dec 14, 2017 . No changes.There are no files selected for viewing
-
johnelm revised this gist
Dec 14, 2017 . 1 changed file with 3 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -259,7 +259,8 @@ I had a gas doing this. I got to play with creating Annotations, and using Groo TODOs: * Inject an additional `sequenceDescription` data parameter to the feature method, containing the (pre-evaluation) contents of the data column, for use in `@Unroll`ed feature method names. * Enforce that added `@TweaksSequence` methods are static (Spock does a lot of funky stuff with scope and it's probably best to avoid any instance state) * Use generics to enforce signatures of `@TweaksSequences` methods: BiFunction<List, List, Object> * Include a couple of Specification features showing all/most usage possibilities, for reference -
johnelm revised this gist
Dec 14, 2017 . 1 changed file with 0 additions and 73 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -1,73 +0,0 @@ -
johnelm revised this gist
Dec 14, 2017 . No changes.There are no files selected for viewing
-
johnelm revised this gist
Dec 14, 2017 . 2 changed files with 17 additions and 17 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -9,12 +9,12 @@ 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" } @@ -60,8 +60,7 @@ class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtensi // 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)" ) } @@ -70,7 +69,7 @@ class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtensi } return inputList }, valueExclusions : { List inputList, valuesToExclude -> inputList.removeAll( valuesToExclude ) // or return inputList.filter{ i -> !valuesToExclude.contains( i ) } return inputList } @@ -81,10 +80,10 @@ class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtensi .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 ] ) { @@ -143,12 +142,13 @@ class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtensi 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() This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -101,20 +101,20 @@ class TweakSequenceUtilSpecification extends Specification { } @TweaksSequence static List addNumberToSequence( List inputSequence, int numberToAdd ) { return inputSequence.stream().mapToInt { i -> i + numberToAdd }.toArray() } @TweaksSequence static List subtractNumberFromSequence( List inputSequence, int numberToSubtract ) { return inputSequence.stream().mapToInt { i -> i - numberToSubtract }.toArray() } @TweaksSequence static List reverseSequence( List inputSequence, boolean whetherToReverse ) { if ( !whetherToReverse ) return inputSequence return new LinkedList( inputSequence ).descendingIterator().toList() } @Unroll -
johnelm revised this gist
Dec 14, 2017 . 2 changed files with 165 additions and 295 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -1,15 +1,15 @@ # Sequence Tweaking Spock Annotation Extension *Quick and easy generation of int[] sequences for Codility and Leetcode algorithm challenge test cases* Recently I've been catching up on modern Java.. I haven't used it on the job since Java 5 came out. I've been practicing algorithms on [Codility](http://www.codility.com) and [Leetcode](http://www.leetcode.com). The algorithm puzzles on these sites almost always use `int` and/or `int[]` arguments and return types, and sometimes have BigO performance requirements. To test your algorithms properly, you need to generate very large input arrays, of sizes up to 100,000 with values from +/- 1 billion. My test cases weren't anywhere near as comprehensive as those run by Codility when you submit you solution, and I wanted a way to quickly create tests to cover edge cases and very large inputs. I started using the [data tables](http://spockframework.org/spock/docs/1.1/data_driven_testing.html#data-tables) feature in the awesome [Spock](http://spockframework.org) testing framework, and right away I started creating some [helper methods](http://spockframework.org/spock/docs/1.1/spock_primer.html#_helper_methods) to help with the generation of `int[]` sequences for input to my solutions. Here's a simple example of what I was doing: ### Simple beginnings ```groovy import spock.lang.* class MySpecification extends Specification { @@ -42,369 +42,224 @@ class MySpecification extends Specification { 42 | range(1, 100) | [ 0: 100, 99: 1 ] // 100, 2, 3, 4 ... 97, 98, 99, 1 ``` I called my `range()` Specification helper method from within my Spock data column to generate a range of ints, and I specified values for arbitrary sequence positions in a separate `indexReplacements` column of position:value maps. The code that applied these replacements was included in the `given:` block. This worked pretty well, but I didn't like cluttering the feature method (or even the Specification) so much - the helper code distracted from the readability of the spec. Also, while Spock's data tables are very expressive, it can be a pretty tedious to add new facets to the generation of test data. For example, say I wanted to add a bit more utility to the Specification - like a new `valueExclusions` column where I could specify values I want yanked out of the sequence: ```groovy // (previous code omitted) given: if ( !!indexReplacements ) { indexReplacements.each { position, value -> sequence[ position ] = value } } if ( !!valueExclusions ) // NEW CODE sequence.removeAll( valueExclusions ) expect: solution.solution( sequence ) == result where: // NEW COLUMN: values (i.e. empty lists) required result | sequence | indexReplacements | valueExclusions 5 | [ 1, 2, 3 ] * 2 | [ : ] | [ ] 5 | range(1, 5) * 2) | [ 1: 99, 8: 99 ] | [ ] 42 | [ 9 ] * 10_000 | [ 0: 0, 9_999: 0 ] | [ ] 5 | range(1, 100) | [ 0: 100, 99: 1 ] | [ ] 42 | range(1, 10_000) | [ : ] | [ 5_000, 5_001 ] // <=== new test case ``` The new column requires a value for each row - requiring the addition of an empty list (`[]`) to every test case just to keep everything working. That's a lot of table fiddling just to add something new; not very conducive for experimentation freedom. Plus, while this example only added two new lines of helper code, it further clutters the Specification and detracts from readability and clarity of the spec. ### Options for improvement It's pretty easy to improve things by extracting helpers like the `range()` method above into a superclass Specification, but the other (non-method) utility code wasn't as easily abstracted. Besides, most of the fiddling I was doing was adding and changing the columns in the data tables. So, I did a little digging on Spock Annotation extensions, and found a chance to catch up and play with Annotations, Reflection, Groovy, and more. The extension I came up with is described below, and included in this Gist. ### Enabling the extension The extension is enabled by simply by applying the `@TweakSequence` Annotation to the Feature Method. The sequenceParameterName attribute can be used to specify which column contains the sequence you want to fiddle with. '`sequence`' is the default, but 'A' is pretty common for int[] input of algorithm challenges. ```groovy @Unroll @TweakSequence( sequenceParameterName = "A" ) def "the sum of #sequence is #result"() { expect: IntStream.of( A ).sum == result where: result | A 12 | [ 1, 2, 3, 1, 2, 3 ] 12 | [ 1, 2, 3 ] * 2 ``` > Note: Henceforth, I use 'sequence column' to refer to the column specified to the annotation. Also, since I'm solely discussing [data tables](http://spockframework.org/spock/docs/1.1/data_driven_testing.html#data-tables) for data-driven testing in Spock, I'm referring to Spock data parameters as 'columns'. ### Generating ranges Now let's use the extension to generate a range of numbers. All four rows below are equivalent: ```groovy @Unroll @TweakSequence( sequenceParameterName = "A" ) def "the sum of #sequence is #result"() { expect: IntStream.of( A ).sum == result where: result | A 12 | [ 1, 2, 3, 1, 2, 3 ] 12 | [ 1, 2, 3 ] * 2 12 | [ range: [ start: 1, end: 3 ]] * 2 12 | [ range: [ start: 1, end: 3, repeat: 2 ]] ``` The first two rows don't use the extension at all.. Those values for `A` are List literals, and `*` is a Groovy operator override for `List.multiply()`, which repeats the List. The last two rows are different. Instead of a List, we're providing a __map__ literal containing a `range` mapping of range attributes, which the extension uses to generate the specified range. The third row again uses the Groovy `*` operator override, while for the fourth, the extension recognizes the `repeat` attribute for the same result. Let's look at another range example - all three rows below are equivalent: ```groovy @Unroll @TweakSequence def "the sum of #sequence is #result"() { expect: IntStream.of( sequence ).sum == result where: result | sequence -6 | [ 2, -1, -4, 2, -1, -4 ] -6 | [ 2, -1, -4 ] * 2 -6 | [ range: [ start: 2, end: -4, step: 3, repeat: 2 ]] ``` The `step` and `repeat` attributes do exactly what their names imply. Further, if the `start` value is higher than the `end` value, the resulting range is produced in descending order. > The absolute value for `step` is effectively used. Negation has no effect, but stepping works the same for ascending and descending ranges. ### Ways to use tweaks Now let's look at a couple more tweaks. There are a few ways to use the tweaks from the extension.. To demonstrate, let's bring back the `indexReplacements` utility I was using earlier. Once again, all three rows in the example below are equivalent. ```groovy expect: sequence == result where: result | sequence | indexReplacements [ 9, 2, 0, 4 ] | [ 1, 2, 3, 4 ] | [ 0: 9, 2: 0 ] [ 9, 2, 0, 4 ] | [ range: [ start: 1, end: 4 ], indexReplacements: [ 0: 9, 2: 0 ] ] | [ : ] [ 9, 2, 0, 4 ] | [ sequence: [ 1, 2, 3, 4 ], indexReplacements: [ 0: 9, 2: 0 ] ] | [ : ] ``` In the first row, we see that if the table includes provide an `indexReplacements` data column of `position:value` mappings, the replacements are performed like before: the element at the index of the map's key is replaced by its value. In the second row, we see that if we're using `range` to generate the sequence, we can include tweaks within the same map as the range. If we only need them for one or two rows, we are spared the trouble of adding column out of the table. > Rows where the indexReplacements value is an empty map are intended to depict the convenience and tradeoffs of leaving the column out of the table altogether. In other words, for those rows, pretend the column isn't there. :-) In the third and final row, we see that we can still avoid adding that column if we're not using `range`: we can specify both the sequence and the tweak(s) within a single map. Instead of `range`, simply specify the sequence using the same attribute name as the sequence column.. just look at the column header. > Hint: to include your sequence plus all your tweaks in a single data column, include them in a map with your sequence, the latter as either a `range` or a `sequence`. ### The `tweaks` column There's one final way to specify a tweak. Let's bring back the `valueExclusions` tweak to demonstrate it: ```groovy expect: sequence == result where: result | sequence | tweaks [ 1, 4 ] | [ 1, 2, 3, 4 ] | [ valueExclusions: [ 2, 3 ] ] [ 1, 4 ] | [ range: [ start: 1, end: 4 ] ] | [ valueExclusions: [ 2, 3 ] ] [ 99, 4 ] | [ 1, 2, 3, 4 ] | [ valueExclusions: [ 2, 3 ], indexReplacements: [ 0: 99 ] ] ``` The extension allows us to include a `tweaks` column containing a map of our desired tweaks. This way you can have a single column for all your tweaks, separate from the sequence column. > An alternate name for the tweaks column can be specified to the extension via the `tweaksParameterName` annotation attribute. I find this is a decent separation and when setting up tests for a new challenge, I'll usually include a single tweaks column. But I still find all the methods of specifying tweaks useful. For example, if there's one tweak that I'll apply to almost every row in a table, it's much less cluttered to include a dedicated column for it. On the other hand, including a tweak in a map (within the sequence or `tweaks` column) is a quick way to experiment with one-off tweaks here and there, without adding a new column. Now let's review everything that's available with the extension: ## Available tweaks | Tweak / Name | what it does | How specified | Contents | |----------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------| | `sequence` <br/>overridden via the `sequenceParameterName` annotation attribute. | specifies the (pre-tweak) sequence, typically as a List literal or an expression resulting in a List | * as a data column <br/> or <br/>* as a map attribute, within the sequence column | List, e.g.<br/> `[ 1, 2, 3 ]`<br/> or<br/> `[ sequence: [ 1, 2, 3 ] ]` | | `range` | | as a map, within the sequence column | Map with attributes: `start`, `end`, `step`, `repeat`:<br/> `[ range: [ start: 1, end: 3 ] ]` | | `tweaks` <br/>overridden via the `tweaksParameterName` annotation attribute. | specifies a map of tweaks as `tweakName:data` mappings | as a data column | a Map with zero or more tweak names and the data used by the tweak (e.g. List, Map, or Number, etc) | | `indexReplacements` | overwrites values at positions corresponding with mapping's key, with the mapping's value | * As a data column <br/> or <br/> * as a map, within the sequence column or the tweaks column | Map of `location:value` mappings:<br/> `[ 1:99, 3: 0 ]` | | `valueExclusions` | removes all occurrences of specified values from the sequence | * As a data column <br/> or <br/>* as a map, within the sequence column or the tweaks column | List of values to exclude from the sequence:<br/> `[ 1, 3, 5 ]` | ## Adding new tweaks You can easily add a new tweak from your Specification class. ```groovy @TweaksSequence static List addNumberToSequence( List inputSequence, int numberToAdd ) { return inputSequence.stream().mapToInt { i -> i + numberToAdd }.toArray() } @TweaksSequence static List reverseSequence( List inputSequence, boolean whetherToReverse ) { if ( !whetherToReverse ) return inputSequence return new LinkedList( inputSequence ).descendingIterator().toList() } @Unroll @TweakSequence def "#iterationCount): added TweakFunctions are used correctly: #sequence"() { expect: sequence == result where: result | sequence [ 1, 2, 3 ] | [ 1, 2, 3 ] [ 1, 2, 3 ] | [ sequence: [ 1, 2, 3 ] ] [ 2, 3, 4 ] | [ sequence: [ 1, 2, 3 ], addNumberToSequence: 1 ] [ 1, 2, 3 ] | [ sequence: [ 1, 2, 3 ], reverseSequence: false ] [ 3, 2, 1 ] | [ sequence: [ 1, 2, 3 ], reverseSequence: true ] } ``` Simply add a static method to your Specification, and apply the `@TweaksSequence` annotation (notice the additional 's' in the annotation name). A few simple rules: * Method should be static * Signature: Two arguments * The first argument should be `List inputList` * The second argument can be any type. It will receive the value specified by the specifying map. * The return type should be `List`, and the method should return the modified List. * Apply the `TweaksSequence` annotation Then, you can freely use the tweak (use the method name) in your data table anywhere you can use a tweak: in a dedicated column, or within the sequence or tweaks columns. ## Final notes: When you're generating very large sequences, you probably want to avoid including the entire sequence in the name of the feature method (i.e. when using Spock's @Unroll feature). (See the related TODO below) Watch out for precendence of tweak operations. You might expect them to be performed in the order you wrote them in, but they'll actually be applied in the order of their addition to the `tweakOperations` map under the covers in the extension. Any added `@TweaksSequence` tweaks are also added to this map, so they'll be performed last. I had a gas doing this. I got to play with creating Annotations, and using Groovy was especially cool. Aside from a bit of build and deploy scripting long ago, I hadn't used it.. but it seemed very natural and familiar from my recent years in ES6+ and Node.js. A lot of Groovy's rather advanced features. For example, the extension relies heavily on Closures. Groovy is like a perfect marriage of Java with the dynamic freedom I've grown accustomed to from using JS. Much easier to debug too! TODOs: * inject an additional `sequenceDescription` data parameter to the feature method, containing the (pre-evaluation) contents of the data column, for use in `@Unroll`ed feature method names. * enforce that added `@TweaksSequence` methods are static (Spock does a lot of funky stuff with scope and it's probably best to avoid any instance state) * Use generics to enforce signatures of `@TweaksSequences` methods: BiFunction<List, List, Object> This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -19,6 +19,21 @@ class TweakSequenceUtilSpecification extends Specification { } @Unroll @TweakSequence def "#iterationCount): sequence using indexReplacements column correctly with multiple replacements: #sequence and #indexReplacements = #result"() { expect: sequence == result where: result | sequence | indexReplacements [ 9, 2, 0, 4 ] | [ 1, 2, 3, 4 ] | [ 0: 9, 2: 0 ] [ 9, 2, 0, 4 ] | [ range: [ start: 1, end: 4 ], indexReplacements: [ 0: 9, 2: 0 ] ] | [ : ] [ 9, 2, 0, 4 ] | [ sequence: [ 1, 2, 3, 4 ], indexReplacements: [ 0: 9, 2: 0 ] ] | [ : ] } @Unroll @TweakSequence def "#iterationCount): sequence or range with tweaks in map: #sequence = #result"() { @@ -46,10 +61,10 @@ class TweakSequenceUtilSpecification extends Specification { where: result | sequence | tweaks [ 1, 4 ] | [ 1, 2, 3, 4 ] | [ valueExclusions: [ 2, 3 ] ] [ 1, 4 ] | [ range: [ start: 1, end: 4 ] ] | [ valueExclusions: [ 2, 3 ] ] [ 99, 4 ] | [ 1, 2, 3, 4 ] | [ valueExclusions: [ 2, 3 ], indexReplacements: [ 0: 99 ] ] } -
johnelm revised this gist
Dec 13, 2017 . 1 changed file with 3 additions and 3 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -5,7 +5,7 @@ class TweakSequenceUtilSpecification extends Specification { @Unroll @TweakSequence def "#iterationCount): sequence using indexReplacements column correctly: #sequence and #indexReplacements = #result"() { expect: sequence == result @@ -21,7 +21,7 @@ class TweakSequenceUtilSpecification extends Specification { @Unroll @TweakSequence def "#iterationCount): sequence or range with tweaks in map: #sequence = #result"() { expect: sequence == result @@ -39,7 +39,7 @@ class TweakSequenceUtilSpecification extends Specification { @Unroll @TweakSequence def "#iterationCount): sequence #sequence using tweaks column #tweaks correctly results as #result"() { expect: sequence == result -
johnelm revised this gist
Dec 13, 2017 . 1 changed file with 0 additions and 17 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -86,23 +86,6 @@ class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtensi } }) def getDataForParameterName = { parameterName -> if ( inputSequence instanceof Map && !!inputSequence[ parameterName ] ) { return inputSequence[ parameterName ] -
johnelm revised this gist
Dec 13, 2017 . 1 changed file with 9 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -76,7 +76,15 @@ class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtensi } ] 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() ] = { arg, List inputList -> return method.invoke( specInstance, arg, inputList ) } }) // http://mrhaki.blogspot.com/2009/08/groovy-goodness-turn-methods-into.html // groovy has some utilities that might help locate and invoke the function easier @@ -96,7 +104,6 @@ class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtensi // and seek methods in the spec class that match the parameter name def getDataForParameterName = { parameterName -> if ( inputSequence instanceof Map && !!inputSequence[ parameterName ] ) { return inputSequence[ parameterName ] } -
johnelm revised this gist
Dec 13, 2017 . 1 changed file with 19 additions and 17 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -2,11 +2,14 @@ Over the last few weeks I've been practicing algorithms on [Codility](http://www.codility.com) and [Leetcode](http://www.leetcode.com). The algorithm puzzles on Codility and Leetcode almost use `int` and/or `int[]` arguments and return types, and sometimes have BigO performance requirements. To whip up test cases in my IDE, I started using the [data tables](http://spockframework.org/spock/docs/1.1/data_driven_testing.html#data-tables) feature in the awesome [Spock](http://spockframework.org) testing framework, and right away I started creating some [helper methods](http://spockframework.org/spock/docs/1.1/spock_primer.html#_helper_methods) to help with the generation of `int[]` sequences for input to the algorithms I was practicing. Here's a simple example (below) of what I was doing. - I had a `range()` helper method (lines 6-9) in my Specification class, which I called from within my Spock data column to generate a range of ints.. - I specified values for arbitrary sequence positions in a separate `indexReplacements` column of index:value maps. The replacements were performed by the code at lines 15-19, below. #### Simple beginnings ```groovy import spock.lang.* class MySpecification extends Specification { @@ -39,9 +42,9 @@ class MySpecification extends Specification { 42 | range(1, 100) | [ 0: 100, 99: 1 ] // 100, 2, 3, 4 ... 97, 98, 99, 1 ``` This worked pretty well, but I didn't like cluttering the feature method (or even the Specification) so much. The helper code distracted from the semantics of the solution and the functionality being tested. Also, while Spock's data tables are very expressive, it can be a little tedious to add new facets to the generation of test data. For example, say I wanted to add a bit more utility to the Specification: ```groovy // (previous code omitted) @@ -65,13 +68,13 @@ Also, Spock's data tables are very expressive, but it can be a little tedious to 5 | range(1, 100) | [ 0: 100, 99: 1 ] | [ ] 42 | range(1, 10_000) | [ : ] | [ 5_000, 5_001 ] // <=== new test case ``` The new column requires a value for each row - requiring the addition of an empty list (`[]`) to every test case just to keep everything working. That's a lot of table fiddling just to something new for one test case. Plus, while this example only added two new lines of helper code, it further clutters the Specification and detracts from readability and clarity. ### Improvement options It's pretty easy to improve things by extracting helpers like the `range()` method above into a subclass of Spock's Specification base class, but most of the fiddling I was doing was adding and changing the columns in the data tables. And the other (non-method) utility code that uses column data for manipulation of the sequence, isn't easily abstracted into a superclass like `range()` is. Aside from practicing algorithms, I've also been catching up on Java, which I loved and used for many years, but not since Java 5 came out. So, I saw this as a chance to practice custom Generics, Annotations, Lambdas, etc. I decided to write a Spock extension to replace the helper fixtures I had. Got to play with Groovy and practice custom Annotations. Just a side note on Groovy - aside from a bit of build and deploy scripting about ten years ago, I hadn't done much with Groovy.. but I'm loving it.. a lot of Groovy's features (e.g. Closures) are pretty familiar from my recent years in ES6+ and Node.js.. it's like a perfect marriage of Java with the dynamic freedom I've grown accustomed to from using JS. @@ -179,15 +182,14 @@ Now let's review everything that's available with the extension: ## Available tweaks | Tweak | required | how named | what it does | How available | Contents | |----------------------------------|:--------:|--------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------|--------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| | sequence<br/> (as a data column) | yes | default is `sequence`. Can be specified via the `sequenceParameterName` annotation attribute: `@TweakSequence( sequenceParameterName="alternateName"` | specifies the (pre-tweak) sequence | * As a data column<br/>* As a map, within the sequence column | List, e.g. [ 1, 2, 3 ]<br/> or<br/> [ sequence: [ 1, 2, 3 ] ] | | sequence | | | | | | | range | | always '`range`' | | * As a map, within the sequence column | Map with attributes: start, end, step, repeat, e.g<br/>[ start: 1, end: 3, step: 2, repeat: 2 ]<br/> start and end are required | | tweaks | | 'tweaks' can be overridden via the `tweaksParameterName` annotation attribute:`@TweakSequence( sequenceParameterName="alternateName"` | | As a data column | each row specifies a Map with zero or more tweaks | | indexReplacements | | | | * As a data column* As a map, within the sequence column or the tweaks column | Map - of location / value mappings | | valueExclusions | | | | * As a data column* As a map within the sequence column* As a map within the tweaks column | List | #### range -
johnelm revised this gist
Dec 13, 2017 . 2 changed files with 16 additions and 23 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -12,21 +12,19 @@ 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 <List>, <List>, <Object> signature class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtension<TweakSequence> { private final static PARAMETER_NAME_RANGE = "range" @@ -120,8 +118,9 @@ class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtensi 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 :-( @@ -144,7 +143,7 @@ class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtensi } } 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 ) This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -85,36 +85,30 @@ class TweakSequenceUtilSpecification extends Specification { } @TweaksSequence static List addNumberToSequence( int numberToAdd, List sequence ) { return sequence.stream().mapToInt { i -> i + numberToAdd }.toArray() } @TweaksSequence static List subtractNumberFromSequence( int numberToSubtract, List sequence ) { return sequence.stream().mapToInt { i -> i - numberToSubtract }.toArray() } @TweaksSequence static List reverseSequence( boolean whetherToReverse, List sequence ) { if ( !whetherToReverse ) return sequence return new LinkedList( sequence ).descendingIterator().toList() } @Unroll @TweakSequence def "#iterationCount): added TweakFunctions are used correctly: #sequence"() { expect: sequence == result where: result | sequence [ 1, 2, 3 ] | [ 1, 2, 3 ] [ 1, 2, 3 ] | [ sequence: [ 1, 2, 3 ] ] -
johnelm revised this gist
Dec 13, 2017 . 1 changed file with 3 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -67,7 +67,7 @@ Also, Spock's data tables are very expressive, but it can be a little tedious to ``` The new column requires a value for each row - requiring the addition of an empty list (`[]`) to every test case just to keep them working. Using the data tables is still worthwhile, but that's a lot of table fiddling just to something new for one test case. Plus, while this example only added two new lines of helper code, it further clutters the Specification and detracts from its value as readable documentation for my solution. ### Improvement options It's pretty easy to improve things by extracting helpers like the `range()` method above into a subclass of Spock's Specification base class, but most of the fiddling I was doing was adding and changing the columns in the table. Adding code to use the data columns for manipulating the sequence, but this code isn't easily abstracted into a superclass like `range()`, which is used within the `sequence` column that is consumed by the solution. @@ -211,6 +211,8 @@ expected: List ## Adding new tweaks use an annitation to make any method available as a tweaks ```groovy -
johnelm revised this gist
Dec 13, 2017 . 1 changed file with 16 additions and 3 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -1,11 +1,24 @@ import spock.lang.* class ExampleSpecification extends Specification { @Unroll @TweakSequence def "#iterationCount: result given #sequence is #result"() { expect: sequence == result where: result | sequence | indexReplacements [ 1, 99, 3 ] | [ 1, 2, 3 ] | [ 1: 99 ] [ 1, 99, 3 ] | [ range: [ start: 1, end: 3 ] ] | [ 1: 99 ] [ 1, 99, 3 ] | [ range: [ start: 1, end: 3 ], indexReplacements: [ 1: 99 ] ] | [ : ] [ 1, 99, 3 ] | [ sequence: [ 1, 2, 3 ], indexReplacements: [ 1: 99 ] ] | [ : ] } // @Unroll // @TweakSequence -
johnelm revised this gist
Dec 13, 2017 . 1 changed file with 4 additions and 4 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -36,8 +36,8 @@ class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtensi 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 @@ -123,10 +123,10 @@ class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtensi 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 } @@ -158,7 +158,7 @@ class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtensi } } // TODO now add a new #sequenceTweakDescription parameter, 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[] -
johnelm revised this gist
Dec 13, 2017 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -140,7 +140,7 @@ class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtensi .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 ) ) { -
johnelm revised this gist
Dec 13, 2017 . 2 changed files with 65 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -19,6 +19,13 @@ import java.util.stream.* //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" TweakFunction[] tweakFunctions() default [ ] } @Repeatable( TweakSequence.class ) @interface TweakFunction { String value() } class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtension<TweakSequence> { @@ -70,6 +77,25 @@ class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtensi return inputList } ] def specInstance = invocation.sharedInstance def methods = specInstance.getClass().getMethods() // http://mrhaki.blogspot.com/2009/08/groovy-goodness-turn-methods-into.html // groovy has some utilities that might help locate and invoke the function easier annotation.tweakFunctions().toList().forEach( { functionFromAnnotation -> def functionName = functionFromAnnotation.value() List nameMatchingMethods = methods.toList().stream().filter { method -> method.getName() == functionName }.toArray() if ( nameMatchingMethods.isEmpty() ) throw new NoSuchMethodError( "Method $functionName not found" ) def firstMatchingMethod = nameMatchingMethods[ 0 ] tweakOperations[ functionFromAnnotation.value() ] = { arg, List inputList -> firstMatchingMethod.invoke( specInstance, arg, inputList ) } } ) // play here with data values - extract the parameter names so I can do this in reverse - // and seek methods in the spec class that match the parameter name def getDataForParameterName = { parameterName -> parameterName This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -85,5 +85,44 @@ class TweakSequenceUtilSpecification extends Specification { } static List addNumberToSequence( int numberToAdd, List sequence ) { return sequence.stream().mapToInt { i -> i + numberToAdd }.toArray() } static List subtractNumberFromSequence( int numberToSubtract, List sequence ) { return sequence.stream().mapToInt { i -> i - numberToSubtract }.toArray() } static List reverseSequence( boolean whetherToReverse, List sequence ) { if ( !whetherToReverse ) return sequence return new LinkedList( sequence ).descendingIterator().toList() } @Unroll @TweakSequence( tweakFunctions = [ @TweakFunction( "addNumberToSequence" ), @TweakFunction( "subtractNumberFromSequence" ), @TweakFunction( "reverseSequence" ) ] ) def "#iterationCount): added TweakFunctions are used correctly"() { expect: sequence == result where: result | sequence [ 1, 2, 3 ] | [ 1, 2, 3 ] [ 1, 2, 3 ] | [ sequence: [ 1, 2, 3 ] ] [ 2, 3, 4 ] | [ sequence: [ 1, 2, 3 ], addNumberToSequence: 1 ] [ 0, 1, 2 ] | [ sequence: [ 1, 2, 3 ], subtractNumberFromSequence: 1 ] [ 1, 2, 3 ] | [ sequence: [ 1, 2, 3 ], reverseSequence: false ] [ 3, 2, 1 ] | [ sequence: [ 1, 2, 3 ], reverseSequence: true ] } } -
johnelm revised this gist
Dec 13, 2017 . 2 changed files with 4 additions and 7 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -4,7 +4,6 @@ // https://opensource.org/licenses/MIT import org.spockframework.runtime.extension.* import org.spockframework.runtime.model.* import java.lang.annotation.* @@ -17,9 +16,8 @@ import java.util.stream.* //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" } @@ -31,8 +29,8 @@ class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtensi 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 @@ -137,8 +135,6 @@ class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtensi // 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() } This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -2,6 +2,7 @@ import spock.lang.* class TweakSequenceUtilSpecification extends Specification { @Unroll @TweakSequence def "#iterationCount): sequence using indexReplacements column correctly results as #result"() { @@ -69,7 +70,7 @@ class TweakSequenceUtilSpecification extends Specification { } @Unroll @TweakSequence( sequenceParameterName = "A" ) def "#iterationCount): alternate sequence name: sequence #A embedded in map correctly results as #result"() { expect: -
johnelm revised this gist
Dec 13, 2017 . 1 changed file with 45 additions and 30 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -107,7 +107,7 @@ Once this is done, the extension makes a few 'tweak' utilities available for the Both of the rows in the table above are equivalent - Spock (actually Groovy) allow us to easiy repeat a List via 'multiplying' it. Now let's apply the TweakSequence Spock extension for another way - all of these are also equivalent: ```groovy @@ -124,7 +124,7 @@ Now let's apply the TweakSequence Spock extension for another way: 12 | [ range: [ start: 1, end: 3 ]] * 2 12 | [ range: [ start: 1, end: 3, repeat: 2 ]] ``` In the last two rows, a Map is specified instead of a List in the `sequence` column, which in turn, contains a `range` map with `start` and `end` attribute. When a map containing `range` is provided, the extension uses its `start` and `end` values to generate the sequence. It also recognizes `repeat` and `step` values, which do exactly what their names imply. If the `start` value is greater than `end`, the sequence is reversed. So, all these are equivalent: ```groovy @@ -137,9 +137,7 @@ In the two new rows, a Map is specified instead of a List in the `sequence` colu -6 | [ range: [ start: 2, end: -4, step: 3, repeat: 2 ] ``` Now let's bring back the `indexReplacements` utility I was using earlier, now available via the extension as a 'tweak'. There are a few ways to use it: ```groovy @@ -148,7 +146,6 @@ Now let's bring back the `indexReplacements` thing I was using before. Thereare where: result | sequence | indexReplacements [ 1, 99, 3 ] | [ 1, 2, 3 ] | [ 1: 99 ] [ 1, 99, 3 ] | [ range: [ start: 1, end: 3 ], indexReplacements: [ 1: 99 ] ] | [ : ] [ 1, 99, 3 ] | [ sequence: [ 1, 2, 3 ], indexReplacements: [ 1: 99 ] ] | [ : ] @@ -157,7 +154,7 @@ In the first two rows, we see that if we provide an `indexReplacements` data col In the third row, we see that if we're using `range` to generate the sequence, we can include the `indexReplacements` mappings within the same map as the range. In the fourth and final row, we see something new: using the name of the sequence column again in the map allows us to specify a List literal as the source sequence, plus any other tweaks within the map, all within the one column. This way, we can specify all our sequences and tweaks without adding any columns. Essentially this is the same that we did earlier with `range`, but where we don't need to generate a range for the sequence. There's one final way to specify a tweak. Let's bring back the `valueExclusions` tweak to demonstrate it: @@ -167,17 +164,52 @@ There's one final way to specify a tweak. Let's bring back the `valueExclusions result | sequence | tweaks [ 1, 3 ] | [ 1, 2, 3 ] | [ valueExclusions: [ 2 ] ] [ 1, 3 ] | [ range: [ start: 1, end: 3 ] ] | [ valueExclusions: [ 2 ] ] [ 99, 3 ] | [ 1, 2, 3 ] | [ valueExclusions: [ 2 ], indexReplacements: 0, 99 ] ``` Here we see that we can provide a `tweaks` column containing a map of our tweaks. This way you can have a single column for all your tweaks, but separate from the sequence column. > Note: an alternate name for the `tweaks` column can be specified to the extension via the annotation: `TweakSequence(tweaksParameterName="otherTweaks")` I find this is a decent separation and when setting up tests for a new challenge, I'll usually include a single tweaks column. But I still find all the methods of specifying tweaks useful. If there's one tweak that I'll apply to almost every row in a table, it's much less cluttered to include a dedicated column for it. On the other hand, including a tweak in a map (within the sequence or `tweaks` column) is a quick way to experiment with one-off tweaks here and there, without adding a new column. Now let's review everything that's available with the extension: ## Available tweaks | Tweak | what it does | How available | Contents | Example | | |-------------------|---------------------------------------------------------|--------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|---------|-------------------------------------------------------------| | sequence | specifies the pre-tweak starting point for the sequence | * As a data column<br/>* As a map, within the sequence column | List: [ 1, 2, 3, ] | | name can be changed via the `value` annotation attribute | | range | | * As a map, within the sequence column | Map - recognized attributes start, end, step, repeat: [ start: 1, end: 3, step: 2, repeat: 2 ] | | attributes: start, end, step, repeat | | tweaks | | As a data column | each row specifies a Map with zero or more tweaks | | name can be changed via the `tweaksParameterName` attribute | | indexReplacements | | * As a data column* As a map, within the sequence column or the tweaks column | Map - of location / value mappings | | | | valueExclusions | | * As a data column* As a map within the sequence column* As a map within the tweaks column | List | | | #### range Where available: #### indexReplacements expected: map #### indexExclusions #### valueExclusions expected: List #### valueReplacements ## Adding new tweaks ```groovy @@ -260,25 +292,6 @@ But, I've still found all the methods of specifying tweaks to be useful. If the PS: For convenience, the sequence, for which ArrayList is its natural form in Groovy, is converted to an int[] @@ -385,6 +398,8 @@ There are several ways.. So you shouldn't have to rewrite your data table to ad Notes: watch out for order of precedence.at the moment, they're applied in this order: when you're generating very large sequences, you probably want to avoid including the entire sequence in the name of the feature method (i.e..if you're naming using Spock's @Unroll annotation). any provided are added after these in order -
johnelm revised this gist
Dec 13, 2017 . 1 changed file with 3 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -20,10 +20,10 @@ import java.util.stream.* //Accept a string value with the name of the feature method parameter we're going to meddle with String value() default "sequence" String tweaksParameterName() default "tweaks" } class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtension<TweakSequence> { private final static PARAMETER_NAME_RANGE = "range" @@ -32,6 +32,7 @@ class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtensi def parameterNames = feature.getParameterNames() def sequenceParameterName = annotation.value() def tweaksParameterName = annotation.tweaksParameterName( ) int parameterIndexToFiddleWith = parameterNames.indexOf( sequenceParameterName ) def dataValues = null @@ -85,7 +86,7 @@ class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtensi } 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 -
johnelm revised this gist
Dec 13, 2017 . 1 changed file with 4 additions and 5 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -23,7 +23,6 @@ import java.util.stream.* } class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtension<TweakSequence> { private final static PARAMETER_NAME_TWEAKS = "tweaks" private final static PARAMETER_NAME_RANGE = "range" @@ -92,11 +91,11 @@ class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtensi // 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 @@ -116,7 +115,7 @@ class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtensi .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 ) ) { -
johnelm revised this gist
Dec 13, 2017 . 1 changed file with 7 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -1,3 +1,10 @@ // 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.* -
johnelm revised this gist
Dec 13, 2017 . 1 changed file with 88 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,88 @@ import spock.lang.* class TweakSequenceUtilSpecification extends Specification { @Unroll @TweakSequence def "#iterationCount): sequence using indexReplacements column correctly results as #result"() { expect: sequence == result where: result | sequence | indexReplacements [ 1, 99, 3 ] | [ 1, 2, 3 ] | [ 1: 99 ] [ 1, 99, 3 ] | [ range: [ start: 1, end: 3 ] ] | [ 1: 99 ] } @Unroll @TweakSequence def "#iterationCount): sequence or range specified with tweaks in a single map correctly results in #result"() { expect: sequence == result where: result | sequence [ 1, 2, 3 ] | [ 1, 2, 3 ] [ 1, 2, 3 ] | [ range: [ start: 1, end: 3 ] ] [ 1, 99, 3 ] | [ range: [ start: 1, end: 3 ], indexReplacements: [ 1: 99 ] ] [ 1, 99, 3 ] | [ sequence: [ 1, 2, 3 ], indexReplacements: [ 1: 99 ] ] } @Unroll @TweakSequence def "#iterationCount): sequence using tweak column correctly results as #result"() { expect: sequence == result where: result | sequence | tweaks [ 1, 3 ] | [ 1, 2, 3 ] | [ valueExclusions: [ 2 ] ] [ 1, 3 ] | [ range: [ start: 1, end: 3 ] ] | [ valueExclusions: [ 2 ] ] } @Unroll @TweakSequence( tweaksParameterName = "otherTweaks" ) def "#iterationCount): alternate tweaks column: sequence using tweak column correctly results as #result"() { expect: sequence == result where: result | sequence | otherTweaks [ 1, 3 ] | [ 1, 2, 3 ] | [ valueExclusions: [ 2 ] ] [ 1, 3 ] | [ range: [ start: 1, end: 3 ] ] | [ valueExclusions: [ 2 ] ] } @Unroll @TweakSequence( value = "A" ) def "#iterationCount): alternate sequence name: sequence #A embedded in map correctly results as #result"() { expect: A == result where: result | A [ 1, 2, 3 ] | [ 1, 2, 3 ] [ 1, 2, 3 ] | [ A: [ 1, 2, 3 ] ] [ 1, 99, 3 ] | [ A: [ 1, 2, 3 ], indexReplacements: [ 1: 99 ] ] } } -
johnelm revised this gist
Dec 13, 2017 . 1 changed file with 241 additions and 84 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -1,166 +1,315 @@ # Sequence Tweaking Spock Annotation Extension Over the last few weeks I've been practicing algorithms on [Codility](http://www.codility.com) and [Leetcode](http://www.leetcode.com). The algorithm puzzles on Codility and Leetcode are almost always dealing with `int` and/or `int[]` arguments and results, and sometimes have BigO performance requirements. I started using the [data tables](http://spockframework.org/spock/docs/1.1/data_driven_testing.html#data-tables) in the awesome [Spock](http://spockframework.org) testing framework, and right away I started creating some [helper methods](http://spockframework.org/spock/docs/1.1/spock_primer.html#_helper_methods) to help with the generation of `int` sequences for input to the algorithms I was practicing. With a bit of code paired up with additional data columns (parameters, in Spock), Here's a simple example - I was using a `range()` helper method from within my data column to generate a range of ints, and specifying overwriting values I wanted to apply at arbitrary spots in the sequence in a separate `indexReplacements` column of index:value maps, which was processed by some code in the fixture method if not empty. #### Without the extension: ```groovy import spock.lang.* class MySpecification extends Specification { def solution = new AlgorithmSolution() // helper for generation of a range of ints ArrayList range( int from, int to ) { return IntStream.rangeClosed( from, to ).boxed().collect( Collectors.toList() ) } @Unroll def "given input sequence #sequence with #indexReplacements, the result is #result"() { given: // replace values at specified positions if indexReplacements is not empty if ( !!indexReplacements ) { indexReplacements.each { position, value -> sequence[ position ] = value } } expect: solution.solution( sequence ) == result where: result | sequence | indexReplacements 5 | [ 1, 2, 3 ] * 2 | [ : ] // 1, 2, 3, 1, 2, 3 5 | range(1, 5) * 2 | [ 1: 99, 8: 99 ] // 1, 99, 3, 4, 5, 1, 2, 3, 99, 5 42 | [ 9 ] * 10_000 | [ 0: 0, 9_999: 0 ] // 0, 9, 9, 9 ... 9, 9, 9, 0 42 | range(1, 100) | [ 0: 100, 99: 1 ] // 100, 2, 3, 4 ... 97, 98, 99, 1 ``` This worked pretty well, but I didn't like cluttering the feature method (or even the Specification) with code that distracted from the semantics of the solution and the functionality being tested. Also, Spock's data tables are very expressive, but it can be a little tedious to add new facets to the generation of test data. Say I wanted to add a bit more utility to the example: ```groovy // (previous code omitted) given: if ( !!indexReplacements ) { indexReplacements.each { position, value -> sequence[ position ] = value } } if ( !!valueExclusions ) // NEW CODE sequence.removeAll( valueExclusions ) expect: solution.solution( sequence ) == result where: // NEW COLUMN: values (i.e. empty lists) required result | sequence | indexReplacements | valueExclusions 5 | [ 1, 2, 3 ] * 2 | [ : ] | [ ] 5 | range(1, 5) * 2) | [ 1: 99, 8: 99 ] | [ ] 42 | [ 9 ] * 10_000 | [ 0: 0, 9_999: 0 ] | [ ] 5 | range(1, 100) | [ 0: 100, 99: 1 ] | [ ] 42 | range(1, 10_000) | [ : ] | [ 5_000, 5_001 ] // <=== new test case ``` The new column requires a value for each row - requiring the addition of an empty list (`[]`) to every test case just to keep them working. Using the data tables is still worthwhile, but that's a lot of table fiddling just to something new for one test case. Plus, while this example only added two new lines of helper code, it further clutters the Specification and detracts from its value as readable documentation for my solution. ###Improvement options It's pretty easy to improve things by extracting helpers like the `range()` method above into a subclass of Spock's Specification base class, but most of the fiddling I was doing was adding and changing the columns in the table. Adding code to use the data columns for manipulating the sequence, but this code isn't easily abstracted into a superclass like `range()`, which is used within the `sequence` column that is consumed by the solution. Aside from learning and practicing algorithms, I've also been catching up on modern Java, which I used and loved for many years but haven't used on the job Java 5 came out. So, I saw this as a chance to practice custom Generics, Annotations, Lambdas, etc. I decided to write a Spock extension to replace the helper fixtures I had. Got to play with Groovy and practice custom Annotations. Just a side note on Groovy - aside from a bit of build and deploy scripting about ten years ago, I hadn't done much with Groovy.. but I'm loving it.. a lot of Groovy's features (e.g. Closures) are pretty familiar from my recent years in ES6+ and Node.js.. it's like a perfect marriage of Java with the dynamic freedom I've grown accustomed to from using JS. Here's what I wound up with. The extension is enabled by simply by applying the `@TweakSequence` Annotation to the Feature Method. It accepts one argument, for specifying which data parameter (column) contains the sequence you want to fiddle with ('`sequence`' is the default): #### Enabling the extension ```groovy @Unroll @TweakSequence("A") // ('sequence' is the default) def "#iterationCount: result is the expected #result"() { ... } ``` > For the remainder of this article, I use 'sequence column' to refer to the column specified to the annotation, and since I'm solely discussing data tables for data-driven testing in Spock, I refer to Spock data variables as 'columns'. TODO - rename columns/parameters to data variables Once this is done, the extension makes a few 'tweak' utilities available for the Spock feature: `range`, `indexReplacements`, `valueReplacements`, `valueExclusions`, `sequence`, and `tweaks`. It's easiest to show how they're used in Spock data tables: ```groovy @Unroll @TweakSequence def "the sum of #sequence is #result"() { expect: IntStream.of( sequence ).sum == result where: result | sequence 12 | [ 1, 2, 3, 1, 2, 3 ] 12 | [ 1, 2, 3 ] * 2 ``` Both of the rows in the table above are equivalent - Spock (actually Groovy) allow us to easiy repeat a List via 'multiplying' it. Now let's apply the TweakSequence Spock extension for another way: ```groovy @Unroll @TweakSequence def "the sum of #sequence is #result"() { expect: IntStream.of( sequence ).sum == result where: result | sequence 12 | [ 1, 2, 3, 1, 2, 3 ] 12 | [ 1, 2, 3 ] * 2 12 | [ range: [ start: 1, end: 3 ]] * 2 12 | [ range: [ start: 1, end: 3, repeat: 2 ]] ``` In the two new rows, a Map is specified instead of a List in the `sequence` column, which, in turn, contains a `range=` mapping to a map of `start` and `end` values. When a map containing `range` is provided, the extension uses its `start` and `end` values to generate the sequence. It also recognizes `repeat` and `step` values, which do exactly what their names imply. If the `start` value is greater than `end`, the sequence is reversed. So, all these are equivalent: ```groovy expect: IntStream.of( sequence ).sum == result where: result | sequence -6 | [ 2, -1, -4, 2, -1, -4 ] -6 | [ 2, -1, -4 ] * 2 -6 | [ range: [ start: 2, end: -4, step: 3, repeat: 2 ] ``` Now let's bring back the `indexReplacements` thing I was using before. Thereare a few ways to use the extension to perform positional replacements in the sequence: ```groovy expect: sequence == result where: result | sequence | indexReplacements [ 1, 99, 3 ] | [ 1, 2, 3 ] | [ 1: 99 ] [ 1, 99, 3 ] | [ range: [ start: 1, end: 3 ] ] | [ 1: 99 ] [ 1, 99, 3 ] | [ range: [ start: 1, end: 3 ], indexReplacements: [ 1: 99 ] ] | [ : ] [ 1, 99, 3 ] | [ sequence: [ 1, 2, 3 ], indexReplacements: [ 1: 99 ] ] | [ : ] ``` In the first two rows, we see that if we provide an `indexReplacements` data column of position:value mappings, the replacements are performed like before. In the third row, we see that if we're using `range` to generate the sequence, we can include the `indexReplacements` mappings within the same map as the range. In the fourth and final row, we see something new: using `sequence` (or whatever data variable name was specified to the extension Annotation) allows us to specify a List literal as the source sequence, plus any other tweaks within a map, all within the one `sequence` column. This way, we can specify all our sequences and tweaks without adding any columns. Essentially this is the same that we did earlier with `range`, but without using `range`. There's one final way to specify a tweak. Let's bring back the `valueExclusions` tweak to demonstrate it: ```groovy result | sequence | tweaks [ 1, 3 ] | [ 1, 2, 3 ] | [ valueExclusions: [ 2 ] ] [ 1, 3 ] | [ range: [ start: 1, end: 3 ] ] | [ valueExclusions: [ 2 ] ] ``` Here we see that we can provide a `tweaks` column containing a map of tweaks. This way you can have a single column for all your tweaks, but separate from the sequence column. > Note: an alternate name for the `tweaks` column can be specified using the `tweaksParameterName` attribute for the extension annotation: TweakSequence( tweaksParameterName="otherName" ) can be specified using the `tweaks` column can be I find this is a decent separation. But, I've still found all the methods of specifying tweaks to be useful. If there's one tweak that I'll apply to almost every row in a table, it's much less cluttered to include a dedicated column for it. On the other hand, including a tweak in a map (within the sequence or `tweaks` column) is a quick way to experiment with one-off tweaks here and there, without adding a new column. ```groovy 6 | [ 1, 3, 4 ] * 2 | [ indexReplacements: [ 3: 100 ] ] 3 | [ ].range( 1, 10 ) | [ indexReplacements: [ 5: 1 ] ] 3 | [ range: [ start: 1, end: 10 ] ] | [ indexReplacements: [ 5: 1 ], valueExclusions: [ 5, 6 ] ] 3 | [ range: [ start: 1, end: 10 ] ] | [ indexReplacements: [ 5: 1 ], valueExclusions: [ 5, 6 ] ] 20_002 | [ 10001 ] * 5 | [ : ] 10 | [ 5, 6, 5, 6 ] * 8 | [ : ] 2 | [ 1 ] * 5 | [ : ] 2 | [ 1 ] * 5 | [ : ] 2 | [ 1 ] * 10 | [ : ] 4 | [ 2 ] * 100_000 | [ indexReplacements: [ 0: 1, 100_000: 1 ] ] 2 | [ 2 ] * 100_000 | [ indexReplacements: [ 1: 1, 99_998: 1 ] ] 3 | [ range: [ start: 1, end: 100 ] ] | [ indexReplacements: [ 98: 1 ] ] 6 | [ range: [ start: 10, end: 1 ] ] | [ : ] 6 | [ range: [ start: 100, end: 1 ] ] | [ : ] 6 | [ range: [ start: 1, end: 100 ] ] | [ indexReplacements: [ 2: 1 ] ] 20 | [ range: [ start: 10, end: 100, repeat: 100 ] ] | [ indexReplacements: [ 2: 100 ] ] 3 | [ range: [ start: 1, end: 100 ] ] | [ indexReplacements: [ 3: 1 ] ] 6 | [ range: [ start: 1, end: 100_000 ] ] | [ valueExclusions: [ 5, 6 ] ] 2 | [ range: [ start: 1, end: 100_000 ] ] | [ indexReplacements: [ 1: 1, 99_998: 1 ] ] 3 | [ range: [ start: 1, end: 100_000 ] ] | [ indexReplacements: [ 99_998: 1 ] ] 12 | [ range: [ start: 100_000, end: 10 ] ] | [ indexReplacements: [ 5555: 1 ] ] } ``` ```groovy @Unroll @TweakSequence def "#iterationCount: result is the expected #result"() { where: result | sequence | tweaks 7 | [ 5 ] * 7 | [ indexReplacements: [ 1: 2, 2: 2 ] ] 6 | [ 1, 3, 4 ] * 2 | [ indexReplacements: [ 3: 100 ] ] 3 | [ ].range( 1, 10 ) | [ indexReplacements: [ 5: 1 ] ] 3 | [ range: [ start: 1, end: 10 ] ] | [ indexReplacements: [ 5: 1 ], valueExclusions: [ 5, 6 ] ] 3 | [ range: [ start: 1, end: 10 ] ] | [ indexReplacements: [ 5: 1 ], valueExclusions: [ 5, 6 ] ] 20_002 | [ 10001 ] * 5 | [ : ] 10 | [ 5, 6, 5, 6 ] * 8 | [ : ] 2 | [ 1 ] * 5 | [ : ] 2 | [ 1 ] * 5 | [ : ] 2 | [ 1 ] * 10 | [ : ] 4 | [ 2 ] * 100_000 | [ indexReplacements: [ 0: 1, 100_000: 1 ] ] 2 | [ 2 ] * 100_000 | [ indexReplacements: [ 1: 1, 99_998: 1 ] ] 3 | [ range: [ start: 1, end: 100 ] ] | [ indexReplacements: [ 98: 1 ] ] 6 | [ range: [ start: 10, end: 1 ] ] | [ : ] 6 | [ range: [ start: 100, end: 1 ] ] | [ : ] 6 | [ range: [ start: 1, end: 100 ] ] | [ indexReplacements: [ 2: 1 ] ] 20 | [ range: [ start: 10, end: 100, repeat: 100 ] ] | [ indexReplacements: [ 2: 100 ] ] 3 | [ range: [ start: 1, end: 100 ] ] | [ indexReplacements: [ 3: 1 ] ] 6 | [ range: [ start: 1, end: 100_000 ] ] | [ valueExclusions: [ 5, 6 ] ] 2 | [ range: [ start: 1, end: 100_000 ] ] | [ indexReplacements: [ 1: 1, 99_998: 1 ] ] 3 | [ range: [ start: 1, end: 100_000 ] ] | [ indexReplacements: [ 99_998: 1 ] ] 12 | [ range: [ start: 100_000, end: 10 ] ] | [ indexReplacements: [ 5555: 1 ] ] } ### Available tweaks #### range #### indexReplacements expected: map #### indexExclusions #### valueExclusions expected: List #### valueReplacements PS: For convenience, the sequence, for which ArrayList is its natural form in Groovy, is converted to an int[] Lots of learning.. Java.. and algorithms. all pretty easy by subclassing Spock's Specification class, but I thought it would be fun (and convenient) to get it done with a simple annotation. New to annotations New to groovy, but Javascript concepts helped a lot. New to Spock Needing to generate very large input arrays, of sizes up to 100,000 and values from +/- 1 billion. As I worked through the problems, I wanted to be able to quickly create test cases.. edge cases and very large inputs so I could check out their correctness and how they perform I'd done some basic Groovy build scripting about ten years ago To use, simply use the annotation on your feature method: ```groovy ``` ## Usage @@ -231,4 +380,12 @@ There are several ways.. So you shouldn't have to rewrite your data table to ad [ range: [ start: 1, end: 5 ], indexReplacements: [ 1: 0, 3: 0 ], valueExclusions: [ 1, 2 ] ] ``` Notes: watch out for order of precedence.at the moment, they're applied in this order: any provided are added after these in order and of course any operations added afterward, outside the sequence -
johnelm revised this gist
Dec 12, 2017 . 3 changed files with 297 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,60 @@ import spock.lang.Specification import spock.lang.Unroll class Example extends Specification { // @Unroll // @TweakSequence // def "#iterationCount: result is the expected #result"() { // given: // // def string = new String() // // expect: // // solution.solution( sequence.toArray() as int[] ) == result // // // where: // // result | sequence | indexReplacements // 10 | [ 1 ] * 5 | [ : ] // 9 | [ 1, 2, 3 ] * 3 | [ : ] // 18 | [ 1, 2, 3 ] * 4 | [ : ] // // result | [ 1, 2, 3, 4, 5 ] | [ 1: 0, 3: 0 ] // // // // result | sequence | indexReplacements // 10 | [ 1 ] * 5 | [ : ] // 9 | [ 1, 2, 3 ] * 3 | [ : ] // 18 | [ 1, 2, 3 ] * 4 | [ : ] // 15 | [ 1, 2, 3, 4, 5 ] * 3 | [ : ] // 50 | [ 1, 2, 3, 4, 5 ] * 5 | [ : ] // 49995000 | [ 1 ] * 10_000 | [ : ] // 1_000_000_000 | [ range: [ start: 0, end: 20, repeat: 10_000 ] ] | [ : ] // 1_000_000_000 | [ range: [ start: -10, end: 10, repeat: 10_000 ] ] | [ : ] // 1_000_000_000 | [ range: [ start: -10, end: 10, repeat: 9_999 ] ] | [ : ] // 0 | [ range: [ start: 50_000, end: -49_999 ] ] | [ : ] // 1 | [ range: [ start: 50_000, end: -49_999 ] ] | [ 0: 0 ] // 5 | [ range: [ start: 50_000, end: -49_999 ] ] | [ 0: 0, 100: 1, 200: 2, 30_000: 3, 70_000: 4 ] // 21 | [ range: [ start: 50_000, end: -49_999 ] ] | [ 0: 0, 100: 0, 200: 0, 30_000: 0, 70_000: 0, 90_000: 0 ] // // result | sequence | indexReplacements // 10 | [ ] | [ indexReplacements: [ 1: 0, 3: 0 ], valueExclusions: [ 1, 2 ] ] // // // result | sequence | indexReplacements // // 10 | [ sequence: [ 1, 2, 3, 4, 5, ], indexReplacements: [ 1: 0, 3: 0 ], valueExclusions: [ 1, 2 ] ] // [ range: [ start: 1, end: 5 ], indexReplacements: [ 1: 0, 3: 0 ], valueExclusions: [ 1, 2 ] ] // } } This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -1 +1,234 @@ # Sequence Tweaking Spock Annotation Extension Over the last few weeks I've been practicing algorithms on Codility and Leetcode.. I'm pretty new to this sort of thing. I've also been catching up on Java (haven't used it on the job Java 5 came out), so I've been fiddling practicing Generics, Annotations, Lambdas, etc. I was looking for a way to quickly create test cases for the challenges, which are almost always dealing with `int` and/or `int[]` arguments and results. I found Spock (and it's awesome), and right away I started using some fixture methods to help with the generation of `int` sequences for input to the algorithms I was practicing. I paired up fixture methods and special columns for generating a range of int values, injecting arbitrary values anywhere within, and for excluding arbitrary values. For example, Anyway.. while using Spock for my coding challenges, I wanted to be able to very quickly Spock's use of Groovy data tables are very expressive and flexible, but it can be a little tedious to add new facets to the generation of test data if I have to add a column to the table (requiring values for every row). For example, say I'm using an `indexReplacements` parameter (column) to replace values `int[]` of int values. To add a new column. #### Without the extension: ```groovy import spock.lang.* class MySpecification extends Specification { ArrayList range( int from, int to ) { // <====== HELPER CREATES RANGES return IntStream.rangeClosed( from, to ) .boxed().collect( Collectors.toList() ) } @Unroll def "result given input sequence #sequence with #indexReplacements is the expected #result"() { given: def solution = new AlgorithmSolution() expect: if ( !!indexReplacements ) { // <==== PERFORM REPLACEMENTS indexReplacements.each { position, value -> sequence[ position ] = value } } solution.solution( sequence ) == result where: result | sequence | indexReplacements 5 | [ 1, 2, 3 ] | [ : ] // 1, 2, 3 5 | [ 1 ] * 10 | [ 1: 2, 8: 2 ] // 1, 2, 1, 1, 1, 1, 1, 1, 2, 1 5 | [ 4, 5, 6 ] * 2 | [ 3: 100 ] // 4, 5, 6, 100, 5, 6 5 | range(1, 3) * 3 | [ 4: 100 ] // 1, 2, 3, 1, 100, 3, 1, 2, 3 42 | [ 1 ] * 10_000 | [ 0: 0, 9_999: 0 ] // 0, 1, 1, ... 1, 1, 0 ``` Now say I want to add a bit.. I want to be able to exclude arbitrary values from a sequence. This does the job: ```groovy // (previous code omitted) expect: if ( !!valueExclusions ) sequence.removeAll( valueExclusions ) solution.solution( sequence ) == result where: // (new column) result | sequence | indexReplacements | valueExclusions 5 | [ 1, 2, 3 ] | [ : ] | [ : ] 5 | [ 1 ] * 10 | [ 1: 2, 8: 2 ] | [ : ] 5 | [ 4, 5, 6 ] * 2 | [ 3: 100 ] | [ : ] // values (i.e. empty maps) required 5 | range(1, 3) * 3 | [ 4: 100 ] | [ : ] 42 | [ 1 ] * 10_000 | [ 0: 0, 9_999: 0 ]| [ : ] 42 | range(1, 10_000)| [ : ] | [ 5_000, 5_001 ] // <=== new test case ``` That's a lot to go through to add I could It's pretty easy to improve things by extracting helpers like `range()` into a subclass of Spock's Specification. So I decided to write a Spock extension to replace the helper fixtures I had. Got to play with Groovy and practice custom Annotations. Aside from a bit of build and deploy scripting I did about ten years ago, I hadn't done much with Groovy.. but I'm loving it.. a lot of Groovy's features (e.g. Closures) seem pretty familiar from my recent years in ES6+ and Node.js.. it's like a perfect marriage of Java with the dynamic freedom I've grown accustomed to from using JS. Here's what I wound up with. The extension is applied simply by using the `@TweakSequence` Annotation. It accepts one argument, for specifying which data parameter (column if using a Spock data table) contains the sequence you want to fiddle with. For convenience, the sequence, for which ArrayList is its natural form in Groovy, is converted to an int[] #### With the Extension ```groovy import spock.lang.* class MySpecification extends Specification { @Shared solution = new AlgorithmSolution() @Unroll @TweakSequence def "#iterationCount: result is the expected #result"() { given: solution = new IdenticalPairs() expect: solution.solution( sequence ) == result where: result | sequence | indexReplacements 10 | [ 1 ] * 5 | [ : ] 9 | [ 1, 2, 3 ] * 3 | [ : ] 18 | [ 1, 2, 3 ] * 4 | [ : ] 15 | [ 1, 2, 3, 4, 5 ] * 3 | [ : ] 50 | [ 1, 2, 3, 4, 5 ] * 5 | [ : ] 49995000 | [ 1 ] * 10_000 | [ : ] 1_000_000_000 | [ range: [ start: 0, end: 20, repeat: 10_000 ] ] | [ : ] 1_000_000_000 | [ range: [ start: -10, end: 10, repeat: 10_000 ] ] | [ : ] 1_000_000_000 | [ range: [ start: -10, end: 10, repeat: 9_999 ] ] | [ : ] 0 | [ range: [ start: 50_000, end: -49_999 ] ] | [ : ] 1 | [ range: [ start: 50_000, end: -49_999 ] ] | [ 0: 0 ] 5 | [ range: [ start: 50_000, end: -49_999 ] ] | [ 0: 0, 100: 1, 200: 2, 30_000: 3, 70_000: 4 ] 21 | [ range: [ start: 50_000, end: -49_999 ] ] | [ 0: 0, 100: 0, 200: 0, 30_000: 0, 70_000: 0, 90_000: 0 ] } } ``` Lots of learning.. Java.. and algorithms. all pretty easy by subclassing Spock's Specification class, but I thought it would be fun (and convenient) to get it done with a simple annotation. New to annotations New to groovy, but Javascript concepts helped a lot. New to Spock Needing to generate very large input arrays, of sizes up to 100,000 and values from +/- 1 billion. As I worked through the problems, I wanted to be able to quickly create test cases.. edge cases and very large inputs so I could check out their correctness and how they perform I'd done some basic Groovy build scripting about ten years ago To use, simply use the annotation on your feature method: ```groovy ``` ## Available tweaks ### range ### indexReplacements expected: map ### indexExclusions ### valueExclusions expected: List ### valueReplacements ## Usage There are several ways.. So you shouldn't have to rewrite your data table to add columns etc. just to use a tweak once or twice. ```groovy result | sequence | tweaks 7 | [ 5 ] * 7 | [ indexReplacements: [ 1: 2, 2: 2 ] ] 6 | [ 1, 3, 4 ] * 2 | [ indexReplacements: [ 3: 100 ] ] 3 | [ ].range( 1, 10 ) | [ indexReplacements: [ 5: 1 ] ] 3 | [ range: [ start: 1, end: 10 ] ] | [ indexReplacements: [ 5: 1 ], valueExclusions: [ 5, 6 ] ] 3 | [ range: [ start: 1, end: 10 ] ] | [ indexReplacements: [ 5: 1 ], valueExclusions: [ 5, 6 ] ] 20_002 | [ 10001 ] * 5 | [ : ] 10 | [ 5, 6, 5, 6 ] * 8 | [ : ] 2 | [ 1 ] * 5 | [ : ] 2 | [ 1 ] * 5 | [ : ] 2 | [ 1 ] * 10 | [ : ] 4 | [ 2 ] * 100_000 | [ indexReplacements: [ 0: 1, 100_000: 1 ] ] 2 | [ 2 ] * 100_000 | [ indexReplacements: [ 1: 1, 99_998: 1 ] ] 3 | [ range: [ start: 1, end: 100 ] ] | [ indexReplacements: [ 98: 1 ] ] 6 | [ range: [ start: 10, end: 1 ] ] | [ : ] 6 | [ range: [ start: 100, end: 1 ] ] | [ : ] 6 | [ range: [ start: 1, end: 100 ] ] | [ indexReplacements: [ 2: 1 ] ] 20 | [ range: [ start: 10, end: 100, repeat: 100 ] ] | [ indexReplacements: [ 2: 100 ] ] 3 | [ range: [ start: 1, end: 100 ] ] | [ indexReplacements: [ 3: 1 ] ] 6 | [ range: [ start: 1, end: 100_000 ] ] | [ valueExclusions: [ 5, 6 ] ] 2 | [ range: [ start: 1, end: 100_000 ] ] | [ indexReplacements: [ 1: 1, 99_998: 1 ] ] 3 | [ range: [ start: 1, end: 100_000 ] ] | [ indexReplacements: [ 99_998: 1 ] ] 12 | [ range: [ start: 100_000, end: 10 ] ] | [ indexReplacements: [ 5555: 1 ] ] result | sequence | indexReplacements 10 | [ 1 ] * 5 | [ : ] 9 | [ 1, 2, 3 ] * 3 | [ : ] 18 | [ 1, 2, 3 ] * 4 | [ : ] result | [ 1, 2, 3, 4, 5 ] | [ 1: 0, 3: 0 ] result | sequence | indexReplacements 10 | [ 1 ] * 5 | [ : ] 9 | [ 1, 2, 3 ] * 3 | [ : ] 18 | [ 1, 2, 3 ] * 4 | [ : ] 15 | [ 1, 2, 3, 4, 5 ] * 3 | [ : ] 50 | [ 1, 2, 3, 4, 5 ] * 5 | [ : ] 49995000 | [ 1 ] * 10_000 | [ : ] 1_000_000_000 | [ range: [ start: 0, end: 20, repeat: 10_000 ] ] | [ : ] 1_000_000_000 | [ range: [ start: -10, end: 10, repeat: 10_000 ] ] | [ : ] 1_000_000_000 | [ range: [ start: -10, end: 10, repeat: 9_999 ] ] | [ : ] 0 | [ range: [ start: 50_000, end: -49_999 ] ] | [ : ] 1 | [ range: [ start: 50_000, end: -49_999 ] ] | [ 0: 0 ] 5 | [ range: [ start: 50_000, end: -49_999 ] ] | [ 0: 0, 100: 1, 200: 2, 30_000: 3, 70_000: 4 ] 21 | [ range: [ start: 50_000, end: -49_999 ] ] | [ 0: 0, 100: 0, 200: 0, 30_000: 0, 70_000: 0, 90_000: 0 ] result | sequence | indexReplacements 10 | [ ] | [ indexReplacements: [ 1: 0, 3: 0 ], valueExclusions: [ 1, 2 ] ] result | sequence | indexReplacements 10 | [ sequence: [ 1, 2, 3, 4, 5, ], indexReplacements: [ 1: 0, 3: 0 ], valueExclusions: [ 1, 2 ] ] [ range: [ start: 1, end: 5 ], indexReplacements: [ 1: 0, 3: 0 ], valueExclusions: [ 1, 2 ] ] ``` This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -130,7 +130,9 @@ class SequenceTweakingIterationExtension extends AbstractAnnotationDrivenExtensi // TODO now add a new #sequenceTweakDescription parameter, per the bottomof // http://spockframework.org/spock/docs/1.1-rc-4/extensions.html#_injecting_method_parameters // if ( newSequence.get( 0 ).class ) invocation.arguments[ parameterIndexToFiddleWith ] = newSequence.toArray() as int[] invocation.proceed() } }
NewerOlder