Skip to content

Commit bd9f1f2

Browse files
kanard38knard38
authored andcommitted
DAOS-18304 ddb: add unit tests for open, feature, dtx_aggr, newLogger, closePoolIfOpen
Add critical unit tests that exercise the Go-layer validation and stub dispatch for the most important ddb commands: * TestCmds: add open (default, write_mode, db_path), feature (show, enable, disable), and dtx_aggr (mutual exclusion, neither defined, cmt_time, cmt_date, with path) test cases. Add a skipCmdLine field to the test struct so cases whose flags are shared with CLI-level go-flags options can be tested in command-file mode only (the CLI layer intercepts those flags before grumble can see them). Add a follow-up TODO comment listing the 21 commands still to be covered. * TestHelpCmds: add a follow-up TODO comment pointing to the pattern to use for the remaining commands. * TestNewLogger: 6 sub-cases covering default level, explicit valid debug level, invalid level, valid LogDir, non-existent LogDir, and LogDir pointing to a file. * TestClosePoolIfOpen: 3 sub-cases verifying that Close is NOT called when the pool is already closed, IS called when open, and that a Close error is tolerated (logged only, no panic). Signed-off-by: Cedric Koch-Hofer <cedric.koch-hofer@hpe.com>
1 parent db28efe commit bd9f1f2

3 files changed

Lines changed: 288 additions & 1 deletion

File tree

