internal/webserver_test_helper/webserver_test_helper.go (151 lines of code) (raw):
package webserver_test_helper //nolint:staticcheck
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"runtime"
)
const BinaryName = "gitlab-zoekt"
type WebserverHelper struct {
indexDir string
port int
}
type searchQuery struct {
Q string `json:"q"`
Opts searchOpts `json:"opts"`
RepoIDs []uint32 `json:"repoIDs"`
}
type searchOpts struct {
TotalMaxMatchCount int `json:"totalMaxMatchCount"`
NumContextLines int `json:"numContextLines"`
}
type SearchResponse struct {
Result searchResponseResult `json:"result"`
}
type searchResponseResult struct {
FileCount int `json:"fileCount"`
MatchCount int `json:"matchCount"`
Files []searchResponseFiles `json:"files"`
}
type searchResponseFiles struct {
FileName string `json:"fileName"`
Branches []string `json:"branches"`
}
func (h *SearchResponse) FileNames() []string {
files := make([]string, 0, len(h.Result.Files))
for _, f := range h.Result.Files {
files = append(files, f.FileName)
}
return files
}
// findProjectDir returns the project root directory.
// Since we know this file is in internal/webserver_test_helper,
// we can simply go up two directories to find the project root.
func findProjectDir() string {
// Get the current file's directory
_, filename, _, ok := runtime.Caller(0)
if !ok {
slog.Debug("Failed to get current file path")
return ""
}
// From internal/webserver_test_helper, go up two directories to project root
dir := filepath.Dir(filename) // dir of this file
projectRoot, err := filepath.Abs(filepath.Join(dir, "../..")) // go up two levels
if err != nil {
slog.Debug("Failed to determine project root", "error", err)
return ""
}
return projectRoot
}
func NewWebserverHelper(indexDir string, port int) *WebserverHelper {
return &WebserverHelper{
indexDir: indexDir,
port: port,
}
}
func (h *WebserverHelper) listen() string {
return fmt.Sprintf(":%d", h.port)
}
func (h *WebserverHelper) baseURL() string {
return fmt.Sprintf("http://localhost:%d", h.port)
}
func (h *WebserverHelper) searchURL() string {
res, err := url.JoinPath(h.baseURL(), "api", "search")
if err != nil {
panic(err)
}
return res
}
func (h *WebserverHelper) isReady() bool {
res, err := http.Head(h.baseURL()) //nolint:bodyclose,noctx
if err != nil {
return false
}
return res.StatusCode == 200
}
func (h *WebserverHelper) Start() (func(), error) {
// First, try to find the binary in the project's bin directory
projectDir := findProjectDir()
binaryPath := BinaryName
if projectDir != "" {
candidatePath := fmt.Sprintf("%s/bin/%s", projectDir, BinaryName)
if _, err := os.Stat(candidatePath); err == nil {
binaryPath = candidatePath
}
}
// If not found in project bin, check PATH
if binaryPath == BinaryName {
path, err := exec.LookPath(BinaryName)
if err != nil {
return func() {}, fmt.Errorf("binary %s not found in <PROJECT_DIR>/bin or PATH: %w", BinaryName, err)
}
binaryPath = path
}
cmd := exec.Command(binaryPath, "webserver", "-index_dir", h.indexDir, "-rpc", "-listen", h.listen()) //nolint:gosec
cmd.Stdout = os.Stdout
if err := cmd.Start(); err != nil {
return func() {}, err
}
for !h.isReady() {
}
cancelFunc := func() {
proc, err := os.FindProcess(cmd.Process.Pid)
if err != nil {
slog.Error("find process error", "err", err)
}
if err := proc.Kill(); err != nil {
slog.Error("kill error", "err", err)
}
if err := cmd.Wait(); err != nil {
slog.Error("wait() error", "err", err)
}
}
return cancelFunc, nil
}
func (h *WebserverHelper) Search(repoID uint32, term string) (SearchResponse, error) {
query := searchQuery{
Q: term,
RepoIDs: []uint32{repoID},
Opts: searchOpts{
TotalMaxMatchCount: 20,
NumContextLines: 0,
},
}
queryJSON, err := json.Marshal(query)
if err != nil {
return SearchResponse{}, err
}
res, err := http.Post(h.searchURL(), "application/json", bytes.NewBuffer(queryJSON)) //nolint:bodyclose,noctx
if err != nil {
return SearchResponse{}, err
}
if res.StatusCode != http.StatusOK {
return SearchResponse{}, errors.New("search failed with non OK")
}
var result SearchResponse
dec := json.NewDecoder(res.Body)
if err := dec.Decode(&result); err != nil {
return SearchResponse{}, err
}
return result, nil
}