From d95f19ee44e84261b9b793cbbe7663639d44ecf8 Mon Sep 17 00:00:00 2001 From: altradits Date: Thu, 17 Sep 2026 16:21:15 +0300 Subject: [PATCH] Fix tests for Windows and add Windows to CI Some tests failed on Windows machines due to hardcoded POSIX path separators and assumptions about drive letters. Normalize test expectations across the suite: - internal/cache: use native path separators with filepath.Join and filepath.FromSlash for cache directory lookups. - internal/fs: construct file URIs using span.URIFromPath to properly encode Windows drive letters, and normalize path expectations in TestJoinPath and TestTTCN3Files with filepath.FromSlash. - internal/lsp: normalize expected file paths in hover tests with filepath.FromSlash, and dynamically determine the working drive in uri_windows_test.go so tests pass on CI runners using D:\. - project: use filepath.FromSlash for relative hooks_file assertions and handle Windows relative path behavior for "/file". - ci: add windows-latest to the test matrix, and enable Git core.longpaths on Windows before checkout to handle long paths in conformance tests. Fixes #629 --- .github/workflows/ci.yml | 17 +++++- internal/cache/cache_test.go | 8 ++- internal/fs/fs_test.go | 85 +++++++++++++++++---------- internal/lsp/hover_test.go | 5 +- internal/lsp/span/uri_windows_test.go | 28 +++++++-- project/project_test.go | 16 ++++- 6 files changed, 116 insertions(+), 43 deletions(-) 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) }) }