github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/pkg/testfs/testfs.go (about)

     1  /*This file is part of kuberpult.
     2  
     3  Kuberpult is free software: you can redistribute it and/or modify
     4  it under the terms of the Expat(MIT) License as published by
     5  the Free Software Foundation.
     6  
     7  Kuberpult is distributed in the hope that it will be useful,
     8  but WITHOUT ANY WARRANTY; without even the implied warranty of
     9  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    10  MIT License for more details.
    11  
    12  You should have received a copy of the MIT License
    13  along with kuberpult. If not, see <https://directory.fsf.org/wiki/License:Expat>.
    14  
    15  Copyright 2023 freiheit.com*/
    16  
    17  package testfs
    18  
    19  import (
    20  	"fmt"
    21  	"os"
    22  	"sync"
    23  
    24  	"github.com/go-git/go-billy/v5"
    25  )
    26  
    27  type Operation int
    28  
    29  const (
    30  	NONE Operation = iota
    31  	CREATE
    32  	OPEN
    33  	OPENFILE
    34  	STAT
    35  	RENAME
    36  	REMOVE
    37  	READDIR
    38  	SYMLINK
    39  	READLINK
    40  	MKDIRALL
    41  )
    42  
    43  func (o Operation) String() string {
    44  	switch o {
    45  	case CREATE:
    46  		return "Create"
    47  	case OPEN:
    48  		return "Open"
    49  	case OPENFILE:
    50  		return "OpenFile"
    51  	case STAT:
    52  		return "Stat"
    53  	case RENAME:
    54  		return "Rename"
    55  	case REMOVE:
    56  		return "Remove"
    57  	case READDIR:
    58  		return "ReadDir"
    59  	case SYMLINK:
    60  		return "Symlink"
    61  	case READLINK:
    62  		return "ReadLink"
    63  	case MKDIRALL:
    64  		return "MkdirAll"
    65  	}
    66  	return fmt.Sprintf("unknown(%d)", o)
    67  }
    68  
    69  type FileOperation struct {
    70  	Operation Operation
    71  	Filename  string
    72  }
    73  
    74  type value struct {
    75  	errored bool
    76  }
    77  
    78  // UsageCollector tracks which file operations have been used in a test suite and reports which were not
    79  // tested with an injected error.
    80  type UsageCollector struct {
    81  	mx    sync.Mutex
    82  	usage map[FileOperation]value
    83  }
    84  
    85  func (u *UsageCollector) used(op Operation, filename string) {
    86  	u.mx.Lock()
    87  	defer u.mx.Unlock()
    88  	if u.usage == nil {
    89  		u.usage = map[FileOperation]value{}
    90  	}
    91  	_, ok := u.usage[FileOperation{op, filename}]
    92  	if !ok {
    93  		u.usage[FileOperation{op, filename}] = value{
    94  			errored: false,
    95  		}
    96  	}
    97  }
    98  
    99  func (u *UsageCollector) errored(op Operation, filename string) {
   100  	u.mx.Lock()
   101  	defer u.mx.Unlock()
   102  	if u.usage == nil {
   103  		u.usage = map[FileOperation]value{}
   104  	}
   105  	u.usage[FileOperation{op, filename}] = value{errored: true}
   106  }
   107  
   108  func (u *UsageCollector) UntestedOps() []FileOperation {
   109  	u.mx.Lock()
   110  	defer u.mx.Unlock()
   111  	result := []FileOperation{}
   112  	for k, v := range u.usage {
   113  		if !v.errored {
   114  			result = append(result, k)
   115  		}
   116  	}
   117  	return result
   118  }
   119  
   120  type errorInjector struct {
   121  	operation Operation
   122  	filename  string
   123  	err       error
   124  	used      bool
   125  	collector *UsageCollector
   126  }
   127  
   128  func (e *errorInjector) inject(op Operation, filename string) error {
   129  	if e.used {
   130  		return nil
   131  	} else if e.operation == op && e.filename == filename {
   132  		e.used = true
   133  		e.collector.errored(op, filename)
   134  		return e.err
   135  	}
   136  	e.collector.used(op, filename)
   137  	return nil
   138  }
   139  
   140  func (uc *UsageCollector) WithError(fs billy.Filesystem, op Operation, filename string, err error) *Filesystem {
   141  	return &Filesystem{
   142  		Inner: fs,
   143  		errorInjector: errorInjector{
   144  			used:      false,
   145  			operation: op,
   146  			filename:  filename,
   147  			err:       err,
   148  			collector: uc,
   149  		},
   150  	}
   151  }
   152  
   153  // A special filesystem that allows injecting errors at arbitrary operations.
   154  type Filesystem struct {
   155  	Inner         billy.Filesystem
   156  	errorInjector errorInjector
   157  }
   158  
   159  func (f *Filesystem) Create(filename string) (billy.File, error) {
   160  	err := f.errorInjector.inject(CREATE, filename)
   161  	if err != nil {
   162  		return nil, err
   163  	}
   164  	return f.Inner.Create(filename)
   165  }
   166  
   167  func (f *Filesystem) Open(filename string) (billy.File, error) {
   168  	err := f.errorInjector.inject(OPEN, filename)
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  	return f.Inner.Open(filename)
   173  }
   174  
   175  func (f *Filesystem) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
   176  	err := f.errorInjector.inject(OPENFILE, filename)
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  	return f.Inner.OpenFile(filename, flag, perm)
   181  }
   182  
   183  func (f *Filesystem) Stat(filename string) (os.FileInfo, error) {
   184  	err := f.errorInjector.inject(STAT, filename)
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  	return f.Inner.Stat(filename)
   189  }
   190  
   191  func (f *Filesystem) Rename(oldpath, newpath string) error {
   192  	err := f.errorInjector.inject(RENAME, oldpath)
   193  	if err != nil {
   194  		return err
   195  	}
   196  	return f.Inner.Rename(oldpath, newpath)
   197  }
   198  
   199  func (f *Filesystem) Remove(filename string) error {
   200  	err := f.errorInjector.inject(REMOVE, filename)
   201  	if err != nil {
   202  		return err
   203  	}
   204  	return f.Inner.Remove(filename)
   205  }
   206  
   207  func (f *Filesystem) Join(elem ...string) string {
   208  	return f.Inner.Join(elem...)
   209  }
   210  
   211  func (f *Filesystem) TempFile(dir, prefix string) (billy.File, error) {
   212  	return f.Inner.TempFile(dir, prefix)
   213  }
   214  
   215  func (f *Filesystem) Lstat(filename string) (os.FileInfo, error) {
   216  	return f.Inner.Lstat(filename)
   217  }
   218  
   219  func (f *Filesystem) Symlink(target, link string) error {
   220  	err := f.errorInjector.inject(SYMLINK, link)
   221  	if err != nil {
   222  		return err
   223  	}
   224  	return f.Inner.Symlink(target, link)
   225  }
   226  
   227  func (f *Filesystem) Readlink(link string) (string, error) {
   228  	err := f.errorInjector.inject(READLINK, link)
   229  	if err != nil {
   230  		return "", err
   231  	}
   232  	return f.Inner.Readlink(link)
   233  }
   234  
   235  func (f *Filesystem) ReadDir(path string) ([]os.FileInfo, error) {
   236  	err := f.errorInjector.inject(READDIR, path)
   237  	if err != nil {
   238  		return nil, err
   239  	}
   240  	return f.Inner.ReadDir(path)
   241  }
   242  
   243  func (f *Filesystem) MkdirAll(filename string, perm os.FileMode) error {
   244  	err := f.errorInjector.inject(MKDIRALL, filename)
   245  	if err != nil {
   246  		return err
   247  	}
   248  	return f.Inner.MkdirAll(filename, perm)
   249  }
   250  
   251  func (f *Filesystem) Chroot(path string) (billy.Filesystem, error) {
   252  	panic("Chroot not implemented")
   253  }
   254  
   255  func (f *Filesystem) Root() string {
   256  	panic("Root not implemented")
   257  }