e2etest/declarativeTestFiles.go (514 lines of code) (raw):
// Copyright © Microsoft <wastore@microsoft.com>
//
// 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 e2etest
import (
"encoding/hex"
"fmt"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
bfsfile "github.com/Azure/azure-sdk-for-go/sdk/storage/azdatalake/file"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azfile/file"
"math"
"reflect"
"strconv"
"strings"
"time"
"github.com/Azure/azure-storage-azcopy/v10/cmd"
"github.com/Azure/azure-storage-azcopy/v10/common"
)
///////////////
type contentHeaders struct {
// TODO: do we really need/want the fields here to be individually nillable? For Blob, at least, setting these is an all-or-nothing proposition anyway, so maybe there's little need for individual nillablity
cacheControl *string
contentDisposition *string
contentEncoding *string
contentLanguage *string
contentType *string
contentMD5 []byte
}
func (h *contentHeaders) DeepCopy() *contentHeaders {
if h == nil {
return nil
}
ret := contentHeaders{}
ret.cacheControl = h.cacheControl
ret.contentDisposition = h.contentDisposition
ret.contentEncoding = h.contentEncoding
ret.contentLanguage = h.contentLanguage
ret.contentType = h.contentType
if h.contentMD5 != nil {
ret.contentMD5 = make([]byte, len(h.contentMD5))
copy(ret.contentMD5, h.contentMD5)
}
return &ret
}
func (h *contentHeaders) ToBlob() *blob.HTTPHeaders {
if h == nil {
return nil
}
return &blob.HTTPHeaders{
BlobContentType: h.contentType,
BlobContentDisposition: h.contentDisposition,
BlobContentEncoding: h.contentEncoding,
BlobContentLanguage: h.contentLanguage,
BlobCacheControl: h.cacheControl,
BlobContentMD5: h.contentMD5,
}
}
func (h *contentHeaders) ToFile() *file.HTTPHeaders {
if h == nil {
return nil
}
return &file.HTTPHeaders{
ContentType: h.contentType,
ContentDisposition: h.contentDisposition,
ContentEncoding: h.contentEncoding,
ContentLanguage: h.contentLanguage,
CacheControl: h.cacheControl,
ContentMD5: h.contentMD5,
}
}
func (h *contentHeaders) ToBlobFS() *bfsfile.HTTPHeaders {
if h == nil {
return nil
}
return &bfsfile.HTTPHeaders{
ContentMD5: h.contentMD5,
ContentType: h.contentType,
ContentDisposition: h.contentDisposition,
ContentEncoding: h.contentEncoding,
ContentLanguage: h.contentLanguage,
CacheControl: h.cacheControl,
}
}
func (h *contentHeaders) ToCommonHeaders() common.ResourceHTTPHeaders {
if h == nil {
return common.ResourceHTTPHeaders{}
}
return common.ResourceHTTPHeaders{
ContentType: DerefOrZero(h.contentType),
ContentMD5: h.contentMD5,
ContentEncoding: DerefOrZero(h.contentEncoding),
ContentLanguage: DerefOrZero(h.contentLanguage),
ContentDisposition: DerefOrZero(h.contentDisposition),
CacheControl: DerefOrZero(h.cacheControl),
}
}
func (h *contentHeaders) String() string {
var ret string
if h == nil {
return "[nil]"
}
ret += "[\n"
ret += fmt.Sprintln("cacheControl: " + reflect.ValueOf(h.cacheControl).Elem().String())
ret += fmt.Sprintln("contentDisposition: " + reflect.ValueOf(h.contentDisposition).Elem().String())
ret += fmt.Sprintln("contentEncoding: " + reflect.ValueOf(h.contentLanguage).Elem().String())
ret += fmt.Sprintln("contentType: " + reflect.ValueOf(h.contentType).Elem().String())
ret += fmt.Sprintln("contentMD5: " + hex.EncodeToString(h.contentMD5))
ret += "]\n"
return ret
}
// The full set of properties, dates, info etc, that we can potentially preserve for a file or folder
// This is exposed to the declarativeResourceManagers, to create/check the objects.
// All field are pointers or interfaces to make them nil-able. Nil means "unspecified".
type objectProperties struct {
entityType common.EntityType
symlinkTarget *string
posixProperties *objectUnixStatContainer
size *int64
contentHeaders *contentHeaders
nameValueMetadata map[string]*string
blobTags common.BlobTags
blobType common.BlobType
blobVersions *uint
creationTime *time.Time
lastWriteTime *time.Time
smbAttributes *uint32
smbPermissionsSddl *string
adlsPermissionsACL *string // TODO: Test owner and group; needs a good target though.
cpkInfo *blob.CPKInfo
cpkScopeInfo *blob.CPKScopeInfo
}
type objectUnixStatContainer struct {
// mode can contain THE FOLLOWING file type specifier bits (common.S_IFSOCK, common.S_IFIFO)
// common.S_IFDIR and common.S_IFLNK are achievable using folder() and symlink().
// TODO/Spike: common.S_IFBLK and common.S_IFCHR may be difficult to replicate consistently in a test environment
mode *uint32
accessTime *time.Time
modTime *time.Time
}
func (o *objectUnixStatContainer) HasTimes() bool {
return o != nil && (o.accessTime != nil || o.modTime != nil)
}
func (o *objectUnixStatContainer) Empty() bool {
if o == nil {
return true
}
return o.mode == nil &&
o.accessTime == nil &&
o.modTime == nil
}
func (o *objectUnixStatContainer) DeepCopy() *objectUnixStatContainer {
if o == nil {
return nil
}
out := &objectUnixStatContainer{}
if o.mode != nil {
mode := *o.mode
out.mode = &mode
}
if o.accessTime != nil {
accessTime := *o.accessTime
out.accessTime = &accessTime
}
if o.modTime != nil {
modTime := *o.modTime
out.modTime = &modTime
}
return out
}
func (o *objectUnixStatContainer) EquivalentToStatAdapter(s common.UnixStatAdapter) string {
if o == nil {
return "" // no comparison to make
}
mismatched := make([]string, 0)
// only compare if we set it
if o.mode != nil {
if s.FileMode() != *o.mode {
mismatched = append(mismatched, "mode")
}
}
if o.accessTime != nil {
if o.accessTime.UnixNano() != s.ATime().UnixNano() {
mismatched = append(mismatched, "atime")
}
}
if o.modTime != nil {
if o.modTime.UnixNano() != s.MTime().UnixNano() {
mismatched = append(mismatched, "mtime")
}
}
return strings.Join(mismatched, ", ")
}
func (o *objectUnixStatContainer) AddToMetadata(metadata map[string]*string) {
if o == nil {
return
}
mask := uint32(0)
if o.mode != nil { // always overwrite; perhaps it got changed in one of the hooks.
mask |= common.STATX_MODE
metadata[common.POSIXModeMeta] = to.Ptr(strconv.FormatUint(uint64(*o.mode), 10))
delete(metadata, common.POSIXFIFOMeta)
delete(metadata, common.POSIXSocketMeta)
switch {
case *o.mode&common.S_IFIFO == common.S_IFIFO:
metadata[common.POSIXFIFOMeta] = to.Ptr("true")
case *o.mode&common.S_IFSOCK == common.S_IFSOCK:
metadata[common.POSIXSocketMeta] = to.Ptr("true")
}
}
if o.accessTime != nil {
mask |= common.STATX_ATIME
metadata[common.POSIXATimeMeta] = to.Ptr(strconv.FormatInt(o.accessTime.UnixNano(), 10))
}
if o.modTime != nil {
mask |= common.STATX_MTIME
metadata[common.POSIXModTimeMeta] = to.Ptr(strconv.FormatInt(o.modTime.UnixNano(), 10))
}
metadata[common.LINUXStatxMaskMeta] = to.Ptr(strconv.FormatUint(uint64(mask), 10))
}
// returns op.size, if present, else defaultSize
func (op objectProperties) sizeBytes(a asserter, defaultSize string) int {
if op.size != nil {
if *op.size > math.MaxInt32 {
a.Error(fmt.Sprintf("unsupported size: %d", *op.size))
return 0
}
return int(*op.size)
}
longSize, err := cmd.ParseSizeString(defaultSize, "testFiles.size")
if longSize < math.MaxInt32 {
a.AssertNoErr(err)
return int(longSize)
}
a.Error("unsupported size: " + defaultSize)
return 0
}
func (op objectProperties) DeepCopy() objectProperties {
ret := objectProperties{}
ret.entityType = op.entityType
if op.symlinkTarget != nil {
target := *op.symlinkTarget
ret.symlinkTarget = &target
}
if !op.posixProperties.Empty() {
ret.posixProperties = op.posixProperties.DeepCopy()
}
if op.size != nil {
val := op.size
ret.size = val
}
if op.contentHeaders != nil {
ret.contentHeaders = op.contentHeaders.DeepCopy()
}
ret.nameValueMetadata = make(map[string]*string)
for k, v := range op.nameValueMetadata {
ret.nameValueMetadata[k] = v
}
ret.blobTags = make(map[string]string)
for k, v := range op.blobTags {
ret.blobTags[k] = v
}
if op.blobVersions != nil {
ret.blobVersions = pointerTo(*op.blobVersions)
}
if op.creationTime != nil {
time := *op.creationTime
ret.creationTime = &time
}
if op.lastWriteTime != nil {
time := *op.lastWriteTime
ret.lastWriteTime = &time
}
if op.smbAttributes != nil {
val := *op.smbAttributes
ret.smbAttributes = &val
}
if op.smbPermissionsSddl != nil {
val := *op.smbPermissionsSddl
ret.smbPermissionsSddl = &val
}
if op.adlsPermissionsACL != nil {
val := *op.adlsPermissionsACL
ret.adlsPermissionsACL = &val
}
if op.cpkInfo != nil {
val := *op.cpkInfo
ret.cpkInfo = &val
}
if op.cpkScopeInfo != nil {
val := *op.cpkScopeInfo
ret.cpkScopeInfo = &val
}
return ret
}
// a file or folder. Create these with the f() and folder() functions
type testObject struct {
name string
expectedFailureMessage string // the failure message that we expect to see in the log for this file/folder (only populated for expected failures)
body []byte
// info to be used at creation time. Usually, creationInfo and and verificationInfo will be the same
// I.e. we expect the creation properties to be preserved. But, for flexibility, they can be set to something different.
creationProperties objectProperties
// info to be used at verification time. Will be nil if there is no validation (of properties) to be done
verificationProperties *objectProperties
}
func (t *testObject) DeepCopy() *testObject {
ret := testObject{}
ret.name = t.name
ret.expectedFailureMessage = t.expectedFailureMessage
ret.creationProperties = t.creationProperties.DeepCopy()
if t.body != nil {
ret.body = make([]byte, len(t.body))
copy(ret.body, t.body)
}
if t.verificationProperties != nil {
vp := (*t.verificationProperties).DeepCopy()
ret.verificationProperties = &vp
}
return &ret
}
func (t *testObject) hasContentToValidate() bool {
if t.verificationProperties != nil && t.creationProperties.entityType != t.verificationProperties.entityType {
panic("entityType property is misconfigured")
}
return t.creationProperties.entityType == common.EEntityType.File()
}
func (t *testObject) isFolder() bool {
if t.verificationProperties != nil && t.creationProperties.entityType != t.verificationProperties.entityType {
panic("entityType property is misconfigured")
}
return t.creationProperties.entityType == common.EEntityType.Folder()
}
func (t *testObject) isRootFolder() bool {
return t.name == "" && t.isFolder()
}
// This interface is implemented by types that provide extra information about test files
// It is to be used ONLY as parameters to the f() and folder() methods.
// It is not used in other parts of the code, since the other parts use the testObject instances that are created
// from
type withPropertyProvider interface {
appliesToCreation() bool
appliesToVerification() bool
createObjectProperties() *objectProperties
}
type expectedFailureProvider interface {
expectedFailure() string
}
// Define a file, in the expectations lists on a testFiles struct.
// (Note, if you are not going to supply any parameters other than the name, you can just use a string literal in the list
// instead of calling this function).
// Provide properties by including one or more objects that implement withPropertyProvider.
// Typically, that will just be done like this: f("foo", with{<set properties here>})
// For advanced cases, you can use verifyOnly instead of the normal "with". The normal "with" applies to both creation
// and verification.
// You can also add withFailureMessage{"message"} to files that are expected to fail, to specify what the expected
// failure message will be in the log.
// And withStubMetadata{} to supply the metadata that indicates that an object is a directory stub.
func f(n string, properties ...withPropertyProvider) *testObject {
haveCreationProperties := false
haveVerificationProperties := false
result := &testObject{name: n}
for _, p := range properties {
// special case for specifying expected failure message
if efp, ok := p.(expectedFailureProvider); ok {
if p.createObjectProperties() != nil {
panic("a withPropertyProvider that implements expectedFailureProvider should not provide any objectProperties. It ONLY specifies the failure message.")
}
result.expectedFailureMessage = efp.expectedFailure()
continue
}
// normal case. Not that normally the same p will return true for both p.appliesToCreation and p.appliesToVerification
const mustBeOne = "there must be only one withPropertyProvider that specifies the %s properties. You can't mix 'with{...}' and 'with%sOnly{...}. But you can mix 'withCreationOnly{...}' and 'withVerificationOnly{...}"
if p.appliesToCreation() {
if haveCreationProperties {
panic(fmt.Sprintf(mustBeOne, "creation", "Creation"))
}
haveCreationProperties = true
objProps := p.createObjectProperties()
if objProps == nil {
objProps = &objectProperties{} // for creationProperties, this saves our code from endless nil checks. (But for verification, below, the nil is useful)
}
result.creationProperties = *objProps
}
if p.appliesToVerification() {
if haveVerificationProperties {
panic(fmt.Sprintf(mustBeOne, "verification", "Verification"))
}
haveVerificationProperties = true
objProps := p.createObjectProperties()
result.verificationProperties = objProps // verification props is nilable, and nil signals "nothing to verify"
}
}
return result
}
func symlink(new, target string) *testObject {
name := strings.TrimLeft(new, "/")
result := f(name)
// result.creationProperties
result.creationProperties.entityType = common.EEntityType.Symlink()
result.creationProperties.symlinkTarget = &target
result.verificationProperties = &objectProperties{}
result.verificationProperties.entityType = common.EEntityType.Symlink()
result.verificationProperties.symlinkTarget = &target
return result
}
// define a folder, in the expectations lists on a testFiles struct
func folder(n string, properties ...withPropertyProvider) *testObject {
name := strings.TrimLeft(n, "/")
result := f(name, properties...)
// isFolder is at properties level, not testObject level, because we need it at properties level when reading
// the properties back from the destination (where we don't read testObjects, we just read objectProperties)
result.creationProperties.entityType = common.EEntityType.Folder()
if result.verificationProperties != nil {
result.verificationProperties.entityType = common.EEntityType.Folder()
}
return result
}
//////////
type objectTarget struct {
objectName string
snapshotid bool // add snapshot id
// versions specifies a zero-indexed list of versions to copy.
// ID is automatically filled in based off the versions specified in this field.
// Nil or empty list does nothing. A single version ID will be passed as a part of the URI,
// unless singleVersionList is true.
// Negative cases for list of versions, e.g. specifying nonexistent versions, shouldn't be done here.
// Those get trimmed out by the traverser.
versions []uint
singleVersionList bool
}
// Represents a set of source files, including what we expect should happen to them
// Our expectations, e.g. success or failure, are represented by whether we put each file into
// "shouldTransfer", "shouldFail" etc.
type testFiles struct {
defaultSize string // how big should the files be? Applies to those files that don't specify individual sizes. Uses the same K, M, G suffixes as benchmark mode's size-per-file
objectTarget objectTarget // should we target only a single file/folder?
destTarget string // do we want to copy under a folder or rename?
sourcePublic *container.PublicAccessType // should the source blob container be public? (ONLY APPLIES TO BLOB.)
// The files/folders that we expect to be transferred. Elements of the list must be strings or testObject's.
// A string can be used if no properties need to be specified.
// Folders included here are ignored by the verification code when we are not transferring between folder-aware
// locations.
shouldTransfer []interface{}
// the files/folders that we expect NOT to be found by the enumeration. See comments on shouldTransfer
shouldIgnore []interface{}
// the files/folders that we expect to fail with error (unlike the other fields, this one is composite object instead of just a filename
shouldFail []interface{}
// files/folders that we expect to be skipped due to an overwrite setting
shouldSkip []interface{}
}
func (tf testFiles) cloneShouldTransfers() testFiles {
return testFiles{
defaultSize: tf.defaultSize,
objectTarget: tf.objectTarget,
destTarget: tf.destTarget,
sourcePublic: tf.sourcePublic,
shouldTransfer: tf.shouldTransfer,
}
}
func (tf testFiles) DeepCopy() testFiles {
ret := testFiles{}
ret.defaultSize = tf.defaultSize
ret.objectTarget = tf.objectTarget
ret.destTarget = tf.destTarget
ret.sourcePublic = tf.sourcePublic
ret.shouldTransfer = tf.copyList(tf.shouldTransfer)
ret.shouldIgnore = tf.copyList(tf.shouldIgnore)
ret.shouldFail = tf.copyList(tf.shouldFail)
ret.shouldSkip = tf.copyList(tf.shouldSkip)
return ret
}
func (*testFiles) copyList(src []interface{}) []interface{} {
var ret []interface{}
for _, r := range src {
if aTestObj, ok := r.(*testObject); ok {
ret = append(ret, aTestObj.DeepCopy())
} else if asString, ok := r.(string); ok {
ret = append(ret, asString)
} else {
panic("testFiles lists may contain only strings and testObjects. Create your test objects with the f() and folder() functions")
}
}
return ret
}
// takes a mixed list of (potentially) strings and testObjects, and returns them all as test objects
// TODO: do we want to continue supporting plain strings in the expectation file lists (for convenience of test coders)
//
// or force them to use f() for every file?
func (*testFiles) toTestObjects(rawList []interface{}, isFail bool) []*testObject {
result := make([]*testObject, 0, len(rawList))
for k, r := range rawList {
if asTestObject, ok := r.(*testObject); ok {
if asTestObject.expectedFailureMessage != "" && !isFail {
panic("expected failures are only allowed in the shouldFail list. They are not allowed for other test files")
}
result = append(result, asTestObject)
} else if asString, ok := r.(string); ok {
rawList[k] = &testObject{name: asString} // convert to a full deal so we can apply md5
result = append(result, rawList[k].(*testObject))
} else {
panic("testFiles lists may contain only strings and testObjects. Create your test objects with the f() and folder() functions")
}
}
return result
}
func (tf *testFiles) allObjects(isSource bool) []*testObject {
if isSource {
result := make([]*testObject, 0)
result = append(result, tf.toTestObjects(tf.shouldTransfer, false)...)
result = append(result, tf.toTestObjects(tf.shouldIgnore, false)...) // these must be present at the source. Enumeration filters are expected to skip them
result = append(result, tf.toTestObjects(tf.shouldSkip, false)...) // these must be present at the source. Overwrite processing is expected to skip them
result = append(result, tf.toTestObjects(tf.shouldFail, true)...) // these must also be present at the source. Their transferring is expected to fail
return result
}
// destination only needs the things that overwrite will skip
return tf.toTestObjects(tf.shouldSkip, false)
}
func (tf *testFiles) isListOfVersions() bool {
return tf.objectTarget.objectName != "" && (len(tf.objectTarget.versions) > 1 || (len(tf.objectTarget.versions) == 1 && tf.objectTarget.singleVersionList))
}
func (tf *testFiles) getForStatus(s *scenario, status common.TransferStatus, expectFolders bool, expectRootFolder bool) []*testObject {
if status == common.ETransferStatus.Success() && tf.isListOfVersions() {
s.a.Assert(s.fromTo.From(), equals(), common.ELocation.Blob(), "List of Versions must be used with Blob")
versions := s.GetSource().(*resourceBlobContainer).getVersions(s.a, tf.objectTarget.objectName)
// track down the original testObject
var target *testObject
for _, v := range tf.toTestObjects(tf.shouldTransfer, false) {
if v.name == tf.objectTarget.objectName {
target = v
break
}
}
s.a.Assert(target, notEquals(), nil, "objectTarget must exist in successful transfers")
out := make([]*testObject, len(tf.objectTarget.versions))
for k, v := range tf.objectTarget.versions {
// flatten the version ID
versions[v] = strings.ReplaceAll(versions[v], ":", "-")
out[k] = target.DeepCopy()
out[k].name = versions[v] + "-" + out[k].name
}
return out
}
shouldInclude := func(f *testObject) bool {
if !f.isFolder() {
return true
}
if expectFolders {
if f.isRootFolder() {
return expectRootFolder
}
return true
}
return false
}
result := make([]*testObject, 0)
switch status {
case common.ETransferStatus.Success():
for _, f := range tf.toTestObjects(tf.shouldTransfer, false) {
if shouldInclude(f) {
result = append(result, f)
}
}
case common.ETransferStatus.Failed():
for _, f := range tf.toTestObjects(tf.shouldFail, true) {
if shouldInclude(f) {
result = append(result, f)
}
}
default:
panic("unsupported status type")
}
return result
}