add nextflow d30e48d

This commit is contained in:
2026-04-29 23:01:54 +02:00
parent d0b12d668d
commit 97cc9058d3
2840 changed files with 730250 additions and 0 deletions

View File

@@ -0,0 +1,179 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.seqera.wave.plugin
import spock.lang.Specification
import spock.lang.Unroll
/**
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
class ContainerConfigTest extends Specification {
@Unroll
def 'should merge env' () {
given:
def config = new ContainerConfig()
expect:
config.mergeEnv(ENV1, ENV2) == EXPECTED
where:
ENV1 | ENV2 | EXPECTED
null | null | null
[] | null | []
['foo=1'] | [] | ['foo=1']
['foo=1'] | ['bar=2'] | ['foo=1','bar=2']
['foo=1'] | ['foo=2'] | ['foo=2'] // <-- env2 overrides env1
['foo=1','baz=3'] | ['foo=2'] | ['foo=2','baz=3'] // <-- overrides 'foo' in env1 and keep as first entry
['foo=1'] | ['baz=3','foo=2'] | ['foo=2','baz=3'] // <-- overrides 'foo' in env1 and keep as first entry
}
def 'should override entry' () {
expect:
new ContainerConfig(entrypoint: LEFT) + new ContainerConfig(entrypoint: RIGHT)
== new ContainerConfig(entrypoint: EXPECTED)
where:
LEFT | RIGHT | EXPECTED
null | null | null
['entry1.sh'] | null | ['entry1.sh']
null | ['entry2.sh'] | ['entry2.sh']
['entry1.sh'] | ['entry2.sh'] | ['entry2.sh']
}
def 'should override cmd' () {
expect:
new ContainerConfig(cmd: LEFT) + new ContainerConfig(cmd: RIGHT)
== new ContainerConfig(cmd: EXPECTED)
where:
LEFT | RIGHT | EXPECTED
null | null | null
['cmd1.sh'] | null | ['cmd1.sh']
null | ['cmd2.sh'] | ['cmd2.sh']
['cmd1.sh'] | ['cmd2.sh'] | ['cmd2.sh']
}
def 'should override workdir' () {
expect:
new ContainerConfig(workingDir: LEFT) + new ContainerConfig(workingDir: RIGHT)
== new ContainerConfig(workingDir: EXPECTED)
where:
LEFT | RIGHT | EXPECTED
null | null | null
'/foo' | null | '/foo'
null | '/bar' | '/bar'
'/foo' | '/bar' | '/bar'
}
def 'should merge env config' () {
expect:
new ContainerConfig(env:LEFT) + new ContainerConfig(env: RIGHT) == new ContainerConfig(env: EXPECTED)
where:
LEFT | RIGHT | EXPECTED
null | null | null
['alpha=1'] | null | ['alpha=1']
null | ['beta=2'] | ['beta=2']
['alpha=1','delta=x'] | ['beta=2','delta=z'] | ['alpha=1','delta=z','beta=2']
}
@Unroll
def 'should merge layers' () {
expect:
new ContainerConfig(layers:LEFT) + new ContainerConfig(layers: RIGHT) == new ContainerConfig(layers: EXPECTED)
where:
LEFT | RIGHT | EXPECTED
null | null | null
[] | [] | []
[new ContainerLayer(location: 'http://x')] | null | [new ContainerLayer(location: 'http://x')]
and:
[new ContainerLayer(location: 'http://x')] | [new ContainerLayer(location: 'http://y'),new ContainerLayer(location: 'http://y')] | [new ContainerLayer(location: 'http://x'), new ContainerLayer(location: 'http://y'),new ContainerLayer(location: 'http://y')]
}
def 'should compute config fingerprint' () {
given:
def config1 = new ContainerConfig(entrypoint: ['entry.sh'], cmd: ['the.cmd'], env: ['x=2'], workingDir: '/foo')
def config2 = new ContainerConfig(entrypoint: ['entry.sh'], cmd: ['the.cmd'], env: ['x=2'], workingDir: '/foo')
def config3 = new ContainerConfig(entrypoint: ['entry.sh'], cmd: ['the.cmd'], env: ['x=2'], workingDir: '/bar')
and:
def layer1 = new ContainerLayer('http://this/that', 'abd', 100, 'efg')
def layer2 = new ContainerLayer('http://this/that', 'abd', 100, 'efg')
def layer3 = new ContainerLayer('http://xxx/yyy', 'abd', 200, 'efg')
and:
def fusion1 = new ContainerLayer('abc', 'xyz', 300, 'efg', true)
def fusion2 = new ContainerLayer('efg', 'pqr', 400, 'efg', true)
expect:
config1.fingerprint() == config2.fingerprint()
config1.fingerprint() != config3.fingerprint()
when:
config1.appendLayer(layer1)
config2.appendLayer(layer2)
then:
// layers have the same fingerprint
layer1.fingerprint() == layer2.fingerprint()
// configs have the same fingerprint
config1.fingerprint() == config2.fingerprint()
when:
config1.layers.clear()
config2.layers.clear()
and:
config1.appendLayer(layer1)
config2.appendLayer(layer3)
then:
// different layer fingerprint cause the config to have different fingerprints
layer1.fingerprint() != layer3.fingerprint()
config1.fingerprint() != config2.fingerprint()
when:
config1.layers.clear()
config2.layers.clear()
and:
config1.appendLayer(fusion1)
config2.appendLayer(fusion2)
then:
// the layer have different fingerprint BUT the `skipHashing`
// makes the config to ignore it
fusion1.fingerprint() != fusion2.fingerprint()
config1.fingerprint() == config2.fingerprint()
}
def 'should validate empty' () {
expect:
new ContainerConfig().empty()
new ContainerConfig([], null, null, null, null).empty()
new ContainerConfig(null, [], null, null, null).empty()
new ContainerConfig(null, null, [], null, null).empty()
new ContainerConfig(null, null, null, '', null).empty()
new ContainerConfig(null, null, null, null, []).empty()
and:
!new ContainerConfig(['x'], null, null, null, null).empty()
!new ContainerConfig(null, ['x'], null, null, null).empty()
!new ContainerConfig(null, null, ['x'], null, null).empty()
!new ContainerConfig(null, null, null, 'x', null).empty()
!new ContainerConfig(null, null, null, null, [new ContainerLayer()]).empty()
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.seqera.wave.plugin
import spock.lang.Specification
/**
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
class ContainerLayerTest extends Specification {
def 'should convert to string' () {
when:
def l1 = new ContainerLayer( 'data:ABC1234567890', 'sha256:12345', 100, 'sha256:67890' )
then:
l1.toString() == 'ContainerLayer[location=data:ABC1234567890; tarDigest=sha256:67890; gzipDigest=sha256:12345; gzipSize=100]'
when:
def l2 = new ContainerLayer( 'data:12345678901234567890', 'sha256:12345', 100, 'sha256:67890' )
then:
l2.toString() == 'ContainerLayer[location=data:12345678901234567890; tarDigest=sha256:67890; gzipDigest=sha256:12345; gzipSize=100]'
when:
def l3= new ContainerLayer( 'data:12345678901234567890x', 'sha256:12345', 100, 'sha256:67890' )
then:
l3.toString() == 'ContainerLayer[location=data:12345678901234567890...; tarDigest=sha256:67890; gzipDigest=sha256:12345; gzipSize=100]'
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.seqera.wave.plugin
import nextflow.script.bundle.ResourcesBundle
import nextflow.util.CacheHelper
import spock.lang.Specification
/**
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
class WaveAssetsTest extends Specification {
def 'should compute hash key' () {
given:
def IMAGE = 'foo:latest'
def BUNDLE = Mock(ResourcesBundle) { fingerprint() >> '12345' }
expect:
new WaveAssets(IMAGE).fingerprint() == CacheHelper.hasher([IMAGE]).hash().toString()
}
def 'should validate container name' () {
when:
WaveAssets.validateContainerName('ubuntu')
then:
noExceptionThrown()
when:
WaveAssets.validateContainerName('ubuntu:latest')
then:
noExceptionThrown()
when:
WaveAssets.validateContainerName('quay.io/wtsicgp/nanoseq:3.3.0')
then:
noExceptionThrown()
when:
WaveAssets.validateContainerName('docker://quay.io/wtsicgp/nanoseq:3.3.0')
then:
thrown(IllegalArgumentException)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,124 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.seqera.wave.plugin
import nextflow.Session
import nextflow.SysEnv
import nextflow.exception.AbortOperationException
import spock.lang.Specification
import spock.lang.Unroll
/**
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
class WaveFactoryTest extends Specification {
@Unroll
def 'should not change config' () {
given:
def session = Mock(Session) { getConfig() >> CONFIG }
def factory = new WaveFactory()
when:
factory.create(session)
then:
CONFIG == EXPECTED
and:
DISABLED * session.setDisableRemoteBinDir(true) >> null
where:
CONFIG | EXPECTED | DISABLED
[:] | [:] | 0
[wave:[enabled:true]] | [wave:[enabled:true]] | 0
[wave:[enabled:true], fusion:[enabled:true]] | [wave:[enabled:true,bundleProjectResources:true], fusion:[enabled:true]] | 1
}
@Unroll
def 'should get wave enable status' () {
given:
SysEnv.push(ENV)
def session = Mock(Session) { getConfig() >> CONFIG }
when:
def result = WaveFactory.shouldEnable(session)
then:
result == ENABLED
CONFIG == EXPECTED
cleanup:
SysEnv.pop()
where:
ENABLED | ENV | CONFIG | EXPECTED
false | [:] | [:] | [:]
true | [:] | [wave:[enabled:true]] | [wave:[enabled:true]]
true | [:] | [wave:[enabled:true], fusion:[enabled:true]] | [wave:[enabled:true,bundleProjectResources:true], fusion:[enabled:true]]
false | [NXF_DISABLE_WAVE_SERVICE: 'true']| [wave:[enabled:true], fusion:[enabled:true]] | [wave:[enabled:false], fusion:[enabled:true]]
}
@Unroll
def 'should check s5cmd is enabled' () {
given:
def factory = new WaveFactory()
expect:
factory.isAwsBatchFargateMode(CONFIG) == EXPECTED
where:
CONFIG | EXPECTED
[:] | false
[aws:[batch:[platformType:'foo']]] | false
[aws:[batch:[platformType:'fargate']]] | true
[aws:[batch:[platformType:'Fargate']]] | true
}
def 'should fail when wave is disabled' () {
given:
def CONFIG = [wave:[:], fusion:[enabled:true]]
def session = Mock(Session) { getConfig() >> CONFIG }
def factory = new WaveFactory()
when:
factory.create(session)
then:
def e = thrown(AbortOperationException)
e.message == 'Fusion feature requires enabling Wave service'
}
def 'should not fail when wave is disabled' () {
given:
SysEnv.push(NXF_DISABLE_WAVE_SERVICE: 'true')
def CONFIG = [wave:[:], fusion:[enabled:true]]
def session = Mock(Session) { getConfig() >> CONFIG }
def factory = new WaveFactory()
when:
factory.create(session)
then:
noExceptionThrown()
and:
0 * session.setDisableRemoteBinDir(true) >> null
and:
CONFIG == [wave:[:], fusion:[enabled:true]]
cleanup:
SysEnv.pop()
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.seqera.wave.plugin.cli
import java.nio.file.Files
import java.nio.file.Path
import groovy.json.JsonSlurper
import io.seqera.wave.plugin.packer.TarHelper
import nextflow.extension.FilesEx
import nextflow.file.FileHelper
import org.junit.Rule
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.TempDir
import spock.lang.Unroll
import test.OutputCapture
/**
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
class WaveCmdEntryTest extends Specification implements TarHelper {
@Shared
@TempDir
Path folder
@Rule
OutputCapture capture = new OutputCapture()
def 'should create create pack' () {
given:
def rootPath = folder.resolve('bundle'); rootPath.mkdir()
def untarPath = folder.resolve('untar'); untarPath.mkdir()
rootPath.resolve('main.nf').text = "I'm the main file"
rootPath.resolve('this/that').mkdirs()
Files.write(rootPath.resolve('this/hola.txt'), "Hola".bytes)
Files.write(rootPath.resolve('this/hello.txt'), "Hello".bytes)
Files.write(rootPath.resolve('this/that/ciao.txt'), "Ciao".bytes)
and:
FileHelper.visitFiles([type:'any'], rootPath, '**', {
final mode = it.isDirectory() ? 0700 : 0600
FilesEx.setPermissionsMode(it, mode)
})
and:
def cmd = new WaveCmdEntry()
when:
def result = cmd.packContainer( [rootPath.toString()] )
then:
def gzipFile = folder.resolve('bundle.tar.gz')
gzipFile.exists()
and:
def json = new JsonSlurper().parseText(result)
and:
json.layers[0].gzipSize == Files.size(gzipFile)
json.layers[0].location == gzipFile.toUri().toString()
json.layers[0].tarDigest == 'sha256:f556b94e9b6f5f72b86e44833614b465df9f65cb4210e3f4416292dca1618360'
json.layers[0].gzipDigest == 'sha256:e58685a82452a11faa926843e7861c94bdb93e2c8f098b5c5354ec9b6fee2b68'
when:
def tar = uncompress(Files.readAllBytes(gzipFile))
untar( new ByteArrayInputStream(tar), untarPath )
then:
untarPath.resolve('main.nf').text == rootPath.resolve('main.nf').text
untarPath.resolve('this/hola.txt').text == rootPath.resolve('this/hola.txt').text
untarPath.resolve('this/hello.txt').text == rootPath.resolve('this/hello.txt').text
untarPath.resolve('this/that/ciao.txt').text == rootPath.resolve('this/that/ciao.txt').text
}
@Unroll
def 'should find base name' () {
expect:
WaveCmdEntry.baseName(PATH) == EXPECTED
where:
PATH | EXPECTED
null | null
'/some/name' | 'name'
'http://some/name.tar' | 'name'
'/some/name.tar.gz' | 'name'
'/some/name.tar.gzip' | 'name'
'http://some/name.tar.gz' | 'name'
'http://some/' | null
'http://some' | null
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.seqera.wave.plugin.cli
import spock.lang.Specification
import spock.lang.Unroll
/**
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
class WaveDebugCmdTest extends Specification {
@Unroll
def 'should check remote path' () {
expect:
WaveDebugCmd.isRemotePath(PATH) == EXPECTED
where:
PATH | EXPECTED
null | false
'foo' | false
'/some/file' | false
and:
's3://foo/bar' | true
'gs://foo/bar' | true
and:
'file:/foo/bar' | false
'file://foo/bar' | false
'file:///foo/bar' | false
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.seqera.wave.plugin.config
import java.time.Duration
import nextflow.util.RateUnit
import spock.lang.Specification
/**
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
class HttpOptsTest extends Specification {
def 'should get http options' () {
when:
def opts = new HttpOpts([:])
then:
opts.connectTimeout() == Duration.ofSeconds(30)
opts.maxRate() == RateUnit.of('1/sec')
when:
opts = new HttpOpts([connectTimeout:'50s', maxRate: '10/s'])
then:
opts.connectTimeout() == Duration.ofSeconds(50)
opts.maxRate() == RateUnit.of('10/s')
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.seqera.wave.plugin.config
import nextflow.util.Duration
import spock.lang.Specification
/**
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
class RetryOptsTest extends Specification {
def 'should create retry config' () {
expect:
new RetryOpts().delay == Duration.of('450ms')
new RetryOpts().maxDelay == Duration.of('90s')
new RetryOpts().maxAttempts == 5
new RetryOpts().jitter == 0.25d
new RetryOpts().multiplier == 2.0d
and:
new RetryOpts([maxAttempts: 20]).maxAttempts == 20
new RetryOpts([delay: '1s']).delay == Duration.of('1s')
new RetryOpts([maxDelay: '1m']).maxDelay == Duration.of('1m')
new RetryOpts([jitter: '0.5']).jitter == 0.5d
new RetryOpts([multiplier: '5.0']).multiplier == 5.0d
}
}

View File

@@ -0,0 +1,152 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.seqera.wave.plugin.config
import spock.lang.Specification
/**
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
class TowerConfigTest extends Specification {
def 'should get tower config' () {
given:
TowerConfig config
Map env
when:
config = new TowerConfig([:], [:])
then:
!config.getAccessToken()
!config.getWorkspaceId()
when:
env = [TOWER_ACCESS_TOKEN:'foo', TOWER_WORKSPACE_ID: '123']
config = new TowerConfig([:], env)
then:
config.accessToken == 'foo'
config.workspaceId == 123
config.workflowId == null
when:
env = [TOWER_ACCESS_TOKEN:'foo', TOWER_WORKSPACE_ID: '123']
config = new TowerConfig(accessToken: 'bar', workspaceId: '789', env)
then:
config.accessToken == 'bar'
config.workspaceId == 789
config.workflowId == null
when:
env = [TOWER_ACCESS_TOKEN:'foo', TOWER_WORKSPACE_ID: '123']
config = new TowerConfig(accessToken: null, workspaceId: '789', env)
then:
config.accessToken == null
config.workspaceId == 789
config.workflowId == null
// when TOWER_WORKFLOW_ID is defined env has priority
when:
env = [TOWER_ACCESS_TOKEN:'foo', TOWER_WORKSPACE_ID: '123', TOWER_WORKFLOW_ID: 'xyz']
config = new TowerConfig(accessToken: 'bar', workspaceId: '789', env)
then:
config.accessToken == 'foo'
config.workspaceId == 123
config.workflowId == 'xyz'
}
def 'should get refresh token' () {
given:
TowerConfig config
Map env
when:
config = new TowerConfig([:], [:])
then:
!config.refreshToken
when:
env = [TOWER_REFRESH_TOKEN:'foo', TOWER_WORKSPACE_ID: '123']
config = new TowerConfig([:], env)
then:
config.refreshToken == 'foo'
config.workspaceId == 123
when:
env = [TOWER_REFRESH_TOKEN:'foo', TOWER_WORKSPACE_ID: '123']
config = new TowerConfig(refreshToken: 'bar', workspaceId: '789', env)
then:
config.refreshToken == 'bar'
config.workspaceId == 789
when:
env = [TOWER_REFRESH_TOKEN:'foo', TOWER_WORKSPACE_ID: '123']
config = new TowerConfig(refreshToken: null, workspaceId: '789', env)
then:
config.refreshToken == null
config.workspaceId == 789
// when TOWER_WORKFLOW_ID is defined env has priority
when:
env = [TOWER_REFRESH_TOKEN:'foo', TOWER_WORKSPACE_ID: '123', TOWER_WORKFLOW_ID: 'xyz']
config = new TowerConfig(refreshToken: 'bar', workspaceId: '789', env)
then:
config.refreshToken == 'foo'
config.workspaceId == 123
}
def 'should config endpoint' () {
given:
TowerConfig config
Map env
when:
config = new TowerConfig([:], [:])
then:
config.endpoint == 'https://api.cloud.seqera.io'
when:
config = new TowerConfig([endpoint:'-'], [:])
then:
config.endpoint == 'https://api.cloud.seqera.io'
when:
config = new TowerConfig([endpoint:'http://foo.com'], [:])
then:
config.endpoint == 'http://foo.com'
when:
config = new TowerConfig([endpoint:'http://foo.com//'], [:])
then:
config.endpoint == 'http://foo.com'
when:
config = new TowerConfig([endpoint:'http://foo.com'], [TOWER_API_ENDPOINT:'http://bar.com'])
then:
config.endpoint == 'http://foo.com'
when:
config = new TowerConfig([endpoint:'-'], [TOWER_API_ENDPOINT:'http://bar.com/'])
then:
config.endpoint == 'http://bar.com'
when:
config = new TowerConfig([:], [TOWER_API_ENDPOINT:'http://bar.com/'])
then:
config.endpoint == 'http://bar.com'
}
}

View File

@@ -0,0 +1,298 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.seqera.wave.plugin.config
import io.seqera.wave.api.BuildCompression
import io.seqera.wave.api.ScanLevel
import io.seqera.wave.api.ScanMode
import nextflow.util.Duration
import spock.lang.Specification
import spock.lang.Unroll
/**
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
class WaveConfigTest extends Specification {
def 'should create empty opts' () {
when:
def opts = new WaveConfig([:])
then:
!opts.enabled()
opts.endpoint() == 'https://wave.seqera.io'
}
def 'should create from env' () {
given:
def ENV = [WAVE_API_ENDPOINT: 'http://foo']
when:
def opts = new WaveConfig([:], ENV)
then:
!opts.enabled()
opts.endpoint() == 'http://foo'
}
def 'should create config options' () {
given:
def ENV = [WAVE_API_ENDPOINT: 'http://foo']
when:
// config options have priority over sys env
def opts = new WaveConfig([enabled:true, endpoint: 'http://localhost'], ENV)
then:
opts.enabled()
opts.endpoint() == 'http://localhost'
}
def 'should remove ending slash' () {
when:
def opts = new WaveConfig([enabled:true, endpoint: 'http://localhost/v1//'])
then:
opts.enabled()
opts.endpoint() == 'http://localhost/v1'
}
@Unroll
def 'should add config urls' () {
when:
def opts = new WaveConfig(OPTS, ENV)
then:
opts.containerConfigUrl() == EXPECTED
where:
OPTS | ENV | EXPECTED
[:] | [:] | []
[containerConfigUrl: 'http://foo.com'] | [:] | [ new URL('http://foo.com')]
[containerConfigUrl: 'http://foo.com'] | [WAVE_CONTAINER_CONFIG_URL:'http://something.com'] | [ new URL('http://foo.com')]
[:] | [WAVE_CONTAINER_CONFIG_URL:'http://something.com'] | [ new URL('http://something.com')]
[containerConfigUrl: ['http://foo.com','https://bar.com']] | [:] | [ new URL('http://foo.com'), new URL('https://bar.com')]
[containerConfigUrl: ['http://foo.com','https://bar.com']] | [WAVE_CONTAINER_CONFIG_URL:'http://boo.com'] | [ new URL('http://foo.com'), new URL('https://bar.com')]
}
def 'should get conda config' () {
when:
def opts = new WaveConfig([:])
then:
opts.condaOpts().mambaImage == 'mambaorg/micromamba:1.5.10-noble'
opts.condaOpts().baseImage == 'ubuntu:24.04'
opts.condaOpts().commands == null
when:
opts = new WaveConfig([build:[conda:[mambaImage:'mambaorg/foo:1', commands:['USER hola']]]])
then:
opts.condaOpts().mambaImage == 'mambaorg/foo:1'
opts.condaOpts().commands == ['USER hola']
when:
opts = new WaveConfig([build:[conda:[baseImage:'debian:12', mambaImage:'mambaorg/micromamba:2-amazon2023']]])
then:
opts.condaOpts().baseImage == 'debian:12'
opts.condaOpts().mambaImage == 'mambaorg/micromamba:2-amazon2023'
}
def 'should get build and cache repos' () {
when:
def opts = new WaveConfig([:])
then:
opts.buildRepository() == null
opts.cacheRepository() == null
when:
opts = new WaveConfig([build:[repository:'some/repo', cacheRepository:'some/cache']])
then:
opts.buildRepository() == 'some/repo'
opts.cacheRepository() == 'some/cache'
}
def 'should get build template' () {
when:
def opts = new WaveConfig([:])
then:
opts.buildTemplate() == null
when:
opts = new WaveConfig([build:[template:'conda/pixi:v1']])
then:
opts.buildTemplate() == 'conda/pixi:v1'
}
@Unroll
def 'should set strategy' () {
when:
def opts = new WaveConfig([:])
then:
opts.strategy() == ['container','dockerfile','conda']
when:
opts = new WaveConfig([strategy:STRATEGY])
then:
opts.strategy() == EXPECTED
where:
STRATEGY | EXPECTED
null | ['container','dockerfile','conda']
'dockerfile' | ['dockerfile']
'conda,container' | ['conda','container']
'conda , container' | ['conda','container']
['conda','container'] | ['conda','container']
[' conda',' container'] | ['conda','container']
}
def 'should fail to set strategy' () {
when:
new WaveConfig([strategy:['foo']])
then:
def e = thrown(IllegalArgumentException)
e.message == "Invalid value for 'wave.strategy' configuration attribute - offending value: foo"
}
def 'should get retry policy' () {
when:
def opts = new WaveConfig([:])
then:
opts.retryOpts().delay == Duration.of('450ms')
opts.retryOpts().maxAttempts == 5
opts.retryOpts().maxDelay == Duration.of('90s')
when:
opts = new WaveConfig([retryPolicy:[ maxAttempts: 20, jitter: 1.0, delay: '1s', maxDelay: '10s' ]])
then:
opts.retryOpts().maxAttempts == 20
opts.retryOpts().jitter == 1.0d
opts.retryOpts().delay == Duration.of('1s')
opts.retryOpts().maxDelay == Duration.of('10s')
// legacy
when:
opts = new WaveConfig([retry:[ maxAttempts: 10, jitter: 2.0, delay: '3s', maxDelay: '40s' ]])
then:
opts.retryOpts().maxAttempts == 10
opts.retryOpts().jitter == 2.0d
opts.retryOpts().delay == Duration.of('3s')
opts.retryOpts().maxDelay == Duration.of('40s')
}
def 'should get http config options' () {
when:
def opts = new WaveConfig([:])
then:
opts.httpOpts().connectTimeout() == java.time.Duration.ofSeconds(30)
when:
opts = new WaveConfig([httpClient: [connectTimeout: '90s']])
then:
opts.httpOpts().connectTimeout() == java.time.Duration.ofSeconds(90)
}
def 'should dump config' () {
given:
def config = new WaveConfig([enabled: true])
expect:
config.toString() == 'WaveConfig(build:BuildOpts(repository:null, cacheRepository:null, template:null, conda:CondaOpts(mambaImage=mambaorg/micromamba:1.5.10-noble; basePackages=conda-forge::procps-ng, commands=null, baseImage=ubuntu:24.04), compression:null, maxDuration:40m), enabled:true, endpoint:https://wave.seqera.io, freeze:false, httpClient:HttpOpts(), mirror:false, retryPolicy:RetryOpts(delay:450ms, maxDelay:1m 30s, maxAttempts:5, jitter:0.25, multiplier:2.0, delayAsDuration:PT0.45S, maxDelayAsDuration:PT1M30S), scan:ScanOpts(allowedLevels:null, mode:null), strategy:[container, dockerfile, conda], bundleProjectResources:null, containerConfigUrl:[], preserveFileTimestamp:null, tokensCacheMaxDuration:30m)'
}
def 'should not allow invalid setting' () {
when:
new WaveConfig(endpoint: 'foo')
then:
def e = thrown(IllegalArgumentException)
e.message == "Endpoint URL should start with 'http:' or 'https:' protocol prefix - offending value: 'foo'"
when:
new WaveConfig(endpoint: 'ftp://foo.com')
then:
e = thrown(IllegalArgumentException)
e.message == "Endpoint URL should start with 'http:' or 'https:' protocol prefix - offending value: 'ftp://foo.com'"
when:
new WaveConfig(build: [repository: 'http://foo.com'])
then:
e = thrown(IllegalArgumentException)
e.message == "Config setting 'wave.build.repository' should not include any protocol prefix - offending value: 'http://foo.com'"
when:
new WaveConfig(build: [cacheRepository: 'http://foo.com'])
then:
e = thrown(IllegalArgumentException)
e.message == "Config setting 'wave.build.cacheRepository' should not include any protocol prefix - offending value: 'http://foo.com'"
}
def 'should set preserve timestamp' () {
when:
def config = new WaveConfig([:])
then:
!config.preserveFileTimestamp()
when:
config = new WaveConfig(preserveFileTimestamp: true)
then:
config.preserveFileTimestamp()
}
def 'should enabled mirror mode' () {
expect:
!new WaveConfig([:]).mirrorMode()
and:
new WaveConfig([mirror:true]).mirrorMode()
}
@Unroll
def 'should validate scan mode' () {
expect:
new WaveConfig(scan: [mode: MODE]).scanMode() == EXPECTED
where:
MODE | EXPECTED
null | null
'none' | ScanMode.none
'async' | ScanMode.async
'required' | ScanMode.required
}
@Unroll
def 'should validate scan levels' () {
expect:
new WaveConfig(scan: [allowedLevels: LEVEL]).scanAllowedLevels() == EXPECTED
where:
LEVEL | EXPECTED
null | null
'low' | List.of(ScanLevel.LOW)
'LOW' | List.of(ScanLevel.LOW)
'low,high' | List.of(ScanLevel.LOW,ScanLevel.HIGH)
'LOW, HIGH' | List.of(ScanLevel.LOW,ScanLevel.HIGH)
['medium','high'] | List.of(ScanLevel.MEDIUM,ScanLevel.HIGH)
}
def 'should validate build compression' () {
expect:
new WaveConfig(build: [compression: COMPRESSION]).buildCompression() == EXPECTED
where:
COMPRESSION | EXPECTED
null | null
[mode:'gzip'] | BuildCompression.gzip
[mode:'estargz'] | BuildCompression.estargz
and:
[mode:'gzip', level: 1] | new BuildCompression().withMode(BuildCompression.Mode.gzip).withLevel(1)
[mode:'estargz', level: 2]| new BuildCompression().withMode(BuildCompression.Mode.estargz).withLevel(2)
[mode:'zstd', level: 3] | new BuildCompression().withMode(BuildCompression.Mode.zstd).withLevel(3)
and:
[mode:'gzip', level: 1, force:true] | new BuildCompression().withMode(BuildCompression.Mode.gzip).withLevel(1).withForce(true)
[mode:'estargz', level: 2, force:true ] | new BuildCompression().withMode(BuildCompression.Mode.estargz).withLevel(2).withForce(true)
[mode:'zstd', level: 3,force:true] | new BuildCompression().withMode(BuildCompression.Mode.zstd).withLevel(3).withForce(true)
}
}

View File

@@ -0,0 +1,159 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.seqera.wave.plugin.packer
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.attribute.FileTime
import nextflow.extension.FilesEx
import nextflow.file.FileHelper
import spock.lang.Specification
/**
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
class PackerTest extends Specification implements TarHelper {
def 'should tar bundle' () {
given:
def folder = Files.createTempDirectory('test')
and:
def result = folder.resolve('result')
def result2 = folder.resolve('result2')
and:
def rootPath = folder.resolve('bundle'); rootPath.mkdir()
rootPath.resolve('main.nf').text = "I'm the main file"
rootPath.resolve('this/that').mkdirs()
Files.write(rootPath.resolve('this/hola.txt'), "Hola".bytes)
Files.write(rootPath.resolve('this/hello.txt'), "Hello".bytes)
Files.write(rootPath.resolve('this/that/ciao.txt'), "Ciao".bytes)
and:
def files = new ArrayList<Path>()
FileHelper.visitFiles([type:'any'], rootPath, '**', {
final mode = it.isDirectory() ? 0700 : 0600
FilesEx.setPermissionsMode(it, mode)
//
files.add(it)
})
and:
def packer = new Packer()
when:
def buffer = new ByteArrayOutputStream()
packer.makeTar(rootPath, files, buffer)
and:
untar( new ByteArrayInputStream(buffer.toByteArray()), result )
then:
result.resolve('main.nf').text == rootPath.resolve('main.nf').text
result.resolve('this/hola.txt').text == rootPath.resolve('this/hola.txt').text
result.resolve('this/hello.txt').text == rootPath.resolve('this/hello.txt').text
result.resolve('this/that/ciao.txt').text == rootPath.resolve('this/that/ciao.txt').text
and:
result.resolve('main.nf').getPermissionsMode() == 0600
result.resolve('this/hola.txt').getPermissionsMode() == 0600
result.resolve('this/that').getPermissionsMode() == 0700
and:
Files.getLastModifiedTime(result.resolve('main.nf')) == FileTime.fromMillis(0)
when:
def layer = packer.layer(rootPath, files)
then:
layer.tarDigest == 'sha256:f556b94e9b6f5f72b86e44833614b465df9f65cb4210e3f4416292dca1618360'
layer.gzipDigest == 'sha256:e58685a82452a11faa926843e7861c94bdb93e2c8f098b5c5354ec9b6fee2b68'
layer.gzipSize == 251
and:
def gzip = layer.location.replace('data:','').decodeBase64()
def tar = uncompress(gzip)
untar( new ByteArrayInputStream(tar), result2)
and:
result2.resolve('main.nf').text == rootPath.resolve('main.nf').text
result2.resolve('this/hola.txt').text == rootPath.resolve('this/hola.txt').text
result2.resolve('this/hello.txt').text == rootPath.resolve('this/hello.txt').text
result2.resolve('this/that/ciao.txt').text == rootPath.resolve('this/that/ciao.txt').text
and:
Files.getLastModifiedTime(result2.resolve('main.nf')) == FileTime.fromMillis(0)
cleanup:
folder?.deleteDir()
}
def 'should tar bundle and preserve timestamps' () {
given:
def LAST_MODIFIED = FileTime.fromMillis(1_000_000_000_000)
def folder = Files.createTempDirectory('test')
and:
def result = folder.resolve('result')
def result2 = folder.resolve('result2')
and:
def rootPath = folder.resolve('bundle'); rootPath.mkdir()
rootPath.resolve('main.nf').text = "I'm the main file"
rootPath.resolve('this/that').mkdirs()
Files.write(rootPath.resolve('this/hola.txt'), "Hola".bytes)
Files.write(rootPath.resolve('this/hello.txt'), "Hello".bytes)
Files.write(rootPath.resolve('this/that/ciao.txt'), "Ciao".bytes)
and:
def files = new ArrayList<Path>()
FileHelper.visitFiles([type:'any'], rootPath, '**', {
Files.setLastModifiedTime(it, LAST_MODIFIED)
final mode = it.isDirectory() ? 0700 : 0600
FilesEx.setPermissionsMode(it, mode)
//
files.add(it)
})
and:
def packer = new Packer(preserveFileTimestamp: true)
when:
def buffer = new ByteArrayOutputStream()
packer.makeTar(rootPath, files, buffer)
and:
untar( new ByteArrayInputStream(buffer.toByteArray()), result )
then:
result.resolve('main.nf').text == rootPath.resolve('main.nf').text
result.resolve('this/hola.txt').text == rootPath.resolve('this/hola.txt').text
result.resolve('this/hello.txt').text == rootPath.resolve('this/hello.txt').text
result.resolve('this/that/ciao.txt').text == rootPath.resolve('this/that/ciao.txt').text
and:
result.resolve('main.nf').getPermissionsMode() == 0600
result.resolve('this/hola.txt').getPermissionsMode() == 0600
result.resolve('this/that').getPermissionsMode() == 0700
and:
Files.getLastModifiedTime(result.resolve('main.nf')) == LAST_MODIFIED
when:
def layer = packer.layer(rootPath, files)
then:
layer.tarDigest == 'sha256:81200f6ad32793567d8070375dc51312a1711fedf6a1c6f5e4a97fa3014f3491'
layer.gzipDigest == 'sha256:09a2deca4293245909223db505cf69affa1a8ff8acb745fe3cad38bc0b719110'
layer.gzipSize == 254
and:
def gzip = layer.location.replace('data:','').decodeBase64()
def tar = uncompress(gzip)
untar( new ByteArrayInputStream(tar), result2)
and:
result2.resolve('main.nf').text == rootPath.resolve('main.nf').text
result2.resolve('this/hola.txt').text == rootPath.resolve('this/hola.txt').text
result2.resolve('this/hello.txt').text == rootPath.resolve('this/hello.txt').text
result2.resolve('this/that/ciao.txt').text == rootPath.resolve('this/that/ciao.txt').text
cleanup:
folder?.deleteDir()
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.seqera.wave.plugin.packer
import static java.nio.file.StandardOpenOption.*
import java.nio.file.Files
import java.nio.file.Path
import org.apache.commons.compress.archivers.ArchiveStreamFactory
import org.apache.commons.compress.archivers.tar.TarArchiveEntry
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream
/**
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
trait TarHelper {
static List<Path> untar(final InputStream is, final Path outputDir) {
final untaredFiles = new LinkedList<Path>();
final debInputStream = (TarArchiveInputStream) new ArchiveStreamFactory().createArchiveInputStream("tar", is);
TarArchiveEntry entry = null;
while ((entry = (TarArchiveEntry)debInputStream.getNextEntry()) != null) {
final outputFile = outputDir.resolve(entry.getName())
if (entry.isDirectory()) {
if (!outputFile.exists()) {
if (!outputFile.mkdirs()) {
throw new IllegalStateException(String.format("Couldn't create directory %s.", outputFile));
}
outputFile.setPermissionsMode( entry.getMode() )
outputFile.setLastModified( entry.getLastModifiedDate().getTime() )
}
}
else {
println "outputFile=$outputFile; mode=${entry.mode}"
outputFile.parent.mkdirs()
final outputFileStream = Files.newOutputStream(outputFile, CREATE, APPEND)
debInputStream.transferTo(outputFileStream)
outputFileStream.close()
outputFile.setPermissionsMode(entry.getMode())
outputFile.setLastModified( entry.getLastModifiedDate().getTime() )
}
untaredFiles.add(outputFile);
}
debInputStream.close();
return untaredFiles
}
byte[] uncompress( byte[] bytes ) {
try (def stream = new GzipCompressorInputStream(new ByteArrayInputStream(bytes))) {
def buffer = new ByteArrayOutputStream()
stream.transferTo(buffer)
return buffer.toByteArray()
}
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.seqera.wave.plugin.resolver
import io.seqera.wave.plugin.WaveClient
import io.seqera.wave.plugin.config.WaveConfig
import nextflow.container.ContainerConfig
import nextflow.container.resolver.ContainerInfo
import nextflow.container.resolver.ContainerMeta
import nextflow.container.resolver.DefaultContainerResolver
import nextflow.executor.Executor
import nextflow.processor.TaskProcessor
import nextflow.processor.TaskRun
import spock.lang.Specification
/**
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
class WaveContainerResolverTest extends Specification {
def 'should resolve docker container with wave container' () {
given:
def CONTAINER_NAME = "ubuntu:latest"
def WAVE_CONTAINER = new ContainerInfo(CONTAINER_NAME, "wave.io/ubuntu:latest", "12345")
def ORAS_CONTAINER = new ContainerInfo(CONTAINER_NAME, "oras://wave.io/ubuntu:latest", "12345")
def SINGULARITY_CONTAINER = new ContainerInfo('ubuntu:latest', '/some/singularity/ubuntu.img')
and:
def defaultResolver = Spy(DefaultContainerResolver)
def executor = Mock(Executor)
def resolver = Spy(new WaveContainerResolver(defaultResolver: defaultResolver))
def task = Mock(TaskRun) {
getProcessor() >> Mock(TaskProcessor) {
getExecutor() >> executor
}
}
// docker images
when:
def result = resolver.resolveImage(task, CONTAINER_NAME)
then:
resolver.client() >> Mock(WaveClient) { config()>>Mock(WaveConfig) }
_ * task.getContainerConfig() >> Mock(ContainerConfig) { getEngine()>>'docker' }
and:
1 * resolver.waveContainer(task, CONTAINER_NAME, false) >> WAVE_CONTAINER
and:
result == WAVE_CONTAINER
// singularity images
when:
result = resolver.resolveImage(task, CONTAINER_NAME)
then:
resolver.client() >> Mock(WaveClient) { config()>>Mock(WaveConfig) }
_ * task.getContainerConfig() >> Mock(ContainerConfig) { getEngine()>>'singularity'; isEnabled()>>true }
and:
1 * resolver.waveContainer(task, CONTAINER_NAME, false) >> WAVE_CONTAINER
1 * defaultResolver.resolveImage(task, WAVE_CONTAINER.target, WAVE_CONTAINER.hashKey) >> SINGULARITY_CONTAINER
and:
result == SINGULARITY_CONTAINER
// singularity images + oras protocol
when:
result = resolver.resolveImage(task, CONTAINER_NAME)
then:
resolver.client() >> Mock(WaveClient) { config()>>Mock(WaveConfig) { freezeMode()>>true } }
_ * task.getContainerConfig() >> Mock(ContainerConfig) { getEngine()>>'singularity'; isEnabled()>>true }
and:
1 * resolver.waveContainer(task, CONTAINER_NAME, true) >> ORAS_CONTAINER
0 * defaultResolver.resolveImage(task, WAVE_CONTAINER.target) >> null
and:
result == ORAS_CONTAINER
}
def 'should return container meta' () {
given:
def containerKey = 'abc'
def client = Mock(WaveClient)
def resolver = Spy(new WaveContainerResolver())
def meta = Mock(ContainerMeta)
when:
def result = resolver.getContainerMeta(containerKey)
then:
resolver.client()>>client
and:
client.enabled()>>true
client.getContainerMeta(containerKey)>>meta
and:
result == meta
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.seqera.wave.plugin.util
import spock.lang.Specification
import spock.lang.Unroll
/**
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
class BasicCliOptsTest extends Specification {
@Unroll
def 'should parse options' () {
when:
def cli = BasicCliOpts.parse(CLI?.tokenize(' '))
then:
cli.options == OPTS
cli.args == ARGS
where:
CLI | OPTS | ARGS
null | [:] | []
'' | [:] | []
'alpha=1 beta=2 foo delta=3 bar' | [alpha:'1', beta:'2', delta:'3'] | ['foo','bar']
'alpha= foo' | [alpha:''] | ['foo']
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.seqera.wave.plugin.util
import spock.lang.Specification
/**
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
class GnuCliOptsTest extends Specification {
def 'should parse cli' () {
when:
def cli = GnuCliOpts.parse(['-i', '-v', 'x=y', 'nextflow', '--', 'this', '--that'])
then:
cli.options == ['-i':'','-v':'x=y']
cli.container == 'nextflow'
cli.args == ['this','--that']
when:
cli = GnuCliOpts.parse(['-v', 'x=y', '-w', '$PWD', 'nextflow', '-it', '--', 'this', '--that'])
then:
cli.options == ['-v':'x=y', '-w':'$PWD', '-it':'']
cli.container == 'nextflow'
cli.args == ['this','--that']
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.processor
import nextflow.Global
import nextflow.Session
import nextflow.executor.Executor
import spock.lang.Specification
import spock.lang.Unroll
/**
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
class TaskRunTest2 extends Specification {
@Unroll
def 'should get container platform' () {
given:
def session = Global.session = Mock(Session) { getConfig()>>SESSION }
def executor = Mock(Executor) { getSession()>>session }
def processor = Mock(TaskProcessor) { getExecutor()>>executor; getSession()>>session }
and:
def config = new TaskConfig(CONFIG)
def task = new TaskRun(config: config, processor: processor)
expect:
task.getContainerPlatform() == EXPECTED
where:
CONFIG | SESSION | EXPECTED
[:] | [:] | null
[arch:'amd64'] | [:] | 'linux/amd64'
[arch:'arm64'] | [:] | 'linux/arm64'
and:
[:] | [wave:[enabled:true]] | 'linux/amd64'
[arch:'amd64'] | [wave:[enabled:true]] | 'linux/amd64'
[arch:'arm64'] | [wave:[enabled:true]] | 'linux/arm64'
}
}