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 }