diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 028e26539..264c484e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,15 +9,28 @@ on: jobs: test: name: Tests - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] steps: + # The TTCN-3 conformance test suite under + # testdata/ttcn3-conformance-tests/ has paths > 260 chars, + # which trips Windows' MAX_PATH limit during checkout. This + # config must run BEFORE actions/checkout so the clone itself + # can write the long paths. + - name: Enable Git long paths (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: git config --system core.longpaths true - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: stable - name: Test - run: go test -race -v ./... + run: go test -race ./... lint: name: Linting diff --git a/internal/cache/cache_test.go b/internal/cache/cache_test.go index 41ec80b6b..fe3d10b8c 100644 --- a/internal/cache/cache_test.go +++ b/internal/cache/cache_test.go @@ -2,6 +2,7 @@ package cache_test import ( "os" + "path/filepath" "testing" "github.com/nokia/ntt/internal/cache" @@ -14,7 +15,10 @@ func init() { } func TestLookup(t *testing.T) { - os.Setenv("NTT_CACHE", "testdata/cache") + // The cache directory is joined with filepath.Join in the + // implementation, so on Windows the expected separator is "\". + cacheDir := filepath.FromSlash("testdata/cache") + os.Setenv("NTT_CACHE", cacheDir) assert.Equal(t, "./file", cache.Lookup("./file")) assert.Equal(t, "./cache.go", cache.Lookup("./cache.go")) @@ -23,5 +27,5 @@ func TestLookup(t *testing.T) { assert.Equal(t, ".", cache.Lookup(".")) assert.Equal(t, "..", cache.Lookup("..")) assert.Equal(t, "cache.go", cache.Lookup("cache.go")) - assert.Equal(t, "testdata/cache/other.go", cache.Lookup("other.go")) + assert.Equal(t, filepath.Join(cacheDir, "other.go"), cache.Lookup("other.go")) } diff --git a/internal/fs/fs_test.go b/internal/fs/fs_test.go index 04709431f..8538e3592 100644 --- a/internal/fs/fs_test.go +++ b/internal/fs/fs_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/nokia/ntt/internal/fs" + "github.com/nokia/ntt/internal/lsp/span" "github.com/stretchr/testify/assert" ) @@ -23,7 +24,8 @@ func TestBytesFromURL(t *testing.T) { panic(err) } - f := fs.Open("file://" + path) + // Constructing the URL + f := fs.Open(string(span.URIFromPath(path))) b, err := f.Bytes() assert.Nil(t, err) assert.Equal(t, expected, b) @@ -32,83 +34,106 @@ func TestBytesFromURL(t *testing.T) { func TestCaching(t *testing.T) { assert.Equal(t, "package.yml", fs.Open("package.yml").Path()) - os.Setenv("NTT_CACHE", "testdata/cache") - assert.Equal(t, "testdata/cache/package.yml", fs.Open("package.yml").Path()) + cacheDir := filepath.FromSlash("testdata/cache") + os.Setenv("NTT_CACHE", cacheDir) + assert.Equal(t, filepath.Join(cacheDir, "package.yml"), fs.Open("package.yml").Path()) } func TestJoinPath(t *testing.T) { - tests := []struct { + // JoinPath returns OS-native file paths but keeps URLs untouched. + // We mark URL expectations explicitly so we don't accidentally + // run them through filepath.FromSlash. + type joinCase struct { first, second string want string - }{ - {"", "", ""}, - {".", "", "."}, - {".", "a", "a"}, - {"/", "b", "/b"}, - {"//", "c", "/c"}, - {"/", "/d", "/d"}, - {"e", "f", "e/f"}, - {"/g", "h", "/g/h"}, - {"/i", "../j", "/j"}, - {"file://k", "l", "file://k/l"}, - {"file:///m", "n", "file:///m/n"}, - {"file:///o", "../p", "file:///p"}, + isURL bool + } + tests := []joinCase{ + {"", "", "", false}, + {".", "", ".", false}, + {".", "a", "a", false}, + {"/", "b", "/b", false}, + {"//", "c", "/c", false}, + {"/", "/d", "/d", false}, + {"e", "f", "e/f", false}, + {"/g", "h", "/g/h", false}, + {"/i", "../j", "/j", false}, + {"file://k", "l", "file://k/l", true}, + {"file:///m", "n", "file:///m/n", true}, + {"file:///o", "../p", "file:///p", true}, } for _, test := range tests { + want := test.want + if !test.isURL { + want = filepath.FromSlash(want) + } got := fs.JoinPath(test.first, test.second) - assert.Equal(t, test.want, got) + assert.Equal(t, want, got) } } func TestTTCN3Files(t *testing.T) { + // fromSlash converts the slash-style literals we keep in this + // test to whatever path separator the host OS uses, so the + // suite runs on Windows as well as Unix. + fromSlash := func(paths []string) []string { + out := make([]string, len(paths)) + for i, p := range paths { + out[i] = filepath.FromSlash(p) + } + return out + } + t.Run("empty", func(t *testing.T) { got, err := fs.TTCN3Files() assert.Nil(t, err) assert.Nil(t, got) }) t.Run("dir", func(t *testing.T) { - got, err := fs.TTCN3Files("testdata/TestTTCN3Files") + got, err := fs.TTCN3Files(filepath.FromSlash("testdata/TestTTCN3Files")) assert.Nil(t, err) assert.Nil(t, got) }) t.Run("dir", func(t *testing.T) { - got, err := fs.TTCN3Files("testdata/TestTTCN3Files/some-dir") + got, err := fs.TTCN3Files(filepath.FromSlash("testdata/TestTTCN3Files/some-dir")) assert.Nil(t, err) assert.Nil(t, got) }) t.Run("dir", func(t *testing.T) { - want := []string{ + want := fromSlash([]string{ "testdata/TestTTCN3Files/ttcn3-dir/a.ttcn3", "testdata/TestTTCN3Files/ttcn3-dir/b.ttcn", "testdata/TestTTCN3Files/ttcn3-dir/c.ttcnpp", - } - got, err := fs.TTCN3Files("testdata/TestTTCN3Files/ttcn3-dir") + }) + got, err := fs.TTCN3Files(filepath.FromSlash("testdata/TestTTCN3Files/ttcn3-dir")) assert.Nil(t, err) assert.Equal(t, want, got) }) t.Run("errors", func(t *testing.T) { - want := []string{ + want := fromSlash([]string{ "testdata/TestTTCN3Files/xxx-dir/a.ttcn3", - } - got, err := fs.TTCN3Files("testdata/TestTTCN3Files/xxx-dir/a.ttcn3") + }) + got, err := fs.TTCN3Files(filepath.FromSlash("testdata/TestTTCN3Files/xxx-dir/a.ttcn3")) assert.True(t, errors.Is(err, os.ErrNotExist)) assert.Equal(t, want, got) }) t.Run("file", func(t *testing.T) { - want := []string{ + want := fromSlash([]string{ "testdata/TestTTCN3Files/ttcn3-dir/a.ttcn3", "testdata/TestTTCN3Files/ttcn3-dir/a.ttcn3", - } + }) got, err := fs.TTCN3Files( - "testdata/TestTTCN3Files/ttcn3-dir/a.ttcn3", - "testdata/TestTTCN3Files/ttcn3-dir/a.ttcn3", + filepath.FromSlash("testdata/TestTTCN3Files/ttcn3-dir/a.ttcn3"), + filepath.FromSlash("testdata/TestTTCN3Files/ttcn3-dir/a.ttcn3"), ) assert.Nil(t, err) assert.Equal(t, want, got) }) t.Run("URI", func(t *testing.T) { + // URIs always use forward slashes regardless of host OS, + // so no conversion here. want := []string{"foo://a.ttcn3"} got, err := fs.TTCN3Files("foo://a.ttcn3") assert.Nil(t, err) diff --git a/internal/lsp/hover_test.go b/internal/lsp/hover_test.go index e8160d133..5c11b2a54 100644 --- a/internal/lsp/hover_test.go +++ b/internal/lsp/hover_test.go @@ -2,6 +2,7 @@ package lsp_test import ( "fmt" + "path/filepath" "testing" "github.com/nokia/ntt/internal/fs" @@ -80,7 +81,7 @@ func TestPlainTextHoverForPortDefFromDecl(t *testing.T) { "port P p1\n" + "possible map / connect statements\n" + "_________________________________\n" + - "/TestPlainTextHoverForPortDefFromDecl.ttcn3:9\n" + filepath.FromSlash("/TestPlainTextHoverForPortDefFromDecl.ttcn3") + ":9\n" assert.Equal(t, expected, actual.Contents.Value) } @@ -105,7 +106,7 @@ func TestPlainTextHoverForPortDefFromUsage(t *testing.T) { "port P p1\n" + "possible map / connect statements\n" + "_________________________________\n" + - "/TestPlainTextHoverForPortDefFromUsage.ttcn3:9\n" + filepath.FromSlash("/TestPlainTextHoverForPortDefFromUsage.ttcn3") + ":9\n" assert.Equal(t, expected, actual.Contents.Value) } diff --git a/internal/lsp/span/uri_windows_test.go b/internal/lsp/span/uri_windows_test.go index 68233b1d7..9f5f2bb80 100644 --- a/internal/lsp/span/uri_windows_test.go +++ b/internal/lsp/span/uri_windows_test.go @@ -7,16 +7,36 @@ package span_test import ( + "os" + "strings" "testing" "github.com/nokia/ntt/internal/lsp/span" ) +// currentDrive returns the upper-case drive letter of the current +// working directory (e.g. "C"). We use this to build expectations in +// TestURIFromPath dynamically, because GitHub Actions Windows runners +// expose D: as the default drive whereas developers typically run on +// C: - either way the test should still pass. +func currentDrive(t *testing.T) string { + t.Helper() + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("getwd: %v", err) + } + if len(cwd) < 2 || cwd[1] != ':' { + t.Fatalf("cwd %q has no drive letter", cwd) + } + return strings.ToUpper(string(cwd[0])) +} + // TestURI tests the conversion between URIs and filenames. The test cases // include Windows-style URIs and filepaths, but we avoid having OS-specific // tests by using only forward slashes, assuming that the standard library // functions filepath.ToSlash and filepath.FromSlash do not need testing. func TestURIFromPath(t *testing.T) { + drive := currentDrive(t) for _, test := range []struct { path, wantFile string wantURI span.URI @@ -43,13 +63,13 @@ func TestURIFromPath(t *testing.T) { }, { path: `\path\to\dir`, - wantFile: `C:\path\to\dir`, - wantURI: span.URI("file:///C:/path/to/dir"), + wantFile: drive + `:\path\to\dir`, + wantURI: span.URI("file:///" + drive + ":/path/to/dir"), }, { path: `\a\b\c\src\bob.go`, - wantFile: `C:\a\b\c\src\bob.go`, - wantURI: span.URI("file:///C:/a/b/c/src/bob.go"), + wantFile: drive + `:\a\b\c\src\bob.go`, + wantURI: span.URI("file:///" + drive + ":/a/b/c/src/bob.go"), }, { path: `c:\Go\src\bob george\george\george.go`, diff --git a/project/project_test.go b/project/project_test.go index f9918a7a4..353324eef 100644 --- a/project/project_test.go +++ b/project/project_test.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "strings" "testing" "time" @@ -229,7 +230,9 @@ func TestWithManifest(t *testing.T) { t.Run("paths", func(t *testing.T) { c, err := manifest("foo/bar/package.yml", "hooks_file: file") assert.Nil(t, err) - assert.Equal(t, "foo/bar/file", c.HooksFile) + // hooks_file is resolved relative to the manifest, so the + // expectation must use the host's native separator. + assert.Equal(t, filepath.FromSlash("foo/bar/file"), c.HooksFile) }) t.Run("paths", func(t *testing.T) { c, err := manifest("foo/bar/package.yml", "hooks_file: $VAR") @@ -239,7 +242,14 @@ func TestWithManifest(t *testing.T) { t.Run("paths", func(t *testing.T) { c, err := manifest("foo/bar/package.yml", "hooks_file: /file") assert.Nil(t, err) - assert.Equal(t, "/file", c.HooksFile) + // On POSIX "/file" is absolute and stays as-is; on Windows + // it lacks a drive letter so it's treated as relative and + // joined with the manifest directory. + want := "/file" + if runtime.GOOS == "windows" { + want = filepath.FromSlash("foo/bar/file") + } + assert.Equal(t, want, c.HooksFile) }) t.Run("paths", func(t *testing.T) { c, err := manifest("foo/bar/package.yml", "hooks_file: https://file.txt") @@ -260,7 +270,7 @@ func TestWithManifest(t *testing.T) { VAR: file hooks_file: $VAR`) assert.Nil(t, err) - assert.Equal(t, "foo/bar/file", c.HooksFile) + assert.Equal(t, filepath.FromSlash("foo/bar/file"), c.HooksFile) }) }