diff --git a/docopt.go b/docopt.go index 6dd4b3e..1af4a7c 100644 --- a/docopt.go +++ b/docopt.go @@ -4,7 +4,8 @@ import "github.com/docopt/docopt-go" const version = "0.2.0" -func parseArguments() (map[string]interface{}, error) { +// ParseArguments parses arguments, that were passed to the dotbro, by docopt. +func ParseArguments(argv []string) (map[string]interface{}, error) { usage := `dotbro - simple yet effective dotfiles manager. Usage: @@ -27,5 +28,5 @@ Other options: -V --version Show version. ` - return docopt.Parse(usage, nil, true, "dotbro "+version, false) + return docopt.Parse(usage, argv, true, "dotbro "+version, false) } diff --git a/docopt_test.go b/docopt_test.go new file mode 100644 index 0000000..bc4a6cf --- /dev/null +++ b/docopt_test.go @@ -0,0 +1,10 @@ +package main + +import "testing" + +func TestParseArguments(t *testing.T) { + _, err := ParseArguments([]string{"--quiet"}) + if err != nil { + t.Fatalf("Error parsing arguments: %v", err.Error()) + } +} diff --git a/fs.go b/fs.go index 9f56c9f..c55e64d 100644 --- a/fs.go +++ b/fs.go @@ -4,6 +4,19 @@ package main import "os" +// Intefaces + +type OS interface { + MkdirAll(path string, perm os.FileMode) error + + Symlink(oldname, newname string) error + + Stat(name string) (os.FileInfo, error) + IsNotExist(err error) bool + + Rename(oldpath, newpath string) error +} + type DirMaker interface { MkdirAll(path string, perm os.FileMode) error } @@ -17,9 +30,9 @@ type Stater interface { IsNotExist(err error) bool } -type FakeMkdirSymlinker struct { - *FakeDirMaker - *FakeSymlinker +type MkdirSymlinker interface { + DirMaker + Symlinker } type StatDirMaker interface { @@ -27,8 +40,41 @@ type StatDirMaker interface { DirMaker } +type Renamer interface { + Rename(oldpath, newpath string) error +} + // Fake implementation of interface +type FakeOS struct { + MkdirAllError error + SymlinkError error + StatFileInfo os.FileInfo + StatError error + IsNotExistResult bool + RenameError error +} + +func (f *FakeOS) MkdirAll(path string, perm os.FileMode) error { + return f.MkdirAllError +} + +func (f *FakeOS) Symlink(oldname, newname string) error { + return f.SymlinkError +} + +func (f *FakeOS) Stat(name string) (os.FileInfo, error) { + return f.StatFileInfo, f.StatError +} + +func (f *FakeOS) IsNotExist(err error) bool { + return f.IsNotExistResult +} + +func (f *FakeOS) Rename(oldname, newname string) error { + return f.RenameError +} + type FakeDirMaker struct { MkdirAllError error } @@ -59,13 +105,48 @@ func (f *FakeStater) IsNotExist(err error) bool { return f.IsNotExistResult } -type MkdirSymlinker interface { - DirMaker - Symlinker +type FakeMkdirSymlinker struct { + *FakeDirMaker + *FakeSymlinker +} + +type FakeStatDirMaker struct { + *FakeStater + *FakeDirMaker +} + +type FakeRenamer struct { + RenameError error +} + +func (f *FakeRenamer) Rename(oldpath, newpath string) error { + return f.RenameError } // Actual implementation of interface using os package. +type OSFS struct{} + +func (f *OSFS) MkdirAll(path string, perm os.FileMode) error { + return os.MkdirAll(path, perm) +} + +func (f *OSFS) Symlink(oldname, newname string) error { + return os.Symlink(oldname, newname) +} + +func (s *OSFS) Stat(name string) (os.FileInfo, error) { + return os.Stat(name) +} + +func (s *OSFS) IsNotExist(err error) bool { + return os.IsNotExist(err) +} + +func (f *OSFS) Rename(oldpath, newpath string) error { + return os.Rename(oldpath, newpath) +} + type OsDirMaker struct { } @@ -79,11 +160,6 @@ func (f *OsSymlinker) Symlink(oldname, newname string) error { return os.Symlink(oldname, newname) } -type OsMkdirSymlinker struct { - *OsDirMaker - *OsSymlinker -} - type OsStater struct{} func (s *OsStater) Stat(name string) (os.FileInfo, error) { @@ -93,3 +169,19 @@ func (s *OsStater) Stat(name string) (os.FileInfo, error) { func (s *OsStater) IsNotExist(err error) bool { return os.IsNotExist(err) } + +type OsMkdirSymlinker struct { + *OsDirMaker + *OsSymlinker +} + +type OsStatDirMaker struct { + *OsStater + *OsDirMaker +} + +type OsRenamer struct{} + +func (f *OsRenamer) Rename(oldpath, newpath string) error { + return os.Rename(oldpath, newpath) +} diff --git a/linker.go b/linker.go index b2903e4..0243acc 100644 --- a/linker.go +++ b/linker.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "os" "path" "path/filepath" @@ -8,13 +9,13 @@ import ( type Linker struct { outputer IOutputer - fs MkdirSymlinker + os OS } -func NewLinker(outputer IOutputer, fs MkdirSymlinker) Linker { +func NewLinker(outputer IOutputer, os OS) Linker { return Linker{ outputer: outputer, - fs: fs, + os: os, } } @@ -71,19 +72,26 @@ func needBackup(dest string) (bool, error) { return false, nil } -// backup copies existing destination file to backup dir. -func backup(dest string, destAbs string, backupDir string) error { +// Move moves oldpath to newpath, creating target directories if need. +func (l *Linker) Move(oldpath, newpath string) error { // todo: if dry-run, just print - dir := path.Dir(backupDir + "/" + dest) - err := os.MkdirAll(dir, 0700) + // check if destination file exists + exists, err := IsExists(l.os, oldpath) + if err != nil { + return err + } + if !exists { + return fmt.Errorf("File %s not exists", oldpath) + } + + err = l.os.MkdirAll(path.Dir(newpath), 0700) if err != nil { return err } - backupPath := backupDir + "/" + dest - outputer.OutVerbose(" → backup %s to %s", destAbs, backupPath) - err = os.Rename(destAbs, backupPath) + l.outputer.OutVerbose(" → backup %s to %s", oldpath, newpath) + err = l.os.Rename(oldpath, newpath) return err } @@ -112,11 +120,11 @@ func backupCopy(filename, backupDir string) error { func (l *Linker) SetSymlink(srcAbs string, destAbs string) error { dir := path.Dir(destAbs) - if err := l.fs.MkdirAll(dir, 0700); err != nil { + if err := l.os.MkdirAll(dir, 0700); err != nil { return err } - if err := l.fs.Symlink(srcAbs, destAbs); err != nil { + if err := l.os.Symlink(srcAbs, destAbs); err != nil { return err } diff --git a/linker_test.go b/linker_test.go index 5677b43..d8f1f55 100644 --- a/linker_test.go +++ b/linker_test.go @@ -117,23 +117,61 @@ func TestNeedBackup(t *testing.T) { assert.False(t, actual) } -func TestBackup(t *testing.T) { - - os.RemoveAll("/tmp/dotbro") // Cleanup - - dest := "new" - destAbs := "/tmp/dotbro/linker/TestBackup/new" - backupDir := "/tmp/dotbro/linker/TestBackup/backup" +func TestMove(t *testing.T) { + cases := []struct { + os *FakeOS + oldpath string + newpath string + expectedError error + }{ + { + // Failure when IsExists fails + os: &FakeOS{ + StatError: errors.New("Some error"), + }, + expectedError: errors.New("Some error"), + }, + { + // Failure when dest file not exists + os: &FakeOS{ + IsNotExistResult: true, + }, + oldpath: "/path/dest", + expectedError: errors.New("File /path/dest not exists"), + }, + { + // Failure when MkdirAll fails + os: &FakeOS{ + IsNotExistResult: false, + MkdirAllError: errors.New("MkdirAll error"), + }, + expectedError: errors.New("MkdirAll error"), + }, + { + // Failure when Rename fails + os: &FakeOS{ + IsNotExistResult: false, + RenameError: errors.New("Rename error"), + }, + expectedError: errors.New("Rename error"), + }, + { + // Success + os: &FakeOS{ + IsNotExistResult: false, + }, + expectedError: nil, + }, + } - err := backup(dest, destAbs, backupDir) - assert.Error(t, err) + for _, c := range cases { + linker := NewLinker(&FakeOutputer{}, c.os) + err := linker.Move(c.oldpath, c.newpath) - err = os.MkdirAll(destAbs, 0700) - if err != nil { - t.Fatal(err) + if !reflect.DeepEqual(err, c.expectedError) { + t.Errorf("Expected err to be %v but it was %v\n", c.expectedError, err) + } } - err = backup(dest, destAbs, backupDir) - assert.Empty(t, err) } func TestBackupCopy(t *testing.T) { @@ -176,33 +214,33 @@ func (o *FakeOutputer) OutError(format string, v ...interface{}) { func TestNewLinker(t *testing.T) { cases := []struct { - mkdirSymlinker *FakeMkdirSymlinker + os *FakeOS srcAbs string destAbs string expectedError error }{ { - mkdirSymlinker: &FakeMkdirSymlinker{ - &FakeDirMaker{MkdirAllError: nil}, - &FakeSymlinker{SymlinkError: nil}, + os: &FakeOS{ + MkdirAllError: nil, + SymlinkError: nil, }, srcAbs: "/src/path", destAbs: "/dest/path", expectedError: nil, }, { - mkdirSymlinker: &FakeMkdirSymlinker{ - &FakeDirMaker{MkdirAllError: errors.New("Permission denied")}, - &FakeSymlinker{SymlinkError: nil}, + os: &FakeOS{ + MkdirAllError: errors.New("Permission denied"), + SymlinkError: nil, }, srcAbs: "/src/path", destAbs: "/dest/path", expectedError: errors.New("Permission denied"), }, { - mkdirSymlinker: &FakeMkdirSymlinker{ - &FakeDirMaker{MkdirAllError: nil}, - &FakeSymlinker{SymlinkError: errors.New("File exists")}, + os: &FakeOS{ + MkdirAllError: nil, + SymlinkError: errors.New("File exists"), }, srcAbs: "/src/path", destAbs: "/dest/path", @@ -211,7 +249,7 @@ func TestNewLinker(t *testing.T) { } for _, c := range cases { - linker := NewLinker(&FakeOutputer{}, c.mkdirSymlinker) + linker := NewLinker(&FakeOutputer{}, c.os) err := linker.SetSymlink(c.srcAbs, c.destAbs) if !reflect.DeepEqual(err, c.expectedError) { diff --git a/main.go b/main.go index ca705c8..144babf 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ var ( osStater = new(OsStater) osDirMaker = new(OsDirMaker) osMkdirSymlinker = new(OsMkdirSymlinker) + osfs = new(OSFS) ) func main() { @@ -28,7 +29,7 @@ func main() { // Parse arguments - args, err := parseArguments() + args, err := ParseArguments(nil) if err != nil { outputer.OutError("Error parsing aruments: %s", err) exit(1) @@ -133,13 +134,13 @@ func addAction(filename string, config *Configuration) error { return fmt.Errorf("Cannot backup file %s: %s", filename, err) } - // move file to dotfiles root + // Move file to dotfiles root newPath := config.Directories.Dotfiles + "/" + path.Base(filename) if err = os.Rename(filename, newPath); err != nil { return err } - linker := NewLinker(&outputer, osMkdirSymlinker) + linker := NewLinker(&outputer, osfs) // Add a symlink to the moved file if err = linker.SetSymlink(newPath, filename); err != nil { @@ -171,7 +172,7 @@ func installAction(config *Configuration) error { } mapping := getMapping(config, srcDirAbs) - linker := NewLinker(&outputer, osMkdirSymlinker) + linker := NewLinker(&outputer, osfs) outputer.OutInfo("Installing dotfiles...") for src, dst := range mapping { @@ -302,9 +303,11 @@ func installDotfile(src, dest string, linker Linker, config *Configuration, srcD } if needBackup { - err = backup(dest, destAbs, config.Directories.Backup) + oldpath := destAbs + newpath := config.Directories.Backup + "/" + dest + err = linker.Move(oldpath, newpath) if err != nil { - outputer.OutError("Error backuping file %s: %s", destAbs, err) + outputer.OutError("Error on file backup %s: %s", oldpath, err) exit(1) } }