add nextflow d30e48d
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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]'
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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']
|
||||
}
|
||||
}
|
||||
@@ -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']
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user