fake.go

v0.5.0
Doc Versions Source
1
// Package legatustest provides test doubles for the legatus core: a scripted
2
// Conn and a Dialer that hands them out per host. Shared so engine, steps, and
3
// sync tests do not each reimplement a fake (GPC8). The doubles are safe for the
4
// concurrent use the Runner makes of them under fan-out.
5
package legatustest
6
7
import (
8
	"context"
9
	"io"
10
	"sync"
11
12
	"go.bigb.es/auxilia/legatus"
13
)
14
15
// FakeConn is a scripted legatus.Conn. Set ExecFn to script command responses;
16
// uploads are captured in Files; every call is recorded.
17
//
18
// ExecFn must honor the Conn.Exec contract: a command that "runs" returns a
19
// nil error even on a non-zero exit, with the status in ExecResult.ExitCode —
20
// matching the real SSH transport. Returning a non-nil error models a failure
21
// to run at all (no session, IO error), NOT a non-zero exit code.
22
type FakeConn struct {
23
	ExecFn func(ctx context.Context, cmd legatus.Cmd) (legatus.ExecResult, error)
24
	// StreamFn, when set, backs ExecStream instead of the ExecFn+copy default — for
25
	// fakes that must write to the sinks incrementally or honor ctx mid-stream.
26
	StreamFn func(ctx context.Context, cmd legatus.Cmd, stdout, stderr io.Writer) (legatus.ExecResult, error)
27
	Files    map[string][]byte
28
29
	mu     sync.Mutex
30
	Execs  []legatus.Cmd
31
	Gets   []string
32
	Puts   []PutCall
33
	closed bool
34
}
35
36
// PutCall records one Put: the destination, mode, and resolved options — so a
37
// test can assert sudo/owner threading, which the Files map alone can't show.
38
type PutCall struct {
39
	Remote string
40
	Mode   legatus.FileMode
41
	Opts   legatus.PutOptions
42
}
43
44
// NewFakeConn returns a ready FakeConn.
45
func NewFakeConn() *FakeConn { return &FakeConn{Files: map[string][]byte{}} }
46
47
// Exec records the command and delegates to ExecFn (zero result if unset).
48
func (c *FakeConn) Exec(ctx context.Context, cmd legatus.Cmd) (legatus.ExecResult, error) {
49
	c.mu.Lock()
50
	c.Execs = append(c.Execs, cmd)
51
	fn := c.ExecFn
52
	c.mu.Unlock()
53
	if fn != nil {
54
		return fn(ctx, cmd)
55
	}
56
	return legatus.ExecResult{}, nil
57
}
58
59
// ExecStream records the command and streams output. When StreamFn is set it
60
// backs the call directly; otherwise it runs ExecFn and copies the scripted
61
// ExecResult.Stdout/Stderr to each non-nil writer (a nil writer keeps its bytes
62
// in the result), mirroring the real transports' contract.
63
func (c *FakeConn) ExecStream(ctx context.Context, cmd legatus.Cmd, stdout, stderr io.Writer) (legatus.ExecResult, error) {
64
	c.mu.Lock()
65
	c.Execs = append(c.Execs, cmd)
66
	sfn, fn := c.StreamFn, c.ExecFn
67
	c.mu.Unlock()
68
	if sfn != nil {
69
		return sfn(ctx, cmd, stdout, stderr)
70
	}
71
	var res legatus.ExecResult
72
	var err error
73
	if fn != nil {
74
		res, err = fn(ctx, cmd)
75
	}
76
	if err != nil {
77
		return res, err
78
	}
79
	if stdout != nil && len(res.Stdout) > 0 {
80
		if _, werr := stdout.Write(res.Stdout); werr != nil {
81
			return res, werr
82
		}
83
		res.Stdout = nil
84
	}
85
	if stderr != nil && len(res.Stderr) > 0 {
86
		if _, werr := stderr.Write(res.Stderr); werr != nil {
87
			return res, werr
88
		}
89
		res.Stderr = nil
90
	}
91
	return res, nil
92
}
93
94
// Put captures the uploaded content keyed by remote path, and records the call
95
// (mode + resolved options) in Puts.
96
func (c *FakeConn) Put(_ context.Context, content io.Reader, remote string, mode legatus.FileMode, opts ...legatus.PutOption) error {
97
	b, err := io.ReadAll(content)
98
	if err != nil {
99
		return err
100
	}
101
	var o legatus.PutOptions
102
	for _, opt := range opts {
103
		opt(&o)
104
	}
105
	c.mu.Lock()
106
	if c.Files == nil {
107
		c.Files = map[string][]byte{}
108
	}
109
	c.Files[remote] = b
110
	c.Puts = append(c.Puts, PutCall{Remote: remote, Mode: mode, Opts: o})
111
	c.mu.Unlock()
112
	return nil
113
}
114
115
// Get writes previously-Put content for remote, if any.
116
func (c *FakeConn) Get(_ context.Context, remote string, w io.Writer) error {
117
	c.mu.Lock()
118
	c.Gets = append(c.Gets, remote)
119
	b, ok := c.Files[remote]
120
	c.mu.Unlock()
121
	if ok {
122
		_, err := w.Write(b)
123
		return err
124
	}
125
	return nil
126
}
127
128
// Close marks the connection closed.
129
func (c *FakeConn) Close() error {
130
	c.mu.Lock()
131
	c.closed = true
132
	c.mu.Unlock()
133
	return nil
134
}
135
136
// Closed reports whether Close was called.
137
func (c *FakeConn) Closed() bool {
138
	c.mu.Lock()
139
	defer c.mu.Unlock()
140
	return c.closed
141
}
142
143
// FakeDialer hands out FakeConns. Conns maps host name to a specific conn;
144
// otherwise Default (if set) builds one, else a fresh FakeConn is returned. Dial
145
// is safe for concurrent use (the Runner dials hosts in parallel).
146
type FakeDialer struct {
147
	Conns   map[string]*FakeConn
148
	Default func(*legatus.Host) *FakeConn
149
150
	mu    sync.Mutex
151
	Dials []string
152
}
153
154
// Dial records the host and returns its conn.
155
func (d *FakeDialer) Dial(_ context.Context, h *legatus.Host) (legatus.Conn, error) {
156
	d.mu.Lock()
157
	d.Dials = append(d.Dials, h.Name)
158
	c, ok := d.Conns[h.Name]
159
	def := d.Default
160
	d.mu.Unlock()
161
	if ok {
162
		return c, nil
163
	}
164
	if def != nil {
165
		return def(h), nil
166
	}
167
	return NewFakeConn(), nil
168
}
169

Source Files