Skip to content

Commit a519a18

Browse files
committed
Add missing unit tests for script/config providers
Signed-off-by: Ben Sherman <bentshermann@gmail.com>
1 parent 7db3ee8 commit a519a18

13 files changed

Lines changed: 1124 additions & 3 deletions

src/test/groovy/nextflow/lsp/TestLanguageClient.groovy

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
package nextflow.lsp
1818

1919
import java.util.concurrent.CompletableFuture
20+
import java.util.concurrent.ConcurrentHashMap
2021

22+
import org.eclipse.lsp4j.Diagnostic
2123
import org.eclipse.lsp4j.MessageActionItem
2224
import org.eclipse.lsp4j.MessageParams
2325
import org.eclipse.lsp4j.PublishDiagnosticsParams
@@ -30,6 +32,8 @@ import org.eclipse.lsp4j.services.LanguageClient
3032
*/
3133
class TestLanguageClient implements LanguageClient {
3234

35+
private final Map<String,List<Diagnostic>> diagnostics = new ConcurrentHashMap<>()
36+
3337
@Override
3438
public void telemetryEvent(Object object) {
3539
}
@@ -44,11 +48,35 @@ class TestLanguageClient implements LanguageClient {
4448
}
4549

4650
@Override
47-
public void publishDiagnostics(PublishDiagnosticsParams diagnostics) {
51+
public void publishDiagnostics(PublishDiagnosticsParams params) {
52+
diagnostics.put(params.getUri(), params.getDiagnostics())
4853
}
4954

5055
@Override
5156
public void logMessage(MessageParams message) {
5257
System.err.println(message.getMessage())
5358
}
59+
60+
/**
61+
* Get the most recently published diagnostics for a given file.
62+
*
63+
* @param uri
64+
*/
65+
List<Diagnostic> getDiagnostics(String uri) {
66+
return diagnostics.getOrDefault(uri, Collections.emptyList())
67+
}
68+
69+
/**
70+
* Get the most recently published diagnostics for all files.
71+
*/
72+
Map<String,List<Diagnostic>> getAllDiagnostics() {
73+
return diagnostics
74+
}
75+
76+
/**
77+
* Forget all captured diagnostics.
78+
*/
79+
void resetDiagnostics() {
80+
diagnostics.clear()
81+
}
5482
}

src/test/groovy/nextflow/lsp/TestUtils.groovy

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,19 @@ class TestUtils {
4646
* Get a language service instance for Nextflow config files.
4747
*/
4848
static ConfigService getConfigService() {
49+
return getConfigService(new TestLanguageClient())
50+
}
51+
52+
/**
53+
* Get a language service instance for Nextflow config files, connected to
54+
* the given client so that published diagnostics can be inspected.
55+
*
56+
* @param client
57+
*/
58+
static ConfigService getConfigService(TestLanguageClient client) {
4959
def service = new ConfigService(workspaceRoot.toUri().toString())
5060
def configuration = LanguageServerConfiguration.defaults()
51-
service.connect(new TestLanguageClient())
61+
service.connect(client)
5262
service.initialize(configuration)
5363
// skip workspace scan
5464
open(service, getUri('nextflow.config'), '')
@@ -60,10 +70,20 @@ class TestUtils {
6070
* Get a language service instance for Nextflow scripts.
6171
*/
6272
static ScriptService getScriptService() {
73+
return getScriptService(new TestLanguageClient())
74+
}
75+
76+
/**
77+
* Get a language service instance for Nextflow scripts, connected to the
78+
* given client so that published diagnostics can be inspected.
79+
*
80+
* @param client
81+
*/
82+
static ScriptService getScriptService(TestLanguageClient client) {
6383
def service = new ScriptService(workspaceRoot.toUri().toString())
6484
def configuration = LanguageServerConfiguration.defaults()
6585
def pluginSpecCache = new PluginSpecCache(configuration.pluginRegistryUrl())
66-
service.connect(new TestLanguageClient())
86+
service.connect(client)
6787
service.initialize(configuration, pluginSpecCache)
6888
// skip workspace scan
6989
open(service, getUri('main.nf'), '')
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright 2024-2025, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package nextflow.lsp.services.config
18+
19+
import org.eclipse.lsp4j.CompletionItem
20+
import org.eclipse.lsp4j.CompletionItemKind
21+
import org.eclipse.lsp4j.CompletionParams
22+
import org.eclipse.lsp4j.Position
23+
import org.eclipse.lsp4j.TextDocumentIdentifier
24+
import spock.lang.Specification
25+
26+
import static nextflow.lsp.TestUtils.*
27+
28+
/**
29+
*
30+
* @author Ben Sherman <bentshermann@gmail.com>
31+
*/
32+
class ConfigCompletionTest extends Specification {
33+
34+
// NOTE: completion() runs its own update internally, so (unlike hover or
35+
// definition) the file must NOT be flushed with updateNow() beforehand.
36+
37+
def either(ConfigService service, String uri, Position position, int maxItems = 100) {
38+
return service.completion(new CompletionParams(new TextDocumentIdentifier(uri), position), maxItems, false)
39+
}
40+
41+
List<CompletionItem> getCompletions(ConfigService service, String uri, Position position, int maxItems = 100) {
42+
def result = either(service, uri, position, maxItems)
43+
return result.isLeft() ? result.getLeft() : result.getRight().getItems()
44+
}
45+
46+
def 'should provide top-level completions for a partial scope name' () {
47+
given:
48+
def service = getConfigService()
49+
def uri = getUri('nextflow.config')
50+
51+
when:
52+
open(service, uri, 'wor\n')
53+
def completions = getCompletions(service, uri, new Position(0, 3))
54+
def byLabel = completions.collectEntries { [(it.getLabel()): it] }
55+
then: 'a scope is offered both as a bare name and as a block snippet'
56+
byLabel['process']?.getKind() == CompletionItemKind.Property
57+
byLabel['process {']?.getKind() == CompletionItemKind.Property
58+
and: 'a top-level option carries its type in the detail'
59+
byLabel['workDir']?.getDetail()?.startsWith('workDir:')
60+
}
61+
62+
def 'should provide option completions for a config scope' () {
63+
given:
64+
def service = getConfigService()
65+
def uri = getUri('nextflow.config')
66+
67+
when:
68+
open(service, uri, 'process.\n')
69+
def completions = getCompletions(service, uri, new Position(0, 8))
70+
def cpus = completions.find { it.getLabel() == 'cpus' }
71+
then:
72+
cpus != null
73+
cpus.getKind() == CompletionItemKind.Property
74+
cpus.getDetail() == 'cpus: Integer'
75+
}
76+
77+
def 'should provide option completions inside a profile, ignoring the profile scope' () {
78+
given:
79+
def service = getConfigService()
80+
def uri = getUri('nextflow.config')
81+
82+
when:
83+
open(service, uri, '''\
84+
profiles {
85+
standard {
86+
process.
87+
}
88+
}
89+
''')
90+
def completions = getCompletions(service, uri, new Position(2, 16))
91+
then: 'the "profiles.<name>" prefix is stripped, so process options are offered'
92+
completions.find { it.getLabel() == 'cpus' }?.getDetail() == 'cpus: Integer'
93+
}
94+
95+
def 'should mark the completion list incomplete when items exceed maxItems' () {
96+
given:
97+
def service = getConfigService()
98+
def uri = getUri('nextflow.config')
99+
100+
when:
101+
open(service, uri, 'process.\n')
102+
def result = either(service, uri, new Position(0, 8), 1)
103+
then:
104+
result.isRight()
105+
result.getRight().isIncomplete()
106+
result.getRight().getItems().size() == 1
107+
}
108+
109+
def 'should return no completions for a file that is not open' () {
110+
given:
111+
def service = getConfigService()
112+
113+
expect:
114+
getCompletions(service, getUri('other.config'), new Position(0, 0)).isEmpty()
115+
}
116+
117+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2024-2025, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package nextflow.lsp.services.config
18+
19+
import org.eclipse.lsp4j.DocumentLinkParams
20+
import org.eclipse.lsp4j.TextDocumentIdentifier
21+
import spock.lang.Specification
22+
23+
import static nextflow.lsp.TestUtils.*
24+
25+
/**
26+
*
27+
* @author Ben Sherman <bentshermann@gmail.com>
28+
*/
29+
class ConfigLinkTest extends Specification {
30+
31+
List getLinks(ConfigService service, String uri) {
32+
return service.documentLink(new DocumentLinkParams(new TextDocumentIdentifier(uri)))
33+
}
34+
35+
def 'should resolve a relative config include against the parent directory' () {
36+
given:
37+
def service = getConfigService()
38+
def uri = getUri('nextflow.config')
39+
40+
when:
41+
open(service, uri, '''\
42+
includeConfig 'conf/base.config'
43+
''')
44+
service.updateNow()
45+
def links = getLinks(service, uri)
46+
then:
47+
links.size() == 1
48+
links[0].getTarget() == getUri('conf/base.config')
49+
}
50+
51+
def 'should pass through an absolute include URI unchanged' () {
52+
given:
53+
def service = getConfigService()
54+
def uri = getUri('nextflow.config')
55+
56+
when:
57+
open(service, uri, '''\
58+
includeConfig 'https://example.com/shared.config'
59+
''')
60+
service.updateNow()
61+
def links = getLinks(service, uri)
62+
then:
63+
links.size() == 1
64+
links[0].getTarget() == 'https://example.com/shared.config'
65+
}
66+
67+
def 'should produce no links for a config without includes' () {
68+
given:
69+
def service = getConfigService()
70+
def uri = getUri('nextflow.config')
71+
72+
when:
73+
open(service, uri, '''\
74+
process.cpus = 2
75+
''')
76+
service.updateNow()
77+
then:
78+
getLinks(service, uri).isEmpty()
79+
}
80+
81+
}

0 commit comments

Comments
 (0)