src/control/cmd/ddb/ddb_commands_test.go

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ func TestHelpCmds(t *testing.T) {
6767
cmdStr: "open",
6868
helpSubStr: "Usage:\n open [flags] path\n",
6969
},
70+
// TODO(follow-up PR): Add help tests for the remaining commands.
71+
// Use runHelpCmd(t, "<cmd>", "Usage:\n <cmd>") following the same pattern.
7072
} {
7173
t.Run(name, func(t *testing.T) {
7274
runHelpCmd(t, tc.cmdStr, tc.helpSubStr)
@@ -80,6 +82,11 @@ func TestCmds(t *testing.T) {
8082
setup func()
8183
expStdout []string
8284
expErr error
85+
// skipCmdLine skips the command-line sub-test with a message. Use when
86+
// a flag is shared between the CLI layer and the grumble command: go-flags
87+
// consumes it before grumble can see it, making a clean command-line test
88+
// impossible for that particular flag.
89+
skipCmdLine string
8390
}{
8491
"ls invalid options": {
8592
args: []string{"ls", "--bar"},
@@ -180,6 +187,183 @@ func TestCmds(t *testing.T) {
180187
},
181188
expStdout: []string{"ls called"},
182189
},
190+
191+
// --- open command ---
192+
// Note: the -w/--write_mode and -p/--db_path flags of the grumble 'open'
193+
// command share names with CLI-level flags that are consumed by go-flags
194+
// before reaching grumble in command-line mode. The command-line test for
195+
// those flags would silently test wrong values. They are correctly exercised
196+
// in command-file mode; see TestRun for CLI-level flag coverage.
197+
"open default": {
198+
args: []string{"open", "/path/to/vos-0"},
199+
setup: func() {
200+
ddb_run_open_Fn = func(path, dbPath string, writeMode bool) error {
201+
fmt.Println("open called")
202+
if err := isArgEqual("/path/to/vos-0", path, "path"); err != nil {
203+
return err
204+
}
205+
if err := isArgEqual("", dbPath, "db_path"); err != nil {
206+
return err
207+
}
208+
if err := isArgEqual(false, writeMode, "write_mode"); err != nil {
209+
return err
210+
}
211+
return nil
212+
}
213+
},
214+
expStdout: []string{"open called"},
215+
},
216+
"open write mode": {
217+
args: []string{"open", "-w", "/path/to/vos-0"},
218+
skipCmdLine: "-w is consumed by the CLI write_mode flag before reaching grumble",
219+
setup: func() {
220+
ddb_run_open_Fn = func(path, dbPath string, writeMode bool) error {
221+
fmt.Println("open called")
222+
if err := isArgEqual(true, writeMode, "write_mode"); err != nil {
223+
return err
224+
}
225+
return nil
226+
}
227+
},
228+
expStdout: []string{"open called"},
229+
},
230+
"open with db path": {
231+
args: []string{"open", "-p", "/sysdb", "/path/to/vos-0"},
232+
skipCmdLine: "-p is consumed by the CLI db_path flag before reaching grumble",
233+
setup: func() {
234+
ddb_run_open_Fn = func(path, dbPath string, writeMode bool) error {
235+
fmt.Println("open called")
236+
if err := isArgEqual("/path/to/vos-0", path, "path"); err != nil {
237+
return err
238+
}
239+
if err := isArgEqual("/sysdb", dbPath, "db_path"); err != nil {
240+
return err
241+
}
242+
return nil
243+
}
244+
},
245+
expStdout: []string{"open called"},
246+
},
247+
248+
// --- feature command ---
249+
// feature --show: verifies the show flag is forwarded to the C layer.
250+
"feature show": {
251+
args: []string{"feature", "--show"},
252+
setup: func() {
253+
ddb_run_feature_Fn = func(path, dbPath, enable, disable string, show bool) error {
254+
fmt.Println("feature called")
255+
if err := isArgEqual(true, show, "show"); err != nil {
256+
return err
257+
}
258+
return nil
259+
}
260+
},
261+
expStdout: []string{"feature called"},
262+
},
263+
// feature --enable: verifies that the enable string reaches ddb_feature_string2flags.
264+
"feature enable": {
265+
args: []string{"feature", "--enable=myflag"},
266+
setup: func() {
267+
var capturedFlag string
268+
ddb_feature_string2flags_Fn = func(s string) (uint64, uint64, error) {
269+
capturedFlag = s
270+
return 0, 0, nil
271+
}
272+
ddb_run_feature_Fn = func(path, dbPath, enable, disable string, show bool) error {
273+
fmt.Println("feature called")
274+
if err := isArgEqual("myflag", capturedFlag, "enable flag string"); err != nil {
275+
return err
276+
}
277+
return nil
278+
}
279+
},
280+
expStdout: []string{"feature called"},
281+
},
282+
// feature --disable: verifies that the disable string reaches ddb_feature_string2flags.
283+
"feature disable": {
284+
args: []string{"feature", "--disable=otherflag"},
285+
setup: func() {
286+
var capturedFlag string
287+
ddb_feature_string2flags_Fn = func(s string) (uint64, uint64, error) {
288+
capturedFlag = s
289+
return 0, 0, nil
290+
}
291+
ddb_run_feature_Fn = func(path, dbPath, enable, disable string, show bool) error {
292+
fmt.Println("feature called")
293+
if err := isArgEqual("otherflag", capturedFlag, "disable flag string"); err != nil {
294+
return err
295+
}
296+
return nil
297+
}
298+
},
299+
expStdout: []string{"feature called"},
300+
},
301+
302+
// --- dtx_aggr command ---
303+
// The Run handler in ddb_commands.go enforces that exactly one of --cmt_time or
304+
// --cmt_date is provided. These tests exercise that Go-layer validation.
305+
"dtx_aggr both cmt_time and cmt_date": {
306+
args: []string{"dtx_aggr", "--cmt_time=0", "--cmt_date=2024-01-01"},
307+
expErr: ddbTestErr("mutually exclusive"),
308+
},
309+
"dtx_aggr neither cmt_time nor cmt_date": {
310+
args: []string{"dtx_aggr"},
311+
expErr: ddbTestErr("has to be defined"),
312+
},
313+
"dtx_aggr cmt_time": {
314+
args: []string{"dtx_aggr", "--cmt_time=1000"},
315+
setup: func() {
316+
ddb_run_dtx_aggr_Fn = func(path string, cmtTime uint64, cmtDate string) error {
317+
fmt.Println("dtx_aggr called")
318+
if err := isArgEqual(uint64(1000), cmtTime, "cmtTime"); err != nil {
319+
return err
320+
}
321+
if err := isArgEqual("", cmtDate, "cmtDate"); err != nil {
322+
return err
323+
}
324+
return nil
325+
}
326+
},
327+
expStdout: []string{"dtx_aggr called"},
328+
},
329+
"dtx_aggr cmt_date": {
330+
args: []string{"dtx_aggr", "--cmt_date=2024-01-01"},
331+
setup: func() {
332+
ddb_run_dtx_aggr_Fn = func(path string, cmtTime uint64, cmtDate string) error {
333+
fmt.Println("dtx_aggr called")
334+
if err := isArgEqual("2024-01-01", cmtDate, "cmtDate"); err != nil {
335+
return err
336+
}
337+
return nil
338+
}
339+
},
340+
expStdout: []string{"dtx_aggr called"},
341+
},
342+
"dtx_aggr with path": {
343+
args: []string{"dtx_aggr", "--cmt_time=0", "[0]"},
344+
setup: func() {
345+
ddb_run_dtx_aggr_Fn = func(path string, cmtTime uint64, cmtDate string) error {
346+
fmt.Println("dtx_aggr called")
347+
if err := isArgEqual("[0]", path, "path"); err != nil {
348+
return err
349+
}
350+
if err := isArgEqual(uint64(0), cmtTime, "cmtTime"); err != nil {
351+
return err
352+
}
353+
return nil
354+
}
355+
},
356+
expStdout: []string{"dtx_aggr called"},
357+
},
358+
359+
// TODO(follow-up PR): Add TestCmds cases for the remaining commands.
360+
// Each new test case follows the same pattern as the cases above: set the
361+
// corresponding ddb_run_<cmd>_Fn hook in setup() to verify argument passing,
362+
// then add the case to this table.
363+
// Commands still to be covered: close, superblock_dump, value_dump, rm,
364+
// value_load, ilog_dump, ilog_commit, ilog_clear, dtx_dump, dtx_cmt_clear,
365+
// smd_sync, vea_dump, vea_update, dtx_act_commit, dtx_act_abort, rm_pool,
366+
// dtx_act_discard_invalid, dev_list, dev_replace, dtx_stat, prov_mem.
183367
} {
184368
t.Run(name, func(t *testing.T) {
185369
checkCmd := func(t *testing.T, stdout string, err error) {
@@ -195,6 +379,9 @@ func TestCmds(t *testing.T) {
195379
}
196380

197381
t.Run("command-line", func(t *testing.T) {
382+
if tc.skipCmdLine != "" {
383+
t.Skipf("skipping command-line mode: %s", tc.skipCmdLine)
384+
}
198385
ctx := newTestContext(t)
199386
if tc.setup != nil {
200387
tc.setup()

src/control/cmd/ddb/main_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,3 +423,103 @@ func TestStrToLogLevels(t *testing.T) {
423423
})
424424
}
425425
}
426+
427+
// TestNewLogger verifies the newLogger code paths: default level, explicit debug
428+
// level, invalid level, and all three LogDir branches (valid dir, non-existent
429+
// path, path that is a file rather than a directory).
430+
func TestNewLogger(t *testing.T) {
431+
t.Run("no LogDir default level", func(t *testing.T) {
432+
log, err := newLogger(cliOptions{})
433+
if err != nil {
434+
t.Fatalf("unexpected error: %v", err)
435+
}
436+
if log == nil {
437+
t.Fatal("expected non-nil logger")
438+
}
439+
})
440+
441+
t.Run("explicit valid debug level", func(t *testing.T) {
442+
log, err := newLogger(cliOptions{Debug: "DEBUG"})
443+
if err != nil {
444+
t.Fatalf("unexpected error: %v", err)
445+
}
446+
if log == nil {
447+
t.Fatal("expected non-nil logger")
448+
}
449+
})
450+
451+
t.Run("invalid debug level", func(t *testing.T) {
452+
_, err := newLogger(cliOptions{Debug: "INVALID"})
453+
if err == nil {
454+
t.Fatal("expected error for invalid log level, got nil")
455+
}
456+
})
457+
458+
t.Run("valid LogDir", func(t *testing.T) {
459+
tmpDir := t.TempDir()
460+
log, err := newLogger(cliOptions{LogDir: tmpDir})
461+
if err != nil {
462+
t.Fatalf("unexpected error: %v", err)
463+
}
464+
if log == nil {
465+
t.Fatal("expected non-nil logger")
466+
}
467+
})
468+
469+
t.Run("non-existent LogDir", func(t *testing.T) {
470+
_, err := newLogger(cliOptions{LogDir: "/non/existent/path"})
471+
if err == nil {
472+
t.Fatal("expected error for non-existent log dir, got nil")
473+
}
474+
})
475+
476+
t.Run("LogDir is a file", func(t *testing.T) {
477+
tmpDir := t.TempDir()
478+
tmpFile := filepath.Join(tmpDir, "not-a-dir")
479+
if err := os.WriteFile(tmpFile, []byte(""), 0644); err != nil {
480+
t.Fatalf("failed to create temp file: %v", err)
481+
}
482+
_, err := newLogger(cliOptions{LogDir: tmpFile})
483+
if err == nil {
484+
t.Fatal("expected error when LogDir is a file, got nil")
485+
}
486+
})
487+
}
488+
489+
// TestClosePoolIfOpen verifies that closePoolIfOpen only calls Close when the
490+
// pool is actually open, and that it tolerates a Close error (log only, no panic).
491+
func TestClosePoolIfOpen(t *testing.T) {
492+
log := logging.NewCommandLineLogger()
493+
494+
t.Run("pool not open, close not called", func(t *testing.T) {
495+
ctx := newTestContext(t)
496+
ddb_pool_is_open_RC = false
497+
ddb_run_close_Fn = func() error {
498+
t.Fatal("Close should not be called when pool is not open")
499+
return nil
500+
}
501+
closePoolIfOpen(ctx, log)
502+
})
503+
504+
t.Run("pool open, close called", func(t *testing.T) {
505+
ctx := newTestContext(t)
506+
ddb_pool_is_open_RC = true
507+
closeCalled := false
508+
ddb_run_close_Fn = func() error {
509+
closeCalled = true
510+
return nil
511+
}
512+
closePoolIfOpen(ctx, log)
513+
test.AssertTrue(t, closeCalled, "expected Close to have been called when pool is open")
514+
})
515+
516+
t.Run("pool open, close returns error", func(t *testing.T) {
517+
ctx := newTestContext(t)
518+
ddb_pool_is_open_RC = true
519+
ddb_run_close_Fn = func() error {
520+
return fmt.Errorf("close failed")
521+
}
522+
// Should not panic; the error is only logged.
523+
closePoolIfOpen(ctx, log)
524+
})
525+
}

src/control/cmd/ddb/test_helpers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func (dte ddbTestErr) Error() string {
2828
}
2929

3030
const (
31-
errUnknownCmd = ddbTestErr(grumbleUnknownCmdErr)
31+
errUnknownCmd = ddbTestErr("Unknown command:")
3232
)
3333

3434
// newTestContext creates a fresh DdbContext for use in tests, resetting all

0 commit comments

Comments
 (0)