// Copyright 2015 RedHat, Inc. // Copyright 2015 CoreOS, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package sdjournal import ( "bytes" "errors" "fmt" "io" "io/ioutil" "math/rand" "os" "strings" "testing" "time" "github.com/coreos/go-systemd/journal" ) func TestJournalFollow(t *testing.T) { r, err := NewJournalReader(JournalReaderConfig{ Since: time.Duration(-15) * time.Second, Matches: []Match{ { Field: SD_JOURNAL_FIELD_SYSTEMD_UNIT, Value: "NetworkManager.service", }, }, }) if err != nil { t.Fatalf("Error opening journal: %s", err) } if r == nil { t.Fatal("Got a nil reader") } defer r.Close() // start writing some test entries done := make(chan struct{}, 1) errCh := make(chan error, 1) defer close(done) go func() { for { select { case <-done: return default: if perr := journal.Print(journal.PriInfo, "test message %s", time.Now()); err != nil { errCh <- perr return } time.Sleep(time.Second) } } }() // and follow the reader synchronously timeout := time.Duration(5) * time.Second if err = r.Follow(time.After(timeout), os.Stdout); err != ErrExpired { t.Fatalf("Error during follow: %s", err) } select { case err := <-errCh: t.Fatalf("Error writing to journal: %s", err) default: } } func TestJournalWait(t *testing.T) { id := time.Now().String() j, err := NewJournal() if err != nil { t.Fatalf("Error opening journal: %s", err) } if err := j.AddMatch("TEST=TestJournalWait " + id); err != nil { t.Fatalf("Error adding match: %s", err) } if err := j.SeekTail(); err != nil { t.Fatalf("Error seeking to tail: %s", err) } if _, err := j.Next(); err != nil { t.Fatalf("Error retrieving next entry: %s", err) } var t1, t2 time.Time for ret := -1; ret != SD_JOURNAL_NOP; { // Wait() might return for reasons other than timeout. // For example the first call initializes stuff and returns immediately. t1 = time.Now() ret = j.Wait(time.Millisecond * 300) t2 = time.Now() } duration := t2.Sub(t1) if duration > time.Millisecond*325 || duration < time.Millisecond*300 { t.Errorf("Wait did not wait 300ms. Actually waited %s", duration.String()) } journal.Send("test message", journal.PriInfo, map[string]string{"TEST": "TestJournalWait " + id}) for ret := -1; ret != SD_JOURNAL_APPEND; { t1 = time.Now() ret = j.Wait(time.Millisecond * 300) t2 = time.Now() } duration = t2.Sub(t1) if duration >= time.Millisecond*300 { t.Errorf("Wait took longer than 300ms. Actual duration %s", duration.String()) } } func TestJournalGetUsage(t *testing.T) { j, err := NewJournal() if err != nil { t.Fatalf("Error opening journal: %s", err) } if j == nil { t.Fatal("Got a nil journal") } defer j.Close() _, err = j.GetUsage() if err != nil { t.Fatalf("Error getting journal size: %s", err) } } func TestJournalCursorGetSeekAndTest(t *testing.T) { j, err := NewJournal() if err != nil { t.Fatalf("Error opening journal: %s", err) } if j == nil { t.Fatal("Got a nil journal") } defer j.Close() err = journal.Print(journal.PriInfo, "test message for cursor %s", time.Now()) if err != nil { t.Fatalf("Error writing to journal: %s", err) } if err = waitAndNext(j); err != nil { t.Fatalf(err.Error()) } c, err := j.GetCursor() if err != nil { t.Fatalf("Error getting cursor from journal: %s", err) } err = j.SeekCursor(c) if err != nil { t.Fatalf("Error seeking cursor to journal: %s", err) } if err = waitAndNext(j); err != nil { t.Fatalf(err.Error()) } err = j.TestCursor(c) if err != nil { t.Fatalf("Error testing cursor to journal: %s", err) } err = journal.Print(journal.PriInfo, "second message %s", time.Now()) if err != nil { t.Fatalf("Error writing to journal: %s", err) } if err = waitAndNext(j); err != nil { t.Fatalf(err.Error()) } err = j.TestCursor(c) if err != ErrNoTestCursor { t.Fatalf("Error, TestCursor should fail because current cursor has moved from the previous obtained cursor") } } func TestNewJournalFromDir(t *testing.T) { // test for error handling dir := "/ClearlyNonExistingPath/" j, err := NewJournalFromDir(dir) if err == nil { defer j.Close() t.Fatalf("Error expected when opening dummy path (%s)", dir) } // test for main code path dir, err = ioutil.TempDir("", "go-systemd-test") if err != nil { t.Fatalf("Error creating tempdir: %s", err) } defer os.RemoveAll(dir) j, err = NewJournalFromDir(dir) if err != nil { t.Fatalf("Error opening journal: %s", err) } if j == nil { t.Fatal("Got a nil journal") } j.Close() } func setupJournalRoundtrip() (*Journal, map[string]string, error) { j, err := NewJournal() if err != nil { return nil, nil, fmt.Errorf("Error opening journal: %s", err) } if j == nil { return nil, nil, fmt.Errorf("Got a nil journal") } j.FlushMatches() matchField := "TESTJOURNALENTRY" matchValue := fmt.Sprintf("%d", time.Now().UnixNano()) m := Match{Field: matchField, Value: matchValue} err = j.AddMatch(m.String()) if err != nil { return nil, nil, fmt.Errorf("Error adding matches to journal: %s", err) } msg := fmt.Sprintf("test journal get entry message %s", time.Now()) data := map[string]string{matchField: matchValue} err = journal.Send(msg, journal.PriInfo, data) if err != nil { return nil, nil, fmt.Errorf("Error writing to journal: %s", err) } time.Sleep(time.Duration(1) * time.Second) n, err := j.Next() if err != nil { return nil, nil, fmt.Errorf("Error reading from journal: %s", err) } if n == 0 { return nil, nil, fmt.Errorf("Error reading from journal: %s", io.EOF) } data["MESSAGE"] = msg return j, data, nil } func TestJournalGetData(t *testing.T) { j, wantEntry, err := setupJournalRoundtrip() if err != nil { t.Fatal(err.Error()) } defer j.Close() for k, v := range wantEntry { data := fmt.Sprintf("%s=%s", k, v) dataStr, err := j.GetData(k) if err != nil { t.Fatalf("GetData() error: %v", err) } if dataStr != data { t.Fatalf("Invalid data for \"%s\": got %s, want %s", k, dataStr, data) } dataBytes, err := j.GetDataBytes(k) if err != nil { t.Fatalf("GetDataBytes() error: %v", err) } if string(dataBytes) != data { t.Fatalf("Invalid data bytes for \"%s\": got %s, want %s", k, string(dataBytes), data) } valStr, err := j.GetDataValue(k) if err != nil { t.Fatalf("GetDataValue() error: %v", err) } if valStr != v { t.Fatalf("Invalid data value for \"%s\": got %s, want %s", k, valStr, v) } valBytes, err := j.GetDataValueBytes(k) if err != nil { t.Fatalf("GetDataValueBytes() error: %v", err) } if string(valBytes) != v { t.Fatalf("Invalid data value bytes for \"%s\": got %s, want %s", k, string(valBytes), v) } } } func TestJournalGetEntry(t *testing.T) { j, wantEntry, err := setupJournalRoundtrip() if err != nil { t.Fatal(err.Error()) } defer j.Close() entry, err := j.GetEntry() if err != nil { t.Fatalf("Error getting the entry to journal: %s", err) } for k, wantV := range wantEntry { gotV := entry.Fields[k] if gotV != wantV { t.Fatalf("Bad result for entry.Fields[\"%s\"]: got %s, want %s", k, gotV, wantV) } } } // Check for incorrect read into small buffers, // see https://github.com/coreos/go-systemd/issues/172 func TestJournalReaderSmallReadBuffer(t *testing.T) { // Write a long entry ... delim := "%%%%%%" longEntry := strings.Repeat("a", 256) matchField := "TESTJOURNALREADERSMALLBUF" matchValue := fmt.Sprintf("%d", time.Now().UnixNano()) r, err := NewJournalReader(JournalReaderConfig{ Since: time.Duration(-15) * time.Second, Matches: []Match{ { Field: matchField, Value: matchValue, }, }, }) if err != nil { t.Fatalf("Error opening journal: %s", err) } if r == nil { t.Fatal("Got a nil reader") } defer r.Close() want := fmt.Sprintf("%slongentry %s%s", delim, longEntry, delim) err = journal.Send(want, journal.PriInfo, map[string]string{matchField: matchValue}) if err != nil { t.Fatal("Error writing to journal", err) } time.Sleep(time.Second) // ... and try to read it back piece by piece via a small buffer finalBuff := new(bytes.Buffer) var e error for c := -1; c != 0 && e == nil; { smallBuf := make([]byte, 5) c, e = r.Read(smallBuf) if c > len(smallBuf) { t.Fatalf("Got unexpected read length: %d vs %d", c, len(smallBuf)) } _, _ = finalBuff.Write(smallBuf) } b := finalBuff.String() got := strings.Split(b, delim) if len(got) != 3 { t.Fatalf("Got unexpected entry %s", b) } if got[1] != strings.Trim(want, delim) { t.Fatalf("Got unexpected message %s", got[1]) } } func TestJournalGetUniqueValues(t *testing.T) { j, err := NewJournal() if err != nil { t.Fatal(err) } defer j.Close() uniqueString := generateRandomField(20) testEntries := []string{"A", "B", "C", "D"} for _, v := range testEntries { err = journal.Send("TEST: "+uniqueString, journal.PriInfo, map[string]string{uniqueString: v}) if err != nil { t.Fatal(err) } } // TODO: add proper `waitOnMatch` function which should wait for journal entry with filter to commit. time.Sleep(time.Millisecond * 500) values, err := j.GetUniqueValues(uniqueString) if err != nil { t.Fatal(err) } if len(values) != len(testEntries) { t.Fatalf("Expect %d entries. Got %d", len(testEntries), len(values)) } if !contains(values, "A") || !contains(values, "B") || !contains(values, "C") || !contains(values, "D") { t.Fatalf("Expect 4 values for %s field: A,B,C,D. Got %s", uniqueString, values) } } func TestJournalGetCatalog(t *testing.T) { want := []string{ "Subject: ", "Defined-By: systemd", "Support: ", } j, err := NewJournal() if err != nil { t.Fatalf("Error opening journal: %s", err) } if j == nil { t.Fatal("Got a nil journal") } defer j.Close() if err = j.SeekHead(); err != nil { t.Fatalf("Seek to head failed: %s", err) } matchField := SD_JOURNAL_FIELD_SYSTEMD_UNIT m := Match{Field: matchField, Value: "systemd-journald.service"} if err = j.AddMatch(m.String()); err != nil { t.Fatalf("Error adding matches to journal: %s", err) } if err = waitAndNext(j); err != nil { t.Fatalf(err.Error()) } catalog, err := j.GetCatalog() if err != nil { t.Fatalf("Failed to retrieve catalog entry: %s", err) } for _, w := range want { if !strings.Contains(catalog, w) { t.Fatalf("Failed to find \"%s\" in \n%s", w, catalog) } } } func contains(s []string, v string) bool { for _, entry := range s { if entry == v { return true } } return false } func generateRandomField(n int) string { letters := []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ") s := make([]rune, n) rand.Seed(time.Now().UnixNano()) for i := range s { s[i] = letters[rand.Intn(len(letters))] } return string(s) } func waitAndNext(j *Journal) error { r := j.Wait(time.Duration(1) * time.Second) if r < 0 { return errors.New("Error waiting to journal") } n, err := j.Next() if err != nil { return fmt.Errorf("Error reading to journal: %s", err) } if n == 0 { return fmt.Errorf("Error reading to journal: %s", io.EOF) } return nil }