7

I am trying to use multiple collectEntries in series in my Groovy script. Its better to see the code, right now I've got:

stage('Test') {
            // Reading content of the file
            def portsFileContent = readFile 'UsedPorts.txt'

            // Split the file by next line
            def ports = portsFileContent.split('\n')

            def steps = ports.collectEntries { port ->
                ["UI Test on port $port", {
                    sh "#!/bin/bash -lx \n startServerWithDifferentPort --params=port=$port"
                }]
            }
            parallel steps
        }

In the file "UsedPorts.txt" are different ports seperate by line break like:

4723
4733
4743

So this numbers getting stored in the variable ports and this variable is then used to start an instance of an server for each port. So in this case it would then start 3 different serverinstance by this command:

def steps = ports.collectEntries { port ->
                ["UI Test on port $port", {
                    sh "#!/bin/bash -lx \n startServerWithDifferentPort --params=port=$port"
                }]
            }
            parallel steps

Because of parallel steps its starting 3 instance of the server simultaneously with different ports.

Thats working fine, but I have another file and need to do the same again. So in my second file there are entries like:

name1
name2
name3

I created again a variable where I store my 3 entries:

def anotherFile = readFile 'Names.txt'
def names = anotherFile.split('\n')

This is what I tried:

stage('Test') {
            // Reading content of the file
            def portsFileContent = readFile 'UsedPorts.txt'

            // Split the file by next line
            def ports = portsFileContent.split('\n')

            // Do the same again
            def anotherFile = readFile 'Names.txt'
            def names = anotherFile.split('\n')

            def steps = ports.collectEntries, names.collectEntries { port, name ->
                ["UI Test on $name", {
                    sh "#!/bin/bash -lx \n someMoreShellStuff --params=port=$port"
                }]
            }
            parallel steps
        }

But I can not seperate my second collectEntries by comma, because it gives me a syntax error. And now my problem is, how I can use this variable in the same command. Is it even possible?

Thanks

Update #1

After using the answer of Szymon Stepniak my new code looks like this:

stage('Test') {
            // Reading content of the file
            def portsFileContent = readFile 'AppiumUsedPorts.txt'

            // Split the file by next line
            def ports = portsFileContent.split('\n')

            // Getting device IDs to get properties of device
            def deviceIDFileContent = readFile 'DeviceIDs.txt'
            def deviceIDs = deviceIDFileContent.split('\n')

            // Define port and id as an pair
            def pairs = (0..Math.min(ports.size(), deviceIDs.size())).collect { i -> [id: deviceIDs[i], port: ports[i]] }

            def steps = pairs.collectEntries { pair ->
                ["UI Test on ${pair.id}", {
                    sh "echo 'Running test with port ${pair.port}'"
                }]
            }
            parallel steps
        }

This is causing the error java.lang.ArrayIndexOutOfBoundsException

Update #2

Content of AppiumUsedPorts.txt:

4723
4733

Content of DeviceIDs.txt

5353352c
G000KU0663550R92
rm -rf
  • 968
  • 1
  • 12
  • 28
  • What do you expect from `ports.collectEntries, names.collectEntries { port, name -> ...` ? This syntax is not correct. – Szymon Stepniak Jan 12 '18 at 09:38
  • I know that the Syntax is incorrect, thats just what I try to achive. I want to use the values of ports and of names in the same shell script – rm -rf Jan 12 '18 at 09:39
  • But how you want to use it? You haven't explain that. – Szymon Stepniak Jan 12 '18 at 09:47
  • In the first codeblock, everything was working as expected, but I need a second `collectEntries` and I want to use it with the same variable `steps` because I can execute `steps` only once. As you can see in the second codeblock I was trying to seperate both `collectEntries` by comma, but as you mentioned, it gives me a syntax error. I need your help now how I can use the values of `ports` and `names` in the variable `steps`? – rm -rf Jan 12 '18 at 09:59
  • the way you wrote it is a nonsense in any JVM programming language including java itself. Hence the question again: what do you want to achieve? – injecteer Jan 12 '18 at 10:12
  • I edited my answer, I hope its more clear now what I am trying to achieve – rm -rf Jan 12 '18 at 10:29

1 Answers1

7

Looks like you want to zip elements from two lists - ports and names and use these pairs in creating steps for parallel execution. So assuming that ports and names contain something like:

def ports = [8080, 8081, 8082, 8083]
def names = ['Host A', 'Host B', 'Host C', 'Host D', 'Host E']

you need a list of pairs like:

def pairs = [[port: 8080, name: 'Host A'], [port: 8081, name: 'Host B'], [port: 8082, name: 'Host C'], [port:8083, 'Host D']]

I used two different size lists on purpose, to explain that result of zipping two lists is always the same size then the shortest list.

Groovy has a method GroovyCollections.transpose(List lists) that takes a list of lists (e.g. [[8080, 8081, 8082, 8083], ['Host A', 'Host B', 'Host C', 'Host D', 'Host E']]) and "zips" two lists together like:

