Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.wit text eol=lf
3 changes: 2 additions & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ jobs:
# Test with Go
test-go:
name: Test with Go
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
go-version: ["1.23", "1.24"]
steps:
- name: Checkout repo
Expand Down
13 changes: 4 additions & 9 deletions internal/go/gen/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,12 @@ func PackagePath(dir string) (string, error) {
file = filepath.Join(dir, "go.mod")
info, err := os.Stat(file)
if err != nil {
if dir == "/" {
parent := filepath.Dir(dir)
if parent == dir {
return "", errors.New("unable to locate a go.mod file")
}
// Pop up to parent dir
var rest string
dir, rest = filepath.Split(dir)
if dir == "" {
return "", errors.New("unable to locate a go.mod file")
}
dir = filepath.Clean(dir)
subdirs = path.Join(rest, subdirs)
subdirs = path.Join(filepath.Base(dir), subdirs)
dir = parent
continue
}
if info.IsDir() {
Expand Down
32 changes: 32 additions & 0 deletions internal/go/gen/module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package gen

import (
"os"
"path/filepath"
"testing"

"go.bytecodealliance.org/internal/relpath"
Expand All @@ -28,3 +29,34 @@ func TestPackagePath(t *testing.T) {
t.Errorf("PackagePath(%q): expected error, got nil", tmp)
}
}

func TestPackagePathNestedModule(t *testing.T) {
root, err := os.MkdirTemp("", t.Name()+"-")
if err != nil {
t.Fatal(err)
}
module := filepath.Join(root, "go.mod")
internal := filepath.Join(root, "internal")
nested := filepath.Join(internal, "bindings")
// TinyGo lacks recursive removal on Windows and WASI.
t.Cleanup(func() {
for _, path := range []string{nested, internal, module, root} {
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
t.Errorf("remove test fixture %s: %v", path, err)
}
}
})
if err := os.WriteFile(module, []byte("module example.com/plugin\n\ngo 1.26.0\n"), 0o600); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(nested, 0o700); err != nil {
t.Fatal(err)
}
got, err := PackagePath(nested)
if err != nil {
t.Fatal(err)
}
if want := "example.com/plugin/internal/bindings"; got != want {
t.Errorf("PackagePath(%q) = %q, want %q", nested, got, want)
}
}
77 changes: 70 additions & 7 deletions internal/witcli/dir_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,21 @@ import (
"errors"
"io/fs"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
)

func TestFindOrCreateDirFind(t *testing.T) {
temp := t.TempDir()
temp, err := os.MkdirTemp("", t.Name()+"-")
if err != nil {
t.Fatal(err)
}

dir := filepath.Join(temp, "existing")
cleanupEmptyDirs(t, dir, temp)
if err := os.Mkdir(dir, os.ModeDir|os.ModePerm); err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -47,9 +52,20 @@ func TestFindOrCreateDirFind(t *testing.T) {
}

func TestFindOrCreateDirCreate(t *testing.T) {
temp := t.TempDir()

temp, err := os.MkdirTemp("", t.Name()+"-")
if err != nil {
t.Fatal(err)
}
control := filepath.Join(temp, "control")
dir := filepath.Join(temp, "new")
cleanupEmptyDirs(t, dir, control, temp)
if err := os.Mkdir(control, 0o755); err != nil {
t.Fatal(err)
}
controlInfo, err := os.Stat(control)
if err != nil {
t.Fatal(err)
}

if _, err := os.Stat(dir); !os.IsNotExist(err) {
t.Errorf("Expected %s to not exist, but it did.", dir)
Expand All @@ -59,19 +75,42 @@ func TestFindOrCreateDirCreate(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if info == nil || !info.IsDir() {
t.Fatalf("FindOrCreateDir returned non-directory information: %v", info)
}
createdInfo, err := os.Stat(dir)
if err != nil {
t.Fatal(err)
}
if !createdInfo.IsDir() {
t.Fatal("FindOrCreateDir did not create a directory")
}

// Skip remaining tests on incompatible platforms
if runtime.Compiler == "tinygo" || strings.Contains(runtime.GOARCH, "wasm") {
return
}

gotPerm := info.Mode().Perm()
wantPerm := os.FileMode(0o755)
// Compare filesystem-applied permissions, including umask and Windows mode mapping.
wantPerm := controlInfo.Mode().Perm()
if gotPerm != wantPerm {
t.Errorf("FindOrCreateDirExisting returned directory with file permissions %v; want %v", gotPerm, wantPerm)
t.Errorf("FindOrCreateDir returned directory with file permissions %v; want %v", gotPerm, wantPerm)
}
}

// TinyGo lacks recursive removal on Windows and WASI; remove owned leaves first.
func cleanupEmptyDirs(t *testing.T, paths ...string) {
t.Helper()
t.Cleanup(func() {
for _, path := range paths {
if err := os.Remove(path); err != nil && !errors.Is(err, fs.ErrNotExist) {
t.Errorf("remove test directory %s: %v", path, err)
}
}
})
}

func TestFindOrCreateDirPermissionDenied(t *testing.T) {
// Skip test on incompatible platforms
if runtime.Compiler == "tinygo" || strings.Contains(runtime.GOARCH, "wasm") {
Expand All @@ -81,14 +120,38 @@ func TestFindOrCreateDirPermissionDenied(t *testing.T) {
temp := t.TempDir()

protected := filepath.Join(temp, "protected")
if err := os.Mkdir(protected, os.FileMode(0o400)); err != nil {
if err := os.Mkdir(protected, 0o755); err != nil {
t.Fatal(err)
}
if runtime.GOOS == "windows" {
// Windows directory creation uses ACLs; chmod does not remove this permission.
icacls := filepath.Join(os.Getenv("SystemRoot"), "System32", "icacls.exe")
if output, err := exec.Command(icacls, protected, "/deny", "*S-1-1-0:(AD)").CombinedOutput(); err != nil {
t.Fatalf("deny subdirectory creation: %v\n%s", err, output)
}
t.Cleanup(func() {
if output, err := exec.Command(icacls, protected, "/remove:d", "*S-1-1-0").CombinedOutput(); err != nil {
t.Errorf("restore directory permissions: %v\n%s", err, output)
}
})
} else {
if err := os.Chmod(protected, 0o400); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := os.Chmod(protected, 0o755); err != nil {
t.Errorf("restore directory permissions: %v", err)
}
})
}

dir := filepath.Join(protected, "permission-denied")

_, err := FindOrCreateDir(dir)
info, err := FindOrCreateDir(dir)
if !errors.Is(err, fs.ErrPermission) {
t.Errorf("Expected creation within protected directory to fail, but it did not: %v", err)
}
if info != nil {
t.Errorf("failed creation returned file information: %v", info)
}
}
9 changes: 6 additions & 3 deletions wit/bindgen/testdata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"io/fs"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"sync"
Expand Down Expand Up @@ -86,7 +85,7 @@ func validateGeneratedGo(t *testing.T, res *wit.Resolve, origin string) {
return
}

dir := path.Join(generatedPath, origin)
dir := filepath.Join(generatedPath, origin)
err := os.MkdirAll(dir, fs.ModePerm)
if err != nil {
t.Error(err)
Expand Down Expand Up @@ -218,8 +217,12 @@ func TestGenerateTestdata(t *testing.T) {
return
}
err := loadTestdata(func(path string, res *wit.Resolve) error {
relative, err := filepath.Rel(testdataPath, path)
if err != nil {
return err
}
t.Run(path, func(t *testing.T) {
origin := strings.TrimSuffix(strings.TrimPrefix(path, testdataPath), ".wit.json")
origin := strings.TrimSuffix(relative, ".wit.json")
validateGeneratedGo(t, res, origin)
})
return nil
Expand Down
5 changes: 3 additions & 2 deletions wit/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,17 @@ func loadWIT(path string, reader io.Reader) (*Resolve, error) {

if path != "" {
path = strings.TrimPrefix(path, "./")
args = append(args, path)
args = append(args, filepath.ToSlash(path))
dir := filepath.Dir(path)
fsMap[dir] = os.DirFS(dir)
fsMap[filepath.ToSlash(dir)] = os.DirFS(dir)
} else {
stdin = reader
}
wasmTools, err := wasmtools.New(ctx)
if err != nil {
return nil, err
}
defer wasmTools.Close(ctx)
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
err = wasmTools.Run(ctx, stdin, stdout, stderr, fsMap, args...)
Expand Down
83 changes: 83 additions & 0 deletions wit/load_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//go:build !tinygo

package wit

import (
"os"
"path/filepath"
"testing"
)

func TestLoadWITPaths(t *testing.T) {
// Relative paths require the fixture and working directory to share a volume.
root, err := os.MkdirTemp(".", ".wit-load-paths-*")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := os.RemoveAll(root); err != nil {
t.Error(err)
}
})
root, err = filepath.Abs(root)
if err != nil {
t.Fatal(err)
}
file := filepath.Join(root, "nested", "example.wit")
if err := os.MkdirAll(filepath.Dir(file), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(file, []byte("package example:paths; world example {}"), 0o644); err != nil {
t.Fatal(err)
}
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
rel, err := filepath.Rel(cwd, file)
if err != nil {
t.Fatal(err)
}
for _, input := range []struct {
name string
path string
}{
{"absolute", file},
{"absolute slash", filepath.ToSlash(file)},
{"relative", rel},
{"relative slash", filepath.ToSlash(rel)},
} {
t.Run(input.name, func(t *testing.T) {
resolved, err := LoadWIT(input.path)
if err != nil {
t.Fatal(err)
}
if len(resolved.Worlds) != 1 || resolved.Worlds[0].Name != "example" {
t.Fatalf("resolved worlds: %v", resolved.Worlds)
}
})
}
}

func TestLoadWITDirectoryDependencies(t *testing.T) {
root := t.TempDir()
dependency := filepath.Join(root, "deps", "dep")
if err := os.MkdirAll(dependency, 0o755); err != nil {
t.Fatal(err)
}
for name, text := range map[string]string{
filepath.Join(root, "main.wit"): "package example:paths; world example { import example:dep/api; }",
filepath.Join(dependency, "api.wit"): "package example:dep; interface api { ping: func(); }",
} {
if err := os.WriteFile(name, []byte(text), 0o644); err != nil {
t.Fatal(err)
}
}
resolved, err := LoadWIT(root)
if err != nil {
t.Fatal(err)
}
if len(resolved.Packages) != 2 || len(resolved.Worlds) != 1 || resolved.Worlds[0].Name != "example" {
t.Fatalf("resolved packages/worlds: %v / %v", resolved.Packages, resolved.Worlds)
}
}
25 changes: 25 additions & 0 deletions wit/load_tinygo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//go:build tinygo

package wit

import (
"strings"
"testing"
)

func TestLoadWITUnsupportedBackend(t *testing.T) {
for _, input := range []struct {
name string
load func() (*Resolve, error)
}{
{"file", func() (*Resolve, error) { return LoadWIT("example.wit") }},
{"reader", func() (*Resolve, error) { return DecodeWIT(strings.NewReader("package example:paths;")) }},
} {
t.Run(input.name, func(t *testing.T) {
resolved, err := input.load()
if resolved != nil || err == nil || !strings.Contains(err.Error(), "wasm-tools disabled under TinyGo") {
t.Fatalf("expected disabled-backend error, got %v, %v", resolved, err)
}
})
}
}
Loading