Skip to content

Commit f1af2ad

Browse files
committed
add tests
1 parent 0d92e2d commit f1af2ad

1 file changed

Lines changed: 194 additions & 1 deletion

File tree

src/test/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerContextBuilderSpec.groovy

Lines changed: 194 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,26 @@ package com.rundeck.plugins.ansible.ansible
33
import com.dtolabs.rundeck.core.common.Framework
44
import com.dtolabs.rundeck.core.common.INodeSet
55
import com.dtolabs.rundeck.core.execution.ExecutionContext
6+
import com.dtolabs.rundeck.core.execution.ExecutionLogger
7+
import com.dtolabs.rundeck.core.utils.PropertyLookup
68
import com.dtolabs.rundeck.plugins.step.PluginStepContext
79
import com.rundeck.plugins.ansible.plugin.AnsiblePluginGroup
810
import spock.lang.Specification
9-
import com.dtolabs.rundeck.core.execution.ExecutionLogger
11+
import spock.lang.TempDir
12+
13+
import java.nio.file.Files
14+
import java.util.concurrent.ConcurrentLinkedQueue
15+
import java.util.concurrent.CountDownLatch
16+
import java.util.concurrent.CyclicBarrier
17+
import java.util.concurrent.ExecutorService
18+
import java.util.concurrent.Executors
19+
import java.util.concurrent.TimeUnit
1020

