v0.10.0: ports and adapters refactor
- Extract shared manifest types into internal/manifest/types leaf package. - Extract SQLite adapter into internal/manifest/sqlite. - Extract JSONL adapter into internal/manifest/jsonl. - Isolate modernc.org/sqlite import to sqlite/adapter.go. - Add adapter-backed registry with manifest.Default. - Adapter-agnostic ConvertManifest in types/. - MemoryAdapter for in-memory manifest testing. - CLI uses manifest.Default registry directly. - SQLite LogWriter type assertion moved into SQLiteAdapter. - Manifest interface includes Entries(); EntryReader removed. - No behavior changes. 100% coverage across all 6 packages.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type fileLogWriter struct {
|
||||
mu sync.Mutex
|
||||
f *os.File
|
||||
}
|
||||
|
||||
func NewFileLogWriter(path string) (LogWriter, error) {
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &fileLogWriter{f: f}, nil
|
||||
}
|
||||
|
||||
func (w *fileLogWriter) Log(e LogEntry) {
|
||||
data, _ := json.Marshal(e)
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if w.f != nil {
|
||||
w.f.Write(data)
|
||||
w.f.Write([]byte("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
func (w *fileLogWriter) Close() {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if w.f != nil {
|
||||
w.f.Close()
|
||||
w.f = nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Adapter interface {
|
||||
Format() Format
|
||||
Aliases() []string
|
||||
Path(dir string) string
|
||||
Exists(dir string) bool
|
||||
Open(dir string) (Manifest, error)
|
||||
OpenLogWriter(m Manifest, dir string) (LogWriter, error)
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
adapters []Adapter
|
||||
}
|
||||
|
||||
func NewRegistry(adapters ...Adapter) Registry {
|
||||
return Registry{adapters: adapters}
|
||||
}
|
||||
|
||||
func (r Registry) ParseFormat(s string) (Format, error) {
|
||||
want := strings.ToLower(s)
|
||||
for _, adapter := range r.adapters {
|
||||
if want == string(adapter.Format()) {
|
||||
return adapter.Format(), nil
|
||||
}
|
||||
for _, alias := range adapter.Aliases() {
|
||||
if want == strings.ToLower(alias) {
|
||||
return adapter.Format(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("unknown manifest format %q (use jsonl or sqlite)", s)
|
||||
}
|
||||
|
||||
func (r Registry) Open(dir string, format Format) (Manifest, error) {
|
||||
dst, err := r.adapter(format)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dst.Exists(dir) {
|
||||
return dst.Open(dir)
|
||||
}
|
||||
for _, src := range r.adapters {
|
||||
if src.Format() != dst.Format() && src.Exists(dir) {
|
||||
return ConvertManifest(dir, src, dst)
|
||||
}
|
||||
}
|
||||
return dst.Open(dir)
|
||||
}
|
||||
|
||||
func (r Registry) OpenLogWriter(m Manifest, dir string, format Format) (LogWriter, error) {
|
||||
adapter, err := r.adapter(format)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return adapter.OpenLogWriter(m, dir)
|
||||
}
|
||||
|
||||
func (r Registry) adapter(format Format) (Adapter, error) {
|
||||
for _, adapter := range r.adapters {
|
||||
if adapter.Format() == format {
|
||||
return adapter, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("unknown manifest format %q (use jsonl or sqlite)", format)
|
||||
}
|
||||
|
||||
func ConvertManifest(dir string, src Adapter, dst Adapter) (Manifest, error) {
|
||||
source, err := src.Open(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open %s for read: %w", src.Format(), err)
|
||||
}
|
||||
if err := source.OpenAppend(); err != nil {
|
||||
source.Close()
|
||||
return nil, fmt.Errorf("open %s for read: %w", src.Format(), err)
|
||||
}
|
||||
defer source.Close()
|
||||
|
||||
target, err := dst.Open(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open %s for write: %w", dst.Format(), err)
|
||||
}
|
||||
if err := target.OpenAppend(); err != nil {
|
||||
target.Close()
|
||||
return nil, fmt.Errorf("open %s for write: %w", dst.Format(), err)
|
||||
}
|
||||
|
||||
for id, entry := range source.Entries() {
|
||||
entry.ID = id
|
||||
target.AddEntry(entry)
|
||||
}
|
||||
if err := target.Save(); err != nil {
|
||||
target.Close()
|
||||
return nil, fmt.Errorf("save %s: %w", dst.Format(), err)
|
||||
}
|
||||
|
||||
if err := removeFile(src.Path(dir)); err != nil {
|
||||
target.Close()
|
||||
return nil, err
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
var removeFile func(string) error
|
||||
|
||||
func SetRemoveFunc(fn func(string) error) {
|
||||
removeFile = fn
|
||||
}
|
||||
|
||||
func RemoveFunc() func(string) error {
|
||||
return removeFile
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewRegistry(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
if _, err := r.ParseFormat("jsonl"); err == nil {
|
||||
t.Fatal("expected error from empty registry")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryParseFormatAliases(t *testing.T) {
|
||||
r := NewRegistry(testAdapter{format: FormatJSONL, aliases: []string{"json"}}, testAdapter{format: FormatSQLite, aliases: []string{"db", "sqlite3"}})
|
||||
cases := map[string]Format{
|
||||
"jsonl": FormatJSONL,
|
||||
"json": FormatJSONL,
|
||||
"sqlite": FormatSQLite,
|
||||
"db": FormatSQLite,
|
||||
"sqlite3": FormatSQLite,
|
||||
}
|
||||
for input, want := range cases {
|
||||
got, err := r.ParseFormat(input)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseFormat(%q): %v", input, err)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("ParseFormat(%q) = %q, want %q", input, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryParseFormatUnknown(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
if _, err := r.ParseFormat("bad"); err == nil {
|
||||
t.Fatal("expected unknown format error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryOpenDefaultsToOpen(t *testing.T) {
|
||||
r := NewRegistry(testAdapter{format: FormatJSONL, store: &testStore{entries: map[string]Entry{}}, exists: false})
|
||||
m, err := r.Open(t.TempDir(), FormatJSONL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Close()
|
||||
}
|
||||
|
||||
func TestRegistryOpenUnknownFormat(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
if _, err := r.Open(t.TempDir(), FormatSQLite); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryOpenNoConversionFallback(t *testing.T) {
|
||||
r := NewRegistry(
|
||||
testAdapter{format: FormatJSONL, store: &testStore{entries: map[string]Entry{}}, exists: false},
|
||||
testAdapter{format: FormatSQLite, store: &testStore{entries: map[string]Entry{}}, exists: false},
|
||||
)
|
||||
m, err := r.Open(t.TempDir(), FormatJSONL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Close()
|
||||
}
|
||||
|
||||
func TestRegistryOpenSameFormatAdapterInLoop(t *testing.T) {
|
||||
r := NewRegistry(
|
||||
testAdapter{format: FormatJSONL, store: &testStore{entries: map[string]Entry{}}, exists: false},
|
||||
testAdapter{format: FormatJSONL, store: &testStore{entries: map[string]Entry{}}, exists: false},
|
||||
)
|
||||
m, err := r.Open(t.TempDir(), FormatJSONL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Close()
|
||||
}
|
||||
|
||||
func TestRegistryOpenOtherFormatNotExists(t *testing.T) {
|
||||
r := NewRegistry(
|
||||
testAdapter{format: FormatJSONL, store: &testStore{entries: map[string]Entry{}}, exists: false},
|
||||
testAdapter{format: FormatSQLite, store: &testStore{entries: map[string]Entry{}}, exists: false},
|
||||
)
|
||||
m, err := r.Open(t.TempDir(), FormatJSONL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Close()
|
||||
}
|
||||
|
||||
func TestRegistryOpenExistingManifest(t *testing.T) {
|
||||
r := NewRegistry(
|
||||
testAdapter{format: FormatJSONL, store: &testStore{entries: map[string]Entry{}}, exists: true},
|
||||
)
|
||||
m, err := r.Open(t.TempDir(), FormatJSONL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Close()
|
||||
}
|
||||
|
||||
func TestRegistryOpenConverts(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
src := &testStore{entries: map[string]Entry{"x1": {ID: "x1", Filename: "photo.jpg", Path: "photo.jpg"}}}
|
||||
dst := &testStore{entries: map[string]Entry{}}
|
||||
r := NewRegistry(
|
||||
testAdapter{format: FormatJSONL, store: src, exists: true},
|
||||
testAdapter{format: FormatSQLite, store: dst, exists: false},
|
||||
)
|
||||
SetRemoveFunc(func(string) error { return nil })
|
||||
m, err := r.Open(dir, FormatSQLite)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Close()
|
||||
if _, ok := dst.entries["x1"]; !ok {
|
||||
t.Fatal("expected converted entry")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryOpenLogWriter(t *testing.T) {
|
||||
r := NewRegistry(testAdapter{format: FormatJSONL, logWriter: NoopLogWriter})
|
||||
w, err := r.OpenLogWriter(nil, t.TempDir(), FormatJSONL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.Close()
|
||||
}
|
||||
|
||||
func TestRegistryOpenLogWriterUnknown(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
if _, err := r.OpenLogWriter(nil, t.TempDir(), FormatSQLite); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertManifest(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
src := &testStore{entries: map[string]Entry{"x1": {ID: "x1", Filename: "photo.jpg", Path: "photo.jpg"}}}
|
||||
dst := &testStore{entries: map[string]Entry{}}
|
||||
SetRemoveFunc(func(string) error { return nil })
|
||||
m, err := ConvertManifest(dir, testAdapter{format: FormatJSONL, store: src, exists: true}, testAdapter{format: FormatSQLite, store: dst, exists: false})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Close()
|
||||
if _, ok := dst.entries["x1"]; !ok {
|
||||
t.Fatal("expected converted entry")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertManifestSourceOpenError(t *testing.T) {
|
||||
if _, err := ConvertManifest(t.TempDir(), testAdapter{format: FormatJSONL, openErr: fmt.Errorf("boom")}, testAdapter{format: FormatSQLite, store: &testStore{}}); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertManifestSourceOpenAppendError(t *testing.T) {
|
||||
if _, err := ConvertManifest(t.TempDir(), testAdapter{format: FormatJSONL, store: &testStore{openAppendErr: fmt.Errorf("boom")}}, testAdapter{format: FormatSQLite, store: &testStore{}}); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertManifestDstOpenError(t *testing.T) {
|
||||
if _, err := ConvertManifest(t.TempDir(), testAdapter{format: FormatJSONL, store: &testStore{entries: map[string]Entry{}}}, testAdapter{format: FormatSQLite, openErr: fmt.Errorf("boom")}); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertManifestDstOpenAppendError(t *testing.T) {
|
||||
if _, err := ConvertManifest(t.TempDir(), testAdapter{format: FormatJSONL, store: &testStore{entries: map[string]Entry{}}}, testAdapter{format: FormatSQLite, store: &testStore{openAppendErr: fmt.Errorf("boom")}}); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertManifestDstSaveError(t *testing.T) {
|
||||
if _, err := ConvertManifest(t.TempDir(), testAdapter{format: FormatJSONL, store: &testStore{entries: map[string]Entry{}}}, testAdapter{format: FormatSQLite, store: &testStore{saveErr: fmt.Errorf("boom")}}); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertManifestRemoveError(t *testing.T) {
|
||||
SetRemoveFunc(func(string) error { return fmt.Errorf("remove failed") })
|
||||
defer SetRemoveFunc(nil)
|
||||
if _, err := ConvertManifest(t.TempDir(), testAdapter{format: FormatJSONL, store: &testStore{entries: map[string]Entry{}}}, testAdapter{format: FormatSQLite, store: &testStore{}}); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetRemoveFunc(t *testing.T) {
|
||||
SetRemoveFunc(func(string) error { return nil })
|
||||
if RemoveFunc() == nil {
|
||||
t.Fatal("expected non-nil remove func")
|
||||
}
|
||||
SetRemoveFunc(nil)
|
||||
if RemoveFunc() != nil {
|
||||
t.Fatal("expected nil remove func")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoopLogWriterMethods(t *testing.T) {
|
||||
NoopLogWriter.Log(LogEntry{})
|
||||
NoopLogWriter.Close()
|
||||
var n noopLogWriter
|
||||
n.Log(LogEntry{})
|
||||
n.Close()
|
||||
}
|
||||
|
||||
type testAdapter struct {
|
||||
format Format
|
||||
aliases []string
|
||||
store Manifest
|
||||
exists bool
|
||||
openErr error
|
||||
logWriter LogWriter
|
||||
}
|
||||
|
||||
func (a testAdapter) Format() Format { return a.format }
|
||||
func (a testAdapter) Aliases() []string { return a.aliases }
|
||||
func (a testAdapter) Path(string) string { return "test" }
|
||||
func (a testAdapter) Exists(string) bool { return a.exists }
|
||||
func (a testAdapter) Open(string) (Manifest, error) {
|
||||
if a.openErr != nil {
|
||||
return nil, a.openErr
|
||||
}
|
||||
return a.store, nil
|
||||
}
|
||||
func (a testAdapter) OpenLogWriter(Manifest, string) (LogWriter, error) {
|
||||
if a.logWriter != nil {
|
||||
return a.logWriter, nil
|
||||
}
|
||||
return NoopLogWriter, nil
|
||||
}
|
||||
|
||||
type testStore struct {
|
||||
entries map[string]Entry
|
||||
openAppendErr error
|
||||
saveErr error
|
||||
}
|
||||
|
||||
func (s *testStore) Has(id string) bool {
|
||||
_, ok := s.entries[id]
|
||||
return ok
|
||||
}
|
||||
func (s *testStore) Add(id string, filename string, size int64, cloud string) {
|
||||
s.entries[id] = NewEntry(id, filename, filename, size, cloud)
|
||||
}
|
||||
func (s *testStore) AddEntry(e Entry) {
|
||||
if e.Path == "" {
|
||||
e.Path = e.Filename
|
||||
}
|
||||
s.entries[e.ID] = e
|
||||
}
|
||||
func (s *testStore) Save() error { return s.saveErr }
|
||||
func (s *testStore) Close() {}
|
||||
func (s *testStore) OpenAppend() error { return s.openAppendErr }
|
||||
func (s *testStore) Entries() map[string]Entry {
|
||||
out := make(map[string]Entry, len(s.entries))
|
||||
for k, v := range s.entries {
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
type Entry struct {
|
||||
ID string
|
||||
Filename string
|
||||
Path string
|
||||
Size int64
|
||||
Cloud string
|
||||
Checksum string
|
||||
Exported int64
|
||||
}
|
||||
|
||||
type Manifest interface {
|
||||
Has(id string) bool
|
||||
Add(id string, filename string, size int64, cloud string)
|
||||
AddEntry(entry Entry)
|
||||
Save() error
|
||||
Close()
|
||||
OpenAppend() error
|
||||
Entries() map[string]Entry
|
||||
}
|
||||
|
||||
type EntryReader = Manifest
|
||||
|
||||
type Format string
|
||||
|
||||
const (
|
||||
FormatJSONL Format = "jsonl"
|
||||
FormatSQLite Format = "sqlite"
|
||||
)
|
||||
|
||||
type LogEntry struct {
|
||||
Timestamp int64 `json:"ts"`
|
||||
Level string `json:"level"`
|
||||
Event string `json:"event"`
|
||||
AssetID string `json:"asset_id,omitempty"`
|
||||
Album string `json:"album,omitempty"`
|
||||
Filename string `json:"filename,omitempty"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
Cloud string `json:"cloud,omitempty"`
|
||||
DurationMs int64 `json:"duration_ms,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type LogWriter interface {
|
||||
Log(entry LogEntry)
|
||||
Close()
|
||||
}
|
||||
|
||||
type noopLogWriter struct{}
|
||||
|
||||
func (noopLogWriter) Log(LogEntry) { _ = struct{}{} }
|
||||
func (noopLogWriter) Close() { _ = struct{}{} }
|
||||
|
||||
var NoopLogWriter LogWriter = noopLogWriter{}
|
||||
|
||||
type FormatReporter interface {
|
||||
ManifestFormat() Format
|
||||
}
|
||||
|
||||
func LogPath(dir string) string {
|
||||
return dir + "/export.log"
|
||||
}
|
||||
|
||||
func NewEntry(id, filename, path string, size int64, cloud string) Entry {
|
||||
e := Entry{
|
||||
ID: id,
|
||||
Filename: filename,
|
||||
Path: filename,
|
||||
Size: size,
|
||||
Cloud: cloud,
|
||||
Exported: time.Now().Unix(),
|
||||
}
|
||||
if path != "" {
|
||||
e.Path = path
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func NewEntryWithChecksum(id, filename, path string, size int64, cloud, checksum string) Entry {
|
||||
e := NewEntry(id, filename, path, size, cloud)
|
||||
e.Checksum = checksum
|
||||
return e
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewEntryPath(t *testing.T) {
|
||||
e := NewEntry("id1", "file.jpg", "", 123, "local")
|
||||
if e.ID != "id1" || e.Filename != "file.jpg" || e.Path != "file.jpg" || e.Size != 123 || e.Cloud != "local" || e.Exported == 0 {
|
||||
t.Fatalf("unexpected entry: %+v", e)
|
||||
}
|
||||
e = NewEntry("id2", "file2.jpg", "Album/file2.jpg", 456, "cloud")
|
||||
if e.Path != "Album/file2.jpg" || e.Filename != "file2.jpg" || e.Size != 456 || e.Cloud != "cloud" {
|
||||
t.Fatalf("unexpected entry with path: %+v", e)
|
||||
}
|
||||
e = NewEntryWithChecksum("id3", "file3.jpg", "Album/file3.jpg", 789, "local", "sha256:abc")
|
||||
if e.Checksum != "sha256:abc" || e.Path != "Album/file3.jpg" {
|
||||
t.Fatalf("unexpected checksum entry: %+v", e)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoopLogWriter(t *testing.T) {
|
||||
NoopLogWriter.Log(LogEntry{Event: "test"})
|
||||
NoopLogWriter.Close()
|
||||
}
|
||||
|
||||
func TestNewFileLogWriter(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := LogPath(dir)
|
||||
w, err := NewFileLogWriter(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.Log(LogEntry{Timestamp: 1700000000, Level: "info", Event: "export_done", Filename: "photo.jpg", Size: 2048})
|
||||
w.Close()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(data) == 0 {
|
||||
t.Error("expected log data")
|
||||
}
|
||||
if data[len(data)-1] != '\n' {
|
||||
t.Error("expected trailing newline")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileLogWriterClosed(t *testing.T) {
|
||||
w, err := NewFileLogWriter(LogPath(t.TempDir()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.Close()
|
||||
w.Log(LogEntry{Event: "after-close"})
|
||||
}
|
||||
|
||||
func TestNewFileLogWriterError(t *testing.T) {
|
||||
if _, err := NewFileLogWriter("/nonexistent/dir/export.log"); err == nil {
|
||||
t.Error("expected error for bad path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileLogWriterDoubleClose(t *testing.T) {
|
||||
w, err := NewFileLogWriter(LogPath(t.TempDir()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.Close()
|
||||
w.Close()
|
||||
}
|
||||
|
||||
func TestLogPath(t *testing.T) {
|
||||
if got := LogPath("/tmp/out"); got != "/tmp/out/export.log" {
|
||||
t.Fatalf("LogPath = %q", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user