github.com/nycdavid/zeus@v0.0.0-20201208104106-9ba439429e03/go/zeusmaster/zeusmaster_test.go (about)

     1  package zeusmaster_test
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"net"
     9  	"os"
    10  	"path"
    11  	"path/filepath"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/burke/zeus/go/filemonitor"
    16  	slog "github.com/burke/zeus/go/shinylog"
    17  	"github.com/burke/zeus/go/unixsocket"
    18  	"github.com/burke/zeus/go/zeusclient"
    19  	"github.com/burke/zeus/go/zeusmaster"
    20  )
    21  
    22  var testFiles = map[string]string{
    23  	"zeus.json": `
    24  {
    25    "command": "ruby -r./custom_plan -eZeus.go",
    26    "plan": {
    27      "boot": {
    28        "data": {
    29          "data_srv": {}
    30        },
    31        "code": {
    32          "code_srv": {}
    33        },
    34        "cmd": []
    35      }
    36    }
    37  }
    38  `,
    39  	"custom_plan.rb": `
    40  $LOAD_PATH.unshift(File.readlink('./lib'))
    41  require 'zeus'
    42  
    43  class CustomPlan < Zeus::Plan
    44    def self.command(name, &block)
    45      define_method(name) do
    46        begin
    47          self.instance_eval(&block)
    48        rescue => e
    49          STDERR.puts "#{name} terminated with exception: #{e.message}"
    50          STDERR.puts e.backtrace.map {|line| " #{line}"}
    51          raise
    52        end
    53      end
    54    end
    55  
    56    command :boot do
    57      require_relative 'srv'
    58    end
    59  
    60    command :data do
    61      redirect_log('data')
    62      require_relative 'data'
    63    end
    64  
    65    command :code do
    66      redirect_log('code')
    67      require_relative 'code'
    68    end
    69  
    70    command :cmd do
    71      puts "bijagua"
    72      STDERR.puts "bazinga"
    73    end
    74  
    75    command :data_srv do
    76      redirect_log('data_srv')
    77      serve('data.sock')
    78    end
    79  
    80    command :code_srv do
    81      redirect_log('code_srv')
    82      serve('code.sock')
    83    end
    84  
    85    def redirect_log(cmd)
    86      log_file = File.open("zeus_test_#{cmd}.log", 'a')
    87      log_file.sync = true
    88      STDOUT.reopen(log_file)
    89      STDERR.reopen(log_file)
    90      STDOUT.sync = STDERR.sync = true
    91    end
    92  end
    93  
    94  Zeus.plan = CustomPlan.new
    95  `,
    96  	"data.rb": `
    97  require 'yaml'
    98  $response = YAML::load_file('data.yaml')['response']
    99  `,
   100  	"data.yaml": `
   101  response: YAML the Camel is a Mammal with Enamel
   102  `,
   103  	"other-data.yaml": `
   104  response: Hi
   105  `,
   106  	"code.rb": `
   107  $response = "Hello, world!"
   108  `,
   109  	"other-code.rb": `
   110  $response = "there!"
   111  `,
   112  	"srv.rb": `
   113  $response = "pong"
   114  
   115  def serve(sock_path)
   116    sock = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM, 0)
   117    sock.connect(Socket.pack_sockaddr_un(sock_path))
   118  
   119    b = sock.send($response, 0)
   120    puts "Wrote #{b} bytes to #{sock_path}"
   121  end
   122  `,
   123  }
   124  
   125  func writeTestFiles(dir string) error {
   126  	for name, contents := range testFiles {
   127  		if err := ioutil.WriteFile(path.Join(dir, name), []byte(contents), 0644); err != nil {
   128  			return fmt.Errorf("error writing %s: %v", name, err)
   129  		}
   130  	}
   131  
   132  	gempath := os.Getenv("ZEUS_TEST_GEMPATH")
   133  	if gempath == "" {
   134  		var err error
   135  		gempath, err = filepath.Abs("rubygem")
   136  		if err != nil {
   137  			return fmt.Errorf("error finding gempath: %v", err)
   138  		}
   139  	}
   140  
   141  	if err := os.Symlink(filepath.Join(gempath, "lib"), filepath.Join(dir, "lib")); err != nil {
   142  		return fmt.Errorf("error linking zeus gem: %v", err)
   143  	}
   144  
   145  	return nil
   146  }
   147  
   148  func enableTracing() {
   149  	slog.SetTraceLogger(slog.NewTraceLogger(os.Stderr))
   150  }
   151  
   152  func TestZeusBoots(t *testing.T) {
   153  	dir, err := ioutil.TempDir("", "zeus_test")
   154  	if err != nil {
   155  		t.Fatal(err)
   156  	}
   157  	defer os.RemoveAll(dir)
   158  
   159  	if err := writeTestFiles(dir); err != nil {
   160  		t.Fatal(err)
   161  	}
   162  
   163  	unixsocket.SetZeusSockName(filepath.Join(dir, ".zeus.sock"))
   164  
   165  	connections := map[string]*net.UnixConn{
   166  		"cmd":  nil,
   167  		"data": nil,
   168  		"code": nil,
   169  	}
   170  
   171  	for name := range connections {
   172  		sockName := filepath.Join(dir, fmt.Sprintf("%s.sock", name))
   173  
   174  		c, err := net.ListenUnixgram("unixgram", &net.UnixAddr{
   175  			Name: sockName, Net: "unixgram",
   176  		})
   177  		if err != nil {
   178  			t.Fatalf("Error opening %q socket: %v", sockName, err)
   179  		}
   180  		defer c.Close()
   181  
   182  		connections[name] = c
   183  	}
   184  
   185  	me, err := os.FindProcess(os.Getpid())
   186  	if err != nil {
   187  		t.Fatal(err)
   188  	}
   189  	defer me.Signal(os.Interrupt)
   190  
   191  	if err := os.Chdir(dir); err != nil {
   192  		t.Fatal(err)
   193  	}
   194  
   195  	// TODO: Find a way to redirect stdout so we can look for crashed
   196  	// processes.
   197  	enableTracing()
   198  	zexit := make(chan int)
   199  	go func() {
   200  		zexit <- zeusmaster.Run(filepath.Join(dir, "zeus.json"), filemonitor.DefaultFileChangeDelay, false)
   201  	}()
   202  
   203  	expects := map[string]string{
   204  		// TODO: Use the zeusclient to spawn a command to test
   205  		// that path.
   206  		// "cmd":  "pong",
   207  		"data": "YAML the Camel is a Mammal with Enamel",
   208  		"code": "Hello, world!",
   209  	}
   210  
   211  	for name, want := range expects {
   212  		if err := readAndCompare(connections[name], want); err != nil {
   213  			t.Fatalf("%s: %v", name, err)
   214  		}
   215  	}
   216  
   217  	time.Sleep(400 * time.Millisecond)
   218  
   219  	for _, f := range []string{"code.rb", "data.yaml"} {
   220  		from := filepath.Join(dir, fmt.Sprintf("other-%s", f))
   221  		to := filepath.Join(dir, f)
   222  		if err := os.Rename(from, to); err != nil {
   223  			t.Fatalf("Error renaming %s: %v", f, err)
   224  		}
   225  	}
   226  
   227  	expects = map[string]string{
   228  		"data": "Hi",
   229  		"code": "there!",
   230  	}
   231  
   232  	for name, want := range expects {
   233  		if err := readAndCompare(connections[name], want); err != nil {
   234  			t.Fatalf("%s: %v", name, err)
   235  		}
   236  	}
   237  
   238  	readCloser := make(chan struct{})
   239  	defer func() { close(readCloser) }()
   240  
   241  	cmdReader, cmdWriter, err := os.Pipe()
   242  
   243  	if err != nil {
   244  		t.Fatal(err)
   245  	}
   246  
   247  	cmdErrReader, cmdErrWriter, err := os.Pipe()
   248  
   249  	if err != nil {
   250  		t.Fatal(err)
   251  	}
   252  
   253  	cexit := make(chan int, 1)
   254  	go func() {
   255  		cexit <- zeusclient.Run([]string{"cmd"}, hangingReader{readCloser}, cmdWriter, cmdErrWriter)
   256  		time.Sleep(100 * time.Millisecond)
   257  		cmdWriter.Close()
   258  		cmdErrWriter.Close()
   259  	}()
   260  
   261  	have, err := ioutil.ReadAll(cmdReader)
   262  	if err != nil {
   263  		t.Fatal(err)
   264  	}
   265  	if want := "bijagua\n"; string(have) != want {
   266  		t.Errorf("expected %q, got %q", want, have)
   267  	}
   268  	if code := <-cexit; code != 0 {
   269  		t.Errorf("cmd exited with %d", code)
   270  	}
   271  
   272  	have, err = ioutil.ReadAll(cmdErrReader)
   273  	if err != nil {
   274  		t.Fatal(err)
   275  	}
   276  	if want := "bazinga\n"; string(have) != want {
   277  		t.Errorf("expected stderr %q, got %q", want, have)
   278  	}
   279  
   280  	// The zeusmaster catches the interrupt and exits gracefully
   281  	me.Signal(os.Interrupt)
   282  	if code := <-zexit; code != 0 {
   283  		t.Fatalf("Zeus exited with %d", code)
   284  	}
   285  }
   286  
   287  func readAndCompare(conn *net.UnixConn, want string) error {
   288  	buf := make([]byte, 128)
   289  
   290  	// File system events can take a long time to propagate
   291  	conn.SetDeadline(time.Now().Add(2 * time.Second))
   292  
   293  	if _, _, err := conn.ReadFrom(buf); err != nil {
   294  		return err
   295  	}
   296  	if have := string(bytes.Trim(buf, "\x00")); have != want {
   297  		return fmt.Errorf("expected %q, got %q", want, have)
   298  	}
   299  
   300  	return nil
   301  }
   302  
   303  type hangingReader struct {
   304  	close chan struct{}
   305  }
   306  
   307  func (r hangingReader) Read([]byte) (int, error) {
   308  	<-r.close
   309  	return 0, io.EOF
   310  }