[[8080, 'Host A'], [8081, 'Host B'], [8082, 'Host C'], [8083, 'Host D']]

but it wont work in Jenkins Pipeline - if you try to use it you will get:

org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: Scripts not permitted to use staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods transpose java.util.List

Anyway you can simply do pretty the same using collect on range from 0 to min(ports.size(), names.size()) to create a list those pairs/maps. Take a look at following example:

node {
    stage('Test') {
        def ports = [8080, 8081, 8082, 8083]
        def names = ['Host A', 'Host B', 'Host C', 'Host D', 'Host E']

        def pairs = (0..<Math.min(ports.size(), names.size())).collect { i -> [name: names[i], port: ports[i]] }

        def steps = pairs.collectEntries { pair ->
            ["UI Test on ${pair.name}", {
                sh "echo 'Running test with port ${pair.port}'"
            }]
        }

        parallel steps
    }
}

In this example we transpose two lists into a list of maps like [port: ..., name: ...] and we call collectEntries on that list of maps to get both - port and name in the same execution step. Running this script in Jenkins Pipeline produces following output:

[Pipeline] {
[Pipeline] stage
[Pipeline] { (Test)
[Pipeline] parallel
[Pipeline] [UI Test on Host A] { (Branch: UI Test on Host A)
[Pipeline] [UI Test on Host B] { (Branch: UI Test on Host B)
[Pipeline] [UI Test on Host C] { (Branch: UI Test on Host C)
[Pipeline] [UI Test on Host D] { (Branch: UI Test on Host D)
[Pipeline] [UI Test on Host A] sh
[UI Test on Host A] [test-pipeline] Running shell script
[Pipeline] [UI Test on Host B] sh
[UI Test on Host B] [test-pipeline] Running shell script
[Pipeline] [UI Test on Host C] sh
[UI Test on Host C] [test-pipeline] Running shell script
[Pipeline] [UI Test on Host D] sh
[UI Test on Host A] + echo Running test with port 8080
[UI Test on Host A] Running test with port 8080
[UI Test on Host B] + echo Running test with port 8081
[UI Test on Host B] Running test with port 8081
[UI Test on Host D] [test-pipeline] Running shell script
[Pipeline] [UI Test on Host A] }
[UI Test on Host C] + echo Running test with port 8082
[UI Test on Host C] Running test with port 8082
[UI Test on Host D] + echo Running test with port 8083
[UI Test on Host D] Running test with port 8083
[Pipeline] [UI Test on Host B] }
[Pipeline] [UI Test on Host C] }
[Pipeline] [UI Test on Host D] }
[Pipeline] // parallel
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS

Hope it helps.

Szymon Stepniak
  • 40,216
  • 10
  • 104
  • 131
  • Thats exactly what I needed, but when its trying to read the second file I always get `java.lang.ArrayIndexOutOfBoundsException` – rm -rf Jan 12 '18 at 11:53
  • It throws this exception while reading a file? Double check what causes this exception. `Math.min(names.size(), ports.size())` is used to prevent from such exception when transposing two lists of different sizes. It creates a list of pairs of size of shorter list in this case. – Szymon Stepniak Jan 12 '18 at 12:03
  • In my output I see that reading the first file was successfull, but while trying to read value of the second it gives me the said error. My both lists have always the same length, if it somehow helps? – rm -rf Jan 12 '18 at 12:08
  • But can you `println names` or the exception is thrown at that point? Check which line throws this exception, otherwise it's hard to suggest a solution – Szymon Stepniak Jan 12 '18 at 12:13
  • leads to the same error, here is an image of the error: https://picload.org/view/ddllwdgi/error.png.html but console output doesnt provide more information: [Pipeline] [install-apk] } [Pipeline] // parallel [Pipeline] } [Pipeline] // stage [Pipeline] stage [Pipeline] { (Test) [Pipeline] readFile [Pipeline] readFile [Pipeline] } [Pipeline] // stage [Pipeline] echo java.lang.ArrayIndexOutOfBoundsException: 2 [Pipeline] stage [Pipeline] { (Clean) [Pipeline] archiveArtifacts – rm -rf Jan 12 '18 at 12:27
  • Yes, if you want us help you finding the cause. I can't deduct from the console output what's going on. – Szymon Stepniak Jan 12 '18 at 12:37
  • Can you also post `AppiumUsedPorts.txt` and `DeviceIDs.txt` contents? Testing your updated script with fixed `ports` and `deviceIDs` lists does not throw any exception, so I assume there is something going on with text files you are trying to read. – Szymon Stepniak Jan 12 '18 at 12:58
  • sure, I edit my question with **Update #2**. Thanks for your help – rm -rf Jan 12 '18 at 13:07
  • Ok, got it. Range used here `def pairs = (0..Math.min(ports.size(), deviceIDs.size()))` has to be excluding - `def pairs = (0.. – Szymon Stepniak Jan 12 '18 at 13:18