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 }