github.com/Aestek/consul@v1.2.4-0.20190309222502-b2c31e33971a/command/debug/debug_test.go (about) 1 package debug 2 3 import ( 4 "archive/tar" 5 "compress/gzip" 6 "fmt" 7 "io" 8 "os" 9 "path/filepath" 10 "strings" 11 "testing" 12 13 "github.com/hashicorp/consul/agent" 14 "github.com/hashicorp/consul/logger" 15 "github.com/hashicorp/consul/testrpc" 16 "github.com/hashicorp/consul/testutil" 17 "github.com/mitchellh/cli" 18 ) 19 20 func TestDebugCommand_noTabs(t *testing.T) { 21 t.Parallel() 22 23 if strings.ContainsRune(New(cli.NewMockUi(), nil).Help(), '\t') { 24 t.Fatal("help has tabs") 25 } 26 } 27 28 func TestDebugCommand(t *testing.T) { 29 t.Parallel() 30 31 testDir := testutil.TempDir(t, "debug") 32 defer os.RemoveAll(testDir) 33 34 a := agent.NewTestAgent(t, t.Name(), ` 35 enable_debug = true 36 `) 37 a.Agent.LogWriter = logger.NewLogWriter(512) 38 39 defer a.Shutdown() 40 testrpc.WaitForLeader(t, a.RPC, "dc1") 41 42 ui := cli.NewMockUi() 43 cmd := New(ui, nil) 44 cmd.validateTiming = false 45 46 outputPath := fmt.Sprintf("%s/debug", testDir) 47 args := []string{ 48 "-http-addr=" + a.HTTPAddr(), 49 "-output=" + outputPath, 50 "-duration=100ms", 51 "-interval=50ms", 52 } 53 54 code := cmd.Run(args) 55 56 if code != 0 { 57 t.Errorf("should exit 0, got code: %d", code) 58 } 59 60 errOutput := ui.ErrorWriter.String() 61 if errOutput != "" { 62 t.Errorf("expected no error output, got %q", errOutput) 63 } 64 } 65 66 func TestDebugCommand_Archive(t *testing.T) { 67 t.Parallel() 68 69 testDir := testutil.TempDir(t, "debug") 70 defer os.RemoveAll(testDir) 71 72 a := agent.NewTestAgent(t, t.Name(), ` 73 enable_debug = true 74 `) 75 defer a.Shutdown() 76 testrpc.WaitForLeader(t, a.RPC, "dc1") 77 78 ui := cli.NewMockUi() 79 cmd := New(ui, nil) 80 cmd.validateTiming = false 81 82 outputPath := fmt.Sprintf("%s/debug", testDir) 83 args := []string{ 84 "-http-addr=" + a.HTTPAddr(), 85 "-output=" + outputPath, 86 "-capture=agent", 87 } 88 89 if code := cmd.Run(args); code != 0 { 90 t.Fatalf("should exit 0, got code: %d", code) 91 } 92 93 archivePath := fmt.Sprintf("%s%s", outputPath, debugArchiveExtension) 94 file, err := os.Open(archivePath) 95 if err != nil { 96 t.Fatalf("failed to open archive: %s", err) 97 } 98 gz, err := gzip.NewReader(file) 99 if err != nil { 100 t.Fatalf("failed to read gzip archive: %s", err) 101 } 102 tr := tar.NewReader(gz) 103 104 for { 105 h, err := tr.Next() 106 107 if err == io.EOF { 108 break 109 } 110 if err != nil { 111 t.Fatalf("failed to read file in archive: %s", err) 112 } 113 114 // ignore the outer directory 115 if h.Name == "debug" { 116 continue 117 } 118 119 // should only contain this one capture target 120 if h.Name != "debug/agent.json" && h.Name != "debug/index.json" { 121 t.Fatalf("archive contents do not match: %s", h.Name) 122 } 123 } 124 125 } 126 127 func TestDebugCommand_ArgsBad(t *testing.T) { 128 t.Parallel() 129 130 testDir := testutil.TempDir(t, "debug") 131 defer os.RemoveAll(testDir) 132 133 ui := cli.NewMockUi() 134 cmd := New(ui, nil) 135 136 args := []string{ 137 "foo", 138 "bad", 139 } 140 141 if code := cmd.Run(args); code == 0 { 142 t.Fatalf("should exit non-zero, got code: %d", code) 143 } 144 145 errOutput := ui.ErrorWriter.String() 146 if !strings.Contains(errOutput, "Too many arguments") { 147 t.Errorf("expected error output, got %q", errOutput) 148 } 149 } 150 151 func TestDebugCommand_OutputPathBad(t *testing.T) { 152 t.Parallel() 153 154 testDir := testutil.TempDir(t, "debug") 155 defer os.RemoveAll(testDir) 156 157 a := agent.NewTestAgent(t, t.Name(), "") 158 defer a.Shutdown() 159 testrpc.WaitForLeader(t, a.RPC, "dc1") 160 161 ui := cli.NewMockUi() 162 cmd := New(ui, nil) 163 cmd.validateTiming = false 164 165 outputPath := "" 166 args := []string{ 167 "-http-addr=" + a.HTTPAddr(), 168 "-output=" + outputPath, 169 "-duration=100ms", 170 "-interval=50ms", 171 } 172 173 if code := cmd.Run(args); code == 0 { 174 t.Fatalf("should exit non-zero, got code: %d", code) 175 } 176 177 errOutput := ui.ErrorWriter.String() 178 if !strings.Contains(errOutput, "no such file or directory") { 179 t.Errorf("expected error output, got %q", errOutput) 180 } 181 } 182 183 func TestDebugCommand_OutputPathExists(t *testing.T) { 184 t.Parallel() 185 186 testDir := testutil.TempDir(t, "debug") 187 defer os.RemoveAll(testDir) 188 189 a := agent.NewTestAgent(t, t.Name(), "") 190 a.Agent.LogWriter = logger.NewLogWriter(512) 191 defer a.Shutdown() 192 testrpc.WaitForLeader(t, a.RPC, "dc1") 193 194 ui := cli.NewMockUi() 195 cmd := New(ui, nil) 196 cmd.validateTiming = false 197 198 outputPath := fmt.Sprintf("%s/debug", testDir) 199 args := []string{ 200 "-http-addr=" + a.HTTPAddr(), 201 "-output=" + outputPath, 202 "-duration=100ms", 203 "-interval=50ms", 204 } 205 206 // Make a directory that conflicts with the output path 207 err := os.Mkdir(outputPath, 0755) 208 if err != nil { 209 t.Fatalf("duplicate test directory creation failed: %s", err) 210 } 211 212 if code := cmd.Run(args); code == 0 { 213 t.Fatalf("should exit non-zero, got code: %d", code) 214 } 215 216 errOutput := ui.ErrorWriter.String() 217 if !strings.Contains(errOutput, "directory already exists") { 218 t.Errorf("expected error output, got %q", errOutput) 219 } 220 } 221 222 func TestDebugCommand_CaptureTargets(t *testing.T) { 223 t.Parallel() 224 225 cases := map[string]struct { 226 // used in -target param 227 targets []string 228 // existence verified after execution 229 files []string 230 // non-existence verified after execution 231 excludedFiles []string 232 }{ 233 "single": { 234 []string{"agent"}, 235 []string{"agent.json"}, 236 []string{"host.json", "cluster.json"}, 237 }, 238 "static": { 239 []string{"agent", "host", "cluster"}, 240 []string{"agent.json", "host.json", "cluster.json"}, 241 []string{"*/metrics.json"}, 242 }, 243 "metrics-only": { 244 []string{"metrics"}, 245 []string{"*/metrics.json"}, 246 []string{"agent.json", "host.json", "cluster.json"}, 247 }, 248 "all-but-pprof": { 249 []string{ 250 "metrics", 251 "logs", 252 "host", 253 "agent", 254 "cluster", 255 }, 256 []string{ 257 "host.json", 258 "agent.json", 259 "cluster.json", 260 "*/metrics.json", 261 "*/consul.log", 262 }, 263 []string{}, 264 }, 265 } 266 267 for name, tc := range cases { 268 testDir := testutil.TempDir(t, "debug") 269 defer os.RemoveAll(testDir) 270 271 a := agent.NewTestAgent(t, t.Name(), ` 272 enable_debug = true 273 `) 274 a.Agent.LogWriter = logger.NewLogWriter(512) 275 276 defer a.Shutdown() 277 testrpc.WaitForLeader(t, a.RPC, "dc1") 278 279 ui := cli.NewMockUi() 280 cmd := New(ui, nil) 281 cmd.validateTiming = false 282 283 outputPath := fmt.Sprintf("%s/debug-%s", testDir, name) 284 args := []string{ 285 "-http-addr=" + a.HTTPAddr(), 286 "-output=" + outputPath, 287 "-archive=false", 288 "-duration=100ms", 289 "-interval=50ms", 290 } 291 for _, t := range tc.targets { 292 args = append(args, "-capture="+t) 293 } 294 295 if code := cmd.Run(args); code != 0 { 296 t.Fatalf("should exit 0, got code: %d", code) 297 } 298 299 errOutput := ui.ErrorWriter.String() 300 if errOutput != "" { 301 t.Errorf("expected no error output, got %q", errOutput) 302 } 303 304 // Ensure the debug data was written 305 _, err := os.Stat(outputPath) 306 if err != nil { 307 t.Fatalf("output path should exist: %s", err) 308 } 309 310 // Ensure the captured static files exist 311 for _, f := range tc.files { 312 path := fmt.Sprintf("%s/%s", outputPath, f) 313 // Glob ignores file system errors 314 fs, _ := filepath.Glob(path) 315 if len(fs) <= 0 { 316 t.Fatalf("%s: output data should exist for %s", name, f) 317 } 318 } 319 320 // Ensure any excluded files do not exist 321 for _, f := range tc.excludedFiles { 322 path := fmt.Sprintf("%s/%s", outputPath, f) 323 // Glob ignores file system errors 324 fs, _ := filepath.Glob(path) 325 if len(fs) > 0 { 326 t.Fatalf("%s: output data should not exist for %s", name, f) 327 } 328 } 329 } 330 } 331 332 func TestDebugCommand_ProfilesExist(t *testing.T) { 333 t.Parallel() 334 335 testDir := testutil.TempDir(t, "debug") 336 defer os.RemoveAll(testDir) 337 338 a := agent.NewTestAgent(t, t.Name(), ` 339 enable_debug = true 340 `) 341 a.Agent.LogWriter = logger.NewLogWriter(512) 342 defer a.Shutdown() 343 testrpc.WaitForLeader(t, a.RPC, "dc1") 344 345 ui := cli.NewMockUi() 346 cmd := New(ui, nil) 347 cmd.validateTiming = false 348 349 outputPath := fmt.Sprintf("%s/debug", testDir) 350 println(outputPath) 351 args := []string{ 352 "-http-addr=" + a.HTTPAddr(), 353 "-output=" + outputPath, 354 // CPU profile has a minimum of 1s 355 "-archive=false", 356 "-duration=1s", 357 "-interval=1s", 358 "-capture=pprof", 359 } 360 361 if code := cmd.Run(args); code != 0 { 362 t.Fatalf("should exit 0, got code: %d", code) 363 } 364 365 profiles := []string{"heap.prof", "profile.prof", "goroutine.prof", "trace.out"} 366 // Glob ignores file system errors 367 for _, v := range profiles { 368 fs, _ := filepath.Glob(fmt.Sprintf("%s/*/%s", outputPath, v)) 369 if len(fs) == 0 { 370 t.Errorf("output data should exist for %s", v) 371 } 372 } 373 374 errOutput := ui.ErrorWriter.String() 375 if errOutput != "" { 376 t.Errorf("expected no error output, got %s", errOutput) 377 } 378 } 379 380 func TestDebugCommand_ValidateTiming(t *testing.T) { 381 t.Parallel() 382 383 cases := map[string]struct { 384 duration string 385 interval string 386 output string 387 code int 388 }{ 389 "both": { 390 "20ms", 391 "10ms", 392 "duration must be longer", 393 1, 394 }, 395 "short interval": { 396 "10s", 397 "10ms", 398 "interval must be longer", 399 1, 400 }, 401 "lower duration": { 402 "20s", 403 "30s", 404 "must be longer than interval", 405 1, 406 }, 407 } 408 409 for name, tc := range cases { 410 // Because we're only testng validation, we want to shut down 411 // the valid duration test to avoid hanging 412 shutdownCh := make(chan struct{}) 413 414 testDir := testutil.TempDir(t, "debug") 415 defer os.RemoveAll(testDir) 416 417 a := agent.NewTestAgent(t, t.Name(), "") 418 defer a.Shutdown() 419 testrpc.WaitForLeader(t, a.RPC, "dc1") 420 421 ui := cli.NewMockUi() 422 cmd := New(ui, shutdownCh) 423 424 args := []string{ 425 "-http-addr=" + a.HTTPAddr(), 426 "-duration=" + tc.duration, 427 "-interval=" + tc.interval, 428 "-capture=agent", 429 } 430 code := cmd.Run(args) 431 432 if code != tc.code { 433 t.Errorf("%s: should exit %d, got code: %d", name, tc.code, code) 434 } 435 436 errOutput := ui.ErrorWriter.String() 437 if !strings.Contains(errOutput, tc.output) { 438 t.Errorf("%s: expected error output '%s', got '%q'", name, tc.output, errOutput) 439 } 440 } 441 } 442 443 func TestDebugCommand_DebugDisabled(t *testing.T) { 444 t.Parallel() 445 446 testDir := testutil.TempDir(t, "debug") 447 defer os.RemoveAll(testDir) 448 449 a := agent.NewTestAgent(t, t.Name(), ` 450 enable_debug = false 451 `) 452 a.Agent.LogWriter = logger.NewLogWriter(512) 453 defer a.Shutdown() 454 testrpc.WaitForLeader(t, a.RPC, "dc1") 455 456 ui := cli.NewMockUi() 457 cmd := New(ui, nil) 458 cmd.validateTiming = false 459 460 outputPath := fmt.Sprintf("%s/debug", testDir) 461 args := []string{ 462 "-http-addr=" + a.HTTPAddr(), 463 "-output=" + outputPath, 464 "-archive=false", 465 // CPU profile has a minimum of 1s 466 "-duration=1s", 467 "-interval=1s", 468 } 469 470 if code := cmd.Run(args); code != 0 { 471 t.Fatalf("should exit 0, got code: %d", code) 472 } 473 474 profiles := []string{"heap.prof", "profile.prof", "goroutine.prof", "trace.out"} 475 // Glob ignores file system errors 476 for _, v := range profiles { 477 fs, _ := filepath.Glob(fmt.Sprintf("%s/*/%s", outputPath, v)) 478 if len(fs) > 0 { 479 t.Errorf("output data should not exist for %s", v) 480 } 481 } 482 483 errOutput := ui.ErrorWriter.String() 484 if !strings.Contains(errOutput, "Unable to capture pprof") { 485 t.Errorf("expected warn output, got %s", errOutput) 486 } 487 }