Skip to content

Commit 5622d89

Browse files
committed
cmd env: adding an env package to aid in testing.
This change adds an Env object that can be injected into the various commands. The Env can be configured with an in memory store for testing purposes. This changes also adds a function to inject arbitrary errors in the in memory store and fixes the getters to avoid a panic on a nil result.
1 parent 4eff6a0 commit 5622d89

File tree

2 files changed

+205
-11
lines changed

2 files changed

+205
-11
lines changed

internal/cmd/env/env.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Copyright 2022 Cockroach Labs 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+
// 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 env provides the environment for the commands to run.
16+
package env
17+
18+
import (
19+
"bufio"
20+
"bytes"
21+
"context"
22+
"io"
23+
"os"
24+
25+
"github.com/cockroachlabs/visus/internal/database"
26+
"github.com/cockroachlabs/visus/internal/store"
27+
"github.com/spf13/cobra"
28+
)
29+
30+
// Env defines the environment to run the commands.
31+
type Env struct {
32+
testing bool
33+
store store.Store
34+
alt io.Reader
35+
}
36+
37+
// Default returns the default environment, for real world use.
38+
func Default() *Env {
39+
return &Env{
40+
alt: os.Stdin,
41+
}
42+
}
43+
44+
// Testing sets up an environment for testing commands.
45+
func Testing(ctx context.Context) *Env {
46+
store := &store.Memory{}
47+
err := store.Init(ctx)
48+
if err != nil {
49+
// this shouldn't happen for in memory store.
50+
panic("failed to initialize the store")
51+
}
52+
return &Env{
53+
testing: true,
54+
store: store,
55+
}
56+
}
57+
58+
// InjectReader sets the alternative reader to use in the env.
59+
func (e *Env) InjectReader(r io.Reader) {
60+
if e.testing {
61+
e.alt = r
62+
}
63+
}
64+
65+
// InjectStoreError forces the in memory store to return an error.
66+
func (e *Env) InjectStoreError(err error) {
67+
if mem, ok := e.store.(*store.Memory); e.testing && ok {
68+
mem.InjectError(err)
69+
}
70+
}
71+
72+
// ProvideStore returns the Store to use.
73+
func (e *Env) ProvideStore(ctx context.Context, databaseURL string) (store.Store, error) {
74+
if e.testing {
75+
return e.store, nil
76+
}
77+
conn, err := database.New(ctx, databaseURL)
78+
if err != nil {
79+
return nil, err
80+
}
81+
e.store = store.New(conn)
82+
return e.store, nil
83+
}
84+
85+
// ReadFile reads the named file. If the filename is "-", it will read
86+
// the content from the alternate reader in the environment.
87+
func (e *Env) ReadFile(file string) ([]byte, error) {
88+
var data []byte
89+
if file == "-" {
90+
var buffer bytes.Buffer
91+
scanner := bufio.NewScanner(e.alt)
92+
for scanner.Scan() {
93+
buffer.Write(scanner.Bytes())
94+
buffer.WriteString("\n")
95+
}
96+
if err := scanner.Err(); err != nil {
97+
return nil, err
98+
}
99+
data = buffer.Bytes()
100+
return data, nil
101+
}
102+
return os.ReadFile(file)
103+
}
104+
105+
// TestCommand executes the given command with the supplied arguments
106+
// and returns the standard output as a string.
107+
func (e *Env) TestCommand(f func(*Env) *cobra.Command, args []string) (string, error) {
108+
cmd := f(e)
109+
buffer := bytes.NewBufferString("")
110+
cmd.SetOut(buffer)
111+
cmd.SetArgs(args)
112+
err := cmd.Execute()
113+
if err != nil {
114+
return "", err
115+
}
116+
out, err := io.ReadAll(buffer)
117+
return string(out), err
118+
}

internal/store/memory.go

Lines changed: 87 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,79 +16,125 @@ package store
1616

1717
import (
1818
"context"
19+
"slices"
1920
"sync"
2021
"time"
2122
)
2223

2324
// Memory stores the configuration in memory. Used for testing.
2425
type Memory struct {
25-
MainNode bool
2626
collections *sync.Map
2727
histograms *sync.Map
2828
scans *sync.Map
29+
30+
mu struct {
31+
sync.RWMutex
32+
err error // Error to return
33+
mainNode bool
34+
}
2935
}
3036

3137
var _ Store = &Memory{}
3238

3339
// DeleteCollection implements store.Store.
3440
func (m *Memory) DeleteCollection(_ context.Context, name string) error {
41+
if m.Error() != nil {
42+
return m.Error()
43+
}
3544
m.collections.Delete(name)
3645
return nil
3746
}
3847

3948
// DeleteHistogram implements store.Store.
4049
func (m *Memory) DeleteHistogram(_ context.Context, name string) error {
50+
if m.Error() != nil {
51+
return m.Error()
52+
}
4153
m.histograms.Delete(name)
4254
return nil
4355
}
4456

4557
// DeleteScan implements store.Store.
4658
func (m *Memory) DeleteScan(_ context.Context, name string) error {
59+
if m.Error() != nil {
60+
return m.Error()
61+
}
4762
m.scans.Delete(name)
4863
return nil
4964
}
5065

