-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcommand_func_test.go
More file actions
322 lines (291 loc) · 9.92 KB
/
Copy pathcommand_func_test.go
File metadata and controls
322 lines (291 loc) · 9.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
package main
import (
"io"
"os"
"path/filepath"
"strings"
"testing"
"github.com/urfave/cli/v2"
"github.com/cpflat/dot2net/pkg/model"
"github.com/cpflat/dot2net/pkg/types"
)
// topologyRoots are the directories a dot2net topology can live in: topologies/
// holds the ones worth deploying, example/ the ones that demonstrate a notation.
var topologyRoots = []string{"topologies", "example"}
// findTopologyDir returns the directory of the named topology, whichever root it
// sits under.
func findTopologyDir(t *testing.T, rootDir string, topology string) string {
t.Helper()
for _, root := range topologyRoots {
dir := filepath.Join(rootDir, root, topology)
if _, err := os.Stat(filepath.Join(dir, "input.dot")); err == nil {
return dir
}
}
t.Fatalf("topology %q not found under any of %v", topology, topologyRoots)
return ""
}
// listGeneratedFiles computes the set of files that a build would generate for
// the topology in the current working directory, using the same model pipeline
// as CmdClean/CmdFiles.
func listGeneratedFiles(t *testing.T, dotFile, cfgFile string) []string {
t.Helper()
d, err := model.DiagramFromDotFile(dotFile)
if err != nil {
t.Fatalf("DiagramFromDotFile: %v", err)
}
cfg, err := types.LoadConfig(cfgFile)
if err != nil {
t.Fatalf("LoadConfig: %v", err)
}
nm, err := model.BuildNetworkModelForFileList(cfg, d)
if err != nil {
t.Fatalf("BuildNetworkModelForFileList: %v", err)
}
files, err := model.ListGeneratedFiles(cfg, nm, false)
if err != nil {
t.Fatalf("ListGeneratedFiles: %v", err)
}
return files
}
// TestCmdClean_DeletesOnlyGeneratedFiles is the safety test called out by
// CR-077: `clean` must delete only the files a build would generate (and only
// directories it emptied), never user-authored files. It runs the real
// commandBuild/commandClean CLI actions end-to-end in a temp dir.
func TestCmdClean_DeletesOnlyGeneratedFiles(t *testing.T) {
wd, err := os.Getwd()
if err != nil {
t.Fatalf("Getwd: %v", err)
}
topologyDir := findTopologyDir(t, wd, "ospf_simple")
tmpDir := t.TempDir()
// copy top-level input files
entries, err := os.ReadDir(topologyDir)
if err != nil {
t.Fatalf("ReadDir topology: %v", err)
}
for _, e := range entries {
if e.IsDir() {
continue
}
data, err := os.ReadFile(filepath.Join(topologyDir, e.Name()))
if err != nil {
t.Fatalf("read input %s: %v", e.Name(), err)
}
if err := os.WriteFile(filepath.Join(tmpDir, e.Name()), data, 0644); err != nil {
t.Fatalf("write input %s: %v", e.Name(), err)
}
}
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Chdir: %v", err)
}
defer os.Chdir(wd)
const dotFile = "input.dot"
const cfgFile = "input.yaml"
app := &cli.App{
Writer: io.Discard,
ErrWriter: io.Discard,
Commands: []*cli.Command{commandBuild, commandClean},
}
// 1. build -> generate files
if err := app.Run([]string{"dot2net", "build", "-c", cfgFile, dotFile}); err != nil {
t.Fatalf("build run: %v", err)
}
generated := listGeneratedFiles(t, dotFile, cfgFile)
if len(generated) == 0 {
t.Fatalf("topology generated no files")
}
// sanity: generated files actually exist on disk after build
for _, f := range generated {
if _, err := os.Stat(filepath.FromSlash(f)); err != nil {
t.Fatalf("expected generated file %s to exist after build: %v", f, err)
}
}
// Identify a directory that holds generated files, to (a) drop a user file
// inside it and confirm the directory (and the user file) survive clean,
// and (b) find another generated directory that should be emptied+removed.
dirsWithGenerated := map[string]bool{}
for _, f := range generated {
if dir := filepath.Dir(f); dir != "." && dir != "" {
dirsWithGenerated[dir] = true
}
}
if len(dirsWithGenerated) < 2 {
t.Fatalf("topology needs >=2 generated subdirectories, got %v", dirsWithGenerated)
}
var keepDir, emptyDir string
for d := range dirsWithGenerated {
if keepDir == "" {
keepDir = d
} else if emptyDir == "" && d != keepDir {
emptyDir = d
}
}
// 2. add user-authored files: one at top level, one inside a generated dir
topUserFile := "user_notes.txt"
if err := os.WriteFile(topUserFile, []byte("keep me"), 0644); err != nil {
t.Fatalf("write user file: %v", err)
}
nestedUserFile := filepath.Join(keepDir, "user_keep.conf")
if err := os.WriteFile(nestedUserFile, []byte("keep me too"), 0644); err != nil {
t.Fatalf("write nested user file: %v", err)
}
// 3. clean
if err := app.Run([]string{"dot2net", "clean", "-c", cfgFile, dotFile}); err != nil {
t.Fatalf("clean run: %v", err)
}
// 4a. every generated file must be gone
for _, f := range generated {
if _, err := os.Stat(filepath.FromSlash(f)); err == nil {
t.Errorf("generated file %s should have been deleted", f)
}
}
// 4b. user files must survive
if _, err := os.Stat(topUserFile); err != nil {
t.Errorf("top-level user file was deleted: %v", err)
}
if _, err := os.Stat(nestedUserFile); err != nil {
t.Errorf("nested user file was deleted: %v", err)
}
// 4c. input files must survive
for _, f := range []string{dotFile, cfgFile} {
if _, err := os.Stat(f); err != nil {
t.Errorf("input file %s was deleted: %v", f, err)
}
}
// 4d. a directory still holding a user file must NOT be removed
if _, err := os.Stat(keepDir); err != nil {
t.Errorf("directory %s with a surviving user file was removed: %v", keepDir, err)
}
// 4e. a directory that only held generated files must be removed (empty)
if _, err := os.Stat(emptyDir); err == nil {
t.Errorf("emptied generated directory %s should have been removed", emptyDir)
}
}
// copyTopologyInputs copies a topology's top-level files into a fresh temp dir
// and chdirs into it, returning to the original directory when the test ends.
func copyTopologyInputs(t *testing.T, topology string) {
t.Helper()
wd, err := os.Getwd()
if err != nil {
t.Fatalf("Getwd: %v", err)
}
topologyDir := findTopologyDir(t, wd, topology)
tmpDir := t.TempDir()
entries, err := os.ReadDir(topologyDir)
if err != nil {
t.Fatalf("ReadDir topology: %v", err)
}
for _, e := range entries {
if e.IsDir() {
continue
}
data, err := os.ReadFile(filepath.Join(topologyDir, e.Name()))
if err != nil {
t.Fatalf("read input %s: %v", e.Name(), err)
}
if err := os.WriteFile(filepath.Join(tmpDir, e.Name()), data, 0644); err != nil {
t.Fatalf("write input %s: %v", e.Name(), err)
}
}
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Chdir: %v", err)
}
t.Cleanup(func() { os.Chdir(wd) })
}
// TestCmdFiles_ListsPerMachineTopologyFiles guards the file-list pipeline against
// missing what a module hands out during classification. A topology file scoped
// to a worker group is assigned there, not when the module is loaded, so a
// pipeline that stops before classification reports a short list - and `clean`,
// which deletes exactly that list, used to leave every per-machine topo.yaml
// behind. Single-machine topologies cannot catch this: their topology file is
// network-scoped and registered at load time.
func TestCmdFiles_ListsPerMachineTopologyFiles(t *testing.T) {
copyTopologyInputs(t, "ospf_multihost")
const dotFile = "input.dot"
const cfgFile = "input.yaml"
app := &cli.App{Writer: io.Discard, ErrWriter: io.Discard, Commands: []*cli.Command{commandBuild, commandClean}}
if err := app.Run([]string{"dot2net", "build", "-c", cfgFile, dotFile}); err != nil {
t.Fatalf("build run: %v", err)
}
listed := map[string]bool{}
for _, f := range listGeneratedFiles(t, dotFile, cfgFile) {
listed[f] = true
}
for _, want := range []string{"host1/topo.yaml", "host2/topo.yaml"} {
// The list names a file the way this machine does, so the wanted path
// is put in the same terms before either is used.
want = filepath.FromSlash(want)
if _, err := os.Stat(want); err != nil {
t.Fatalf("the build did not write %s: %v", want, err)
}
if !listed[want] {
t.Errorf("%s was generated but is missing from the file list", want)
}
}
if err := app.Run([]string{"dot2net", "clean", "-c", cfgFile, dotFile}); err != nil {
t.Fatalf("clean run: %v", err)
}
for _, gone := range []string{"host1/topo.yaml", "host2/topo.yaml"} {
if _, err := os.Stat(filepath.FromSlash(gone)); err == nil {
t.Errorf("clean left %s behind", gone)
}
}
}
// TestCmdClean_DryRun verifies that --dry-run reports without deleting.
func TestCmdClean_DryRun(t *testing.T) {
wd, err := os.Getwd()
if err != nil {
t.Fatalf("Getwd: %v", err)
}
topologyDir := findTopologyDir(t, wd, "ospf_simple")
tmpDir := t.TempDir()
entries, err := os.ReadDir(topologyDir)
if err != nil {
t.Fatalf("ReadDir topology: %v", err)
}
for _, e := range entries {
if e.IsDir() {
continue
}
data, err := os.ReadFile(filepath.Join(topologyDir, e.Name()))
if err != nil {
t.Fatalf("read input: %v", err)
}
if err := os.WriteFile(filepath.Join(tmpDir, e.Name()), data, 0644); err != nil {
t.Fatalf("write input: %v", err)
}
}
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Chdir: %v", err)
}
defer os.Chdir(wd)
const dotFile = "input.dot"
const cfgFile = "input.yaml"
// capture stdout to confirm the dry-run report mentions "Would delete"
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
app := &cli.App{Writer: io.Discard, ErrWriter: io.Discard, Commands: []*cli.Command{commandBuild, commandClean}}
if err := app.Run([]string{"dot2net", "build", "-c", cfgFile, dotFile}); err != nil {
os.Stdout = oldStdout
t.Fatalf("build run: %v", err)
}
generated := listGeneratedFiles(t, dotFile, cfgFile)
runErr := app.Run([]string{"dot2net", "clean", "--dry-run", "-c", cfgFile, dotFile})
w.Close()
os.Stdout = oldStdout
out, _ := io.ReadAll(r)
if runErr != nil {
t.Fatalf("clean --dry-run run: %v", runErr)
}
// dry-run must not delete anything
for _, f := range generated {
if _, err := os.Stat(filepath.FromSlash(f)); err != nil {
t.Errorf("dry-run deleted generated file %s: %v", f, err)
}
}
if !strings.Contains(string(out), "Would delete") {
t.Errorf("dry-run output lacks 'Would delete':\n%s", out)
}
}