diff --git a/internal/manager/job_compilers/job_compilers_test.go b/internal/manager/job_compilers/job_compilers_test.go
index e5bba10b..6ca19ba3 100644
--- a/internal/manager/job_compilers/job_compilers_test.go
+++ b/internal/manager/job_compilers/job_compilers_test.go
@@ -84,13 +84,10 @@ func TestSimpleBlenderRenderHappy(t *testing.T) {
sj := exampleSubmittedJob()
aj, err := s.Compile(ctx, sj)
if err != nil {
- t.Logf("job compiler failed: %v", err)
- t.FailNow()
+ t.Fatalf("job compiler failed: %v", err)
}
- assert.NotNil(t, aj)
if aj == nil {
- // Don't bother with the rest of the test, it'll dereference a nil pointer anyway.
- return
+ t.Fatalf("job compiler returned nil but no error")
}
// Properties should be copied as-is.
@@ -149,3 +146,68 @@ func TestSimpleBlenderRenderHappy(t *testing.T) {
}
assert.Equal(t, expectDeps, tVideo.Dependencies)
}
+
+func TestSimpleBlenderRenderWindowsPaths(t *testing.T) {
+ c := mockedClock(t)
+
+ s, err := Load(c)
+ assert.NoError(t, err)
+
+ // Compiling a job should be really fast.
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
+ defer cancel()
+
+ sj := exampleSubmittedJob()
+
+ // Adjust the job to get paths in Windows notation.
+ sj.Settings.AdditionalProperties["filepath"] = "R:\\sf\\jobs\\scene123.blend"
+ sj.Settings.AdditionalProperties["render_output"] = "R:\\sprites\\farm_output\\promo\\square_ellie\\square_ellie.lighting_light_breakdown2\\######"
+
+ aj, err := s.Compile(ctx, sj)
+ if err != nil {
+ t.Fatalf("job compiler failed: %v", err)
+ }
+ if aj == nil {
+ t.Fatalf("job compiler returned nil but no error")
+ }
+
+ // Properties should be copied as-is, so also with filesystem paths as-is.
+ assert.Equal(t, sj.Name, aj.Name)
+ assert.Equal(t, sj.Type, aj.JobType)
+ assert.Equal(t, sj.Priority, aj.Priority)
+ assert.EqualValues(t, sj.Settings.AdditionalProperties, aj.Settings)
+ assert.EqualValues(t, sj.Metadata.AdditionalProperties, aj.Metadata)
+
+ settings := sj.Settings.AdditionalProperties
+
+ // Tasks should have been created to render the frames: 1-3, 4-6, 7-9, 10, video-encoding
+ assert.Equal(t, 5, len(aj.Tasks))
+ t0 := aj.Tasks[0]
+ expectCliArgs := []interface{}{ // They are strings, but Goja doesn't know that and will produce an []interface{}.
+ // The render output is constructed by the job compiler, and thus transforms to forward slashes.
+ "--render-output", "R:/sprites/farm_output/promo/square_ellie/square_ellie.lighting_light_breakdown2__intermediate-2006-01-02_090405/######",
+ "--render-format", settings["format"].(string),
+ "--render-frame", "1-3",
+ }
+ assert.NotEmpty(t, t0.UUID)
+ assert.Equal(t, "render-1-3", t0.Name)
+ assert.Equal(t, 1, len(t0.Commands))
+ assert.Equal(t, "blender-render", t0.Commands[0].Name)
+ assert.EqualValues(t, AuthoredCommandParameters{
+ "exe": "{blender}",
+ "blendfile": "R:\\sf\\jobs\\scene123.blend", // The blendfile parameter is just copied as-is, so keeps using backslash notation.
+ "args": expectCliArgs,
+ "argsBefore": make([]interface{}, 0),
+ }, t0.Commands[0].Parameters)
+
+ tVideo := aj.Tasks[4] // This should be a video encoding task
+ assert.NotEmpty(t, tVideo.UUID)
+ assert.Equal(t, "create-video", tVideo.Name)
+ assert.Equal(t, 1, len(tVideo.Commands))
+ assert.Equal(t, "create-video", tVideo.Commands[0].Name)
+ assert.EqualValues(t, AuthoredCommandParameters{
+ "input_files": "R:/sprites/farm_output/promo/square_ellie/square_ellie.lighting_light_breakdown2__intermediate-2006-01-02_090405/*.png",
+ "output_file": "R:/sprites/farm_output/promo/square_ellie/square_ellie.lighting_light_breakdown2__intermediate-2006-01-02_090405/scene123-1-10.mp4",
+ "fps": int64(24),
+ }, tVideo.Commands[0].Parameters)
+}
diff --git a/internal/manager/job_compilers/path.go b/internal/manager/job_compilers/path.go
index 2d11a730..90daa432 100644
--- a/internal/manager/job_compilers/path.go
+++ b/internal/manager/job_compilers/path.go
@@ -21,8 +21,7 @@ package job_compilers
* ***** END GPL LICENSE BLOCK ***** */
import (
- "path/filepath"
-
+ "git.blender.org/flamenco/pkg/crosspath"
"github.com/dop251/goja"
"github.com/rs/zerolog/log"
)
@@ -38,14 +37,8 @@ func PathModule(r *goja.Runtime, module *goja.Object) {
}
}
- mustExport("basename", filepath.Base)
- mustExport("dirname", filepath.Dir)
- mustExport("join", filepath.Join)
- mustExport("stem", Stem)
-}
-
-func Stem(fpath string) string {
- base := filepath.Base(fpath)
- ext := filepath.Ext(base)
- return base[:len(base)-len(ext)]
+ mustExport("basename", crosspath.Base)
+ mustExport("dirname", crosspath.Dir)
+ mustExport("join", crosspath.Join)
+ mustExport("stem", crosspath.Stem)
}
diff --git a/internal/manager/job_compilers/path_test.go b/internal/manager/job_compilers/path_test.go
deleted file mode 100644
index 95a8b94b..00000000
--- a/internal/manager/job_compilers/path_test.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package job_compilers
-
-/* ***** BEGIN GPL LICENSE BLOCK *****
- *
- * Original Code Copyright (C) 2022 Blender Foundation.
- *
- * This file is part of Flamenco.
- *
- * Flamenco is free software: you can redistribute it and/or modify it under
- * the terms of the GNU General Public License as published by the Free Software
- * Foundation, either version 3 of the License, or (at your option) any later
- * version.
- *
- * Flamenco is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * Flamenco. If not, see .
- *
- * ***** END GPL LICENSE BLOCK ***** */
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestStem(t *testing.T) {
- assert.Equal(t, "", Stem(""))
- assert.Equal(t, "stem", Stem("stem.txt"))
- assert.Equal(t, "stem.a", Stem("stem.a.b"))
- assert.Equal(t, "file", Stem("/path/to/file.txt"))
- // assert.Equal(t, "file", Stem("C:\\path\\to\\file.txt"))
-}
diff --git a/pkg/crosspath/crosspath.go b/pkg/crosspath/crosspath.go
new file mode 100644
index 00000000..6ca8c6d9
--- /dev/null
+++ b/pkg/crosspath/crosspath.go
@@ -0,0 +1,80 @@
+// Package crosspath deals with file/directory paths in a cross-platform way.
+//
+// This package tries to understand Windows paths on UNIX and vice versa.
+// Returned paths may be using forward slashes as separators.
+package crosspath
+
+/* ***** BEGIN GPL LICENSE BLOCK *****
+ *
+ * Original Code Copyright (C) 2022 Blender Foundation.
+ *
+ * This file is part of Flamenco.
+ *
+ * Flamenco is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later
+ * version.
+ *
+ * Flamenco is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * Flamenco. If not, see .
+ *
+ * ***** END GPL LICENSE BLOCK ***** */
+
+import (
+ path_module "path" // import under other name so that parameters can be called 'path'
+ "strings"
+)
+
+// Base returns the last element of path. Trailing slashes are removed before
+// extracting the last element. If the path is empty, Base returns ".". If the
+// path consists entirely of slashes, Base returns "/".
+func Base(path string) string {
+ slashed := ToSlash(path)
+ return path_module.Base(slashed)
+}
+
+// Dir returns all but the last element of path, typically the path's directory.
+// If the path is empty, Dir returns ".".
+func Dir(path string) string {
+ if path == "" {
+ return "."
+ }
+
+ slashed := ToSlash(path)
+
+ // Don't use path.Dir(), as that cleans up the path and removes double
+ // slashes. However, Windows UNC paths start with double blackslashes, which
+ // will translate to double slashes and should not be removed.
+ dir, _ := path_module.Split(slashed)
+ switch {
+ case dir == "":
+ return "."
+ case len(dir) > 1:
+ // Remove trailing slash.
+ return dir[:len(dir)-1]
+ default:
+ return dir
+ }
+}
+
+func Join(elem ...string) string {
+ return ToSlash(path_module.Join(elem...))
+}
+
+// Stem returns the filename without extension.
+func Stem(path string) string {
+ base := Base(path)
+ ext := path_module.Ext(base)
+ return base[:len(base)-len(ext)]
+}
+
+// ToSlash replaces all backslashes with forward slashes.
+// Contrary to filepath.ToSlash(), this also happens on Linux; it does not
+// expect `path` to be in platform-native notation.
+func ToSlash(path string) string {
+ return strings.ReplaceAll(path, "\\", "/")
+}
diff --git a/pkg/crosspath/crosspath_test.go b/pkg/crosspath/crosspath_test.go
new file mode 100644
index 00000000..2de2afd0
--- /dev/null
+++ b/pkg/crosspath/crosspath_test.go
@@ -0,0 +1,107 @@
+package crosspath
+
+/* ***** BEGIN GPL LICENSE BLOCK *****
+ *
+ * Original Code Copyright (C) 2022 Blender Foundation.
+ *
+ * This file is part of Flamenco.
+ *
+ * Flamenco is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later
+ * version.
+ *
+ * Flamenco is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * Flamenco. If not, see .
+ *
+ * ***** END GPL LICENSE BLOCK ***** */
+
+import (
+ "path"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestBase(t *testing.T) {
+ tests := []struct {
+ expect, input string
+ }{
+ {".", ""},
+ {"justafile.txt", "justafile.txt"},
+ {"with spaces.txt", "/Linux path/with spaces.txt"},
+ {"awésom.tar.gz", "C:\\ünicode\\is\\awésom.tar.gz"},
+ }
+ for _, test := range tests {
+ assert.Equal(t, test.expect, Base(test.input))
+ }
+}
+
+func TestDir(t *testing.T) {
+ // Just to show how path.Dir() behaves:
+ assert.Equal(t, ".", path.Dir(""))
+ assert.Equal(t, ".", path.Dir("justafile.txt"))
+
+ tests := []struct {
+ expect, input string
+ }{
+ // Follow path.Dir() when it comes to empty directories:
+ {".", ""},
+ {".", "justafile.txt"},
+
+ {"/", "/"},
+ {"/", "/file-at-root"},
+ {"C:", "C:\\file-at-root"},
+ {"/Linux path", "/Linux path/with spaces.txt"},
+ {"C:/ünicode/is", "C:\\ünicode\\is\\awésom.tar.gz"},
+ {"//SERVER/ünicode/is", "\\\\SERVER\\ünicode\\is\\awésom.tar.gz"},
+ }
+ for _, test := range tests {
+ assert.Equal(t,
+ test.expect, Dir(test.input),
+ "for input %q", test.input)
+ }
+}
+
+func TestJoin(t *testing.T) {
+ // Just to show how path.Join() behaves:
+ assert.Equal(t, "", path.Join())
+ assert.Equal(t, "", path.Join(""))
+ assert.Equal(t, "", path.Join("", ""))
+ assert.Equal(t, "a/b", path.Join("", "", "a", "", "b", ""))
+
+ tests := []struct {
+ expect string
+ input []string
+ }{
+ // Should behave the same as path.Join():
+ {"", []string{}},
+ {"", []string{""}},
+ {"", []string{"", ""}},
+ {"a/b", []string{"", "", "a", "", "b", ""}},
+
+ {"/file-at-root", []string{"/", "file-at-root"}},
+ {"C:/file-at-root", []string{"C:", "file-at-root"}},
+
+ {"/Linux path/with spaces.txt", []string{"/Linux path", "with spaces.txt"}},
+ {"C:/ünicode/is/awésom.tar.gz", []string{"C:\\ünicode", "is\\awésom.tar.gz"}},
+ {"//SERVER/mount/dir/file.txt", []string{"\\\\SERVER", "mount", "dir", "file.txt"}},
+ }
+ for _, test := range tests {
+ assert.Equal(t,
+ test.expect, Join(test.input...),
+ "for input %q", test.input)
+ }
+}
+
+func TestStem(t *testing.T) {
+ assert.Equal(t, "", Stem(""))
+ assert.Equal(t, "stem", Stem("stem.txt"))
+ assert.Equal(t, "stem.tar", Stem("stem.tar.gz"))
+ assert.Equal(t, "file", Stem("/path/to/file.txt"))
+ assert.Equal(t, "file", Stem("C:\\path\\to\\file.txt"))
+}