Skip to content

Commit

Permalink
first usable version for environment aware mocking
Browse files Browse the repository at this point in the history
  • Loading branch information
243826 committed Aug 30, 2023
1 parent 38314f9 commit 7561961
Show file tree
Hide file tree
Showing 3 changed files with 226 additions and 0 deletions.
105 changes: 105 additions & 0 deletions env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package envygo

import (
"fmt"
"reflect"
)

type Env interface {
Mock(env interface{}) func()
}

var registered []Env

func Publish(envs ...Env) {
registered = append(registered, envs...)
}

func Mock(envs ...Env) func() {
var funcs []func()

for _, unknown := range envs {
unknownType := reflect.ValueOf(unknown).Type()
for _, known := range registered {
if reflect.ValueOf(known).Type() == unknownType {
funcs = append(funcs, mock(known, reflect.ValueOf(unknown).Elem().Interface()))
unknownType = nil
}
}

if unknownType != nil {
panic(fmt.Sprintf("Attempt to mock unregistered type via %v", unknown))
}
}

return func() {
Unmock(funcs...)
}
}

func Unmock(unmockers ...func()) {
for _, function := range unmockers {
defer function()
}
}

func mock(old Env, new interface{}) func() {
valueOfNew := reflect.ValueOf(new)
typeOfNew := reflect.TypeOf(new)

blankPtrValue := reflect.New(typeOfNew)
valueOfBlank := blankPtrValue.Elem()

oldPtrVal := reflect.New(reflect.TypeOf(old))
oldPtrVal.Elem().Set(reflect.ValueOf(old))
valueOfOld := oldPtrVal.Elem().Elem()

includeUnset := typeOfNew != reflect.ValueOf(old).Elem().Type()

for i := valueOfNew.NumField(); i > 0; {
i--
if typeOfNew.Field(i).IsExported() {
newField := valueOfNew.Field(i)
value := newField.Interface()
if includeUnset || !isZero(reflect.ValueOf(value)) {
name := typeOfNew.Field(i).Name
oldField := valueOfOld.FieldByName(name)
if oldField.CanSet() {
blankField := valueOfBlank.Field(i)
blankField.Set(oldField)
oldField.Set(newField)
} else {
panic(fmt.Sprintf("Attempt to mock unregistered field via %s", name))
}
}
}
}

blank := valueOfBlank.Interface()

return func() {
mock(old, blank)
}
}

func isZero(v reflect.Value) bool {
switch v.Kind() {
case reflect.Func, reflect.Map, reflect.Slice:
return v.IsNil()
case reflect.Array:
z := true
for i := 0; i < v.Len(); i++ {
z = z && isZero(v.Index(i))
}
return z
case reflect.Struct:
z := true
for i := 0; i < v.NumField(); i++ {
z = z && isZero(v.Field(i))
}
return z
}

// this takes care of the non-exported fields
return v.IsValid() && v.IsZero()
}
118 changes: 118 additions & 0 deletions env_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package envygo

import (
"errors"
"fmt"
"os"
"testing"
)

type OsEnv struct {
Create func(string) (*os.File, error)
Remove func(string) error
Path string
Properties interface{}
umask int
}

type UtilEnv struct {
Log func(...interface{}) (int, error)
}

func (e *OsEnv) Mock(iface interface{}) func() {
return mock(e, iface)
}

func (e *UtilEnv) Mock(iface interface{}) func() {
return mock(e, iface)
}

func TestEnvMockRandom(t *testing.T) {
path := "file:///this/is/sample/path"
var env = OsEnv{
Create: os.Create,
Remove: os.Remove,
Path: path,
Properties: struct{ property string }{property: "sample property"},
umask: 0x022,
}

if env.Path != path {
t.Errorf("paths are not the same even before mocking!")
}

url := "http://host:port/this/is/sample/path"
defer env.Mock(struct {
Create func(string) (*os.File, error)
Path string
umask int
}{
func(s string) (*os.File, error) {
return nil, errors.New("cant create a file")
},
url,
0,
})()

if env.Path != url {
t.Errorf("path should have been changed to url")
}
}

func TestEnvMockSame(t *testing.T) {
path := "file:///this/is/sample/path"
var env = OsEnv{
Create: os.Create,
Remove: os.Remove,
Path: path,
Properties: struct{ property string }{property: "sample property"},
umask: 0x022,
}

if env.Path != path {
t.Errorf("paths are not the same even before mocking!")
}

url := "http://host:port/this/is/sample/path"
defer env.Mock(OsEnv{
Create: func(s string) (*os.File, error) {
return nil, errors.New("cant create a file")
},
Remove: nil,
Path: url,
Properties: struct {
blah int
}{10},
})()

if env.Path != url {
t.Errorf("path should have been changed to url")
}
}

func TestMock(t *testing.T) {
path := "file:///this/is/sample/path"
var env = OsEnv{
Create: os.Create,
Remove: os.Remove,
Path: path,
Properties: struct{ property string }{property: "sample property"},
umask: 0x022,
}

Publish(&env, &UtilEnv{})

defer Mock(
&OsEnv{
Create: func(s string) (*os.File, error) {
return nil, errors.New("cant mock")
},
},
&UtilEnv{
Log: fmt.Println,
})()

if _, err := env.Create("hello"); err == nil {
t.Errorf("error was expected")
}
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/celeral/envygo

go 1.21

0 comments on commit 7561961

Please sign in to comment.