package cli import ( "io" "os" "path/filepath" "slices" "strings" "testing" "github.com/rs/zerolog" "github.com/Control-D-Inc/ctrld" ) // setupLogSetupTest puts ctrld in cd mode with no log_path and a temporary // home directory, and puts back every global that the logging setup touches. // The globals are the reason why no test of this package calls t.Parallel. func setupLogSetupTest(t *testing.T) string { t.Helper() dir := t.TempDir() origLogPath, origLogLevel := cfg.Service.LogPath, cfg.Service.LogLevel origSilent, origVerbose, origCdUID, origHomedir := silent, verbose, cdUID, homedir origConsoleWriter := consoleWriter origMainLog, origProxyLogger := mainLog.Load(), ctrld.ProxyLogger.Load() origLevel, origTimeFormat := zerolog.GlobalLevel(), zerolog.TimeFieldFormat origLogPathFile, origPendingRotation := logPathFile.Load(), pendingLogPathRotation.Load() t.Cleanup(func() { if rf := logPathFile.Load(); rf != nil && rf != origLogPathFile { _ = rf.close() } logPathFile.Store(origLogPathFile) pendingLogPathRotation.Store(origPendingRotation) cfg.Service.LogPath, cfg.Service.LogLevel = origLogPath, origLogLevel silent, verbose, cdUID, homedir = origSilent, origVerbose, origCdUID, origHomedir consoleWriter = origConsoleWriter mainLog.Store(origMainLog) ctrld.ProxyLogger.Store(origProxyLogger) zerolog.SetGlobalLevel(origLevel) zerolog.TimeFieldFormat = origTimeFormat }) cfg.Service.LogPath, cfg.Service.LogLevel = "", "info" silent, verbose, cdUID, homedir = false, 0, "", dir consoleWriter = zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) { w.Out = io.Discard }) discard := zerolog.New(io.Discard) mainLog.Store(&discard) logPathFile.Store(nil) pendingLogPathRotation.Store(nil) stubHeaderSnapshotSources(t) stubLogHeaderNetworkRead(t, "en0") return dir } // Test_initLoggingAfterProvisioning_opensTheJournal drives a --cd-org start: // the first logging setup runs before the resolver UID is known, and the // provisioning step then opens the internal streams. func Test_initLoggingAfterProvisioning_opensTheJournal(t *testing.T) { dir := setupLogSetupTest(t) p := &prog{cfg: &cfg} p.initLogging(true) t.Cleanup(p.closeInternalLogs) journalPath := filepath.Join(dir, journalLogFileName) if _, err := os.Stat(journalPath); !os.IsNotExist(err) { t.Fatalf("journal exists before the UID is known (stat err = %v)", err) } cdUID = "provisioned-uid" p.initLoggingAfterProvisioning() firstLine, headers := logHeaderFileLines(t, journalPath) if firstLine.Message != logHeaderMessage || headers != 1 { t.Fatalf("journal starts with %q and holds %d headers, want the header once", firstLine.Message, headers) } mainLog.Load().Warn().Msg("retained after provisioning") if content := rotatingFileTestContent(t, journalPath); !strings.Contains(content, "retained after provisioning") { t.Fatalf("journal misses the line after provisioning: %s", content) } p.initLoggingAfterProvisioning() if _, headers := logHeaderFileLines(t, journalPath); headers != 1 { t.Fatalf("a second call added a header: %d headers", headers) } } // Test_switchLogPath_leadsTheNewFileAndClosesTheInternalFiles drives the // log_path that the API config sets after the first logging setup. func Test_switchLogPath_leadsTheNewFileAndClosesTheInternalFiles(t *testing.T) { dir := setupLogSetupTest(t) cdUID = "test-uid" p := &prog{cfg: &cfg} p.initLogging(true) t.Cleanup(p.closeInternalLogs) mainLog.Load().Info().Msg("line before the switch") newPath := filepath.Join(dir, "logs", "ctrld.log") cfg.Service.LogPath = newPath p.switchLogPath("", newPath) firstLine, headers := logHeaderFileLines(t, newPath) if firstLine.Message != logHeaderMessage || headers != 1 { t.Fatalf("log_path file starts with %q and holds %d headers, want the header once", firstLine.Message, headers) } lw, jlw := p.internalWriters() if lw.rotating() != nil || jlw.rotating() != nil { t.Fatal("the internal files stayed open after the switch") } if files := p.logHeaderFiles(); !slices.Equal(files, []string{newPath}) { t.Fatalf("log_files = %v, want the log_path only", files) } // cd mode has no local log_path, so the lines of the run stay in the // internal debug stream that this call closes. if content := rotatingFileTestContent(t, newPath); strings.Contains(content, "line before the switch") { t.Fatalf("a line moved although the run had no local log_path: %s", content) } } // Test_switchLogPath_carriesTheOldFileBelowOneHeader drives a run that already // wrote a local log_path file. The new file must hold one header, and the // header must lead the lines that the old file holds. func Test_switchLogPath_carriesTheOldFileBelowOneHeader(t *testing.T) { dir := setupLogSetupTest(t) cdUID = "test-uid" oldPath := filepath.Join(dir, "old", "ctrld.log") cfg.Service.LogPath = oldPath p := &prog{cfg: &cfg} p.initLogging(true) t.Cleanup(p.closeInternalLogs) mainLog.Load().Info().Msg("line before the switch") newPath := filepath.Join(dir, "logs", "ctrld.log") cfg.Service.LogPath = newPath p.switchLogPath(oldPath, newPath) firstLine, headers := logHeaderFileLines(t, newPath) if firstLine.Message != logHeaderMessage || headers != 1 { t.Fatalf("log_path file starts with %q and holds %d headers, want the header once", firstLine.Message, headers) } if !slices.Equal(firstLine.LogFiles, []string{newPath}) { t.Fatalf("header log_files = %v, want the new log_path", firstLine.LogFiles) } if content := rotatingFileTestContent(t, newPath); !strings.Contains(content, "line before the switch") { t.Fatalf("the new file misses the lines of the old file: %s", content) } } // Test_initLoggingAfterProvisioning_skipsTheGuardedRuns covers the two runs // that keep no internal files: a run with a local log_path, and a silent run. // Neither may gain a journal when the provisioning step learns the UID. func Test_initLoggingAfterProvisioning_skipsTheGuardedRuns(t *testing.T) { for _, tc := range []struct { name string guard func(t *testing.T, dir string) }{ {"log_path set", func(t *testing.T, dir string) { cfg.Service.LogPath = filepath.Join(dir, "operator.log") }}, {"silent set", func(t *testing.T, _ string) { silent = true }}, } { t.Run(tc.name, func(t *testing.T) { dir := setupLogSetupTest(t) tc.guard(t, dir) p := &prog{cfg: &cfg} p.initLogging(true) t.Cleanup(p.closeInternalLogs) cdUID = "provisioned-uid" p.initLoggingAfterProvisioning() journalPath := filepath.Join(dir, journalLogFileName) if _, err := os.Stat(journalPath); !os.IsNotExist(err) { t.Fatalf("the guarded run opened a journal (stat err = %v)", err) } }) } }