package app import ( "os" "path/filepath" "testing" "codeberg.org/turbo-editors/turbo-core/profile" ) // touch creates an empty file, making its directory first. func touch(t *testing.T, path string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatalf("creating %s: %v", filepath.Dir(path), err) } if err := os.WriteFile(path, nil, 0o644); err != nil { t.Fatalf("writing %s: %v", path, err) } } func TestProjectRootWalksUpToTheMarker(t *testing.T) { root := t.TempDir() touch(t, filepath.Join(root, "go.mod")) deep := filepath.Join(root, "internal", "app", "app.go") touch(t, deep) got := ProjectRoot(profile.Profile{RootMarkers: []string{"go.mod"}}, []string{deep}) if got != root { t.Errorf("ProjectRoot() = %q, want %q", got, root) } } func TestProjectRootTakesTheFirstMarkerItMeetsGoingUp(t *testing.T) { // A nested module is its own project; walking past it to the outer one // would hand the language server a workspace the file is not in. outer := t.TempDir() touch(t, filepath.Join(outer, "Cargo.toml")) inner := filepath.Join(outer, "crates", "parser") touch(t, filepath.Join(inner, "Cargo.toml")) file := filepath.Join(inner, "src", "lib.rs") touch(t, file) got := ProjectRoot(profile.Profile{RootMarkers: []string{"Cargo.toml"}}, []string{file}) if got != inner { t.Errorf("ProjectRoot() = %q, want the nearest root %q", got, inner) } } func TestProjectRootAcceptsAnyOfTheMarkers(t *testing.T) { root := t.TempDir() touch(t, filepath.Join(root, "Cargo.toml")) file := filepath.Join(root, "src", "main.rs") touch(t, file) got := ProjectRoot(profile.Profile{RootMarkers: []string{"rust-toolchain.toml", "Cargo.toml"}}, []string{file}) if got != root { t.Errorf("ProjectRoot() = %q, want %q", got, root) } } func TestProjectRootFallsBackToTheFilesOwnDirectory(t *testing.T) { // A file outside any project still has to give the server somewhere; the // filesystem root would be absurd. dir := t.TempDir() file := filepath.Join(dir, "scratch.go") touch(t, file) got := ProjectRoot(profile.Profile{RootMarkers: []string{"no-such-marker-file"}}, []string{file}) if got != dir { t.Errorf("ProjectRoot() = %q, want %q", got, dir) } } func TestProjectRootWithNoFilesUsesTheWorkingDirectory(t *testing.T) { dir := t.TempDir() touch(t, filepath.Join(dir, "go.mod")) t.Chdir(dir) got := ProjectRoot(profile.Profile{RootMarkers: []string{"go.mod"}}, nil) // The temporary directory may be reached through a symlink, so the // comparison is on what the two resolve to. if resolve(t, got) != resolve(t, dir) { t.Errorf("ProjectRoot() = %q, want %q", got, dir) } } func TestAProfileWithNoMarkersDoesNotWalk(t *testing.T) { // An editor for a language with no project file at all: the directory the // file is in is the best answer available. dir := t.TempDir() file := filepath.Join(dir, "notes.txt") touch(t, file) got := ProjectRoot(profile.Profile{}, []string{file}) if got != dir { t.Errorf("ProjectRoot() = %q, want %q", got, dir) } } // resolve follows symlinks, which is what /tmp is on some systems. func resolve(t *testing.T, path string) string { t.Helper() resolved, err := filepath.EvalSymlinks(path) if err != nil { return path } return resolved }