github.com/google/fleetspeak@v0.1.15-0.20240426164851-4f31f62c1aea/fleetspeak/src/client/daemonservice/execution/execution_test.go (about)

     1  // Copyright 2017 Google Inc.
     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  //     https://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 execution
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"os"
    21  	"strings"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/google/fleetspeak/fleetspeak/src/client/channel"
    26  	"github.com/google/fleetspeak/fleetspeak/src/client/clitesting"
    27  	"github.com/google/fleetspeak/fleetspeak/src/client/service"
    28  	"google.golang.org/protobuf/proto"
    29  
    30  	dspb "github.com/google/fleetspeak/fleetspeak/src/client/daemonservice/proto/fleetspeak_daemonservice"
    31  	fspb "github.com/google/fleetspeak/fleetspeak/src/common/proto/fleetspeak"
    32  	mpb "github.com/google/fleetspeak/fleetspeak/src/common/proto/fleetspeak_monitoring"
    33  )
    34  
    35  func patchDuration(t *testing.T, d *time.Duration, value time.Duration) {
    36  	t.Helper()
    37  	prev := *d
    38  	*d = value
    39  	t.Cleanup(func() { *d = prev })
    40  }
    41  
    42  func TestFailures(t *testing.T) {
    43  	patchDuration(t, &channel.MagicTimeout, 50*time.Millisecond)
    44  	patchDuration(t, &channel.MessageTimeout, 50*time.Millisecond)
    45  	patchDuration(t, &startupDataTimeout, time.Second)
    46  
    47  	sc := clitesting.MockServiceContext{
    48  		OutChan: make(chan *fspb.Message, 5),
    49  	}
    50  	if _, err := os.Stat(testClient(t)); err != nil {
    51  		t.Fatalf("can't stat testclient binary [%v]: %v", testClient(t), err)
    52  	}
    53  
    54  	// These misbehaviors should fail after MagicTimeout, or sooner.
    55  	for _, mode := range []string{"freeze", "freezeHard", "garbage", "die"} {
    56  		t.Run(mode, func(t *testing.T) {
    57  			dsc := &dspb.Config{
    58  				Argv: []string{testClient(t), "--mode=" + mode},
    59  			}
    60  
    61  			ex, err := New(context.Background(), "TestService", dsc, &sc)
    62  			if err != nil {
    63  				t.Fatalf("execution.New returned error: %v", err)
    64  			}
    65  			defer ex.Shutdown()
    66  
    67  			waitRes := make(chan error)
    68  			go func() {
    69  				waitRes <- ex.Wait()
    70  			}()
    71  			select {
    72  			case err := <-waitRes:
    73  				if err == nil {
    74  					t.Errorf("expected error from process, got nil")
    75  				}
    76  			case <-time.After(10 * time.Second):
    77  				t.Errorf("process should have been canceled, but kept running")
    78  			}
    79  		})
    80  	}
    81  }
    82  
    83  func TestLoopback(t *testing.T) {
    84  	sc := clitesting.MockServiceContext{
    85  		OutChan: make(chan *fspb.Message),
    86  	}
    87  	dsc := &dspb.Config{
    88  		Argv: []string{testClient(t), "--mode=loopback"},
    89  	}
    90  	ex := mustNew(t, "TestService", dsc, &sc)
    91  	msgs := []*fspb.Message{
    92  		{
    93  			MessageId:   []byte("\000\000\000"),
    94  			MessageType: "RequestTypeA",
    95  		},
    96  		{
    97  			MessageId:   []byte("\000\000\001"),
    98  			MessageType: "RequestTypeB",
    99  		},
   100  	}
   101  	go func() {
   102  		for _, m := range msgs {
   103  			err := ex.SendMsg(context.Background(), m)
   104  			if err != nil {
   105  				t.Errorf("ex.SendMsg: %v", err)
   106  			}
   107  		}
   108  	}()
   109  	for _, m := range msgs {
   110  		got := <-sc.OutChan
   111  		want := proto.Clone(m).(*fspb.Message)
   112  		want.MessageType = want.MessageType + "Response"
   113  		if !proto.Equal(got, want) {
   114  			t.Errorf("Unexpected message from loopback: got [%v], want [%v]", got, want)
   115  		}
   116  	}
   117  }
   118  
   119  func TestStd(t *testing.T) {
   120  	sc := clitesting.MockServiceContext{
   121  		OutChan: make(chan *fspb.Message, 20),
   122  	}
   123  	dsc := &dspb.Config{
   124  		Argv: []string{testClient(t), "--mode=stdSpam"},
   125  		StdParams: &dspb.Config_StdParams{
   126  			ServiceName:      "TestService",
   127  			FlushTimeSeconds: 5,
   128  		},
   129  	}
   130  	ex := mustNew(t, "TestService", dsc, &sc)
   131  	_ = ex // only indirectly used through input and output
   132  
   133  	wantIn := []byte(strings.Repeat("The quick brown fox jumped over the lazy dogs.\n", 128*1024))
   134  	wantErr := []byte(strings.Repeat("The brown quick fox jumped over some lazy dogs.\n", 128*1024))
   135  
   136  	var bufIn bytes.Buffer
   137  	bufIn.Grow(len(wantIn))
   138  	var bufErr bytes.Buffer
   139  	bufErr.Grow(len(wantErr))
   140  
   141  	start := time.Now()
   142  	for bufIn.Len() < len(wantIn) || bufErr.Len() < len(wantErr) {
   143  		m := <-sc.OutChan
   144  		if m.MessageType == "ResourceUsage" {
   145  			continue
   146  		}
   147  		if m.MessageType != "StdOutput" {
   148  			t.Errorf("Received unexpected message type: %s", m.MessageType)
   149  			continue
   150  		}
   151  		od := &dspb.StdOutputData{}
   152  		if err := m.Data.UnmarshalTo(od); err != nil {
   153  			t.Fatalf("Unable to unmarshal StdOutputData: %v", err)
   154  		}
   155  		bufIn.Write(od.Stdout)
   156  		bufErr.Write(od.Stderr)
   157  	}
   158  
   159  	// Flushes should happen when the buffer is full. The last flush might take up
   160  	// to 5 seconds occur, the rest should occur quickly.
   161  	runtime := time.Since(start)
   162  	deadline := 10 * time.Second
   163  	if runtime > deadline {
   164  		t.Errorf("took %v to receive std data, should be less than %v", runtime, deadline)
   165  	}
   166  
   167  	if !bytes.Equal(bufIn.Bytes(), wantIn) {
   168  		t.Error("received stdout does not match expected")
   169  	}
   170  	if !bytes.Equal(bufErr.Bytes(), wantErr) {
   171  		t.Error("received stderr does not match expected")
   172  	}
   173  }
   174  
   175  func TestStats(t *testing.T) {
   176  	sc := clitesting.MockServiceContext{
   177  		OutChan: make(chan *fspb.Message, 2000),
   178  	}
   179  	dsc := &dspb.Config{
   180  		Argv:                                  []string{testClient(t), "--mode=loopback"},
   181  		ResourceMonitoringSampleSize:          2,
   182  		ResourceMonitoringSamplePeriodSeconds: 1,
   183  	}
   184  	ex := mustNew(t, "TestService", dsc, &sc)
   185  
   186  	// Run TestService for 4.2 seconds. We expect a resource-usage
   187  	// report to be sent every 2000 milliseconds (samplePeriod * sampleSize).
   188  	done := make(chan struct{})
   189  	go func() {
   190  		for i := range 42 {
   191  			time.Sleep(100 * time.Millisecond)
   192  			m := &fspb.Message{
   193  				MessageId:   []byte{byte(i)},
   194  				MessageType: "DummyMessage",
   195  			}
   196  			err := ex.SendMsg(context.TODO(), m)
   197  			if err != nil {
   198  				t.Errorf("ex.SendMsg: %v", err)
   199  			}
   200  		}
   201  		close(done)
   202  	}()
   203  
   204  	ruCnt := 0
   205  	finalRUReceived := false
   206  
   207  	for !finalRUReceived {
   208  		select {
   209  		case <-done:
   210  			return
   211  		case m := <-sc.OutChan:
   212  			if m.MessageType == "DummyMessageResponse" {
   213  				continue
   214  			}
   215  			if m.MessageType != "ResourceUsage" {
   216  				t.Errorf("Received unexpected message type: %s", m.MessageType)
   217  				continue
   218  			}
   219  			rud := &mpb.ResourceUsageData{}
   220  			if err := m.Data.UnmarshalTo(rud); err != nil {
   221  				t.Fatalf("Unable to unmarshal ResourceUsageData: %v", err)
   222  			}
   223  			ruCnt++
   224  			if rud.ProcessTerminated {
   225  				finalRUReceived = true
   226  				continue
   227  			}
   228  			ru := rud.ResourceUsage
   229  			if ru == nil {
   230  				t.Error("ResourceUsageData should have non-nil ResourceUsage")
   231  				continue
   232  			}
   233  			if ru.MeanResidentMemory <= 0.0 {
   234  				t.Errorf("ResourceUsage.MeanResidentMemory should be >0, got: %.2f", ru.MeanResidentMemory)
   235  				continue
   236  			}
   237  		}
   238  	}
   239  
   240  	if !finalRUReceived {
   241  		t.Error("Last resource-usage report from finished process was not received.")
   242  	}
   243  
   244  	// We expect floor(4200 / 2000) regular resource-usage reports, plus the last one sent after
   245  	// the process terminates.
   246  	if ruCnt != 3 {
   247  		t.Errorf("Unexpected number of resource-usage reports received. Got %d. Want 3.", ruCnt)
   248  	}
   249  }
   250  
   251  func mustNew(t *testing.T, name string, cfg *dspb.Config, sc service.Context) *Execution {
   252  	t.Helper()
   253  	ex, err := New(context.Background(), name, cfg, sc)
   254  	if err != nil {
   255  		t.Fatalf("execution.New returned error: %v", err)
   256  	}
   257  	t.Cleanup(func() {
   258  		ex.Shutdown()
   259  		err := ex.Wait()
   260  		if err != nil {
   261  			t.Errorf("unexpected execution error: %v", err)
   262  		}
   263  	})
   264  	return ex
   265  }
   266  
   267  func init() {
   268  	// Hack so that the testclient logs get written to the right place.
   269  	if d := os.Getenv("TEST_UNDECLARED_OUTPUTS_DIR"); d != "" {
   270  		os.Setenv("GOOGLE_LOG_DIR", d)
   271  	}
   272  }