@@ -187,3 +187,139 @@ func TestMapTask(t *testing.T) {
187187 t .Fatal ("path unmapping failed" )
188188 }
189189}
190+
191+ // TestMapTaskInputOutputSameDirNoDuplicateVolume reproduces the bug where a
192+ // task with both an input and an output that resolve to the same container
193+ // directory produced a duplicate volume mount (invalid in Kubernetes).
194+ func TestMapTaskInputOutputSameDirNoDuplicateVolume (t * testing.T ) {
195+ tmp , err := os .MkdirTemp ("" , "funnel-test-mapper-dedup" )
196+ if err != nil {
197+ t .Fatal (err )
198+ }
199+ defer os .RemoveAll (tmp )
200+
201+ f := FileMapper {WorkDir : tmp }
202+
203+ task := & tes.Task {
204+ Inputs : []* tes.Input {
205+ {
206+ Name : "in1" ,
207+ Path : "/data/input.txt" ,
208+ Content : "hello" ,
209+ },
210+ },
211+ Outputs : []* tes.Output {
212+ {
213+ Name : "out1" ,
214+ Url : "file:///out/output.txt" ,
215+ Path : "/data/output.txt" ,
216+ Type : tes .FileType_FILE ,
217+ },
218+ },
219+ }
220+
221+ if err := f .MapTask (task ); err != nil {
222+ t .Fatal (err )
223+ }
224+
225+ // Verify no two volumes share the same ContainerPath (the K8s duplicate mount error).
226+ seen := map [string ]int {}
227+ for _ , v := range f .Volumes {
228+ seen [v .ContainerPath ]++
229+ }
230+ for path , count := range seen {
231+ if count > 1 {
232+ t .Errorf ("duplicate ContainerPath %q appears %d times in Volumes (causes K8s invalid mountPath)" , path , count )
233+ }
234+ }
235+
236+ // /data should appear exactly once and cover both the input and the output.
237+ if seen ["/data" ] != 1 {
238+ t .Errorf ("expected /data to appear exactly once in Volumes, got %d; volumes: %+v" , seen ["/data" ], f .Volumes )
239+ }
240+ }
241+
242+ // TestMapTaskInputOutputOverlapNoDuplicateVolume tests the variant where the
243+ // output directory is a parent of the input directory.
244+ func TestMapTaskInputOutputOverlapNoDuplicateVolume (t * testing.T ) {
245+ tmp , err := os .MkdirTemp ("" , "funnel-test-mapper-overlap" )
246+ if err != nil {
247+ t .Fatal (err )
248+ }
249+ defer os .RemoveAll (tmp )
250+
251+ f := FileMapper {WorkDir : tmp }
252+
253+ // Input under /data/inputs/, output dir is /data (parent of input ancestor).
254+ task := & tes.Task {
255+ Inputs : []* tes.Input {
256+ {
257+ Name : "in1" ,
258+ Path : "/data/inputs/file.txt" ,
259+ Content : "hello" ,
260+ },
261+ },
262+ Outputs : []* tes.Output {
263+ {
264+ Name : "out1" ,
265+ Url : "file:///out/result" ,
266+ Path : "/data/result" ,
267+ Type : tes .FileType_DIRECTORY ,
268+ },
269+ },
270+ }
271+
272+ if err := f .MapTask (task ); err != nil {
273+ t .Fatal (err )
274+ }
275+
276+ seen := map [string ]int {}
277+ for _ , v := range f .Volumes {
278+ seen [v .ContainerPath ]++
279+ }
280+ for path , count := range seen {
281+ if count > 1 {
282+ t .Errorf ("duplicate ContainerPath %q appears %d times in Volumes" , path , count )
283+ }
284+ }
285+ }
286+
287+ // TestMapTaskMultipleInputsAndOutputs mirrors the concrete failure case from
288+ // the bug report: one input and one output with a /tmp volume also present.
289+ func TestMapTaskMultipleInputsAndOutputs (t * testing.T ) {
290+ tmp , err := os .MkdirTemp ("" , "funnel-test-mapper-multi" )
291+ if err != nil {
292+ t .Fatal (err )
293+ }
294+ defer os .RemoveAll (tmp )
295+
296+ f := FileMapper {WorkDir : tmp }
297+
298+ task := & tes.Task {
299+ Inputs : []* tes.Input {
300+ {Name : "a" , Path : "/data/a.txt" , Content : "a" },
301+ {Name : "b" , Path : "/data/b.txt" , Content : "b" },
302+ },
303+ Outputs : []* tes.Output {
304+ {Name : "out" , Url : "file:///out/c.txt" , Path : "/data/c.txt" , Type : tes .FileType_FILE },
305+ },
306+ }
307+
308+ if err := f .MapTask (task ); err != nil {
309+ t .Fatal (err )
310+ }
311+
312+ seen := map [string ]int {}
313+ for _ , v := range f .Volumes {
314+ seen [v .ContainerPath ]++
315+ }
316+ for path , count := range seen {
317+ if count > 1 {
318+ t .Errorf ("duplicate ContainerPath %q appears %d times in Volumes" , path , count )
319+ }
320+ }
321+
322+ if seen ["/data" ] != 1 {
323+ t .Errorf ("expected /data once in Volumes, got %d; volumes: %+v" , seen ["/data" ], f .Volumes )
324+ }
325+ }
0 commit comments