1121
class AnsibleRunnerContextBuilderSpec extends Specification {
1222

23+
@TempDir
24+
File baseTmpDir
25+
1326
def "test plugin group"(){
1427
given:
1528

@@ -80,4 +93,184 @@ class AnsibleRunnerContextBuilderSpec extends Specification {
8093
!contextBuilder.encryptExtraVars()
8194
contextBuilder.getPlaybookPath() == "path/to/playbook"
8295
}
96+
97+
// ----------------------------------------------------------------------------
98+
// Race condition fix: getExecutionSpecificTmpDir() / cleanupTempFiles()
99+
// See ansible-plugin-race-condition-context.md
100+
// ----------------------------------------------------------------------------
101+
102+
/**
103+
* Builds a fresh AnsibleRunnerContextBuilder wired so that
104+
* AnsibleUtil.getCustomTmpPathDir(framework) resolves to {@code baseTmpDir} and
105+
* the data context exposes the given {@code execId}.
106+
*/
107+
private AnsibleRunnerContextBuilder newBuilder(String execId) {
108+
def propertyLookup = Mock(PropertyLookup)
109+
propertyLookup.getProperty("framework.tmp.dir") >> baseTmpDir.absolutePath
110+
111+
def framework = Mock(Framework)
112+
framework.getPropertyLookup() >> propertyLookup
113+
framework.hasProjectProperty(_, _) >> false
114+
framework.hasProperty(_) >> false
115+
116+
def execContext = Mock(ExecutionContext)
117+
execContext.getDataContext() >> ['job': ['execid': execId]]
118+
execContext.getFrameworkProject() >> 'test-project'
119+
120+
def nodes = Mock(INodeSet)
121+
nodes.getNodes() >> []
122+
123+
return new AnsibleRunnerContextBuilder(execContext, framework, nodes, [:])
124+
}
125+
126+
/**
127+
* Drops a fake "inventory" file inside the builder's working directory, mirroring
128+
* what AnsibleInventoryBuilder.buildInventory() does in production.
129+
*/
130+
private File seedInventoryFile(AnsibleRunnerContextBuilder builder) {
131+
File dir = new File(builder.getExecutionSpecificTmpDir())
132+
File inventory = File.createTempFile("ansible-inventory", ".json", dir)
133+
inventory.text = '{"all":{"hosts":{}}}'
134+
return inventory
135+
}
136+
137+
def "two builders with the same executionId get distinct directories with the executionId prefix"() {
138+
given:
139+
def builderA = newBuilder("4242")
140+
def builderB = newBuilder("4242")
141+
142+
when:
143+
File dirA = new File(builderA.getExecutionSpecificTmpDir())
144+
File dirB = new File(builderB.getExecutionSpecificTmpDir())
145+
146+
then: "both directories exist"
147+
dirA.exists() && dirA.isDirectory()
148+
dirB.exists() && dirB.isDirectory()
149+
150+
and: "they are different directories"
151+
dirA.absolutePath != dirB.absolutePath
152+
153+
and: "they sit directly under the configured base tmp dir (no shared parent)"
154+
dirA.parentFile.absolutePath == baseTmpDir.absolutePath
155+
dirB.parentFile.absolutePath == baseTmpDir.absolutePath
156+
157+
and: "their names start with the ansible-exec-<execId>-builder- prefix for filtering"
158+
dirA.name.startsWith("ansible-exec-4242-builder-")
159+
dirB.name.startsWith("ansible-exec-4242-builder-")
160+
}
161+
162+
def "directories of distinct executions can coexist under the same base tmp dir"() {
163+
given:
164+
def builderX = newBuilder("4242")
165+
def builderY = newBuilder("9999")
166+
167+
when:
168+
File dirX = new File(builderX.getExecutionSpecificTmpDir())
169+
File dirY = new File(builderY.getExecutionSpecificTmpDir())
170+
171+
then:
172+
dirX.parentFile.absolutePath == baseTmpDir.absolutePath
173+
dirY.parentFile.absolutePath == baseTmpDir.absolutePath
174+
dirX.name.startsWith("ansible-exec-4242-builder-")
175+
dirY.name.startsWith("ansible-exec-9999-builder-")
176+
}
177+
178+
def "cleanupTempFiles on one builder must not delete a sibling's inventory"() {
179+
given:
180+
def builderA = newBuilder("9001")
181+
def builderB = newBuilder("9001")
182+
183+
File invA = seedInventoryFile(builderA)
184+
File invB = seedInventoryFile(builderB)
185+
File dirA = new File(builderA.getExecutionSpecificTmpDir())
186+
File dirB = new File(builderB.getExecutionSpecificTmpDir())
187+
188+
expect:
189+
invA.exists()
190+
invB.exists()
191+
192+
when: "builder A finishes and cleans up"
193+
builderA.cleanupTempFiles()
194+
195+
then: "A's directory and inventory are gone, but B's is intact"
196+
!dirA.exists()
197+
!invA.exists()
198+
dirB.exists()
199+
invB.exists()
200+
201+
when: "builder B finishes and cleans up"
202+
builderB.cleanupTempFiles()
203+
204+
then: "B's directory is also gone"
205+
!dirB.exists()
206+
!invB.exists()
207+
}
208+
209+
def "parallel builders never lose their inventory files before their own cleanup"() {
210+
given:
211+
int parallelism = 16
212+
ExecutorService pool = Executors.newFixedThreadPool(parallelism)
213+
CyclicBarrier barrier = new CyclicBarrier(parallelism)
214+
CountDownLatch done = new CountDownLatch(parallelism)
215+
ConcurrentLinkedQueue<String> failures = new ConcurrentLinkedQueue<>()
216+
217+
when:
218+
parallelism.times {
219+
pool.submit({
220+
AnsibleRunnerContextBuilder builder = null
221+
try {
222+
builder = newBuilder("55555")
223+
File workDir = new File(builder.getExecutionSpecificTmpDir())
224+
File inventory = File.createTempFile("ansible-inventory", ".json", workDir)
225+
inventory.text = '{"all":{"hosts":{}}}'
226+
227+
// All threads have created their own dir+inventory; now hold them open
228+
// simultaneously to maximise the chance of a sibling's cleanup running while
229+
// ours is still alive.
230+
barrier.await(10, TimeUnit.SECONDS)
231+
232+
if (!inventory.exists()) {
233+
failures.add("inventory disappeared while builder was alive: ${inventory.absolutePath}")
234+
}
235+
if (!workDir.exists()) {
236+
failures.add("work dir disappeared while builder was alive: ${workDir.absolutePath}")
237+
}
238+
} catch (Throwable t) {
239+
failures.add("worker threw: ${t.message}")
240+
} finally {
241+
builder?.cleanupTempFiles()
242+
done.countDown()
243+
}
244+
} as Runnable)
245+
}
246+
247+
boolean finished = done.await(30, TimeUnit.SECONDS)
248+
pool.shutdownNow()
249+
250+
then:
251+
finished
252+
failures.isEmpty()
253+
254+
and: "no leftover ansible-exec-55555-builder-* directories remain after cleanup"
255+
baseTmpDir.listFiles({ f -> f.name.startsWith("ansible-exec-55555-builder-") } as FileFilter).length == 0
256+
}
257+
258+
def "debug mode preserves the per-builder directory"() {
259+
given:
260+
def builder = newBuilder("debug-123")
261+
// ansible-debug=true must short-circuit cleanup paths.
262+
Map<String, Object> jobConf = (Map<String, Object>) builder.getJobConf()
263+
jobConf.put("ansible-debug", "true")
264+
265+
File workDir = new File(builder.getExecutionSpecificTmpDir())
266+
File inventory = seedInventoryFile(builder)
267+
268+
when:
269+
builder.cleanupTempFiles()
270+
271+
then: "nothing is deleted in debug mode, mirroring legacy behaviour"
272+
inventory.exists()
273+
workDir.exists()
274+
workDir.name.startsWith("ansible-exec-debug-123-builder-")
275+
}
83276
}

0 commit comments

Comments
 (0)