Skip to content

Commit

Permalink
Refactor code to move baton tests into baton pkg and add new tests
Browse files Browse the repository at this point in the history
  • Loading branch information
rk1274 committed Feb 20, 2025
1 parent 891de16 commit 12d625d
Show file tree
Hide file tree
Showing 7 changed files with 320 additions and 48 deletions.
56 changes: 32 additions & 24 deletions baton/baton.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,8 @@ func (b *Baton) setClientByIndex(clientIndex int, client *ex.Client) {
}
}

//TODO

// CollectionsDone closes the connections used for connection creation, and
// creates new ones for doing puts and metadata operations.
func (b *Baton) CollectionsDone() error {
Expand All @@ -349,7 +351,9 @@ func (b *Baton) CollectionsDone() error {
close(b.collErrCh)
b.collRunning = false

return b.createPutRemoveMetaClients()
return nil

// return b.createPutRemoveMetaClients()
}

func (b *Baton) createPutRemoveMetaClients() error {
Expand All @@ -369,23 +373,32 @@ func (b *Baton) createPutRemoveMetaClients() error {
return nil
}

// TODO
// InitClients sets three clients if they do not currently exist. Returns error
// if some exist and some do not.
func (b *Baton) InitClients() error {
if b.putClient == nil && b.MetaClient == nil && b.removeClient == nil {
return b.createPutRemoveMetaClients()
}

if b.putClient != nil && b.MetaClient != nil && b.removeClient != nil {
if !b.putClient.IsRunning() && !b.MetaClient.IsRunning() && !b.removeClient.IsRunning() {
return b.createPutRemoveMetaClients()
}

return nil
}

return internal.Error{"Clients are not in the same state", ""}
}

// CloseClients closes remove, put and meta clients.
func (b *Baton) CloseClients() {
var openClients []*ex.Client

for _, client := range []*ex.Client{b.removeClient, b.putClient, b.MetaClient} {
openClients = append(openClients, client)
if client != nil {
openClients = append(openClients, client)
}
}

b.closeConnections(openClients)
Expand All @@ -403,15 +416,13 @@ func (b *Baton) closeConnections(clients []*ex.Client) {
}
}

// TODO only take remote

// Stat gets mtime and metadata info for the request Remote object.
func (b *Baton) Stat(local, remote string) (bool, map[string]string, error) {
func (b *Baton) Stat(remote string) (bool, map[string]string, error) {
var it ex.RodsItem

err := TimeoutOp(func() error {
var errl error
it, errl = b.MetaClient.ListItem(ex.Args{Timestamp: true, AVU: true}, *requestToRodsItem(local, remote))
it, errl = b.MetaClient.ListItem(ex.Args{Timestamp: true, AVU: true}, *requestToRodsItem("", remote))

return errl
}, "stat failed: "+remote)
Expand All @@ -428,13 +439,19 @@ func (b *Baton) Stat(local, remote string) (bool, map[string]string, error) {
}

// requestToRodsItem converts a Request in to an extendo RodsItem without AVUs.
// If you provide an empty local path, it will not be set.
func requestToRodsItem(local, remote string) *ex.RodsItem {
return &ex.RodsItem{
IDirectory: filepath.Dir(local),
IFile: filepath.Base(local),
IPath: filepath.Dir(remote),
IName: filepath.Base(remote),
item := &ex.RodsItem{
IPath: filepath.Dir(remote),
IName: filepath.Base(remote),
}

if local != "" {
item.IDirectory = filepath.Dir(local)
item.IFile = filepath.Base(local)
}

return item
}

// RodsItemToMeta pulls out the AVUs from a RodsItem and returns them as a map.
Expand All @@ -451,8 +468,8 @@ func RodsItemToMeta(it ex.RodsItem) map[string]string {
// Put uploads request Local to the Remote object, overwriting it if it already
// exists. It calculates and stores the md5 checksum remotely, comparing to the
// local checksum.
func (b *Baton) Put(local, remote string, meta map[string]string) error {
item := requestToRodsItemWithAVUs(local, remote, meta)
func (b *Baton) Put(local, remote string) error {
item := requestToRodsItem(local, remote)

// iRODS treats /dev/null specially, so unless that changes we have to check
// for it and create a temporary empty file in its place.
Expand Down Expand Up @@ -483,15 +500,6 @@ func (b *Baton) Put(local, remote string, meta map[string]string) error {
return err
}

// requestToRodsItemWithAVUs converts a Request in to an extendo RodsItem with
// AVUs.
func requestToRodsItemWithAVUs(local, remote string, meta map[string]string) *ex.RodsItem {
item := requestToRodsItem(local, remote)
item.IAVUs = MetaToAVUs(meta)

return item
}

func MetaToAVUs(meta map[string]string) []ex.AVU {
avus := make([]ex.AVU, len(meta))
i := 0
Expand Down Expand Up @@ -645,7 +653,7 @@ func (b *Baton) QueryMeta(dirToSearch string, meta map[string]string) ([]string,
paths := make([]string, len(items))

for i, item := range items {
paths[i] = item.IPath
paths[i] = filepath.Join(item.IPath, item.IName)
}

return paths, err
Expand Down
243 changes: 243 additions & 0 deletions baton/baton_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
/*******************************************************************************
* Copyright (c) 2025 Genome Research Ltd.
*
* Author: Rosie Kern <rk18@sanger.ac.uk>
* Author: Iaroslav Popov <ip13@sanger.ac.uk>
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************************************************************/

package baton

import (
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"testing"

. "github.com/smartystreets/goconvey/convey"
"github.com/wtsi-hgi/ibackup/internal"
)

func TestBaton(t *testing.T) {
h, errgbh := GetBatonHandler()
if errgbh != nil {
t.Logf("GetBatonHandler error: %s", errgbh)
SkipConvey("Skipping baton tests since couldn't find baton", t, func() {})

return
}

remotePath := os.Getenv("IBACKUP_TEST_COLLECTION")
if remotePath == "" {
SkipConvey("Skipping baton tests since IBACKUP_TEST_COLLECTION is not defined", t, func() {})

return
}

resetIRODS(remotePath)

localPath := t.TempDir()

Convey("Given clients on a baton handler", t, func() {
err := h.InitClients()
So(err, ShouldBeNil)

So(h.removeClient.IsRunning(), ShouldBeTrue)
So(h.MetaClient.IsRunning(), ShouldBeTrue)
So(h.putClient.IsRunning(), ShouldBeTrue)

meta := map[string]string{
"ibackup:test:a": "1",
"ibackup:test:b": "2",
}

Convey("And a local file", func() {
file1local := filepath.Join(localPath, "file1")
file1remote := filepath.Join(remotePath, "file1")

internal.CreateTestFileOfLength(t, file1local, 1)

Convey("You can stat a file not in iRODS", func() {
exists, _, errs := h.Stat(file1remote)
So(errs, ShouldBeNil)
So(exists, ShouldBeFalse)
})

Convey("You can put the file and its metadata in iRODS", func() {
err = h.Put(file1local, file1remote)
So(err, ShouldBeNil)

So(isObjectInIRODS(remotePath, "file1"), ShouldBeTrue)

Convey("And putting a new file in the same location will overwrite existing object", func() {
file2local := filepath.Join(localPath, "file2")
sizeOfFile2 := 10

internal.CreateTestFileOfLength(t, file2local, sizeOfFile2)

err = h.Put(file2local, file1remote)
So(err, ShouldBeNil)

So(getSizeOfObject(file1remote), ShouldEqual, sizeOfFile2)
})

Convey("And then you can add metadata to it", func() {
errm := h.AddMeta(file1remote, meta)
So(errm, ShouldBeNil)

So(getRemoteMeta(file1remote), ShouldContainSubstring, "ibackup:test:a")
So(getRemoteMeta(file1remote), ShouldContainSubstring, "ibackup:test:b")
})

Convey("And given metadata on the file in iRODS", func() {
for k, v := range meta {
addRemoteMeta(file1remote, k, v)
}

Convey("You can get the metadata from a file in iRODS", func() {
fileMeta, errm := h.GetMeta(file1remote)
So(errm, ShouldBeNil)

So(fileMeta, ShouldResemble, meta)
})

Convey("You can stat a file and get its metadata", func() {
exists, fileMeta, errm := h.Stat(file1remote)
So(errm, ShouldBeNil)
So(exists, ShouldBeTrue)
So(fileMeta, ShouldResemble, meta)
})

Convey("You can query if files contain specific metadata", func() {
files, errq := h.QueryMeta(remotePath, map[string]string{"ibackup:test:a": "1"})
So(errq, ShouldBeNil)

So(len(files), ShouldEqual, 1)
So(files, ShouldContain, file1remote)

files, errq = h.QueryMeta(remotePath, map[string]string{"ibackup:test:a": "2"})
So(errq, ShouldBeNil)

So(len(files), ShouldEqual, 0)
})

Convey("You can remove specific metadata from a file in iRODS", func() {
errm := h.RemoveMeta(file1remote, map[string]string{"ibackup:test:a": "1"})
So(errm, ShouldBeNil)

fileMeta, errm := h.GetMeta(file1remote)
So(errm, ShouldBeNil)

So(fileMeta, ShouldResemble, map[string]string{"ibackup:test:b": "2"})
})
})

Convey("And then you can remove it from iRODS", func() {
err = h.RemoveFile(file1remote)
So(err, ShouldBeNil)

So(isObjectInIRODS(remotePath, "file1"), ShouldBeFalse)
})
})
})

Convey("You can open collection clients and put an empty dir in iRODS", func() {
dir1remote := filepath.Join(remotePath, "dir1")
err = h.EnsureCollection(dir1remote)
So(err, ShouldBeNil)

So(h.collPool.IsOpen(), ShouldBeTrue)

for _, client := range h.collClients {
So(client.IsRunning(), ShouldBeTrue)
}

So(isObjectInIRODS(remotePath, "dir1"), ShouldBeTrue)

Convey("Then you can close the collection clients", func() {
err = h.CollectionsDone()
So(err, ShouldBeNil)

So(h.collPool.IsOpen(), ShouldBeFalse)
So(len(h.collClients), ShouldEqual, 0)

Convey("And then you can remove the dir from iRODS", func() {
err = h.RemoveDir(dir1remote)
So(err, ShouldBeNil)

So(isObjectInIRODS(remotePath, "dir1"), ShouldBeFalse)
})
})
})

Convey("You can close those clients", func() {
h.CloseClients()

So(h.AllClientsStopped(), ShouldBeTrue)
})
})
}

func resetIRODS(remotePath string) {
if remotePath == "" {
return
}

exec.Command("irm", "-r", remotePath).Run() //nolint:errcheck

exec.Command("imkdir", remotePath).Run() //nolint:errcheck
}

func isObjectInIRODS(remotePath, name string) bool {
output, err := exec.Command("ils", remotePath).CombinedOutput()
So(err, ShouldBeNil)

return strings.Contains(string(output), name)
}

func getRemoteMeta(path string) string {
output, err := exec.Command("imeta", "ls", "-d", path).CombinedOutput()
So(err, ShouldBeNil)

return string(output)
}

func addRemoteMeta(path, key, val string) {
output, err := exec.Command("imeta", "add", "-d", path, key, val).CombinedOutput()
if strings.Contains(string(output), "CATALOG_ALREADY_HAS_ITEM_BY_THAT_NAME") {
return
}

So(err, ShouldBeNil)
}

func getSizeOfObject(path string) int {
output, err := exec.Command("ils", "-l", path).CombinedOutput()
So(err, ShouldBeNil)

cols := strings.Fields(string(output))
size, err := strconv.Atoi(cols[3])
So(err, ShouldBeNil)

return size
}
Loading

0 comments on commit 12d625d

Please # to comment.