66+
// Error returns the injected error
67+
func (m *Memory) Error() error {
68+
m.mu.RLock()
69+
defer m.mu.RUnlock()
70+
return m.mu.err
71+
}
72+
5173
// GetCollection implements store.Store.
5274
func (m *Memory) GetCollection(_ context.Context, name string) (*Collection, error) {
53-
res, _ := m.collections.Load(name)
75+
if m.Error() != nil {
76+
return nil, m.Error()
77+
}
78+
res, ok := m.collections.Load(name)
79+
if !ok {
80+
return nil, nil
81+
}
5482
return res.(*Collection), nil
5583
}
5684

5785
// GetCollectionNames implements store.Store.
5886
func (m *Memory) GetCollectionNames(_ context.Context) ([]string, error) {
59-
return getNames(m.collections)
87+
return m.getNames(m.collections)
6088
}
6189

6290
// GetHistogram implements store.Store.
6391
func (m *Memory) GetHistogram(_ context.Context, name string) (*Histogram, error) {
64-
res, _ := m.histograms.Load(name)
65-
return res.(*Histogram), nil
92+
if m.Error() != nil {
93+
return nil, m.Error()
94+
}
95+
res, ok := m.histograms.Load(name)
96+
if !ok {
97+
return nil, m.Error()
98+
}
99+
return res.(*Histogram), m.Error()
66100
}
67101

68102
// GetHistogramNames implements store.Store.
69103
func (m *Memory) GetHistogramNames(_ context.Context) ([]string, error) {
70-
return getNames(m.histograms)
104+
return m.getNames(m.histograms)
71105
}
72106

73107
// GetMetrics implements store.Store.
74108
func (m *Memory) GetMetrics(ctx context.Context, name string) ([]Metric, error) {
109+
if m.Error() != nil {
110+
return nil, m.Error()
111+
}
75112
coll, _ := m.GetCollection(ctx, name)
76113
return coll.Metrics, nil
77114
}
78115

79116
// GetScan implements store.Store.
80117
func (m *Memory) GetScan(_ context.Context, name string) (*Scan, error) {
81-
res, _ := m.scans.Load(name)
118+
if m.Error() != nil {
119+
return nil, m.Error()
120+
}
121+
res, ok := m.scans.Load(name)
122+
if !ok {
123+
return nil, nil
124+
}
82125
return res.(*Scan), nil
83126
}
84127

85128
// GetScanNames implements store.Store.
86129
func (m *Memory) GetScanNames(_ context.Context) ([]string, error) {
87-
return getNames(m.scans)
130+
return m.getNames(m.scans)
88131
}
89132

90133
// GetScanPatterns implements store.Store.
91134
func (m *Memory) GetScanPatterns(ctx context.Context, name string) ([]Pattern, error) {
135+
if m.Error() != nil {
136+
return nil, m.Error()
137+
}
92138
scan, _ := m.GetScan(ctx, name)
93139
return scan.Patterns, nil
94140
}
@@ -98,37 +144,67 @@ func (m *Memory) Init(_ context.Context) error {
98144
m.collections = &sync.Map{}
99145
m.histograms = &sync.Map{}
100146
m.scans = &sync.Map{}
147+
m.InjectError(nil)
101148
return nil
102149
}
103150

151+
// InjectError sets the error that will be returned on each subsequent call.
152+
func (m *Memory) InjectError(err error) {
153+
m.mu.Lock()
154+
defer m.mu.Unlock()
155+
m.mu.err = err
156+
}
157+
104158
// IsMainNode implements store.Store.
105159
func (m *Memory) IsMainNode(_ context.Context, lastUpdated time.Time) (bool, error) {
106-
return m.MainNode, nil
160+
m.mu.RLock()
161+
defer m.mu.RUnlock()
162+
return m.mu.mainNode, m.mu.err
107163
}
108164

109165
// PutCollection implements store.Store.
110166
func (m *Memory) PutCollection(_ context.Context, collection *Collection) error {
167+
if m.Error() != nil {
168+
return m.Error()
169+
}
111170
m.collections.Store(collection.Name, collection)
112171
return nil
113172
}
114173

115174
// PutHistogram implements store.Store.
116175
func (m *Memory) PutHistogram(_ context.Context, histogram *Histogram) error {
176+
if m.Error() != nil {
177+
return m.Error()
178+
}
117179
m.histograms.Store(histogram.Name, histogram)
118180
return nil
119181
}
120182

121183
// PutScan implements store.Store.
122184
func (m *Memory) PutScan(_ context.Context, scan *Scan) error {
185+
if m.Error() != nil {
186+
return m.Error()
187+
}
123188
m.scans.Store(scan.Name, scan)
124189
return nil
125190
}
126191

127-
func getNames(m *sync.Map) ([]string, error) {
192+
// SetMainNode sets this store as the main node.
193+
func (m *Memory) SetMainNode(main bool) {
194+
m.mu.Lock()
195+
defer m.mu.Unlock()
196+
m.mu.mainNode = main
197+
}
198+
199+
func (m *Memory) getNames(vals *sync.Map) ([]string, error) {
200+
if m.Error() != nil {
201+
return nil, m.Error()
202+
}
128203
names := make([]string, 0)
129-
m.Range(func(key any, value any) bool {
204+
vals.Range(func(key any, value any) bool {
130205
names = append(names, key.(string))
131206
return true
132207
})
208+
slices.Sort(names)
133209
return names, nil
134210
}

0 commit comments

Comments
 (0)