go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/client/butlerlib/streamclient/fake.go (about)

     1  // Copyright 2019 The LUCI 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 streamclient
    16  
    17  import (
    18  	"fmt"
    19  	"io"
    20  	"strconv"
    21  	"strings"
    22  	"sync"
    23  	"sync/atomic"
    24  
    25  	"go.chromium.org/luci/common/errors"
    26  	"go.chromium.org/luci/logdog/client/butlerlib/streamproto"
    27  	"go.chromium.org/luci/logdog/common/types"
    28  )
    29  
    30  // fakeStream represents a dialed stream, and has methods to retrieve the data
    31  // written to it.
    32  type fakeStream struct {
    33  	flags streamproto.Flags
    34  
    35  	mu         sync.RWMutex
    36  	closed     bool
    37  	streamData strings.Builder
    38  	datagrams  []string
    39  }
    40  
    41  var _ interface {
    42  	DatagramStream
    43  	io.Writer
    44  	FakeStreamData
    45  } = (*fakeStream)(nil)
    46  
    47  func (ds *fakeStream) Close() error {
    48  	ds.mu.Lock()
    49  	defer ds.mu.Unlock()
    50  	if ds.closed {
    51  		return io.ErrClosedPipe
    52  	}
    53  	ds.closed = true
    54  	return nil
    55  }
    56  
    57  func (ds *fakeStream) Write(data []byte) (int, error) {
    58  	ds.mu.Lock()
    59  	defer ds.mu.Unlock()
    60  	if ds.closed {
    61  		return 0, io.ErrClosedPipe
    62  	}
    63  	return ds.streamData.Write(data)
    64  }
    65  
    66  func (ds *fakeStream) WriteDatagram(dg []byte) error {
    67  	ds.mu.Lock()
    68  	defer ds.mu.Unlock()
    69  	if ds.closed {
    70  		return io.ErrClosedPipe
    71  	}
    72  	ds.datagrams = append(ds.datagrams, string(dg))
    73  	return nil
    74  }
    75  
    76  func (ds *fakeStream) GetFlags() streamproto.Flags {
    77  	return ds.flags
    78  }
    79  
    80  func (ds *fakeStream) GetStreamData() string {
    81  	ds.mu.RLock()
    82  	defer ds.mu.RUnlock()
    83  	return ds.streamData.String()
    84  }
    85  
    86  func (ds *fakeStream) GetDatagrams() []string {
    87  	ds.mu.RLock()
    88  	defer ds.mu.RUnlock()
    89  	return append(make([]string, 0, len(ds.datagrams)), ds.datagrams...)
    90  }
    91  
    92  func (ds *fakeStream) IsClosed() bool {
    93  	ds.mu.RLock()
    94  	defer ds.mu.RUnlock()
    95  	return ds.closed
    96  }
    97  
    98  type fakeDialer struct {
    99  	mu      sync.RWMutex
   100  	err     error
   101  	streams map[types.StreamName]*fakeStream
   102  }
   103  
   104  var _ dialer = (*fakeDialer)(nil)
   105  
   106  // Makes a shallow interface-based map of all of the fakeStreams.
   107  //
   108  // Because the FakeStreamData interface is read-only, this should be safe.
   109  func (d *fakeDialer) dumpFakeData() map[types.StreamName]FakeStreamData {
   110  	d.mu.RLock()
   111  	defer d.mu.RUnlock()
   112  	ret := make(map[types.StreamName]FakeStreamData, len(d.streams))
   113  	for k, v := range d.streams {
   114  		ret[k] = v
   115  	}
   116  	return ret
   117  }
   118  
   119  func (d *fakeDialer) setFakeError(err error) {
   120  	d.mu.Lock()
   121  	d.err = err
   122  	d.mu.Unlock()
   123  }
   124  
   125  func (d *fakeDialer) getStream(f streamproto.Flags) (*fakeStream, error) {
   126  	d.mu.Lock()
   127  	defer d.mu.Unlock()
   128  
   129  	if d.err != nil {
   130  		return nil, d.err
   131  	}
   132  	streamName := types.StreamName(f.Name)
   133  	if _, hasStream := d.streams[streamName]; hasStream {
   134  		return nil, errors.Reason("stream %q already dialed", streamName).Err()
   135  	}
   136  
   137  	ret := &fakeStream{flags: f}
   138  	d.streams[streamName] = ret
   139  	return ret, nil
   140  }
   141  
   142  func (d *fakeDialer) DialStream(forProcess bool, f streamproto.Flags) (io.WriteCloser, error) {
   143  	return d.getStream(f)
   144  }
   145  
   146  func (d *fakeDialer) DialDgramStream(f streamproto.Flags) (DatagramStream, error) {
   147  	return d.getStream(f)
   148  }
   149  
   150  // These support NewFake and the "fake" protocol handler.
   151  var (
   152  	fakeRegistryMu sync.Mutex
   153  	fakeRegistryID int64
   154  	fakeRegistry   = map[int64]Fake{}
   155  )
   156  
   157  // NewFake makes an in-memory Fake which absorbs all data written via Clients
   158  // connected to it, and allows retrieval of this data via extra methods.
   159  //
   160  // You must call Fake.Unregister to free the collected data.
   161  //
   162  // You can obtain a Client for this Fake by doing:
   163  //
   164  //	fake := streamclient.NewFake()
   165  //	defer fake.Unregister()
   166  //	client, _ := streamclient.New(fake.StreamServerPath(), "whatever")
   167  //
   168  // This is particularly useful for testing programs which use
   169  // bootstrap.GetFromEnv() to obtain the Client:
   170  //
   171  //	env := environ.New(nil)
   172  //	env.Set(bootstrap.EnvStreamServerPath, client.StreamServerPath())
   173  //
   174  //	bs := bootstrap.GetFromEnv(env)   # returns Bootstrap containing `client`.
   175  //
   176  // If you JUST want a single client and don't need streamclient.New to work,
   177  // consider NewUnregisteredFake.
   178  func NewFake() Fake {
   179  	fakeRegistryMu.Lock()
   180  	defer fakeRegistryMu.Unlock()
   181  
   182  	id := fakeRegistryID + 1
   183  	fakeRegistryID++
   184  
   185  	ret := Fake{
   186  		id,
   187  		&fakeDialer{streams: map[types.StreamName]*fakeStream{}},
   188  	}
   189  	fakeRegistry[id] = ret
   190  	return ret
   191  }
   192  
   193  // NewUnregisteredFake returns a new Fake as well as a *Client for this Fake.
   194  //
   195  // Fake.StreamServerPath() will return an empty string and Unregister is
   196  // a no-op.
   197  //
   198  // Aside from the returned *Client there's no way to obtain another *Client
   199  // for this Fake.
   200  func NewUnregisteredFake(namespace types.StreamName) (Fake, *Client) {
   201  	f := Fake{
   202  		0,
   203  		&fakeDialer{streams: map[types.StreamName]*fakeStream{}},
   204  	}
   205  	return f, &Client{f.dial, namespace}
   206  }
   207  
   208  // Fake implements an in-memory sink for Client connections and data.
   209  //
   210  // You can retrieve the collected data via the Data method.
   211  type Fake struct {
   212  	id   int64 // 0 means "unregistered"
   213  	dial *fakeDialer
   214  }
   215  
   216  // Unregister idempotently removes this Fake from the global registry.
   217  //
   218  // This has no effect for a Fake created with NewUnregisteredFake.
   219  //
   220  // After calling Unregister, StreamServerPath will return an empty string.
   221  func (f Fake) Unregister() {
   222  	id := atomic.SwapInt64(&f.id, 0)
   223  	if id == 0 {
   224  		return
   225  	}
   226  	fakeRegistryMu.Lock()
   227  	defer fakeRegistryMu.Unlock()
   228  	delete(fakeRegistry, id)
   229  }
   230  
   231  // StreamServerPath returns a value suitable for streamclient.New that will
   232  // allow construction of a *Client which points to this Fake.
   233  //
   234  // If this Fake was generated via NewUnregisteredFake, or you called
   235  // Unregister on it, returns an empty string.
   236  func (f Fake) StreamServerPath() string {
   237  	id := atomic.LoadInt64(&f.id)
   238  	if id == 0 {
   239  		return ""
   240  	}
   241  	return fmt.Sprintf("fake:%d", id)
   242  }
   243  
   244  // FakeStreamData is data about a single stream for a client created with the
   245  // "fake" protocol. You can retrieve this with Fake.Data().
   246  type FakeStreamData interface {
   247  	// GetFlags returns the stream's streamproto.Flags from when the stream was
   248  	// opened.
   249  	GetFlags() streamproto.Flags
   250  
   251  	// GetStreamData returns the written text/binary data as a string.
   252  	//
   253  	// If this is a datagram stream, returns "".
   254  	GetStreamData() string
   255  
   256  	// GetDatagrams returns the written datagrams as a list of strings.
   257  	//
   258  	// If this is a text/binary stream, returns nil.
   259  	GetDatagrams() []string
   260  
   261  	// Returns true iff the user of the client has closed this stream.
   262  	IsClosed() bool
   263  }
   264  
   265  // Data returns all absorbed data collected by this Fake so far.
   266  //
   267  // Note that the FakeStreamData objects are `live`, and so calling their methods
   268  // will always get you the current value of those streams. You'll need to call
   269  // this method again if you expect a new stream to show up, however.
   270  func (f Fake) Data() map[types.StreamName]FakeStreamData {
   271  	return f.dial.dumpFakeData()
   272  }
   273  
   274  // SetError causes New* methods for Clients of this Fake to return this error.
   275  func (f Fake) SetError(err error) {
   276  	f.dial.setFakeError(err)
   277  }
   278  
   279  func init() {
   280  	protocolRegistry["fake"] = func(fakeId string) (dialer, error) {
   281  		id, err := strconv.ParseInt(fakeId, 10, 64)
   282  		if err != nil {
   283  			return nil, err
   284  		}
   285  
   286  		fakeRegistryMu.Lock()
   287  		defer fakeRegistryMu.Unlock()
   288  		f, ok := fakeRegistry[id]
   289  		if !ok {
   290  			return nil, errors.Reason("unknown Fake %d", id).Err()
   291  		}
   292  		return f.dial, nil
   293  	}
   294  }