github.com/marwan-at-work/consul@v1.4.5/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  }