src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/daemon/server_test.go (about)

     1  package daemon
     2  
     3  import (
     4  	"os"
     5  	"syscall"
     6  	"testing"
     7  	"time"
     8  
     9  	"src.elv.sh/pkg/daemon/daemondefs"
    10  	"src.elv.sh/pkg/daemon/internal/api"
    11  	"src.elv.sh/pkg/must"
    12  	. "src.elv.sh/pkg/prog/progtest"
    13  	"src.elv.sh/pkg/store/storetest"
    14  	"src.elv.sh/pkg/testutil"
    15  )
    16  
    17  func TestProgram_TerminatesIfCannotListen(t *testing.T) {
    18  	setup(t)
    19  	must.CreateEmpty("sock")
    20  
    21  	Test(t, &Program{},
    22  		ThatElvish("-daemon", "-sock", "sock", "-db", "db").
    23  			ExitsWith(2).
    24  			WritesStdoutContaining("failed to listen on sock"),
    25  	)
    26  }
    27  
    28  func TestProgram_ServesClientRequests(t *testing.T) {
    29  	setup(t)
    30  	startServer(t, cli("sock", "db"))
    31  	client := startClient(t, "sock")
    32  
    33  	// Test server state requests.
    34  	gotVersion, err := client.Version()
    35  	if gotVersion != api.Version || err != nil {
    36  		t.Errorf(".Version() -> (%v, %v), want (%v, nil)", gotVersion, err, api.Version)
    37  	}
    38  
    39  	gotPid, err := client.Pid()
    40  	wantPid := syscall.Getpid()
    41  	if gotPid != wantPid || err != nil {
    42  		t.Errorf(".Pid() -> (%v, %v), want (%v, nil)", gotPid, err, wantPid)
    43  	}
    44  
    45  	// Test store requests.
    46  	storetest.TestCmd(t, client)
    47  	storetest.TestDir(t, client)
    48  }
    49  
    50  func TestProgram_StillServesIfCannotOpenDB(t *testing.T) {
    51  	setup(t)
    52  	must.WriteFile("db", "not a valid bolt database")
    53  	startServer(t, cli("sock", "db"))
    54  	client := startClient(t, "sock")
    55  
    56  	_, err := client.AddCmd("cmd")
    57  	if err == nil {
    58  		t.Errorf("got nil error, want non-nil")
    59  	}
    60  }
    61  
    62  func TestProgram_QuitsOnSignalChannelWithNoClient(t *testing.T) {
    63  	setup(t)
    64  	sigCh := make(chan os.Signal)
    65  	startServerOpts(t, cli("sock", "db"), ServeOpts{Signals: sigCh})
    66  	close(sigCh)
    67  	// startServerSigCh will wait for server to terminate at cleanup
    68  }
    69  
    70  func TestProgram_QuitsOnSignalChannelWithClients(t *testing.T) {
    71  	setup(t)
    72  	sigCh := make(chan os.Signal)
    73  	server := startServerOpts(t, cli("sock", "db"), ServeOpts{Signals: sigCh})
    74  	client := startClient(t, "sock")
    75  	close(sigCh)
    76  
    77  	server.WaitQuit()
    78  	_, err := client.Version()
    79  	if err == nil {
    80  		t.Errorf("client.Version() returns nil error, want non-nil")
    81  	}
    82  }
    83  
    84  func TestProgram_BadCLI(t *testing.T) {
    85  	Test(t, &Program{},
    86  		ThatElvish().
    87  			ExitsWith(2).
    88  			WritesStderr("internal error: no suitable subprogram\n"),
    89  
    90  		ThatElvish("-daemon", "x").
    91  			ExitsWith(2).
    92  			WritesStderrContaining("arguments are not allowed with -daemon"),
    93  	)
    94  }
    95  
    96  func setup(t *testing.T) {
    97  	testutil.Umask(t, 0)
    98  	testutil.InTempDir(t)
    99  }
   100  
   101  // Calls startServerOpts with a Signals channel that gets closed during cleanup.
   102  func startServer(t *testing.T, args []string) server {
   103  	t.Helper()
   104  	sigCh := make(chan os.Signal)
   105  	s := startServerOpts(t, args, ServeOpts{Signals: sigCh})
   106  	// Cleanup functions added later are run earlier. This will be run before
   107  	// the cleanup function added by startServerOpts that waits for the server
   108  	// to terminate.
   109  	t.Cleanup(func() { close(sigCh) })
   110  	return s
   111  }
   112  
   113  // Start server with custom ServeOpts (opts.Ready is ignored). Makes sure that
   114  // the server terminates during cleanup.
   115  func startServerOpts(t *testing.T, args []string, opts ServeOpts) server {
   116  	t.Helper()
   117  	readyCh := make(chan struct{})
   118  	opts.Ready = readyCh
   119  	doneCh := make(chan serverResult)
   120  	go func() {
   121  		exit, stdout, stderr := Run(&Program{serveOpts: opts}, args...)
   122  		doneCh <- serverResult{exit, stdout, stderr}
   123  		close(doneCh)
   124  	}()
   125  	select {
   126  	case <-readyCh:
   127  	case <-time.After(testutil.Scaled(2 * time.Second)):
   128  		t.Fatal("timed out waiting for daemon to start")
   129  	}
   130  	s := server{t, doneCh}
   131  	t.Cleanup(func() { s.WaitQuit() })
   132  	return s
   133  }
   134  
   135  type server struct {
   136  	t  *testing.T
   137  	ch <-chan serverResult
   138  }
   139  
   140  type serverResult struct {
   141  	exit           int
   142  	stdout, stderr string
   143  }
   144  
   145  func (s server) WaitQuit() (serverResult, bool) {
   146  	s.t.Helper()
   147  	select {
   148  	case r := <-s.ch:
   149  		return r, true
   150  	case <-time.After(testutil.Scaled(2 * time.Second)):
   151  		s.t.Error("timed out waiting for daemon to quit")
   152  		return serverResult{}, false
   153  	}
   154  }
   155  
   156  func cli(sock, db string) []string {
   157  	return []string{"elvish", "-daemon", "-sock", sock, "-db", db}
   158  }
   159  
   160  func startClient(t *testing.T, sock string) daemondefs.Client {
   161  	cl := NewClient("sock")
   162  	if _, err := cl.Version(); err != nil {
   163  		t.Errorf("failed to start client: %v", err)
   164  	}
   165  	t.Cleanup(func() { cl.Close() })
   166  	return cl
   167  }