0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-30 22:34:13 -05:00

coverage: add failure injection framework

Signed-off-by: Ramkumar Chinchani <rchincha@cisco.com>
This commit is contained in:
Ramkumar Chinchani 2021-12-21 04:18:13 +00:00 committed by Ramkumar Chinchani
parent f47c8222c2
commit e0a1a82890
9 changed files with 292 additions and 6 deletions

View file

@ -44,7 +44,7 @@ jobs:
go get github.com/wadey/gocovmerge go get github.com/wadey/gocovmerge
- name: Run build and test - name: Run build and test
timeout-minutes: 30 timeout-minutes: 60
run: | run: |
cd $GITHUB_WORKSPACE && make && make ARCH=arm64 binary-arch-minimal && make ARCH=arm64 binary-arch cd $GITHUB_WORKSPACE && make && make ARCH=arm64 binary-arch-minimal && make ARCH=arm64 binary-arch
env: env:

View file

@ -68,6 +68,9 @@ test: check-skopeo $(NOTATION)
$(shell sudo chmod a=rwx /etc/containers/certs.d/127.0.0.1:8089/*.key) $(shell sudo chmod a=rwx /etc/containers/certs.d/127.0.0.1:8089/*.key)
go test -tags extended,containers_image_openpgp -v -trimpath -race -timeout 15m -cover -coverpkg ./... -coverprofile=coverage-extended.txt -covermode=atomic ./... go test -tags extended,containers_image_openpgp -v -trimpath -race -timeout 15m -cover -coverpkg ./... -coverprofile=coverage-extended.txt -covermode=atomic ./...
go test -tags minimal,containers_image_openpgp -v -trimpath -race -cover -coverpkg ./... -coverprofile=coverage-minimal.txt -covermode=atomic ./... go test -tags minimal,containers_image_openpgp -v -trimpath -race -cover -coverpkg ./... -coverprofile=coverage-minimal.txt -covermode=atomic ./...
# development-mode unit tests possibly using failure injection
go test -tags dev,extended,containers_image_openpgp -v -trimpath -race -timeout 15m -cover -coverpkg ./... -coverprofile=coverage-dev-extended.txt -covermode=atomic ./pkg/api/... ./pkg/test/...
go test -tags dev,minimal,containers_image_openpgp -v -trimpath -race -cover -coverpkg ./... -coverprofile=coverage-dev-minimal.txt -covermode=atomic ./pkg/api/... ./pkg/test/...
.PHONY: run-bench .PHONY: run-bench
run-bench: binary bench run-bench: binary bench
@ -92,8 +95,8 @@ $(NOTATION):
.PHONY: covhtml .PHONY: covhtml
covhtml: covhtml:
tail -n +2 coverage-minimal.txt > tmp.txt && mv tmp.txt coverage-minimal.txt go install github.com/wadey/gocovmerge@latest
cat coverage-extended.txt coverage-minimal.txt > coverage.txt gocovmerge coverage-minimal.txt coverage-extended.txt coverage-dev-minimal.txt coverage-dev-extended.txt > coverage.txt
go tool cover -html=coverage.txt -o coverage.html go tool cover -html=coverage.txt -o coverage.html
$(GOLINTER): $(GOLINTER):
@ -105,6 +108,8 @@ $(GOLINTER):
check: ./golangcilint.yaml $(GOLINTER) check: ./golangcilint.yaml $(GOLINTER)
$(GOLINTER) --config ./golangcilint.yaml run --enable-all --out-format=colored-line-number --build-tags minimal,containers_image_openpgp ./... $(GOLINTER) --config ./golangcilint.yaml run --enable-all --out-format=colored-line-number --build-tags minimal,containers_image_openpgp ./...
$(GOLINTER) --config ./golangcilint.yaml run --enable-all --out-format=colored-line-number --build-tags extended,containers_image_openpgp ./... $(GOLINTER) --config ./golangcilint.yaml run --enable-all --out-format=colored-line-number --build-tags extended,containers_image_openpgp ./...
$(GOLINTER) --config ./golangcilint.yaml run --enable-all --out-format=colored-line-number --build-tags dev,minimal,containers_image_openpgp ./...
$(GOLINTER) --config ./golangcilint.yaml run --enable-all --out-format=colored-line-number --build-tags dev,extended,containers_image_openpgp ./...
swagger/docs.go: swagger/docs.go:
swag -v || go install github.com/swaggo/swag/cmd/swag swag -v || go install github.com/swaggo/swag/cmd/swag

View file

@ -47,4 +47,5 @@ var (
ErrSyncMissingCatalog = errors.New("sync: couldn't fetch upstream registry's catalog") ErrSyncMissingCatalog = errors.New("sync: couldn't fetch upstream registry's catalog")
ErrMethodNotSupported = errors.New("storage: method not supported") ErrMethodNotSupported = errors.New("storage: method not supported")
ErrInvalidMetric = errors.New("metrics: invalid metric func") ErrInvalidMetric = errors.New("metrics: invalid metric func")
ErrInjected = errors.New("test: injected failure")
) )

View file

@ -43,6 +43,7 @@ import (
"zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/storage" "zotregistry.io/zot/pkg/storage"
"zotregistry.io/zot/pkg/test"
. "zotregistry.io/zot/test" . "zotregistry.io/zot/test"
) )
@ -2083,6 +2084,29 @@ func TestAuthorizationWithBasicAuth(t *testing.T) {
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = resty.R().SetBasicAuth(username, passphrase).
Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
Convey("Hard to reach cases", func() {
injected := test.InjectFailure(0)
// get tags with read access should get 200
conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions =
append(conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "read")
resp, err = resty.R().SetBasicAuth(username, passphrase).
Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
if injected {
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
} else {
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
}
})
// head blob should get 200 now // head blob should get 200 now
resp, err = resty.R().SetBasicAuth(username, passphrase). resp, err = resty.R().SetBasicAuth(username, passphrase).
Head(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) Head(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
@ -2838,6 +2862,24 @@ func TestParallelRequests(t *testing.T) {
assert.Equal(t, err, nil, "Error should be nil") assert.Equal(t, err, nil, "Error should be nil")
assert.Equal(t, headResponse.StatusCode(), http.StatusNotFound, "response status code should return 404") assert.Equal(t, headResponse.StatusCode(), http.StatusNotFound, "response status code should return 404")
Convey("Hard to reach cases", t, func() {
_ = test.InjectFailure(0)
headResponse, err := client.R().SetBasicAuth(username, passphrase).
Head(baseURL + "/v2/" + testcase.destImageName + "/manifests/test:1.0")
assert.Equal(t, err, nil, "Error should be nil")
assert.Equal(t, headResponse.StatusCode(), http.StatusNotFound, "response status code should return 404")
})
Convey("Hard to reach cases", t, func() {
_ = test.InjectFailure(1)
headResponse, err := client.R().SetBasicAuth(username, passphrase).
Head(baseURL + "/v2/" + testcase.destImageName + "/manifests/test:1.0")
assert.Equal(t, err, nil, "Error should be nil")
assert.Equal(t, headResponse.StatusCode(), http.StatusNotFound, "response status code should return 404")
})
getResponse, err := client.R().SetBasicAuth(username, passphrase). getResponse, err := client.R().SetBasicAuth(username, passphrase).
Get(baseURL + "/v2/" + testcase.destImageName + "/manifests/" + manifest) Get(baseURL + "/v2/" + testcase.destImageName + "/manifests/" + manifest)
assert.Equal(t, err, nil, "Error should be nil") assert.Equal(t, err, nil, "Error should be nil")

View file

@ -32,6 +32,7 @@ import (
ext "zotregistry.io/zot/pkg/extensions" ext "zotregistry.io/zot/pkg/extensions"
"zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage" "zotregistry.io/zot/pkg/storage"
"zotregistry.io/zot/pkg/test"
// as required by swaggo. // as required by swaggo.
_ "zotregistry.io/zot/swagger" _ "zotregistry.io/zot/swagger"
@ -165,7 +166,7 @@ func (rh *RouteHandler) ListTags(response http.ResponseWriter, request *http.Req
name, ok := vars["name"] name, ok := vars["name"]
if !ok || name == "" { if !test.Ok(ok) || name == "" {
response.WriteHeader(http.StatusNotFound) response.WriteHeader(http.StatusNotFound)
return return
@ -287,7 +288,7 @@ func (rh *RouteHandler) CheckManifest(response http.ResponseWriter, request *htt
vars := mux.Vars(request) vars := mux.Vars(request)
name, ok := vars["name"] name, ok := vars["name"]
if !ok || name == "" { if !test.Ok(ok) || name == "" {
response.WriteHeader(http.StatusNotFound) response.WriteHeader(http.StatusNotFound)
return return
@ -296,7 +297,7 @@ func (rh *RouteHandler) CheckManifest(response http.ResponseWriter, request *htt
imgStore := rh.getImageStore(name) imgStore := rh.getImageStore(name)
reference, ok := vars["reference"] reference, ok := vars["reference"]
if !ok || reference == "" { if !test.Ok(ok) || reference == "" {
WriteJSON(response, WriteJSON(response,
http.StatusNotFound, http.StatusNotFound,
NewErrorList(NewError(MANIFEST_INVALID, map[string]string{"reference": reference}))) NewErrorList(NewError(MANIFEST_INVALID, map[string]string{"reference": reference})))

View file

@ -670,10 +670,26 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = resty.R().Get(baseURL + "/v2/page0/tags/list?n= ")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = resty.R().Get(baseURL + "/v2/page0/tags/list?n=a")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = resty.R().Get(baseURL + "/v2/page0/tags/list?n=0") resp, err = resty.R().Get(baseURL + "/v2/page0/tags/list?n=0")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = resty.R().Get(baseURL + "/v2/page0/tags/list?n=0&last=100")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
resp, err = resty.R().Get(baseURL + "/v2/page0/tags/list?n=0&last=test:0.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = resty.R().Get(baseURL + "/v2/page0/tags/list?n=3") resp, err = resty.R().Get(baseURL + "/v2/page0/tags/list?n=3")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)

81
pkg/test/dev.go Normal file
View file

@ -0,0 +1,81 @@
//go:build dev
// +build dev
// This file should be linked only in **development** mode.
package test
import (
"sync"
zerr "zotregistry.io/zot/errors"
)
func Ok(ok bool) bool {
if !ok {
return ok
}
if injectedFailure() {
return false
}
return true
}
func Error(err error) error {
if err != nil {
return err
}
if injectedFailure() {
return zerr.ErrInjected
}
return nil
}
/**
*
* Failure injection infrastructure to cover hard-to-reach code paths.
*
**/
type inject struct {
skip int
enabled bool
}
//nolint:gochecknoglobals // only used by test code
var (
injlock sync.Mutex
injst = inject{}
)
func InjectFailure(skip int) bool {
injlock.Lock()
injst = inject{enabled: true, skip: skip}
injlock.Unlock()
return true
}
func injectedFailure() bool {
injlock.Lock()
defer injlock.Unlock()
if !injst.enabled {
return false
}
if injst.skip == 0 {
// disable the injection point
injst.enabled = false
return true
}
injst.skip--
return false
}

120
pkg/test/inject_test.go Normal file
View file

@ -0,0 +1,120 @@
//go:build dev
// +build dev
// This file should be linked only in **development** mode.
package test_test
import (
"errors"
"testing"
. "github.com/smartystreets/goconvey/convey"
"zotregistry.io/zot/pkg/test"
)
var (
errKey1 = errors.New("key1 not found")
errKey2 = errors.New("key2 not found")
errNotZero = errors.New("not zero")
errCall1 = errors.New("call1 error")
errCall2 = errors.New("call2 error")
)
func foo() error {
fmap := map[string]string{"key1": "val1", "key2": "val2"}
_, ok := fmap["key1"] // should never fail
if !test.Ok(ok) {
return errKey1
}
_, ok = fmap["key2"] // should never fail
if !test.Ok(ok) {
return errKey2
}
return nil
}
func errgen(i int) error {
if i != 0 {
return errNotZero
}
return nil
}
func bar() error {
err := errgen(0) // should never fail
if test.Error(err) != nil {
return errCall1
}
err = errgen(0) // should never fail
if test.Error(err) != nil {
return errCall2
}
return nil
}
func alwaysErr() error {
return errNotZero
}
func alwaysNotOk() bool {
return false
}
func TestInject(t *testing.T) {
Convey("Injected failure", t, func(c C) {
// should be success without injection
err := foo()
So(err, ShouldBeNil)
Convey("Check Ok", func() {
Convey("Without skipping", func() {
test.InjectFailure(0) // inject a failure
err := foo() // should be a failure
So(err, ShouldNotBeNil) // should be a failure
So(errors.Is(err, errKey1), ShouldBeTrue)
})
Convey("With skipping", func() {
test.InjectFailure(1) // inject a failure but skip first one
err := foo() // should be a failure
So(errors.Is(err, errKey1), ShouldBeFalse)
So(errors.Is(err, errKey2), ShouldBeTrue)
})
})
// should be success without injection
err = bar()
So(err, ShouldBeNil)
Convey("Check Err", func() {
Convey("Without skipping", func() {
test.InjectFailure(0) // inject a failure
err := bar() // should be a failure
So(err, ShouldNotBeNil) // should be a failure
So(errors.Is(err, errCall1), ShouldBeTrue)
})
Convey("With skipping", func() {
test.InjectFailure(1) // inject a failure but skip first one
err := bar() // should be a failure
So(errors.Is(err, errCall1), ShouldBeFalse)
So(errors.Is(err, errCall2), ShouldBeTrue)
})
})
})
Convey("Without injected failure", t, func(c C) {
err := alwaysErr()
So(test.Error(err), ShouldNotBeNil)
ok := alwaysNotOk()
So(test.Ok(ok), ShouldBeFalse)
})
}

20
pkg/test/prod.go Normal file
View file

@ -0,0 +1,20 @@
//go:build !dev
// +build !dev
package test
func Error(err error) error {
return err
}
func Ok(ok bool) bool {
return ok
}
/**
*
* Failure injection infrastructure to cover hard-to-reach code paths (nop in production).
*
**/
func InjectFailure(skip int) bool { return false }