github.com/google/cloudprober@v0.11.3/probes/external/external_test.go (about)

     1  // Copyright 2017-2020 The Cloudprober Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package external
    16  
    17  import (
    18  	"bufio"
    19  	"bytes"
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"os"
    25  	"os/exec"
    26  	"reflect"
    27  	"strings"
    28  	"testing"
    29  	"time"
    30  
    31  	"github.com/golang/protobuf/proto"
    32  	"github.com/google/cloudprober/metrics"
    33  	payloadconfigpb "github.com/google/cloudprober/metrics/payload/proto"
    34  	"github.com/google/cloudprober/metrics/testutils"
    35  	configpb "github.com/google/cloudprober/probes/external/proto"
    36  	serverpb "github.com/google/cloudprober/probes/external/proto"
    37  	"github.com/google/cloudprober/probes/external/serverutils"
    38  	"github.com/google/cloudprober/probes/options"
    39  	probeconfigpb "github.com/google/cloudprober/probes/proto"
    40  	"github.com/google/cloudprober/targets"
    41  	"github.com/google/cloudprober/targets/endpoint"
    42  )
    43  
    44  func isDone(doneChan chan struct{}) bool {
    45  	// If we are done, return immediately.
    46  	select {
    47  	case <-doneChan:
    48  		return true
    49  	default:
    50  	}
    51  	return false
    52  }
    53  
    54  // startProbeServer starts a test probe server to work with the TestProbeServer
    55  // test below.
    56  func startProbeServer(t *testing.T, testPayload string, r io.Reader, w io.WriteCloser, doneChan chan struct{}) {
    57  	rd := bufio.NewReader(r)
    58  	for {
    59  		if isDone(doneChan) {
    60  			return
    61  		}
    62  
    63  		req, err := serverutils.ReadProbeRequest(rd)
    64  		if err != nil {
    65  			// Normal failure because we are finished.
    66  			if isDone(doneChan) {
    67  				return
    68  			}
    69  			t.Errorf("Error reading probe request. Err: %v", err)
    70  			return
    71  		}
    72  		var action, target string
    73  		opts := req.GetOptions()
    74  		for _, opt := range opts {
    75  			if opt.GetName() == "action" {
    76  				action = opt.GetValue()
    77  				continue
    78  			}
    79  			if opt.GetName() == "target" {
    80  				target = opt.GetValue()
    81  				continue
    82  			}
    83  		}
    84  		id := req.GetRequestId()
    85  
    86  		actionToResponse := map[string]*serverpb.ProbeReply{
    87  			"nopayload": &serverpb.ProbeReply{RequestId: proto.Int32(id)},
    88  			"payload": &serverpb.ProbeReply{
    89  				RequestId: proto.Int32(id),
    90  				Payload:   proto.String(testPayload),
    91  			},
    92  			"payload_with_error": &serverpb.ProbeReply{
    93  				RequestId:    proto.Int32(id),
    94  				Payload:      proto.String(testPayload),
    95  				ErrorMessage: proto.String("error"),
    96  			},
    97  		}
    98  		t.Logf("Request id: %d, action: %s, target: %s", id, action, target)
    99  		if action == "pipe_server_close" {
   100  			w.Close()
   101  			return
   102  		}
   103  		if res, ok := actionToResponse[action]; ok {
   104  			serverutils.WriteMessage(res, w)
   105  		}
   106  	}
   107  }
   108  
   109  func setProbeOptions(p *Probe, name, value string) {
   110  	for _, opt := range p.c.Options {
   111  		if opt.GetName() == name {
   112  			opt.Value = proto.String(value)
   113  			break
   114  		}
   115  	}
   116  }
   117  
   118  // runAndVerifyServerProbe executes a server probe and verifies the replies
   119  // received.
   120  func runAndVerifyServerProbe(t *testing.T, p *Probe, action string, tgts []string, total, success map[string]int64, numEventMetrics int) {
   121  	setProbeOptions(p, "action", action)
   122  
   123  	runAndVerifyProbe(t, p, tgts, total, success)
   124  
   125  	// Verify that we got all the expected EventMetrics
   126  	ems, err := testutils.MetricsFromChannel(p.dataChan, numEventMetrics, 1*time.Second)
   127  	if err != nil {
   128  		t.Error(err)
   129  	}
   130  	metricsMap := testutils.MetricsMap(ems)
   131  
   132  	// Convenient wrapper to get the last value from a series.
   133  	lastValue := func(s []*metrics.EventMetrics, metricName string) int64 {
   134  		return s[len(s)-1].Metric(metricName).(metrics.NumValue).Int64()
   135  	}
   136  
   137  	for _, tgt := range tgts {
   138  		vals := make(map[string]int64)
   139  		for _, m := range []string{"total", "success"} {
   140  			s := metricsMap[m][tgt]
   141  			if len(s) == 0 {
   142  				t.Errorf("No %s metric for target: %s", m, tgt)
   143  				continue
   144  			}
   145  			vals[m] = lastValue(s, m)
   146  		}
   147  		if vals["success"] != success[tgt] || vals["total"] != total[tgt] {
   148  			t.Errorf("Target(%s) total=%d, success=%d, wanted: total=%d, success=%d, all_metrics=%s", tgt, vals["total"], vals["success"], total[tgt], success[tgt], ems)
   149  		}
   150  	}
   151  }
   152  
   153  func runAndVerifyProbe(t *testing.T, p *Probe, tgts []string, total, success map[string]int64) {
   154  	p.opts.Targets = targets.StaticTargets(strings.Join(tgts, ","))
   155  	p.updateTargets()
   156  
   157  	p.runProbe(context.Background())
   158  
   159  	for _, target := range p.targets {
   160  		tgt := target.Name
   161  
   162  		if p.results[tgt].total != total[tgt] {
   163  			t.Errorf("p.total[%s]=%d, Want: %d", tgt, p.results[tgt].total, total[tgt])
   164  		}
   165  		if p.results[tgt].success != success[tgt] {
   166  			t.Errorf("p.success[%s]=%d, Want: %d", tgt, p.results[tgt].success, success[tgt])
   167  		}
   168  	}
   169  }
   170  
   171  func createTestProbe(cmd string) *Probe {
   172  	probeConf := &configpb.ProbeConf{
   173  		Options: []*configpb.ProbeConf_Option{
   174  			{
   175  				Name:  proto.String("target"),
   176  				Value: proto.String("@target@"),
   177  			},
   178  			{
   179  				Name:  proto.String("action"),
   180  				Value: proto.String(""),
   181  			},
   182  		},
   183  		Command: &cmd,
   184  	}
   185  
   186  	p := &Probe{
   187  		dataChan: make(chan *metrics.EventMetrics, 20),
   188  	}
   189  
   190  	p.Init("testProbe", &options.Options{
   191  		ProbeConf:  probeConf,
   192  		Timeout:    1 * time.Second,
   193  		LogMetrics: func(em *metrics.EventMetrics) {},
   194  	})
   195  
   196  	return p
   197  }
   198  
   199  func testProbeServerSetup(t *testing.T, readErrorCh chan error) (*Probe, string, chan struct{}) {
   200  	// We create two pairs of pipes to establish communication between this prober
   201  	// and the test probe server (defined above).
   202  	// Test probe server input pipe. We writes on w1 and external command reads
   203  	// from r1.
   204  	r1, w1, err := os.Pipe()
   205  	if err != nil {
   206  		t.Errorf("Error creating OS pipe. Err: %v", err)
   207  	}
   208  	// Test probe server output pipe. External command writes on w2 and we read
   209  	// from r2.
   210  	r2, w2, err := os.Pipe()
   211  	if err != nil {
   212  		t.Errorf("Error creating OS pipe. Err: %v", err)
   213  	}
   214  
   215  	testPayload := "p90 45\n"
   216  	// Start probe server in a goroutine
   217  	doneChan := make(chan struct{})
   218  	go startProbeServer(t, testPayload, r1, w2, doneChan)
   219  
   220  	p := createTestProbe("./testCommand")
   221  	p.cmdRunning = true // don't try to start the probe server
   222  	p.cmdStdin = w1
   223  	p.cmdStdout = r2
   224  	p.mode = "server"
   225  
   226  	// Start the goroutine that reads probe replies.
   227  	go func() {
   228  		err := p.readProbeReplies(doneChan)
   229  		if readErrorCh != nil {
   230  			readErrorCh <- err
   231  			close(readErrorCh)
   232  		}
   233  	}()
   234  
   235  	return p, testPayload, doneChan
   236  }
   237  
   238  func TestProbeServerMode(t *testing.T) {
   239  	p, _, doneChan := testProbeServerSetup(t, nil)
   240  	defer close(doneChan)
   241  
   242  	total, success := make(map[string]int64), make(map[string]int64)
   243  
   244  	// No payload
   245  	tgts := []string{"target1", "target2"}
   246  	for _, tgt := range tgts {
   247  		total[tgt]++
   248  		success[tgt]++
   249  	}
   250  	t.Run("nopayload", func(t *testing.T) {
   251  		runAndVerifyServerProbe(t, p, "nopayload", tgts, total, success, 2)
   252  	})
   253  
   254  	// Payload
   255  	tgts = []string{"target3"}
   256  	for _, tgt := range tgts {
   257  		total[tgt]++
   258  		success[tgt]++
   259  	}
   260  	t.Run("payload", func(t *testing.T) {
   261  		// 2 metrics per target
   262  		runAndVerifyServerProbe(t, p, "payload", tgts, total, success, 1*2)
   263  	})
   264  
   265  	// Payload with error
   266  	tgts = []string{"target2", "target3"}
   267  	for _, tgt := range tgts {
   268  		total[tgt]++
   269  	}
   270  	t.Run("payload_with_error", func(t *testing.T) {
   271  		// 2 targets, 2 EMs per target
   272  		runAndVerifyServerProbe(t, p, "payload_with_error", tgts, total, success, 2*2)
   273  	})
   274  
   275  	// Timeout
   276  	tgts = []string{"target1", "target2", "target3"}
   277  	for _, tgt := range tgts {
   278  		total[tgt]++
   279  	}
   280  
   281  	// Reduce probe timeout to make this test pass quicker.
   282  	p.opts.Timeout = time.Second
   283  	t.Run("timeout", func(t *testing.T) {
   284  		// 3 targets, 1 EM per target
   285  		runAndVerifyServerProbe(t, p, "timeout", tgts, total, success, 3*1)
   286  	})
   287  }
   288  
   289  func TestProbeServerRemotePipeClose(t *testing.T) {
   290  	readErrorCh := make(chan error)
   291  	p, _, doneChan := testProbeServerSetup(t, readErrorCh)
   292  	defer close(doneChan)
   293  
   294  	total, success := make(map[string]int64), make(map[string]int64)
   295  	// Remote pipe close
   296  	tgts := []string{"target"}
   297  	for _, tgt := range tgts {
   298  		total[tgt]++
   299  	}
   300  	// Reduce probe timeout to make this test pass quicker.
   301  	p.opts.Timeout = time.Second
   302  	runAndVerifyServerProbe(t, p, "pipe_server_close", tgts, total, success, 1)
   303  	readError := <-readErrorCh
   304  	if readError == nil {
   305  		t.Error("Didn't get error in reading pipe")
   306  	}
   307  	if readError != io.EOF {
   308  		t.Errorf("Didn't get correct error in reading pipe. Got: %v, wanted: %v", readError, io.EOF)
   309  	}
   310  }
   311  
   312  func TestProbeServerLocalPipeClose(t *testing.T) {
   313  	readErrorCh := make(chan error)
   314  	p, _, doneChan := testProbeServerSetup(t, readErrorCh)
   315  	defer close(doneChan)
   316  
   317  	total, success := make(map[string]int64), make(map[string]int64)
   318  	// Local pipe close
   319  	tgts := []string{"target"}
   320  	for _, tgt := range tgts {
   321  		total[tgt]++
   322  	}
   323  	// Reduce probe timeout to make this test pass quicker.
   324  	p.opts.Timeout = time.Second
   325  	p.cmdStdout.(*os.File).Close()
   326  	runAndVerifyServerProbe(t, p, "pipe_local_close", tgts, total, success, 1)
   327  	readError := <-readErrorCh
   328  	if readError == nil {
   329  		t.Error("Didn't get error in reading pipe")
   330  	}
   331  	if _, ok := readError.(*os.PathError); !ok {
   332  		t.Errorf("Didn't get correct error in reading pipe. Got: %T, wanted: *os.PathError", readError)
   333  	}
   334  }
   335  
   336  func TestProbeOnceMode(t *testing.T) {
   337  	testCmd := "/test/cmd --arg1 --arg2"
   338  
   339  	p := createTestProbe(testCmd)
   340  	p.mode = "once"
   341  	tgts := []string{"target1", "target2"}
   342  
   343  	oldRunCommand := runCommand
   344  	defer func() { runCommand = oldRunCommand }()
   345  
   346  	// Set runCommand to a function that runs successfully and returns a pyload.
   347  	runCommand = func(ctx context.Context, cmd string, cmdArgs []string) ([]byte, error) {
   348  		var resp []string
   349  		resp = append(resp, fmt.Sprintf("cmd \"%s\"", cmd))
   350  		resp = append(resp, fmt.Sprintf("num-args %d", len(cmdArgs)))
   351  		return []byte(strings.Join(resp, "\n")), nil
   352  	}
   353  
   354  	total, success := make(map[string]int64), make(map[string]int64)
   355  
   356  	for _, tgt := range tgts {
   357  		total[tgt]++
   358  		success[tgt]++
   359  	}
   360  
   361  	runAndVerifyProbe(t, p, tgts, total, success)
   362  
   363  	// Try with failing command now
   364  	runCommand = func(ctx context.Context, cmd string, cmdArgs []string) ([]byte, error) {
   365  		return nil, fmt.Errorf("error executing %s", cmd)
   366  	}
   367  
   368  	for _, tgt := range tgts {
   369  		total[tgt]++
   370  	}
   371  	runAndVerifyProbe(t, p, tgts, total, success)
   372  
   373  	// Total numbder of event metrics:
   374  	// num_of_runs x num_targets x (1 for default metrics + 1 for payload metrics)
   375  	ems, err := testutils.MetricsFromChannel(p.dataChan, 8, time.Second)
   376  	if err != nil {
   377  		t.Error(err)
   378  	}
   379  	metricsMap := testutils.MetricsMap(ems)
   380  
   381  	if metricsMap["num-args"] == nil && metricsMap["cmd"] == nil {
   382  		t.Errorf("Didn't get all metrics from the external process output.")
   383  	}
   384  
   385  	if metricsMap["total"] == nil && metricsMap["success"] == nil {
   386  		t.Errorf("Didn't get default metrics from the probe run.")
   387  	}
   388  
   389  	for _, tgt := range tgts {
   390  		// Verify that default metrics were received for both runs -- success and
   391  		// failure. We don't check for the values here as that's already done by
   392  		// runAndVerifyProbe to an extent.
   393  		for _, m := range []string{"total", "success", "latency"} {
   394  			if len(metricsMap[m][tgt]) != 2 {
   395  				t.Errorf("Wrong number of values for default metric (%s) for target (%s). Got=%d, Expected=2", m, tgt, len(metricsMap[m][tgt]))
   396  			}
   397  		}
   398  
   399  		for _, m := range []string{"num-args", "cmd"} {
   400  			if len(metricsMap[m][tgt]) != 1 {
   401  				t.Errorf("Wrong number of values for metric (%s) for target (%s) from the command output. Got=%d, Expected=1", m, tgt, len(metricsMap[m][tgt]))
   402  			}
   403  		}
   404  
   405  		tgtNumArgs := metricsMap["num-args"][tgt][0].Metric("num-args").(metrics.NumValue).Int64()
   406  		expectedNumArgs := int64(len(strings.Split(testCmd, " ")) - 1)
   407  		if tgtNumArgs != expectedNumArgs {
   408  			t.Errorf("Wrong metric value for target (%s) from the command output. Got=%d, Expected=%d", tgt, tgtNumArgs, expectedNumArgs)
   409  		}
   410  
   411  		tgtCmd := metricsMap["cmd"][tgt][0].Metric("cmd").String()
   412  		expectedCmd := fmt.Sprintf("\"%s\"", strings.Split(testCmd, " ")[0])
   413  		if tgtCmd != expectedCmd {
   414  			t.Errorf("Wrong metric value for target (%s) from the command output. got=%s, expected=%s", tgt, tgtCmd, expectedCmd)
   415  		}
   416  	}
   417  }
   418  
   419  func TestUpdateLabelKeys(t *testing.T) {
   420  	c := &configpb.ProbeConf{
   421  		Options: []*configpb.ProbeConf_Option{
   422  			{
   423  				Name:  proto.String("target"),
   424  				Value: proto.String("@target@"),
   425  			},
   426  			{
   427  				Name:  proto.String("probe"),
   428  				Value: proto.String("@probe@"),
   429  			},
   430  		},
   431  	}
   432  	p := &Probe{
   433  		name:    "probeP",
   434  		c:       c,
   435  		cmdArgs: []string{"--server", "@target.label.fqdn@:@port@"},
   436  	}
   437  
   438  	p.updateLabelKeys()
   439  
   440  	expected := map[string]bool{
   441  		"target":            true,
   442  		"port":              true,
   443  		"probe":             true,
   444  		"target.label.fqdn": true,
   445  	}
   446  
   447  	if !reflect.DeepEqual(p.labelKeys, expected) {
   448  		t.Errorf("p.labelKeys got: %v, want: %v", p.labelKeys, expected)
   449  	}
   450  
   451  	gotLabels := p.labels(endpoint.Endpoint{
   452  		Name: "targetA",
   453  		Port: 8080,
   454  		Labels: map[string]string{
   455  			"fqdn": "targetA.svc.local",
   456  		},
   457  	})
   458  	wantLabels := map[string]string{
   459  		"target.label.fqdn": "targetA.svc.local",
   460  		"port":              "8080",
   461  		"probe":             "probeP",
   462  		"target":            "targetA",
   463  	}
   464  	if !reflect.DeepEqual(gotLabels, wantLabels) {
   465  		t.Errorf("p.labels got: %v, want: %v", gotLabels, wantLabels)
   466  	}
   467  }
   468  
   469  func TestSubstituteLabels(t *testing.T) {
   470  	tests := []struct {
   471  		desc   string
   472  		in     string
   473  		labels map[string]string
   474  		want   string
   475  		found  bool
   476  	}{
   477  		{
   478  			desc:  "No replacement",
   479  			in:    "foo bar baz",
   480  			want:  "foo bar baz",
   481  			found: true,
   482  		},
   483  		{
   484  			desc: "Replacement beginning",
   485  			in:   "@foo@ bar baz",
   486  			labels: map[string]string{
   487  				"foo": "h e llo",
   488  			},
   489  			want:  "h e llo bar baz",
   490  			found: true,
   491  		},
   492  		{
   493  			desc: "Replacement middle",
   494  			in:   "beginning @😿@ end",
   495  			labels: map[string]string{
   496  				"😿": "😺",
   497  			},
   498  			want:  "beginning 😺 end",
   499  			found: true,
   500  		},
   501  		{
   502  			desc: "Replacement end",
   503  			in:   "bar baz @foo@",
   504  			labels: map[string]string{
   505  				"foo": "XöX",
   506  				"bar": "nope",
   507  			},
   508  			want:  "bar baz XöX",
   509  			found: true,
   510  		},
   511  		{
   512  			desc: "Replacements",
   513  			in:   "abc@foo@def@foo@ jk",
   514  			labels: map[string]string{
   515  				"def": "nope",
   516  				"foo": "XöX",
   517  			},
   518  			want:  "abcXöXdefXöX jk",
   519  			found: true,
   520  		},
   521  		{
   522  			desc: "Multiple labels",
   523  			in:   "xx @foo@@bar@ yy",
   524  			labels: map[string]string{
   525  				"bar": "_",
   526  				"def": "nope",
   527  				"foo": "XöX",
   528  			},
   529  			want:  "xx XöX_ yy",
   530  			found: true,
   531  		},
   532  		{
   533  			desc: "Not found",
   534  			in:   "A b C @d@ e",
   535  			labels: map[string]string{
   536  				"bar": "_",
   537  				"def": "nope",
   538  				"foo": "XöX",
   539  			},
   540  			want: "A b C @d@ e",
   541  		},
   542  		{
   543  			desc: "@@",
   544  			in:   "hello@@foo",
   545  			labels: map[string]string{
   546  				"bar": "_",
   547  				"def": "nope",
   548  				"foo": "XöX",
   549  			},
   550  			want:  "hello@foo",
   551  			found: true,
   552  		},
   553  		{
   554  			desc: "odd number",
   555  			in:   "hello@foo@bar@xx",
   556  			labels: map[string]string{
   557  				"foo": "yy",
   558  			},
   559  			want:  "helloyybar@xx",
   560  			found: true,
   561  		},
   562  	}
   563  
   564  	for _, tc := range tests {
   565  		got, found := substituteLabels(tc.in, tc.labels)
   566  		if tc.found != found {
   567  			t.Errorf("%v: substituteLabels(%q, %q) = _, %v, want %v", tc.desc, tc.in, tc.labels, found, tc.found)
   568  		}
   569  		if tc.want != got {
   570  			t.Errorf("%v: substituteLabels(%q, %q) = %q, _, want %q", tc.desc, tc.in, tc.labels, got, tc.want)
   571  		}
   572  	}
   573  }
   574  
   575  // TestSendRequest verifies that sendRequest sends appropriately populated
   576  // ProbeRequest.
   577  func TestSendRequest(t *testing.T) {
   578  	p := &Probe{}
   579  	p.Init("testprobe", &options.Options{
   580  		ProbeConf: &configpb.ProbeConf{
   581  			Options: []*configpb.ProbeConf_Option{
   582  				{
   583  					Name:  proto.String("target"),
   584  					Value: proto.String("@target@"),
   585  				},
   586  			},
   587  			Command: proto.String("./testCommand"),
   588  		},
   589  		Targets: targets.StaticTargets("localhost"),
   590  	})
   591  	var buf bytes.Buffer
   592  	p.cmdStdin = &buf
   593  
   594  	requestID := int32(1234)
   595  	target := "localhost"
   596  
   597  	err := p.sendRequest(requestID, endpoint.Endpoint{Name: target})
   598  	if err != nil {
   599  		t.Errorf("Failed to sendRequest: %v", err)
   600  	}
   601  	req := new(serverpb.ProbeRequest)
   602  	var length int
   603  	_, err = fmt.Fscanf(&buf, "\nContent-Length: %d\n\n", &length)
   604  	if err != nil {
   605  		t.Errorf("Failed to read header: %v", err)
   606  	}
   607  	err = proto.Unmarshal(buf.Bytes(), req)
   608  	if err != nil {
   609  		t.Fatalf("Failed to Unmarshal probe Request: %v", err)
   610  	}
   611  	if got, want := req.GetRequestId(), requestID; got != requestID {
   612  		t.Errorf("req.GetRequestId() = %q, want %v", got, want)
   613  	}
   614  	opts := req.GetOptions()
   615  	if len(opts) != 1 {
   616  		t.Errorf("req.GetOptions() = %q (%v), want only one item", opts, len(opts))
   617  	}
   618  	if got, want := opts[0].GetName(), "target"; got != want {
   619  		t.Errorf("opts[0].GetName() = %q, want %q", got, want)
   620  	}
   621  	if got, want := opts[0].GetValue(), target; got != target {
   622  		t.Errorf("opts[0].GetValue() = %q, want %q", got, want)
   623  	}
   624  }
   625  
   626  func TestUpdateTargets(t *testing.T) {
   627  	p := &Probe{}
   628  	err := p.Init("testprobe", &options.Options{
   629  		ProbeConf: &configpb.ProbeConf{
   630  			Command: proto.String("./testCommand"),
   631  		},
   632  		Targets: targets.StaticTargets("2.2.2.2"),
   633  	})
   634  	if err != nil {
   635  		t.Fatalf("Got error while initializing the probe: %v", err)
   636  	}
   637  
   638  	p.updateTargets()
   639  	latVal := p.results["2.2.2.2"].latency
   640  	if _, ok := latVal.(*metrics.Float); !ok {
   641  		t.Errorf("latency value type is not metrics.Float: %v", latVal)
   642  	}
   643  
   644  	// Test with latency distribution option set.
   645  	p.opts.LatencyDist = metrics.NewDistribution([]float64{0.1, 0.2, 0.5})
   646  	delete(p.results, "2.2.2.2")
   647  	p.updateTargets()
   648  	latVal = p.results["2.2.2.2"].latency
   649  	if _, ok := latVal.(*metrics.Distribution); !ok {
   650  		t.Errorf("latency value type is not metrics.Distribution: %v", latVal)
   651  	}
   652  }
   653  
   654  func verifyProcessedResult(t *testing.T, p *Probe, r *result, success int64, name string, val int64, extraLabels map[string]string) {
   655  	t.Helper()
   656  
   657  	t.Log(val)
   658  
   659  	testTarget := "test-target"
   660  	if r.success != success {
   661  		t.Errorf("r.success=%d, expected=%d", r.success, success)
   662  	}
   663  
   664  	m, err := testutils.MetricsFromChannel(p.dataChan, 2, time.Second)
   665  	if err != nil {
   666  		t.Fatal(err.Error())
   667  	}
   668  
   669  	metricsMap := testutils.MetricsMap(m)
   670  
   671  	if metricsMap[name] == nil || len(metricsMap[name][testTarget]) < 1 {
   672  		t.Fatalf("Payload metric %s is missing in %+v", name, metricsMap)
   673  	}
   674  
   675  	em := metricsMap[name][testTarget][0]
   676  	gotValue := em.Metric(name).(metrics.NumValue).Int64()
   677  	if gotValue != val {
   678  		t.Errorf("%s=%d, expected=%d", name, gotValue, val)
   679  	}
   680  
   681  	expectedLabels := map[string]string{"ptype": "external", "probe": "testprobe", "dst": "test-target"}
   682  	for k, v := range extraLabels {
   683  		expectedLabels[k] = v
   684  	}
   685  
   686  	if len(em.LabelsKeys()) != len(expectedLabels) {
   687  		t.Errorf("Labels mismatch: got=%v, expected=%v", em.LabelsKeys(), expectedLabels)
   688  	}
   689  
   690  	for key, val := range expectedLabels {
   691  		if em.Label(key) != val {
   692  			t.Errorf("r.payloadMetrics.Label(%s)=%s, expected=%s", key, r.payloadMetrics.Label(key), val)
   693  		}
   694  	}
   695  }
   696  
   697  func TestProcessProbeResult(t *testing.T) {
   698  	tests := []struct {
   699  		desc             string
   700  		aggregate        bool
   701  		payloads         []string
   702  		additionalLabels map[string]string
   703  		wantValues       []int64
   704  		wantExtraLabels  map[string]string
   705  	}{
   706  		{
   707  			desc:       "with-aggregation-enabled",
   708  			aggregate:  true,
   709  			wantValues: []int64{14, 25},
   710  			payloads:   []string{"p-failures 14", "p-failures 11"},
   711  		},
   712  		{
   713  			desc:      "with-aggregation-disabled",
   714  			aggregate: false,
   715  			payloads: []string{
   716  				"p-failures{service=serviceA,db=dbA} 14",
   717  				"p-failures{service=serviceA,db=dbA} 11",
   718  			},
   719  			wantValues: []int64{14, 11},
   720  			wantExtraLabels: map[string]string{
   721  				"service": "serviceA",
   722  				"db":      "dbA",
   723  			},
   724  		},
   725  		{
   726  			desc:      "with-additional-labels",
   727  			aggregate: false,
   728  			payloads: []string{
   729  				"p-failures{service=serviceA,db=dbA} 14",
   730  				"p-failures{service=serviceA,db=dbA} 11",
   731  			},
   732  			additionalLabels: map[string]string{"dc": "xx"},
   733  			wantValues:       []int64{14, 11},
   734  			wantExtraLabels: map[string]string{
   735  				"service": "serviceA",
   736  				"db":      "dbA",
   737  				"dc":      "xx",
   738  			},
   739  		},
   740  	}
   741  
   742  	for _, test := range tests {
   743  		t.Run(test.desc, func(t *testing.T) {
   744  			p := &Probe{}
   745  			opts := options.DefaultOptions()
   746  			opts.ProbeConf = &configpb.ProbeConf{
   747  				OutputMetricsOptions: &payloadconfigpb.OutputMetricsOptions{
   748  					AggregateInCloudprober: proto.Bool(test.aggregate),
   749  				},
   750  				Command: proto.String("./testCommand"),
   751  			}
   752  			for k, v := range test.additionalLabels {
   753  				opts.AdditionalLabels = append(opts.AdditionalLabels, options.ParseAdditionalLabel(&probeconfigpb.AdditionalLabel{
   754  					Key:   proto.String(k),
   755  					Value: proto.String(v),
   756  				}))
   757  			}
   758  			err := p.Init("testprobe", opts)
   759  			if err != nil {
   760  				t.Fatal(err)
   761  			}
   762  
   763  			p.dataChan = make(chan *metrics.EventMetrics, 20)
   764  
   765  			r := &result{
   766  				latency: metrics.NewFloat(0),
   767  			}
   768  
   769  			// First run
   770  			p.processProbeResult(&probeStatus{
   771  				target:  "test-target",
   772  				success: true,
   773  				latency: time.Millisecond,
   774  				payload: test.payloads[0],
   775  			}, r)
   776  
   777  			wantSuccess := int64(1)
   778  			verifyProcessedResult(t, p, r, wantSuccess, "p-failures", test.wantValues[0], test.wantExtraLabels)
   779  
   780  			// Second run
   781  			p.processProbeResult(&probeStatus{
   782  				target:  "test-target",
   783  				success: true,
   784  				latency: time.Millisecond,
   785  				payload: test.payloads[1],
   786  			}, r)
   787  			wantSuccess++
   788  
   789  			if test.aggregate {
   790  				verifyProcessedResult(t, p, r, wantSuccess, "p-failures", test.wantValues[1], test.wantExtraLabels)
   791  			} else {
   792  				verifyProcessedResult(t, p, r, wantSuccess, "p-failures", test.wantValues[1], test.wantExtraLabels)
   793  			}
   794  		})
   795  	}
   796  }
   797  
   798  func TestCommandParsing(t *testing.T) {
   799  	p := createTestProbe("./test-command --flag1 one --flag23 \"two three\"")
   800  
   801  	wantCmdName := "./test-command"
   802  	if p.cmdName != wantCmdName {
   803  		t.Errorf("Got command name=%s, want command name=%s", p.cmdName, wantCmdName)
   804  	}
   805  
   806  	wantArgs := []string{"--flag1", "one", "--flag23", "two three"}
   807  	if !reflect.DeepEqual(p.cmdArgs, wantArgs) {
   808  		t.Errorf("Got command args=%v, want command args=%v", p.cmdArgs, wantArgs)
   809  	}
   810  }
   811  
   812  type fakeCommand struct {
   813  	exitCtx  context.Context
   814  	startCtx context.Context
   815  	waitErr  error
   816  }
   817  
   818  func (fc *fakeCommand) Wait() error {
   819  	select {
   820  	case <-fc.exitCtx.Done():
   821  	case <-fc.startCtx.Done():
   822  	}
   823  	return fc.waitErr
   824  }
   825  
   826  func TestMonitorCommand(t *testing.T) {
   827  	tests := []struct {
   828  		desc       string
   829  		waitErr    error
   830  		finishCmd  bool
   831  		cancelCtx  bool
   832  		wantErr    bool
   833  		wantStderr bool
   834  	}{
   835  		{
   836  			desc:      "Command exit with no error",
   837  			finishCmd: true,
   838  			wantErr:   false,
   839  		},
   840  		{
   841  			desc:      "Cancel context, no error",
   842  			cancelCtx: true,
   843  			wantErr:   false,
   844  		},
   845  		{
   846  			desc:       "command exit with exit error",
   847  			finishCmd:  true,
   848  			waitErr:    &exec.ExitError{Stderr: []byte("exit-error exiting")},
   849  			wantErr:    true,
   850  			wantStderr: true,
   851  		},
   852  		{
   853  			desc:       "command exit with no exit error",
   854  			finishCmd:  true,
   855  			waitErr:    errors.New("some-error"),
   856  			wantErr:    true,
   857  			wantStderr: false,
   858  		},
   859  	}
   860  
   861  	for _, test := range tests {
   862  		t.Run(test.desc, func(t *testing.T) {
   863  			exitCtx, exitFunc := context.WithCancel(context.Background())
   864  			startCtx, startCancelFunc := context.WithCancel(context.Background())
   865  			cmd := &fakeCommand{
   866  				exitCtx:  exitCtx,
   867  				startCtx: startCtx,
   868  				waitErr:  test.waitErr,
   869  			}
   870  
   871  			p := &Probe{}
   872  			errCh := make(chan error)
   873  			go func() {
   874  				errCh <- p.monitorCommand(startCtx, cmd)
   875  			}()
   876  
   877  			if test.finishCmd {
   878  				exitFunc()
   879  			}
   880  			if test.cancelCtx {
   881  				startCancelFunc()
   882  			}
   883  
   884  			err := <-errCh
   885  			if (err != nil) != test.wantErr {
   886  				t.Errorf("Got error: %v, want error?= %v", err, test.wantErr)
   887  			}
   888  
   889  			if err != nil {
   890  				if test.wantStderr && !strings.Contains(err.Error(), "Stderr") {
   891  					t.Errorf("Want std err: %v, got std err: %v", test.wantStderr, strings.Contains(err.Error(), "Stderr"))
   892  				}
   893  			}
   894  
   895  			exitFunc()
   896  			startCancelFunc()
   897  		})
   898  	}
   899  }