├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── master.yml │ ├── pr.yml │ ├── push.yml │ ├── shellcheck.yml │ └── tag.yml ├── .gitignore ├── .godownloader.sh ├── .goreleaser.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── Tagfile ├── ape.sh ├── coverage_test.go ├── do_pastseed.go ├── docker-bake.hcl ├── fuzzymonkey.star ├── go.mod ├── go.sum ├── golint.sh ├── main.go ├── make_README.sh ├── pkg ├── as │ └── colors.go ├── code │ └── codes.go ├── cwid │ ├── pwd_id.go │ └── pwd_id_test.go ├── internal │ └── fm │ │ ├── chans_bidi.go │ │ ├── counterexample.go │ │ ├── fuzzymonkey.pb.go │ │ ├── fuzzymonkey.proto │ │ ├── fuzzymonkey_grpc.pb.go │ │ ├── fuzzymonkey_vtproto.pb.go │ │ └── proto.lock ├── modeler │ ├── caller.go │ ├── errors.go │ ├── interface.go │ ├── openapiv3 │ │ ├── caller_checks.go │ │ ├── caller_http.go │ │ ├── ir.go │ │ ├── ir_openapi3.go │ │ ├── ir_openapi3_test.go │ │ ├── lint.go │ │ ├── modeler.go │ │ ├── testdata │ │ │ └── specs │ │ │ │ └── openapi3 │ │ │ │ ├── v3.0.0_petstore-expanded.yaml │ │ │ │ ├── v3.0.0_petstore.json │ │ │ │ └── v3.0.0_petstore.yaml │ │ └── validator.go │ └── yaml_control_chars.go ├── progresser │ ├── bar │ │ ├── constants.go │ │ └── progresser.go │ ├── ci │ │ └── progresser.go │ ├── dots │ │ └── progresser.go │ └── interface.go ├── protovalue │ ├── from.go │ └── to.go ├── resetter │ ├── interface.go │ └── shell │ │ ├── cmds.go │ │ ├── lines_writer.go │ │ ├── shell.go │ │ └── singleton.go ├── runtime │ ├── call.go │ ├── call_before_request.go │ ├── ctx_modules.go │ ├── ctx_modules_test.go │ ├── ctxvalues │ │ └── ctxvalues.go │ ├── cx_head.go │ ├── cx_mod_after_response.go │ ├── cx_mod_before_request.go │ ├── cx_request_after_response.go │ ├── cx_request_before_request.go │ ├── cx_response_after_response.go │ ├── endpoints.go │ ├── exec.go │ ├── exec_init.go │ ├── exec_repl.go │ ├── exec_test.go │ ├── fmt.go │ ├── fmt_test.go │ ├── fmt_warnings_test.go │ ├── for_eachs.go │ ├── fuzz.go │ ├── inputs.go │ ├── lang_test.go │ ├── lint.go │ ├── module.go │ ├── module_test.go │ ├── monkey_check.go │ ├── monkey_check_cancellation_test.go │ ├── monkey_check_test.go │ ├── monkey_env.go │ ├── monkey_env_test.go │ ├── monkey_openapi3_test.go │ ├── monkey_shell_test.go │ ├── monkey_starfile_test.go │ ├── os_shower.go │ ├── progress.go │ ├── reset.go │ ├── runtime.go │ ├── starfiledata.go │ ├── startrick.go │ ├── startrick_test.go │ ├── summary.go │ ├── testdata │ │ └── jsonplaceholder.typicode.comv1.0.0_openapiv3.0.1_spec.yml │ └── user_error.go ├── starlarkclone │ ├── clone.go │ └── clone_test.go ├── starlarktruth │ ├── README.md │ ├── assert_that.go │ ├── attrs.go │ ├── bools_test.go │ ├── cmp_test.go │ ├── contains_test.go │ ├── doc.go │ ├── dupe_counter.go │ ├── dupe_counter_test.go │ ├── equal_test.go │ ├── errors.go │ ├── example_test.go │ ├── fail.go │ ├── func_test.go │ ├── is_in_test.go │ ├── is_of_test.go │ ├── iteritems.go │ ├── module.go │ ├── none_test.go │ ├── numbers_test.go │ ├── ordered_test.go │ ├── size_test.go │ ├── strings_test.go │ ├── t.go │ ├── truth_test.go │ ├── type_test.go │ └── unicodes_test.go ├── starlarkunpacked │ ├── strings.go │ ├── strings_test.go │ ├── unique_strings.go │ └── unique_strings_test.go ├── starlarkvalue │ ├── from_protovalue.go │ ├── to_protovalue.go │ └── value_test.go ├── tags │ ├── name_test.go │ ├── tags.go │ ├── unpack.go │ └── unpack_test.go └── update │ └── github.go └── usage.go /.dockerignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/go 2 | 3 | ### Go ### 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 17 | .glide/ 18 | 19 | # Golang project vendor packages which should be ignored 20 | vendor/ 21 | 22 | # End of https://www.gitignore.io/api/go 23 | 24 | /dist/ 25 | /monkey 26 | /monkey.test 27 | /monkey-* 28 | /*.cov 29 | /cov.out 30 | /.git 31 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: 'github-actions' 5 | open-pull-requests-limit: 10 6 | directory: '/' 7 | schedule: 8 | interval: 'daily' 9 | 10 | - package-ecosystem: 'docker' 11 | open-pull-requests-limit: 10 12 | directory: '/' 13 | schedule: 14 | interval: 'daily' 15 | 16 | - package-ecosystem: 'gomod' 17 | open-pull-requests-limit: 10 18 | directory: '/' 19 | schedule: 20 | interval: 'daily' 21 | 22 | # - package-ecosystem: 'cargo' 23 | # open-pull-requests-limit: 10 24 | # directory: '/' 25 | # schedule: 26 | # interval: 'daily' 27 | -------------------------------------------------------------------------------- /.github/workflows/master.yml: -------------------------------------------------------------------------------- 1 | name: master 2 | 3 | on: 4 | push: {} 5 | 6 | jobs: 7 | master: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Set lowercase image name 11 | run: echo IMAGE=ghcr.io/${SLUG,,} >>$GITHUB_ENV 12 | env: 13 | SLUG: ${{ github.repository }} 14 | 15 | - uses: actions/checkout@v3 16 | 17 | - name: Set TAG 18 | run: grep -F . Tagfile && echo TAG=$(cat Tagfile) >>$GITHUB_ENV 19 | 20 | - uses: docker/setup-buildx-action@v2.9.1 21 | 22 | - name: Log in to GitHub Container Registry 23 | uses: docker/login-action@v2.2.0 24 | with: 25 | registry: ghcr.io 26 | username: ${{ github.actor }} 27 | password: ${{ secrets.GITHUB_TOKEN }} 28 | 29 | - uses: docker/bake-action@v3.1.0 30 | with: 31 | files: ./docker-bake.hcl 32 | targets: goreleaser-dist 33 | # TODO: cache-to 34 | # set: | 35 | # *.cache-to=type=registry,ref=${{ env.IMAGE }}:goreleaser-dist,mode=max 36 | 37 | # TODO: cache-to 38 | # - name: If on master push image to GHCR 39 | # run: docker push ${{ env.IMAGE }}:goreleaser-dist 40 | 41 | - name: Test CLI 42 | run: | 43 | tar zxvf ./dist/monkey-Linux-x86_64.tar.gz -C . 44 | ./monkey -h | grep monkey 45 | ./monkey help | grep monkey 46 | ./monkey version 47 | ./monkey fmt 48 | [[ $(./monkey version | wc -l) = 1 ]] 49 | ./monkey version | grep -F $(cat Tagfile) 50 | ./monkey --version | grep -F $(cat Tagfile) 51 | 52 | - uses: ncipollo/release-action@v1.13.0 53 | if: github.ref == 'refs/heads/master' 54 | with: 55 | artifacts: ./dist/* 56 | commit: master # Required to push tag 57 | tag: ${{ env.TAG }} # Required to push tag 58 | token: ${{ secrets.GITHUB_TOKEN }} 59 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: push 2 | 3 | on: 4 | push: {} 5 | 6 | jobs: 7 | buildkit: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | 12 | - name: Ensure download script is in the right path 13 | run: test -f .godownloader.sh 14 | 15 | - name: Log in to GitHub Container Registry 16 | uses: docker/login-action@v2.2.0 17 | with: 18 | registry: ghcr.io 19 | username: ${{ github.actor }} 20 | password: ${{ secrets.GITHUB_TOKEN }} 21 | 22 | - run: DOCKER_BUILDKIT=1 docker build -o=. --platform=local . 23 | - run: ./monkey version 24 | # - run: echo LINUX=$(sha256sum ./monkey | awk '{print $1}') >>$GITHUB_ENV 25 | 26 | # # No docker on macos 27 | # - run: DOCKER_BUILDKIT=1 docker build --platform=darwin/amd64 -o . . 28 | # - run: echo DARWIN=$(sha256sum ./monkey | awk '{print $1}') >>$GITHUB_ENV 29 | # - run: [[ "$DARWIN" != "$LINUX" ]] 30 | 31 | # # invalid windows mount type: 'bind' 32 | # - run: DOCKER_BUILDKIT=1 docker build --platform=windows/amd64 -o . . 33 | # - run: echo WINDOWS=$(sha256sum ./monkey.exe | awk '{print $1}') >>$GITHUB_ENV 34 | # - run: [[ "$WINDOWS" != "$LINUX" ]] 35 | # - run: [[ "$WINDOWS" != "$DARWIN" ]] 36 | -------------------------------------------------------------------------------- /.github/workflows/shellcheck.yml: -------------------------------------------------------------------------------- 1 | name: shellcheck 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | shellcheck: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | # TODO - godownloader -r FuzzyMonkeyCo/monkey -o .godownloader.sh .goreleaser.yml 13 | - name: Run shellcheck 14 | uses: ludeeus/action-shellcheck@2.0.0 15 | with: 16 | check_together: 'yes' 17 | severity: error 18 | -------------------------------------------------------------------------------- /.github/workflows/tag.yml: -------------------------------------------------------------------------------- 1 | name: tag 2 | 3 | on: 4 | push: 5 | tags: ['*'] 6 | branches: ['!*'] 7 | 8 | # defaults: 9 | # run: 10 | # shell: bash 11 | 12 | jobs: 13 | 14 | ubuntu-latest: 15 | name: ubuntu-latest 16 | runs-on: ubuntu-latest 17 | steps: 18 | - run: '! which monkey' 19 | - uses: FuzzyMonkeyCo/setup-monkey@v1 20 | with: 21 | command: version 22 | github_token: ${{ secrets.github_token }} 23 | - uses: actions/checkout@v3 24 | - name: lint 25 | uses: FuzzyMonkeyCo/setup-monkey@v1 26 | with: 27 | command: lint 28 | github_token: ${{ secrets.github_token }} 29 | - run: | 30 | monkey fmt -w 31 | git --no-pager diff --exit-code 32 | 33 | macos-latest: 34 | name: macos-latest 35 | runs-on: macos-latest 36 | steps: 37 | - run: '! which monkey' 38 | - uses: FuzzyMonkeyCo/setup-monkey@v1 39 | with: 40 | command: version 41 | github_token: ${{ secrets.github_token }} 42 | - uses: actions/checkout@v3 43 | - name: lint 44 | uses: FuzzyMonkeyCo/setup-monkey@v1 45 | with: 46 | command: lint 47 | github_token: ${{ secrets.github_token }} 48 | - run: | 49 | monkey fmt -w 50 | git --no-pager diff --exit-code 51 | 52 | windows-latest: 53 | name: windows-latest 54 | runs-on: windows-latest 55 | steps: 56 | - run: '! which monkey' 57 | - uses: FuzzyMonkeyCo/setup-monkey@v1 58 | with: 59 | command: version 60 | github_token: ${{ secrets.github_token }} 61 | - uses: actions/checkout@v3 62 | - name: lint 63 | uses: FuzzyMonkeyCo/setup-monkey@v1 64 | with: 65 | command: lint 66 | github_token: ${{ secrets.github_token }} 67 | - run: | 68 | monkey fmt -w 69 | git --no-pager diff --exit-code 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/go 2 | 3 | ### Go ### 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 17 | .glide/ 18 | 19 | # Golang project vendor packages which should be ignored 20 | vendor/ 21 | 22 | # End of https://www.gitignore.io/api/go 23 | 24 | /dist/ 25 | /monkey 26 | /monkey.test 27 | /monkey-* 28 | /*.cov 29 | /cov.out 30 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: monkey 2 | builds: 3 | - env: 4 | - CGO_ENABLED=0 5 | - GRPCHOST=api.fuzzymonkey.co:7077 6 | goos: 7 | - darwin 8 | # TODO: - freebsd openbsd netbsd dragonfly 9 | - linux 10 | - windows 11 | goarch: 12 | - 386 13 | - amd64 14 | # TODO: - arm arm64 15 | # TODO: goarm: [6, 7] 16 | ignore: [] 17 | ldflags: > 18 | -s 19 | -w 20 | -extldflags "-static" 21 | -X main.binVersion={{.Env.CURRENT_TAG}} 22 | -X github.com/FuzzyMonkeyCo/monkey/pkg/internal/fm.grpcHost={{.Env.GRPCHOST}} 23 | 24 | archives: 25 | - format_overrides: 26 | - goos: windows 27 | format: zip 28 | # uname -m + uname -s compatible 29 | # TODO: arm64: aarch64 30 | name_template: >- 31 | {{ .ProjectName }}- 32 | {{- title .Os }}- 33 | {{- if eq .Arch "amd64" }}x86_64 34 | {{- else if eq .Arch "386" }}i386 35 | {{- else }}{{ .Arch }}{{ end }} 36 | files: 37 | - LICENSE 38 | - README.md 39 | 40 | checksum: 41 | name_template: checksums.sha256.txt 42 | algorithm: sha256 43 | 44 | signs: 45 | - artifacts: none 46 | 47 | # TODO: homebrew 48 | # TODO: snap 49 | # TODO: apt 50 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all update debug lint test ape 2 | 3 | SHELL = /bin/bash -o pipefail -eu 4 | 5 | EXE ?= monkey 6 | 7 | all: pkg/internal/fm/fuzzymonkey.pb.go make_README.sh README.md lint 8 | CGO_ENABLED=0 go build -o $(EXE) $(if $(wildcard $(EXE)),|| (rm $(EXE) && false)) 9 | cat .gitignore >.dockerignore && echo /.git >>.dockerignore 10 | ./$(EXE) fmt -w && ./make_README.sh 11 | 12 | update: 13 | go get -u -a -v ./... 14 | go mod tidy 15 | go mod verify 16 | 17 | latest: bindir ?= $$HOME/.local/bin 18 | latest: 19 | cat .godownloader.sh | BINDIR=$(bindir) sh -ex 20 | $(bindir)/$(EXE) --version 21 | 22 | devdeps: 23 | go install github.com/wadey/gocovmerge@latest 24 | go install github.com/kyoh86/richgo@latest 25 | 26 | pkg/internal/fm/fuzzymonkey.pb.go: pkg/internal/fm/fuzzymonkey.proto 27 | docker buildx bake ci-check--protolock ci-check--protoc #ci-check--protolock-force 28 | touch $@ 29 | 30 | lint: 31 | go fmt ./... 32 | ./golint.sh 33 | ! git grep -F log. pkg/cwid/ 34 | go vet ./... 35 | 36 | debug: all 37 | ./$(EXE) lint 38 | ./$(EXE) fuzz --exclude-tags=failing --progress=ci #dots #=ci #=bar 39 | 40 | distclean: clean 41 | $(if $(wildcard dist/),rm -r dist/) 42 | clean: 43 | $(if $(wildcard $(EXE)),rm $(EXE)) 44 | $(if $(wildcard $(EXE).test),rm $(EXE).test) 45 | $(if $(wildcard *.cov),rm *.cov) 46 | $(if $(wildcard cov.out),rm cov.out) 47 | 48 | test: all 49 | echo 42 | ./$(EXE) schema --validate-against=#/components/schemas/PostId 50 | ! ./$(EXE) exec repl <<<'assert that("malformed" != 42)' 51 | ./$(EXE) exec repl <<<'{"Hullo":41,"how\"":["do","0".isdigit(),{},[],set([13.37])],"you":"do"}' 52 | ./$(EXE) exec repl <<<'assert that("this").is_not_equal_to("that")' 53 | ./$(EXE) exec repl <<<'x = 1.0; print(str(x)); print(str(int(x)))' 54 | ! ./$(EXE) exec repl <<<'assert that(42).is_not_equal_to(42)' 55 | [[ 1 = "$$(./$(EXE) exec start 2>&1 | wc -l)" ]] 56 | [[ 6 = "$$(./$(EXE) exec reset 2>&1 | wc -l)" ]] 57 | [[ 1 = "$$(./$(EXE) exec stop 2>&1 | wc -l)" ]] 58 | richgo test -race -covermode=atomic ./... 59 | 60 | ci: 61 | docker buildx bake ci-checks 62 | 63 | ape: $(EXE).test 64 | ./ape.sh --version 65 | gocovmerge *.cov >cov.out 66 | go tool cover -func cov.out 67 | rm 0.cov cov.out 68 | 69 | # Thanks https://blog.cloudflare.com/go-coverage-with-external-tests 70 | $(EXE).test: lint 71 | $(if $(wildcard *.cov),rm *.cov) 72 | go test -covermode=count -c 73 | 74 | ape-cleanup: 75 | gocovmerge *.cov >cov.out 76 | go tool cover -func cov.out 77 | go tool cover -html cov.out 78 | $(if $(wildcard *.cov),rm *.cov) 79 | -------------------------------------------------------------------------------- /Tagfile: -------------------------------------------------------------------------------- 1 | 0.52.1 2 | -------------------------------------------------------------------------------- /ape.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | set -o pipefail 4 | 5 | dir="$(dirname "$0")" 6 | err=/tmp/.monkey_$RANDOM.code 7 | 8 | i=0 9 | if ls "$dir"/*.cov >/dev/null 2>&1; then 10 | biggest=$(find "$dir" -name '*.cov' | sort -Vr | head -n1) 11 | i=$(basename "$biggest" .cov) 12 | ((i+=1)) 13 | fi 14 | 15 | MONKEY_CODEFILE=$err MONKEY_ARGS="$*" "$dir"/monkey.test \ 16 | -test.coverprofile="$dir/$i".cov \ 17 | -test.run=^TestCov$ 18 | 19 | code=$(cat $err) 20 | rm $err 21 | # shellcheck disable=SC2086 22 | exit $code 23 | -------------------------------------------------------------------------------- /coverage_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestCov(t *testing.T) { 14 | pathErrCode := os.Getenv("MONKEY_CODEFILE") 15 | if pathErrCode == "" { 16 | t.SkipNow() 17 | } 18 | 19 | oldArgs := os.Args 20 | defer func() { os.Args = oldArgs }() 21 | 22 | args := strings.Split(os.Getenv("MONKEY_ARGS"), " ") 23 | os.Args = append([]string{"./" + binName + ".test"}, args...) 24 | 25 | code := actualMain() 26 | 27 | fmt.Println("EXIT", code) 28 | data := []byte(strconv.Itoa(code)) 29 | err := os.WriteFile(pathErrCode, data, 0644) 30 | require.NoError(t, err) 31 | } 32 | -------------------------------------------------------------------------------- /do_pastseed.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "regexp" 8 | 9 | "github.com/FuzzyMonkeyCo/monkey/pkg/code" 10 | "github.com/FuzzyMonkeyCo/monkey/pkg/cwid" 11 | rt "github.com/FuzzyMonkeyCo/monkey/pkg/runtime" 12 | ) 13 | 14 | var rePastseed = regexp.MustCompile(rt.PastSeedMagic + ` ([^\s]+)`) 15 | 16 | // Looks in the logs for the youngest seed that triggered a bug 17 | // Only ever prints best seed and a newline character 18 | // so it can be used as --seed=$(monkey pastseed) 19 | func doPastseed(starfile string) int { 20 | for offset := uint64(1); true; offset++ { 21 | var seed string 22 | ret := -1 23 | func() { 24 | if err := cwid.MakePwdID(binName, starfile, offset); err != nil { 25 | // Fails silently 26 | ret = code.Failed 27 | return 28 | } 29 | 30 | fn := cwid.LogFile() 31 | f, err := os.Open(fn) 32 | if err != nil { 33 | ret = code.Failed 34 | return 35 | } 36 | defer f.Close() 37 | 38 | s := bufio.NewScanner(f) 39 | for s.Scan() { 40 | matches := rePastseed.FindStringSubmatch(s.Text()) 41 | if len(matches) != 0 && matches[1] != "" { 42 | seed = matches[1] 43 | } 44 | } 45 | }() 46 | if ret != -1 { 47 | return ret 48 | } 49 | if seed != "" { 50 | fmt.Println(seed) 51 | return code.OK 52 | } 53 | } 54 | return code.Failed 55 | } 56 | -------------------------------------------------------------------------------- /docker-bake.hcl: -------------------------------------------------------------------------------- 1 | ## Groups 2 | 3 | group "default" { 4 | targets = [ 5 | "binaries", 6 | ] 7 | } 8 | 9 | group "ci-checks" { 10 | targets = [ 11 | "ci-check--lint", 12 | "ci-check--mod", 13 | "ci-check--test", 14 | "ci-check--protoc", 15 | "ci-check--protolock", 16 | # "ci-check--protolock-force", 17 | ] 18 | } 19 | 20 | ## Targets 21 | 22 | target "dockerfile" { 23 | dockerfile = "Dockerfile" 24 | args = { 25 | "BUILDKIT_INLINE_CACHE" = "1" 26 | } 27 | } 28 | 29 | target "binaries" { 30 | inherits = ["dockerfile"] 31 | target = "binaries" 32 | output = ["."] 33 | # cache-from = ["type=registry,ref=ghcr.io/fuzzymonkeyco/monkey:binaries"] 34 | # TODO: cache-to 35 | # error: cache export feature is currently not supported for docker driver 36 | # cache-to = ["type=registry,ref=ghcr.io/fuzzymonkeyco/monkey:binaries,mode=max"] 37 | } 38 | 39 | target "goreleaser-dist" { 40 | inherits = ["dockerfile"] 41 | target = "goreleaser-dist" 42 | output = ["./dist"] 43 | # cache-from = ["type=registry,ref=ghcr.io/fuzzymonkeyco/monkey:goreleaser-dist"] 44 | # cache-to = ["type=registry,ref=ghcr.io/fuzzymonkeyco/monkey:goreleaser-dist,mode=max"] 45 | } 46 | 47 | target "ci-check--lint" { 48 | inherits = ["dockerfile"] 49 | target = "ci-check--lint" 50 | # cache-from = ["type=registry,ref=ghcr.io/fuzzymonkeyco/monkey:ci-check--lint"] 51 | # cache-to = ["type=registry,ref=ghcr.io/fuzzymonkeyco/monkey:ci-check--lint,mode=max"] 52 | } 53 | 54 | target "ci-check--mod" { 55 | inherits = ["dockerfile"] 56 | target = "ci-check--mod" 57 | # cache-from = ["type=registry,ref=ghcr.io/fuzzymonkeyco/monkey:ci-check--mod"] 58 | # cache-to = ["type=registry,ref=ghcr.io/fuzzymonkeyco/monkey:ci-check--mod,mode=max"] 59 | } 60 | 61 | target "ci-check--test" { 62 | inherits = ["dockerfile"] 63 | target = "ci-check--test" 64 | # cache-from = ["type=registry,ref=ghcr.io/fuzzymonkeyco/monkey:ci-check--test"] 65 | # cache-to = ["type=registry,ref=ghcr.io/fuzzymonkeyco/monkey:ci-check--test,mode=max"] 66 | } 67 | 68 | target "ci-check--protolock" { 69 | inherits = ["dockerfile"] 70 | target = "ci-check--protolock" 71 | # cache-from = ["type=registry,ref=ghcr.io/fuzzymonkeyco/monkey:ci-check--protolock"] 72 | # cache-to = ["type=registry,ref=ghcr.io/fuzzymonkeyco/monkey:ci-check--protolock,mode=max"] 73 | } 74 | 75 | target "ci-check--protolock-force" { 76 | inherits = ["dockerfile"] 77 | target = "ci-check--protolock" 78 | args = { 79 | "FORCE" = "1" 80 | } 81 | output = ["./pkg/internal/fm/"] 82 | # cache-from = ["type=registry,ref=ghcr.io/fuzzymonkeyco/monkey:ci-check--protolock"] 83 | # cache-to = ["type=registry,ref=ghcr.io/fuzzymonkeyco/monkey:ci-check--protolock,mode=max"] 84 | } 85 | 86 | target "ci-check--protoc" { 87 | inherits = ["dockerfile"] 88 | target = "ci-check--protoc" 89 | output = ["./pkg/internal/fm/"] 90 | # cache-from = ["type=registry,ref=ghcr.io/fuzzymonkeyco/monkey:ci-check--protoc"] 91 | # cache-to = ["type=registry,ref=ghcr.io/fuzzymonkeyco/monkey:ci-check--protoc,mode=max"] 92 | } 93 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/FuzzyMonkeyCo/monkey 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/alecthomas/chroma/v2 v2.14.0 7 | github.com/bazelbuild/buildtools v0.0.0-20240918101019-be1c24cc9a44 8 | github.com/chzyer/readline v1.5.1 9 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 10 | github.com/fatih/color v1.18.0 11 | github.com/getkin/kin-openapi v0.128.0 12 | github.com/google/gnostic v0.7.0 13 | github.com/google/uuid v1.6.0 14 | github.com/hashicorp/logutils v1.0.0 15 | github.com/mitchellh/mapstructure v1.5.0 16 | github.com/pmezard/go-difflib v1.0.0 17 | github.com/stretchr/testify v1.9.0 18 | github.com/superhawk610/bar v0.0.2 19 | github.com/xeipuuv/gojsonschema v1.2.0 20 | go.starlark.net v0.0.0-20240925182052-1207426daebd 21 | golang.org/x/sync v0.8.0 22 | google.golang.org/grpc v1.67.1 23 | google.golang.org/protobuf v1.35.1 24 | ) 25 | 26 | require ( 27 | github.com/davecgh/go-spew v1.1.1 // indirect 28 | github.com/dlclark/regexp2 v1.11.4 // indirect 29 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 30 | github.com/go-openapi/swag v0.23.0 // indirect 31 | github.com/golang/protobuf v1.5.4 // indirect 32 | github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect 33 | github.com/invopop/yaml v0.3.1 // indirect 34 | github.com/josharian/intern v1.0.0 // indirect 35 | github.com/mailru/easyjson v0.7.7 // indirect 36 | github.com/mattn/go-colorable v0.1.13 // indirect 37 | github.com/mattn/go-isatty v0.0.20 // indirect 38 | github.com/mattn/go-tty v0.0.7 // indirect 39 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 40 | github.com/perimeterx/marshmallow v1.1.5 // indirect 41 | github.com/superhawk610/terminal v0.1.0 // indirect 42 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 43 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 44 | golang.org/x/net v0.30.0 // indirect 45 | golang.org/x/sys v0.26.0 // indirect 46 | golang.org/x/text v0.19.0 // indirect 47 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect 48 | gopkg.in/yaml.v3 v3.0.1 // indirect 49 | ) 50 | -------------------------------------------------------------------------------- /golint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -u 2 | 3 | # golint.sh: farther than golint 4 | # go fmt first 5 | 6 | [ -f go.mod ] || exit 0 7 | 8 | E() { 9 | code=$? 10 | [ $code -ne 0 ] && echo " $*" 11 | errors=$(( errors + code )) 12 | } 13 | 14 | if ! command -v ag >/dev/null 2>&1; then 15 | echo Skipping: silver searcher unavailable 16 | exit 0 17 | fi 18 | 19 | g() { 20 | ag --ignore '*.pb.go' --ignore 'migrations/bindata.go' "$@" 21 | } 22 | 23 | errors=0 24 | 25 | ! g 'return\s+}\s+return\s+}' 26 | E first return can be dropped 27 | 28 | ! g '^\s+fmt\.[^\n]+\s+log\.Print' 29 | E log first 30 | 31 | ! g ', err = [^;\n]+\s+if err ' 32 | E if can be inlined 33 | 34 | ! g '^\s+err :?= [^;\n]+\s+if err ' 35 | E if can be inlined 36 | 37 | ! g '^\s+fmt\.Errorf' 38 | E unused value 39 | 40 | ! g --ignore '*.pb.go' 'fmt\.Errorf."[^%\n]*"' 41 | E prefer errors.New 42 | 43 | ! g '([^ ]+) != nil\s+{\s+for [^=]+ := range \1' 44 | E unnecessary nil check around range 45 | 46 | # ! g 'make\(\[\][^],]+, [^0]' 47 | # E you meant capacity 48 | 49 | ! g '\.Println\((\"[^"]+ \")+' 50 | E unnecessary trailing space 51 | 52 | ! g 'log.Printf\([^,]+\\n.\,' 53 | E superfluous newline 54 | 55 | ! g '[^A-Za-z0-9_\]]byte\("\\?[^"]"\)|'"[^A-Za-z0-9_\\]]byte\\('\\\\?[^']'\\)" 56 | E that\'s just single quotes with extra steps 57 | 58 | exit $errors 59 | -------------------------------------------------------------------------------- /make_README.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | # Generate README.md 4 | 5 | [[ 1 -ne $(git status --porcelain -- README.md | grep -Ec '^.[^ ]') ]] || exit 0 6 | 7 | beg_usage=$(grep -n '```' -- README.md | head -n1 | cut -d: -f1) 8 | end_usage=$(grep -n '```' -- README.md | head -n2 | tail -n1 | cut -d: -f1) 9 | cat <(head -n "$beg_usage" README.md) <(./monkey -h) <(tail -n +"$end_usage" README.md) >_ && mv _ README.md 10 | 11 | beg_example=$(grep -n '```' -- README.md | tail -n2 | head -n1 | cut -d: -f1) 12 | end_example=$(grep -n '```' -- README.md | tail -n2 | tail -n1 | cut -d: -f1) 13 | cat <(head -n "$beg_example" README.md) <(cat fuzzymonkey.star) <(tail -n +"$end_example" README.md) >_ && mv _ README.md 14 | 15 | if 16 | [[ "${CI:-}" != 'true' ]] && 17 | git --no-pager diff -- README.md | grep '[-]monkey M.m.p go' >/dev/null && 18 | git --no-pager diff -- README.md | grep '[+]monkey M.m.p go' >/dev/null && 19 | [[ $(git --no-pager diff -- README.md | wc -l) -eq 13 ]] 20 | then 21 | git checkout -- README.md 22 | fi 23 | -------------------------------------------------------------------------------- /pkg/as/colors.go: -------------------------------------------------------------------------------- 1 | package as 2 | 3 | import "github.com/fatih/color" 4 | 5 | var ( 6 | // ColorERR colors for errors and fatal messages 7 | ColorERR = color.New(color.FgRed) 8 | // ColorWRN colors for warnings and special messages 9 | ColorWRN = color.New(color.FgYellow) 10 | // ColorNFO colors for headlines and topical messages 11 | ColorNFO = color.New(color.Bold) 12 | // ColorOK colors for valid and successful messages 13 | ColorOK = color.New(color.FgGreen) 14 | ) 15 | -------------------------------------------------------------------------------- /pkg/code/codes.go: -------------------------------------------------------------------------------- 1 | package code 2 | 3 | const ( 4 | // OK is the success code 5 | OK = 0 6 | // Failed represents any non-specific failure 7 | Failed = 1 8 | // FailedLint means something happened during linting 9 | FailedLint = 2 10 | // FailedUpdate means `binName` executable could not be upgraded 11 | FailedUpdate = 3 12 | // FailedFmt means the formatting operation failed 13 | FailedFmt = 4 14 | // FailedFuzz means the fuzzing process found a bug with the system under test (SUT) 15 | FailedFuzz = 6 16 | // FailedExec means a user command (start, reset, stop) experienced failure 17 | FailedExec = 7 18 | // FailedSchema means the given payload does not validate provided schema 19 | FailedSchema = 9 20 | // FailedConnecting means `binName` is too old, please update 21 | FailedConnecting = 11 22 | ) 23 | -------------------------------------------------------------------------------- /pkg/cwid/pwd_id.go: -------------------------------------------------------------------------------- 1 | package cwid 2 | 3 | import ( 4 | "fmt" 5 | "hash/fnv" 6 | "os" 7 | "path" 8 | "path/filepath" 9 | "sort" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | const pwdIDDigits = 20 15 | 16 | var pwdID string 17 | 18 | // LogFile points to a usable regular file after a call to MakePwdID() 19 | func LogFile() string { return Prefixed() + ".log" } 20 | 21 | // Prefixed points to a usable regular file prefix after a call to MakePwdID() 22 | func Prefixed() string { return pwdID + "_" } 23 | 24 | // MakePwdID looks for a usable temporary path for var run logfiles 25 | func MakePwdID(name, starfile string, offset uint64) (err error) { 26 | var fi os.FileInfo 27 | if fi, err = os.Lstat(starfile); err != nil { 28 | return 29 | } 30 | if fi.Mode()&os.ModeSymlink == os.ModeSymlink { 31 | err = fmt.Errorf("is a symlink: %q", starfile) 32 | return 33 | } 34 | 35 | if path.Clean(starfile) != path.Base(starfile) { 36 | err = fmt.Errorf("must be in current directory: %q", starfile) 37 | return 38 | } 39 | 40 | var cwd string 41 | if cwd, err = os.Getwd(); err != nil { 42 | return 43 | } 44 | if cwd, err = filepath.EvalSymlinks(cwd); err != nil { 45 | return 46 | } 47 | 48 | h := fnv.New64a() 49 | if _, err = h.Write([]byte(cwd)); err != nil { 50 | return 51 | } 52 | if _, err = h.Write([]byte("/")); err != nil { 53 | return 54 | } 55 | if _, err = h.Write([]byte(path.Base(starfile))); err != nil { 56 | return 57 | } 58 | id := fmt.Sprintf("%d", h.Sum64()) 59 | 60 | tmp := os.TempDir() 61 | if err = os.MkdirAll(tmp, 0700); err != nil { 62 | return 63 | } 64 | 65 | prefix := path.Join(tmp, "."+name+"_"+id) 66 | 67 | var slot string 68 | if slot, err = findIDSlot(prefix, offset); err != nil { 69 | return 70 | } 71 | 72 | pwdID = prefix + "_" + slot 73 | return 74 | } 75 | 76 | func findIDSlot(prefix string, offset uint64) (slot string, err error) { 77 | prefixPattern := prefix + "_" 78 | pattern := prefixPattern + strings.Repeat("?", pwdIDDigits) + "_*" 79 | var paths []string 80 | if paths, err = filepath.Glob(pattern); err != nil { 81 | return 82 | } 83 | 84 | padder := func(n uint64) string { 85 | return fmt.Sprintf("%0"+strconv.Itoa(pwdIDDigits)+"d", n) 86 | } 87 | 88 | prefixLen := len(prefixPattern) 89 | nums := []string{padder(0)} 90 | for _, path := range paths { 91 | nums = append(nums, path[prefixLen:prefixLen+pwdIDDigits]) 92 | } 93 | sort.Strings(nums) 94 | 95 | biggest := nums[len(nums)-1] 96 | var big uint64 97 | if big, err = strconv.ParseUint(biggest, 10, 32); err != nil { 98 | return 99 | } 100 | 101 | slot = padder(big + 1 - offset) 102 | return 103 | } 104 | -------------------------------------------------------------------------------- /pkg/cwid/pwd_id_test.go: -------------------------------------------------------------------------------- 1 | package cwid 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func filesContain(t *testing.T, pattern string) { 13 | t.Helper() 14 | 15 | require.Contains(t, LogFile(), pattern) 16 | require.Contains(t, Prefixed(), pattern) 17 | } 18 | 19 | func TestPwdID(t *testing.T) { 20 | if os.Getenv("TESTPWDID") != "1" { 21 | t.Skipf("Run these tests under docker buildx") 22 | } 23 | 24 | err := copyFile("../../fuzzymonkey.star", "fuzzymonkey.star") 25 | require.NoError(t, err) 26 | defer os.Remove("fuzzymonkey.star") 27 | err = copyFile("../../README.md", "README.md") 28 | require.NoError(t, err) 29 | defer os.Remove("README.md") 30 | 31 | const name, starfile, offset = "monkeh", "fuzzymonkey.star", 0 32 | 33 | err = MakePwdID(name, starfile, offset) 34 | require.NoError(t, err) 35 | filesContain(t, ".monkeh_") 36 | filesContain(t, "_12404825836092798244_") 37 | filesContain(t, "_00000000000000000001_") 38 | 39 | // PwdID changes with offset 40 | 41 | func(filename string) { 42 | newfilename := strings.ReplaceAll(filename, "_00000000000000000001_", "_00000000000000000002_") 43 | err := os.WriteFile(newfilename, nil, 0644) 44 | require.NoError(t, err) 45 | defer os.Remove(newfilename) 46 | 47 | require.NotEqual(t, 1, offset) 48 | err = MakePwdID(name, starfile, 1) 49 | require.NoError(t, err) 50 | filesContain(t, ".monkeh_") 51 | filesContain(t, "_12404825836092798244_") 52 | filesContain(t, "_00000000000000000002") 53 | }(LogFile()) 54 | 55 | // PwdID changes with starfile 56 | 57 | require.NotEqual(t, "README.md", starfile) 58 | err = MakePwdID(name, "README.md", offset) 59 | require.NoError(t, err) 60 | filesContain(t, ".monkeh_") 61 | filesContain(t, "_2078280350767222314_") 62 | filesContain(t, "_00000000000000000001") 63 | 64 | // No symlinks allowed 65 | func() { 66 | 67 | err := os.Symlink(starfile, "fm.star") 68 | require.NoError(t, err) 69 | defer os.Remove("fm.star") 70 | 71 | require.NotEqual(t, "fm.star", starfile) 72 | err = MakePwdID(name, "./fm.star", offset) 73 | require.EqualError(t, err, `is a symlink: "./fm.star"`) 74 | 75 | }() 76 | 77 | // No lurking outside the nest 78 | 79 | require.NotEqual(t, "../../main.go", starfile) 80 | err = MakePwdID(name, "../../main.go", offset) 81 | require.EqualError(t, err, `must be in current directory: "../../main.go"`) 82 | } 83 | 84 | func copyFile(src, dst string) error { 85 | in, err := os.Open(src) 86 | if err != nil { 87 | return err 88 | } 89 | defer in.Close() 90 | 91 | out, err := os.Create(dst) 92 | if err != nil { 93 | return err 94 | } 95 | defer out.Close() 96 | 97 | if _, err = io.Copy(out, in); err != nil { 98 | return err 99 | } 100 | return out.Close() 101 | } 102 | -------------------------------------------------------------------------------- /pkg/internal/fm/chans_bidi.go: -------------------------------------------------------------------------------- 1 | package fm 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "log" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "google.golang.org/grpc" 13 | "google.golang.org/grpc/encoding/gzip" 14 | ) 15 | 16 | const ( 17 | dialTimeout = 4 * time.Second 18 | rcvTimeout = 10 * time.Second 19 | sndTimeout = 10 * time.Second 20 | ) 21 | 22 | // grpcHost is not const so its value can be set with -ldflags 23 | var grpcHost = "do.dev.fuzzymonkey.co:7077" 24 | 25 | // BiDier does bidirectional streaming gRPC 26 | type BiDier interface { 27 | Send(ctx context.Context, msg *Clt) (err error) 28 | Receive(ctx context.Context) (msg *Srv, err error) 29 | Close() 30 | } 31 | 32 | // ChBiDi wraps a Clt<->Srv bidirectional gRPC channel 33 | type ChBiDi struct { 34 | clt FuzzyMonkey_DoClient 35 | close func() 36 | 37 | rcvErr chan error 38 | rcvMsg chan *Srv 39 | 40 | sndErr chan error 41 | sndMsg chan *Clt 42 | } 43 | 44 | var _ BiDier = (*ChBiDi)(nil) 45 | 46 | // NewChBiDi dials server & returns a usable ChBiDi 47 | func NewChBiDi(ctx context.Context) (*ChBiDi, error) { 48 | log.Println("[NFO] dialing", grpcHost) 49 | 50 | options := []grpc.DialOption{ 51 | grpc.WithBlock(), 52 | grpc.WithTimeout(dialTimeout), 53 | grpc.WithDefaultCallOptions( 54 | grpc.UseCompressor(gzip.Name), 55 | grpc.MaxCallRecvMsgSize(10*4194304), 56 | ), 57 | } 58 | if !strings.HasSuffix(grpcHost, ":443") { 59 | options = append(options, grpc.WithInsecure()) 60 | } 61 | conn, err := grpc.DialContext(ctx, grpcHost, options...) 62 | if err != nil { 63 | if err == context.DeadlineExceeded { 64 | err = errors.New("unreachable fuzzymonkey.co server") 65 | } 66 | log.Println("[ERR]", err) 67 | return nil, err 68 | } 69 | 70 | cbd := &ChBiDi{} 71 | if cbd.clt, err = NewFuzzyMonkeyClient(conn).Do(ctx); err != nil { 72 | log.Println("[ERR]", err) 73 | return nil, err 74 | } 75 | 76 | ctx, cancel := context.WithCancel(ctx) 77 | 78 | cbd.rcvMsg = make(chan *Srv) 79 | cbd.rcvErr = make(chan error) 80 | go func() { 81 | defer log.Println("[NFO] terminated rcv-er of Srv") 82 | for { 83 | select { 84 | case <-ctx.Done(): 85 | err := ctx.Err() 86 | log.Println("[ERR]", err) 87 | cbd.rcvErr <- err 88 | return 89 | default: 90 | msg, err := cbd.clt.Recv() 91 | if err != nil { 92 | log.Printf("[DBG] received err: %v", err) 93 | cbd.rcvErr <- err 94 | return 95 | } 96 | log.Printf("[DBG] received %T", msg.GetMsg()) 97 | cbd.rcvMsg <- msg 98 | } 99 | } 100 | }() 101 | 102 | cbd.sndMsg = make(chan *Clt) 103 | cbd.sndErr = make(chan error) 104 | go func() { 105 | defer log.Println("[NFO] terminated snd-er of Clt") 106 | for { 107 | select { 108 | case <-ctx.Done(): 109 | err := ctx.Err() 110 | log.Println("[ERR]", err) 111 | cbd.sndErr <- err 112 | return 113 | case r, ok := <-cbd.sndMsg: 114 | if !ok { 115 | log.Println("[DBG] sndMsg is closed!") 116 | return 117 | } 118 | log.Printf("[DBG] sending %T...", r.GetMsg()) 119 | err := cbd.clt.Send(r) 120 | log.Printf("[DBG] sent! (err: %v)", err) 121 | if err == io.EOF { 122 | // This is usually the reason & helps provide a better message 123 | err = context.DeadlineExceeded 124 | } 125 | cbd.sndErr <- err 126 | } 127 | } 128 | }() 129 | 130 | cbd.close = func() { 131 | log.Println("[NFO] Close()-ing ChBiDi...") 132 | cancel() 133 | if err := conn.Close(); err != nil { 134 | log.Println("[ERR]", err) 135 | } 136 | } 137 | 138 | return cbd, nil 139 | } 140 | 141 | // Close ends the connection 142 | func (cbd *ChBiDi) Close() { 143 | cbd.close() 144 | } 145 | 146 | // Receive returns a Srv message and an error 147 | func (cbd *ChBiDi) Receive(ctx context.Context) (msg *Srv, err error) { 148 | select { 149 | case <-ctx.Done(): 150 | err = ctx.Err() 151 | return 152 | default: 153 | } 154 | select { 155 | case err = <-cbd.rcvErr: 156 | case msg = <-cbd.rcvMsg: 157 | case <-time.After(rcvTimeout): 158 | err = os.ErrDeadlineExceeded 159 | } 160 | return 161 | } 162 | 163 | // Send sends a Clt message, returning an error 164 | func (cbd *ChBiDi) Send(ctx context.Context, msg *Clt) (err error) { 165 | if err = ctx.Err(); err != nil { 166 | return 167 | } 168 | cbd.sndMsg <- msg 169 | select { 170 | case <-time.After(sndTimeout): 171 | err = os.ErrDeadlineExceeded 172 | case err = <-cbd.rcvErr: 173 | case err = <-cbd.sndErr: // Also look for remote hangups 174 | } 175 | return 176 | } 177 | -------------------------------------------------------------------------------- /pkg/internal/fm/counterexample.go: -------------------------------------------------------------------------------- 1 | package fm 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // CLIString is used to display quick data on a CounterexampleItem 9 | func (ceI *Srv_FuzzingResult_CounterexampleItem) CLIString() (s string) { 10 | switch x := ceI.GetCallRequest().GetInput().(type) { 11 | case *Clt_CallRequestRaw_Input_HttpRequest_: 12 | req := ceI.GetCallRequest().GetHttpRequest() 13 | rep := ceI.GetCallResponse().GetHttpResponse() 14 | 15 | var b strings.Builder 16 | indent := func() { b.WriteString(" \\\n ") } 17 | b.WriteString("curl -#fsSL -X ") 18 | b.WriteString(req.GetMethod()) 19 | indent() 20 | for _, kvs := range req.GetHeaders() { 21 | values := strings.Join(kvs.GetValues(), ",") 22 | switch key := kvs.GetKey(); key { 23 | case "User-Agent": 24 | b.WriteString("-A ") 25 | b.WriteString(shellEscape(values)) 26 | default: 27 | b.WriteString("-H ") 28 | b.WriteString(shellEscape(fmt.Sprintf("%s: %s", key, values))) 29 | } 30 | indent() 31 | } 32 | if body := req.GetBody(); len(body) != 0 { 33 | b.WriteString("-d ") 34 | b.WriteString(shellEscape(string(body))) 35 | indent() 36 | } 37 | b.WriteString(shellEscape(req.GetUrl())) 38 | b.WriteString("\n") 39 | b.WriteString("# ") 40 | b.WriteString(rep.GetReason()) 41 | s = b.String() 42 | default: 43 | panic(fmt.Sprintf("unhandled CounterexampleItem %T %+v", x, ceI)) 44 | } 45 | return 46 | } 47 | 48 | func shellEscape(s string) string { 49 | return `'` + strings.ReplaceAll(s, `'`, `'\''`) + `'` 50 | } 51 | -------------------------------------------------------------------------------- /pkg/internal/fm/fuzzymonkey_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.3.0 4 | // - protoc v3.21.12 5 | // source: fuzzymonkey.proto 6 | 7 | package fm 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.32.0 or later. 19 | const _ = grpc.SupportPackageIsVersion7 20 | 21 | const ( 22 | FuzzyMonkey_Do_FullMethodName = "/fm.FuzzyMonkey/Do" 23 | ) 24 | 25 | // FuzzyMonkeyClient is the client API for FuzzyMonkey service. 26 | // 27 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 28 | type FuzzyMonkeyClient interface { 29 | Do(ctx context.Context, opts ...grpc.CallOption) (FuzzyMonkey_DoClient, error) 30 | } 31 | 32 | type fuzzyMonkeyClient struct { 33 | cc grpc.ClientConnInterface 34 | } 35 | 36 | func NewFuzzyMonkeyClient(cc grpc.ClientConnInterface) FuzzyMonkeyClient { 37 | return &fuzzyMonkeyClient{cc} 38 | } 39 | 40 | func (c *fuzzyMonkeyClient) Do(ctx context.Context, opts ...grpc.CallOption) (FuzzyMonkey_DoClient, error) { 41 | stream, err := c.cc.NewStream(ctx, &FuzzyMonkey_ServiceDesc.Streams[0], FuzzyMonkey_Do_FullMethodName, opts...) 42 | if err != nil { 43 | return nil, err 44 | } 45 | x := &fuzzyMonkeyDoClient{stream} 46 | return x, nil 47 | } 48 | 49 | type FuzzyMonkey_DoClient interface { 50 | Send(*Clt) error 51 | Recv() (*Srv, error) 52 | grpc.ClientStream 53 | } 54 | 55 | type fuzzyMonkeyDoClient struct { 56 | grpc.ClientStream 57 | } 58 | 59 | func (x *fuzzyMonkeyDoClient) Send(m *Clt) error { 60 | return x.ClientStream.SendMsg(m) 61 | } 62 | 63 | func (x *fuzzyMonkeyDoClient) Recv() (*Srv, error) { 64 | m := new(Srv) 65 | if err := x.ClientStream.RecvMsg(m); err != nil { 66 | return nil, err 67 | } 68 | return m, nil 69 | } 70 | 71 | // FuzzyMonkeyServer is the server API for FuzzyMonkey service. 72 | // All implementations must embed UnimplementedFuzzyMonkeyServer 73 | // for forward compatibility 74 | type FuzzyMonkeyServer interface { 75 | Do(FuzzyMonkey_DoServer) error 76 | mustEmbedUnimplementedFuzzyMonkeyServer() 77 | } 78 | 79 | // UnimplementedFuzzyMonkeyServer must be embedded to have forward compatible implementations. 80 | type UnimplementedFuzzyMonkeyServer struct { 81 | } 82 | 83 | func (UnimplementedFuzzyMonkeyServer) Do(FuzzyMonkey_DoServer) error { 84 | return status.Errorf(codes.Unimplemented, "method Do not implemented") 85 | } 86 | func (UnimplementedFuzzyMonkeyServer) mustEmbedUnimplementedFuzzyMonkeyServer() {} 87 | 88 | // UnsafeFuzzyMonkeyServer may be embedded to opt out of forward compatibility for this service. 89 | // Use of this interface is not recommended, as added methods to FuzzyMonkeyServer will 90 | // result in compilation errors. 91 | type UnsafeFuzzyMonkeyServer interface { 92 | mustEmbedUnimplementedFuzzyMonkeyServer() 93 | } 94 | 95 | func RegisterFuzzyMonkeyServer(s grpc.ServiceRegistrar, srv FuzzyMonkeyServer) { 96 | s.RegisterService(&FuzzyMonkey_ServiceDesc, srv) 97 | } 98 | 99 | func _FuzzyMonkey_Do_Handler(srv interface{}, stream grpc.ServerStream) error { 100 | return srv.(FuzzyMonkeyServer).Do(&fuzzyMonkeyDoServer{stream}) 101 | } 102 | 103 | type FuzzyMonkey_DoServer interface { 104 | Send(*Srv) error 105 | Recv() (*Clt, error) 106 | grpc.ServerStream 107 | } 108 | 109 | type fuzzyMonkeyDoServer struct { 110 | grpc.ServerStream 111 | } 112 | 113 | func (x *fuzzyMonkeyDoServer) Send(m *Srv) error { 114 | return x.ServerStream.SendMsg(m) 115 | } 116 | 117 | func (x *fuzzyMonkeyDoServer) Recv() (*Clt, error) { 118 | m := new(Clt) 119 | if err := x.ServerStream.RecvMsg(m); err != nil { 120 | return nil, err 121 | } 122 | return m, nil 123 | } 124 | 125 | // FuzzyMonkey_ServiceDesc is the grpc.ServiceDesc for FuzzyMonkey service. 126 | // It's only intended for direct use with grpc.RegisterService, 127 | // and not to be introspected or modified (even as a copy) 128 | var FuzzyMonkey_ServiceDesc = grpc.ServiceDesc{ 129 | ServiceName: "fm.FuzzyMonkey", 130 | HandlerType: (*FuzzyMonkeyServer)(nil), 131 | Methods: []grpc.MethodDesc{}, 132 | Streams: []grpc.StreamDesc{ 133 | { 134 | StreamName: "Do", 135 | Handler: _FuzzyMonkey_Do_Handler, 136 | ServerStreams: true, 137 | ClientStreams: true, 138 | }, 139 | }, 140 | Metadata: "fuzzymonkey.proto", 141 | } 142 | -------------------------------------------------------------------------------- /pkg/modeler/caller.go: -------------------------------------------------------------------------------- 1 | package modeler 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/FuzzyMonkeyCo/monkey/pkg/internal/fm" 7 | ) 8 | 9 | // CheckerFunc returns whether validation succeeded, was skipped or failed. 10 | type CheckerFunc func() (string, string, []string) 11 | 12 | // Caller performs a request and awaits a response. 13 | type Caller interface { 14 | // RequestProto returns call input as used by the client 15 | RequestProto() *fm.Clt_CallRequestRaw 16 | 17 | // Do sends the request and waits for the response 18 | Do(context.Context) 19 | 20 | // ResponseProto returns call output as received by the client 21 | ResponseProto() *fm.Clt_CallResponseRaw 22 | 23 | // NextCallerCheck returns ("",nil) when out of checks to run. 24 | // Otherwise it returns named checks inherent to the caller. 25 | NextCallerCheck() (string, CheckerFunc) 26 | } 27 | -------------------------------------------------------------------------------- /pkg/modeler/errors.go: -------------------------------------------------------------------------------- 1 | package modeler 2 | 3 | // NoSuchRefError represents a schema path that is not found 4 | type NoSuchRefError struct { 5 | ref string 6 | } 7 | 8 | var _ error = (*NoSuchRefError)(nil) 9 | 10 | func (e *NoSuchRefError) Error() string { 11 | return "no such ref: " + string(e.ref) 12 | } 13 | 14 | // NewNoSuchRefError creates a new error with the given absRef 15 | func NewNoSuchRefError(ref string) *NoSuchRefError { 16 | return &NoSuchRefError{ref} 17 | } 18 | -------------------------------------------------------------------------------- /pkg/modeler/interface.go: -------------------------------------------------------------------------------- 1 | package modeler 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | 8 | "go.starlark.net/starlark" 9 | "google.golang.org/protobuf/types/known/structpb" 10 | 11 | "github.com/FuzzyMonkeyCo/monkey/pkg/internal/fm" 12 | "github.com/FuzzyMonkeyCo/monkey/pkg/progresser" 13 | ) 14 | 15 | var ( 16 | ErrUnparsablePayload = errors.New("unparsable piped payload") 17 | ErrNoSuchSchema = errors.New("no such schema") 18 | ) 19 | 20 | // Maker types the New func that instanciates new models 21 | type Maker func(kwargs []starlark.Tuple) (Interface, error) 22 | 23 | // Interface describes checkable models. 24 | // A package defining a type that implements Interface also has to define: 25 | // * a non-empty const Name that names the Starlark builtin 26 | // * a func of type Maker named New that instanciates a new model 27 | type Interface interface { // TODO models.Modeler 28 | // Name uniquely identifies this instance 29 | Name() string 30 | 31 | // ToProto marshals a modeler.Interface implementation into a *fm.Clt_Fuzz_Model 32 | ToProto() *fm.Clt_Fuzz_Model 33 | 34 | // Lint goes through specs and unsures they are valid 35 | Lint(ctx context.Context, showSpec bool) error 36 | 37 | // InputsCount sums the amount of named schemas or types APIs define 38 | InputsCount() int 39 | // WriteAbsoluteReferences pretty-prints the API's named types 40 | WriteAbsoluteReferences(w io.Writer) 41 | // FilterEndpoints restricts which API endpoints are considered 42 | FilterEndpoints(criteria []string) ([]uint32, error) 43 | 44 | ValidateAgainstSchema(ref string, data []byte) error 45 | Validate(uint32, *structpb.Value) []string 46 | 47 | // NewCaller is called before making each call 48 | NewCaller(ctx context.Context, call *fm.Srv_Call, shower progresser.Shower) Caller 49 | } 50 | -------------------------------------------------------------------------------- /pkg/modeler/openapiv3/caller_checks.go: -------------------------------------------------------------------------------- 1 | package openapiv3 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/FuzzyMonkeyCo/monkey/pkg/modeler" 7 | ) 8 | 9 | type namedLambda struct { 10 | name string 11 | lambda modeler.CheckerFunc 12 | } 13 | 14 | func (m *oa3) callerChecks() []namedLambda { 15 | return []namedLambda{ 16 | {"connection to server", m.checkConn}, 17 | {"code < 500", m.checkNot5XX}, 18 | //TODO: when decoupling modeler/caller move these to modeler 19 | {"HTTP code", m.checkHTTPCode}, 20 | //TODO: check media type matches spec here (Content-Type: application/json) https://pkg.go.dev/net/http#DetectContentType https://github.com/gabriel-vasile/mimetype 21 | {"valid JSON response", m.checkValidJSONResponse}, 22 | {"response validates schema", m.checkValidatesJSONSchema}, 23 | } 24 | } 25 | 26 | func (m *oa3) checkConn() (s, skipped string, f []string) { 27 | if err := m.tcap.doErr; err != nil { 28 | f = append(f, "communication with server could not be established") 29 | f = append(f, err.Error()) 30 | return 31 | } 32 | s = "request sent" 33 | return 34 | } 35 | 36 | func (m *oa3) checkNot5XX() (s, skipped string, f []string) { 37 | if code := m.tcap.repProto.StatusCode; code >= 500 { 38 | f = append(f, fmt.Sprintf("server error: '%d'", code)) 39 | return 40 | } 41 | s = "no server error" 42 | return 43 | } 44 | 45 | func (m *oa3) checkHTTPCode() (s, skipped string, f []string) { 46 | if m.tcap.matchedHTTPCode { 47 | s = "HTTP code checked" 48 | } else { 49 | code := m.tcap.repProto.StatusCode 50 | f = append(f, fmt.Sprintf("unexpected HTTP code '%d'", code)) 51 | } 52 | return 53 | } 54 | 55 | func (m *oa3) checkValidJSONResponse() (s, skipped string, f []string) { 56 | if len(m.tcap.repProto.Body) == 0 { 57 | skipped = "response body is empty" 58 | return 59 | } 60 | 61 | if m.tcap.repBodyDecodeErr != nil { 62 | f = append(f, m.tcap.repBodyDecodeErr.Error()) 63 | return 64 | } 65 | 66 | s = "response is valid JSON" 67 | return 68 | } 69 | 70 | func (m *oa3) checkValidatesJSONSchema() (s, skipped string, f []string) { 71 | if m.tcap.matchedSID == 0 { 72 | skipped = "no JSON Schema specified for response" 73 | return 74 | } 75 | if len(m.tcap.repProto.Body) == 0 { 76 | skipped = "response body is empty" 77 | return 78 | } 79 | if errs := m.vald.Validate(m.tcap.matchedSID, m.tcap.repProto.BodyDecoded); len(errs) != 0 { 80 | f = errs 81 | return 82 | } 83 | s = "response validates JSON Schema" 84 | return 85 | } 86 | -------------------------------------------------------------------------------- /pkg/modeler/openapiv3/ir.go: -------------------------------------------------------------------------------- 1 | package openapiv3 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | ) 7 | 8 | type eid = uint32 9 | type sid = uint32 10 | type sids []sid 11 | type schemaJSON = map[string]interface{} 12 | type schemasJSON = map[string]schemaJSON 13 | 14 | func (f sids) Len() int { return len(f) } 15 | func (f sids) Less(i, j int) bool { return f[i] < f[j] } 16 | func (f sids) Swap(i, j int) { f[i], f[j] = f[j], f[i] } 17 | 18 | func (vald *validator) refsFromSIDs(SIDs sids) []string { 19 | sort.Sort(SIDs) 20 | refs := make([]string, 0, len(SIDs)) 21 | for _, SID := range SIDs { 22 | schemaPtr := vald.Spec.Schemas.Json[SID].GetPtr() 23 | if ref := schemaPtr.GetRef(); ref != "" { 24 | ref = strings.TrimPrefix(ref, oa3ComponentsSchemas) 25 | refs = append(refs, ref) 26 | } 27 | } 28 | if len(refs) == 0 { 29 | return []string{"_"} 30 | } 31 | return refs 32 | } 33 | -------------------------------------------------------------------------------- /pkg/modeler/openapiv3/lint.go: -------------------------------------------------------------------------------- 1 | package openapiv3 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "net/url" 9 | "os" 10 | "strings" 11 | 12 | "github.com/getkin/kin-openapi/openapi3" 13 | openapi_v3 "github.com/google/gnostic/openapiv3" 14 | 15 | "github.com/FuzzyMonkeyCo/monkey/pkg/modeler" 16 | ) 17 | 18 | var errLinting = func() error { 19 | msg := "Documentation validation failed." 20 | return errors.New(msg) // Gets around golint 21 | }() 22 | 23 | // Lint goes through OpenAPIv3 specs and unsures they are valid 24 | func (m *oa3) Lint(ctx context.Context, showSpec bool) (err error) { 25 | var blob []byte 26 | if blob, err = os.ReadFile(m.pb.File); err != nil { 27 | log.Println("[ERR]", err) 28 | return 29 | } 30 | log.Printf("[NFO] read %dB", len(blob)) 31 | 32 | if err = modeler.FindControlCharacters(string(blob)); err != nil { 33 | log.Println("[ERR]", err) 34 | fmt.Println(err.Error()) 35 | err = errLinting 36 | } 37 | 38 | if err = validateAndPretty(m.pb.File, blob, showSpec); err != nil { 39 | return 40 | } 41 | 42 | loader := &openapi3.Loader{ 43 | Context: ctx, 44 | IsExternalRefsAllowed: true, 45 | ReadFromURIFunc: m.readFromURI, 46 | } 47 | doc, err := loader.LoadFromData(blob) 48 | if err != nil { 49 | log.Println("[ERR]", err) 50 | return 51 | } 52 | 53 | log.Println("[NFO] first validation pass") 54 | if err = doc.Validate(ctx); err != nil { 55 | log.Println("[ERR]", err) 56 | return 57 | } 58 | 59 | log.Println("[NFO] last validation pass") 60 | if m.vald, err = newSpecFromOA3(doc); err != nil { 61 | return 62 | } 63 | 64 | log.Println("[NFO] model is valid") 65 | return 66 | } 67 | 68 | func validateAndPretty(docPath string, blob []byte, showSpec bool) (err error) { 69 | log.Println("[NFO] parsing whole spec") 70 | doc, err := openapi_v3.ParseDocument(blob) 71 | if err != nil { 72 | log.Println("[ERR]", err) 73 | const topword = "$root." 74 | for _, line := range strings.Split(err.Error(), "\n") { 75 | if !strings.Contains(line, topword) { 76 | fmt.Println(line) 77 | continue 78 | } 79 | es := strings.SplitAfterN(line, topword, 2) // TODO: handle line:col 80 | fmt.Println(es[1]) 81 | } 82 | err = errLinting 83 | return 84 | } 85 | 86 | log.Println("[NFO] ensuring references are valid") 87 | if _, err = doc.ResolveReferences(docPath); err != nil { 88 | log.Println("[ERR]", err) 89 | for _, line := range strings.Split(err.Error(), "\n") { 90 | fmt.Println(strings.TrimPrefix(line, "ERROR ")) 91 | } 92 | err = errLinting 93 | return 94 | } 95 | 96 | if showSpec { 97 | log.Println("[NFO] serialyzing spec to YAML") 98 | var pretty []byte 99 | if pretty, err = doc.YAMLValue(""); err != nil { 100 | log.Println("[ERR]", err) 101 | return 102 | } 103 | fmt.Fprintf(os.Stderr, "%s\n", pretty) 104 | } 105 | return 106 | } 107 | 108 | func (m *oa3) readFromURI(loader *openapi3.Loader, uri *url.URL) ([]byte, error) { 109 | // TODO: support local & remote URIs 110 | return nil, fmt.Errorf("unsupported URI: %q", uri.String()) 111 | } 112 | -------------------------------------------------------------------------------- /pkg/modeler/openapiv3/modeler.go: -------------------------------------------------------------------------------- 1 | package openapiv3 2 | 3 | import ( 4 | "io" 5 | "log" 6 | 7 | "go.starlark.net/starlark" 8 | "google.golang.org/protobuf/types/known/structpb" 9 | 10 | "github.com/FuzzyMonkeyCo/monkey/pkg/internal/fm" 11 | "github.com/FuzzyMonkeyCo/monkey/pkg/modeler" 12 | "github.com/FuzzyMonkeyCo/monkey/pkg/tags" 13 | ) 14 | 15 | // Name names the Starlark builtin 16 | const Name = "openapi3" 17 | 18 | // New instanciates a new model 19 | func New(kwargs []starlark.Tuple) (modeler.Interface, error) { 20 | var lot struct { 21 | name, file, host, headerAuthorization starlark.String 22 | } 23 | if err := starlark.UnpackArgs(Name, nil, kwargs, 24 | "name", &lot.name, 25 | "file", &lot.file, 26 | // NOTE: all args following an optional? are implicitly optional. 27 | "host??", &lot.host, 28 | "header_authorization??", &lot.headerAuthorization, //FIXME: drop 29 | ); err != nil { 30 | log.Println("[ERR]", err) 31 | return nil, err 32 | } 33 | log.Printf("[DBG] unpacked %+v", lot) 34 | 35 | // verify each 36 | 37 | name := lot.name.GoString() 38 | if err := tags.LegalName(name); err != nil { //TODO: newUserError 39 | log.Println("[ERR]", err) 40 | return nil, err 41 | } 42 | 43 | // verify all 44 | 45 | // assemble 46 | 47 | m := &oa3{ 48 | name: name, 49 | pb: &fm.Clt_Fuzz_Model_OpenAPIv3{ 50 | File: lot.file.GoString(), 51 | Host: lot.host.GoString(), 52 | //TODO: tags 53 | }, 54 | } 55 | return m, nil 56 | } 57 | 58 | var _ modeler.Interface = (*oa3)(nil) 59 | 60 | // oa3 implements a modeler.Interface for use by `monkey`. 61 | type oa3 struct { 62 | name string 63 | 64 | pb *fm.Clt_Fuzz_Model_OpenAPIv3 65 | 66 | vald *validator 67 | 68 | tcap *tCapHTTP 69 | } 70 | 71 | // Name uniquely identifies this instance 72 | func (m *oa3) Name() string { return m.name } 73 | 74 | // ToProto marshals a modeler.Interface implementation into a *fm.Clt_Fuzz_Model 75 | func (m *oa3) ToProto() *fm.Clt_Fuzz_Model { 76 | m.pb.Spec = m.vald.Spec 77 | return &fm.Clt_Fuzz_Model{ 78 | Name: m.name, 79 | Model: &fm.Clt_Fuzz_Model_Openapiv3{Openapiv3: m.pb}, 80 | } 81 | } 82 | 83 | // InputsCount sums the amount of named schemas or types APIs define 84 | func (m *oa3) InputsCount() int { 85 | return m.vald.inputsCount() 86 | } 87 | 88 | // FilterEndpoints restricts which API endpoints are considered 89 | func (m *oa3) FilterEndpoints(args []string) ([]eid, error) { 90 | return m.vald.filterEndpoints(args) 91 | } 92 | 93 | func (m *oa3) Validate(SID sid, data *structpb.Value) []string { 94 | return m.vald.Validate(SID, data) 95 | } 96 | 97 | // ValidateAgainstSchema tries to smash the data through the given keyhole 98 | func (m *oa3) ValidateAgainstSchema(absRef string, data []byte) error { 99 | return m.vald.validateAgainstSchema(absRef, data) 100 | } 101 | 102 | // WriteAbsoluteReferences pretty-prints the API's named types 103 | func (m *oa3) WriteAbsoluteReferences(w io.Writer) { 104 | m.vald.writeAbsoluteReferences(w) 105 | } 106 | -------------------------------------------------------------------------------- /pkg/modeler/openapiv3/testdata/specs/openapi3/v3.0.0_petstore.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Swagger Petstore", 6 | "license": { 7 | "name": "MIT" 8 | } 9 | }, 10 | "servers": [ 11 | { 12 | "url": "http://petstore.swagger.io/v1" 13 | } 14 | ], 15 | "paths": { 16 | "/pets": { 17 | "get": { 18 | "summary": "List all pets", 19 | "operationId": "listPets", 20 | "tags": [ 21 | "pets" 22 | ], 23 | "parameters": [ 24 | { 25 | "name": "limit", 26 | "in": "query", 27 | "description": "How many items to return at one time (max 100)", 28 | "required": false, 29 | "schema": { 30 | "type": "integer", 31 | "format": "int32" 32 | } 33 | } 34 | ], 35 | "responses": { 36 | "200": { 37 | "description": "A paged array of pets", 38 | "headers": { 39 | "x-next": { 40 | "description": "A link to the next page of responses", 41 | "schema": { 42 | "type": "string" 43 | } 44 | } 45 | }, 46 | "content": { 47 | "application/json": { 48 | "schema": { 49 | "$ref": "#/components/schemas/Pets" 50 | } 51 | } 52 | } 53 | }, 54 | "default": { 55 | "description": "unexpected error", 56 | "content": { 57 | "application/json": { 58 | "schema": { 59 | "$ref": "#/components/schemas/Error" 60 | } 61 | } 62 | } 63 | } 64 | } 65 | }, 66 | "post": { 67 | "summary": "Create a pet", 68 | "operationId": "createPets", 69 | "tags": [ 70 | "pets" 71 | ], 72 | "responses": { 73 | "201": { 74 | "description": "Null response" 75 | }, 76 | "default": { 77 | "description": "unexpected error", 78 | "content": { 79 | "application/json": { 80 | "schema": { 81 | "$ref": "#/components/schemas/Error" 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | }, 89 | "/pets/{petId}": { 90 | "get": { 91 | "summary": "Info for a specific pet", 92 | "operationId": "showPetById", 93 | "tags": [ 94 | "pets" 95 | ], 96 | "parameters": [ 97 | { 98 | "name": "petId", 99 | "in": "path", 100 | "required": true, 101 | "description": "The id of the pet to retrieve", 102 | "schema": { 103 | "type": "string" 104 | } 105 | } 106 | ], 107 | "responses": { 108 | "200": { 109 | "description": "Expected response to a valid request", 110 | "content": { 111 | "application/json": { 112 | "schema": { 113 | "$ref": "#/components/schemas/Pets" 114 | } 115 | } 116 | } 117 | }, 118 | "default": { 119 | "description": "unexpected error", 120 | "content": { 121 | "application/json": { 122 | "schema": { 123 | "$ref": "#/components/schemas/Error" 124 | } 125 | } 126 | } 127 | } 128 | } 129 | } 130 | } 131 | }, 132 | "components": { 133 | "schemas": { 134 | "Pet": { 135 | "required": [ 136 | "id", 137 | "name" 138 | ], 139 | "properties": { 140 | "id": { 141 | "type": "integer", 142 | "format": "int64" 143 | }, 144 | "name": { 145 | "type": "string" 146 | }, 147 | "tag": { 148 | "type": "string" 149 | } 150 | } 151 | }, 152 | "Pets": { 153 | "type": "array", 154 | "items": { 155 | "$ref": "#/components/schemas/Pet" 156 | } 157 | }, 158 | "Error": { 159 | "required": [ 160 | "code", 161 | "message" 162 | ], 163 | "properties": { 164 | "code": { 165 | "type": "integer", 166 | "format": "int32" 167 | }, 168 | "message": { 169 | "type": "string" 170 | } 171 | } 172 | } 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /pkg/modeler/openapiv3/testdata/specs/openapi3/v3.0.0_petstore.yaml: -------------------------------------------------------------------------------- 1 | # https://github.com/OAI/OpenAPI-Specification/blob/0892c873/examples/v3.0/petstore.yaml 2 | openapi: "3.0.0" 3 | info: 4 | version: 1.0.0 5 | title: Swagger Petstore 6 | license: 7 | name: MIT 8 | servers: 9 | - url: /v1 10 | - url: http://petstore.swagger.io/v1 11 | - url: https://{srv}/v2 12 | variables: 13 | srv: 14 | default: bla.co 15 | enum: [bla.co, bl.op] 16 | paths: 17 | /pets: 18 | get: 19 | summary: List all pets 20 | operationId: listPets 21 | tags: 22 | - pets 23 | parameters: 24 | - name: limit 25 | in: query 26 | description: How many items to return at one time (max 100) 27 | required: false 28 | schema: 29 | type: integer 30 | format: int32 31 | responses: 32 | '200': 33 | description: A paged array of pets 34 | headers: 35 | x-next: 36 | description: A link to the next page of responses 37 | schema: 38 | format: uri 39 | content: 40 | application/json: 41 | schema: 42 | $ref: "#/components/schemas/Pets" 43 | default: 44 | description: unexpected error 45 | content: 46 | application/json: 47 | schema: 48 | $ref: "#/components/schemas/Error" 49 | post: 50 | summary: Create a pet 51 | operationId: createPets 52 | tags: 53 | - pets 54 | responses: 55 | '201': 56 | description: Null response 57 | default: 58 | description: unexpected error 59 | content: 60 | application/json: 61 | schema: 62 | $ref: "#/components/schemas/Error" 63 | /pets/{petId}: 64 | get: 65 | summary: Info for a specific pet 66 | operationId: showPetById 67 | tags: 68 | - pets 69 | parameters: 70 | - name: petId 71 | in: path 72 | required: true 73 | description: The id of the pet to retrieve 74 | schema: 75 | type: string 76 | responses: 77 | '200': 78 | description: Expected response to a valid request 79 | content: 80 | application/json: 81 | schema: 82 | $ref: "#/components/schemas/Pets" 83 | default: 84 | description: unexpected error 85 | content: 86 | application/json: 87 | schema: 88 | $ref: "#/components/schemas/Error" 89 | components: 90 | schemas: 91 | Pet: 92 | required: 93 | - id 94 | - name 95 | properties: 96 | id: 97 | type: integer 98 | format: int64 99 | name: 100 | type: string 101 | tag: 102 | enum: [null, false, 42.42, {a: [good]}] 103 | Pets: 104 | type: array 105 | items: 106 | $ref: "#/components/schemas/Pet" 107 | Error: 108 | required: 109 | - code 110 | - message 111 | properties: 112 | code: 113 | type: integer 114 | format: int32 115 | message: 116 | type: string 117 | -------------------------------------------------------------------------------- /pkg/modeler/yaml_control_chars.go: -------------------------------------------------------------------------------- 1 | package modeler 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // See (v2) https://github.com/go-yaml/yaml/blob/7649d4548cb53a614db133b2a8ac1f31859dda8c/readerc.go#L357 10 | // See (v3) https://github.com/go-yaml/yaml/blob/496545a6307b2a7d7a710fd516e5e16e8ab62dbc/readerc.go#L379 11 | // TODO: whence https://github.com/go-yaml/yaml/issues/737#issuecomment-847519537 12 | func isControl(r rune) rune { 13 | // Check if the character is in the allowed range: 14 | switch { 15 | 16 | // #x9 | #xA | #xD | [#x20-#x7E] (8 bit) 17 | case r == 0x09: 18 | case r == 0x0A: 19 | case r == 0x0D: 20 | case r >= 0x20 && r <= 0x7E: 21 | 22 | // | #x85 | [#xA0-#xD7FF] | [#xE000-#xFFFD] (16 bit) 23 | case r == 0x85: 24 | case r >= 0xA0 && r <= 0xD7FF: 25 | case r >= 0xE000 && r <= 0xFFFD: 26 | 27 | // | [#x10000-#x10FFFF] (32 bit) 28 | case r >= 0x10000 && r <= 0x10FFFF: 29 | 30 | default: 31 | return -1 32 | } 33 | return r 34 | } 35 | 36 | // FindControlCharacters finds control characters that annoy YAML. 37 | func FindControlCharacters(str string) (err error) { 38 | found := make(map[rune]struct{}) 39 | for _, r := range str { 40 | if isControl(r) < 0 { 41 | found[r] = struct{}{} 42 | } 43 | } 44 | 45 | if len(found) != 0 { 46 | var msg strings.Builder 47 | msg.WriteString("found control characters:") 48 | for r := range found { 49 | msg.WriteString(fmt.Sprintf(" %U", r)) 50 | } 51 | err = errors.New(msg.String()) 52 | } 53 | return 54 | } 55 | 56 | // StripControlCharacters removes control characters that annoy YAML. 57 | func StripControlCharacters(str string) string { 58 | return strings.Map(isControl, str) 59 | } 60 | -------------------------------------------------------------------------------- /pkg/progresser/bar/constants.go: -------------------------------------------------------------------------------- 1 | package bar 2 | 3 | var states = [...]string{"🙈", "🙉", "🙊", "🐵"} 4 | 5 | const ( 6 | prefixSucceeded = "●" // ✔ ✓ 🆗 👌 ☑ ✅ 7 | prefixSkipped = "○" // ● • ‣ ◦ ⁃ ○ ◯ ⭕ 💮 8 | prefixFailed = "×" // ⨯ × ✗ x X ☓ ✘ ✖ 9 | ) 10 | -------------------------------------------------------------------------------- /pkg/progresser/bar/progresser.go: -------------------------------------------------------------------------------- 1 | package bar 2 | 3 | // WRT progress bars 4 | // See also: https://github.com/reconquest/barely 5 | // See also: https://github.com/snapcore/snapd/tree/3178a5499f2605329ebd25c7293ae1a0fb9fbd3b/progress 6 | 7 | import ( 8 | "context" 9 | "time" 10 | 11 | pbar "github.com/superhawk610/bar" 12 | 13 | "github.com/FuzzyMonkeyCo/monkey/pkg/as" 14 | "github.com/FuzzyMonkeyCo/monkey/pkg/progresser" 15 | ) 16 | 17 | const tickEvery = 333 * time.Millisecond 18 | 19 | var _ progresser.Interface = (*Progresser)(nil) 20 | 21 | // Progresser implements progresser.Interface 22 | type Progresser struct { 23 | ctx context.Context 24 | maxTestsCount uint32 25 | totalTestsCount, totalCallsCount, totalChecksCount uint32 26 | testCallsCount, callChecksCount uint32 27 | bar *pbar.Bar 28 | ticker *time.Ticker 29 | ticks, stateIdx int 30 | } 31 | 32 | // WithContext sets ctx of a progresser.Interface implementation 33 | func (p *Progresser) WithContext(ctx context.Context) { p.ctx = ctx } 34 | 35 | // MaxTestsCount sets an upper bound before testing starts 36 | func (p *Progresser) MaxTestsCount(v uint32) { 37 | p.maxTestsCount = v 38 | 39 | p.bar = pbar.NewWithOpts( 40 | // pbar.WithDebug(), 41 | pbar.WithDimensions(int(v), 37), 42 | pbar.WithDisplay("", "█", "█", " ", "|"), 43 | // pbar.WithFormat(":state :percent :bar :rate ops/s :eta"), 44 | pbar.WithFormat(":state :bar :rate calls/s :eta"), 45 | ) 46 | 47 | p.ticker = time.NewTicker(tickEvery) 48 | go func() { 49 | defer p.ticker.Stop() 50 | for { 51 | select { 52 | case <-p.ctx.Done(): 53 | return 54 | case <-p.ticker.C: 55 | p.tick(0) 56 | } 57 | } 58 | }() 59 | } 60 | 61 | // Terminate cleans up after a progresser.Interface implementation instance 62 | func (p *Progresser) Terminate() error { 63 | p.ticker.Stop() 64 | p.bar.Done() 65 | return nil 66 | } 67 | 68 | // TotalTestsCount may be called many times during testing 69 | func (p *Progresser) TotalTestsCount(v uint32) { p.totalTestsCount = v } 70 | 71 | // TotalCallsCount may be called many times during testing 72 | func (p *Progresser) TotalCallsCount(v uint32) { 73 | if p.totalCallsCount != v { 74 | p.tick(1) 75 | } 76 | p.totalCallsCount = v 77 | } 78 | 79 | // TotalChecksCount may be called many times during testing 80 | func (p *Progresser) TotalChecksCount(v uint32) { p.totalChecksCount = v } 81 | 82 | // TestCallsCount may be called many times during testing 83 | func (p *Progresser) TestCallsCount(v uint32) { p.testCallsCount = v } 84 | 85 | // CallChecksCount may be called many times during testing 86 | func (p *Progresser) CallChecksCount(v uint32) { p.callChecksCount = v } 87 | 88 | func (p *Progresser) tick(offset int) { 89 | state := states[p.stateIdx%len(states)] 90 | p.stateIdx++ 91 | p.ticks += offset 92 | p.bar.Update(p.ticks, pbar.Context{pbar.Ctx("state", state)}) 93 | } 94 | 95 | // Printf formats informational data 96 | func (p *Progresser) Printf(format string, s ...interface{}) { 97 | p.bar.Interruptf(format, s...) 98 | } 99 | 100 | // Errorf formats error messages 101 | func (p *Progresser) Errorf(format string, s ...interface{}) { 102 | p.bar.Interruptf("%s", as.ColorERR.Sprintf(format, s...)) 103 | } 104 | 105 | func (p *Progresser) show(s string) { p.bar.Interrupt(s) } 106 | func (p *Progresser) nfo(s string) { p.show(as.ColorNFO.Sprintf("%s", s)) } 107 | func (p *Progresser) wrn(s string) { p.show(as.ColorWRN.Sprintf("%s", s)) } 108 | func (p *Progresser) err(s string) { p.show(as.ColorERR.Sprintf("%s", s)) } 109 | 110 | // ChecksPassed may be called many times during testing 111 | func (p *Progresser) ChecksPassed() { 112 | p.nfo(" Checks passed.\n") 113 | } 114 | 115 | // CheckPassed may be called many times during testing 116 | func (p *Progresser) CheckPassed(name, msg string) { 117 | if msg != "" { 118 | if msg == "" { // name of user check 119 | msg = "" 120 | } else { 121 | msg = ": " + msg 122 | } 123 | } 124 | p.bar.Interruptf(" %s %s%s", 125 | as.ColorOK.Sprintf(prefixSucceeded), 126 | as.ColorNFO.Sprintf(name), 127 | msg, 128 | ) 129 | } 130 | 131 | // CheckSkipped may be called many times during testing 132 | func (p *Progresser) CheckSkipped(name, msg string) { 133 | if msg != "" { 134 | if msg == "" { // name of user check 135 | msg = "" 136 | } else { 137 | msg = ": " + msg 138 | } 139 | } 140 | p.bar.Interruptf(" %s %s SKIPPED%s", 141 | as.ColorWRN.Sprintf(prefixSkipped), 142 | name, 143 | msg, 144 | ) 145 | } 146 | 147 | // CheckFailed may be called many times during testing 148 | func (p *Progresser) CheckFailed(name string, ss []string) { 149 | if len(ss) > 0 { 150 | p.show(" " + as.ColorERR.Sprintf(prefixFailed) + " " + as.ColorNFO.Sprintf(ss[0])) 151 | } 152 | if len(ss) > 1 { 153 | for _, s := range ss[1:] { 154 | p.err(s) 155 | } 156 | } 157 | p.nfo(" Found a bug!\n") 158 | } 159 | -------------------------------------------------------------------------------- /pkg/progresser/ci/progresser.go: -------------------------------------------------------------------------------- 1 | package ci 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/FuzzyMonkeyCo/monkey/pkg/as" 8 | "github.com/FuzzyMonkeyCo/monkey/pkg/progresser" 9 | ) 10 | 11 | var _ progresser.Interface = (*Progresser)(nil) 12 | 13 | // Progresser implements progresser.Interface 14 | type Progresser struct { 15 | totalTestsCount, totalCallsCount, totalChecksCount uint32 16 | } 17 | 18 | func dot(n uint32, o *uint32) { 19 | if *o != n { 20 | *o = n 21 | } 22 | } 23 | 24 | // WithContext sets ctx of a progresser.Interface implementation 25 | func (p *Progresser) WithContext(ctx context.Context) {} 26 | 27 | // MaxTestsCount sets an upper bound before testing starts 28 | func (p *Progresser) MaxTestsCount(v uint32) {} 29 | 30 | // Terminate cleans up after a progresser.Interface implementation instance 31 | func (p *Progresser) Terminate() error { return nil } 32 | 33 | // TotalTestsCount may be called many times during testing 34 | func (p *Progresser) TotalTestsCount(v uint32) { dot(v, &p.totalTestsCount) } 35 | 36 | // TotalCallsCount may be called many times during testing 37 | func (p *Progresser) TotalCallsCount(v uint32) { dot(v, &p.totalCallsCount) } 38 | 39 | // TotalChecksCount may be called many times during testing 40 | func (p *Progresser) TotalChecksCount(v uint32) { dot(v, &p.totalChecksCount) } 41 | 42 | // TestCallsCount may be called many times during testing 43 | func (p *Progresser) TestCallsCount(v uint32) {} 44 | 45 | // CallChecksCount may be called many times during testing 46 | func (p *Progresser) CallChecksCount(v uint32) {} 47 | 48 | // Printf formats informational data 49 | func (p *Progresser) Printf(format string, s ...interface{}) { fmt.Printf(format+"\n", s...) } 50 | 51 | // Errorf formats error messages 52 | func (p *Progresser) Errorf(format string, s ...interface{}) { as.ColorERR.Printf(format+"\n", s...) } 53 | 54 | // ChecksPassed may be called many times during testing 55 | func (p *Progresser) ChecksPassed() { 56 | // as.ColorOK.Println("PASSED CHECKS") 57 | } 58 | 59 | // CheckPassed may be called many times during testing 60 | func (p *Progresser) CheckPassed(name, msg string) { 61 | // as.ColorOK.Printf("PASSED ") 62 | // as.ColorNFO.Printf("%s", name) 63 | // if msg != "" { 64 | // fmt.Printf(" (%s)", msg) 65 | // } 66 | // fmt.Println() 67 | } 68 | 69 | // CheckSkipped may be called many times during testing 70 | func (p *Progresser) CheckSkipped(name, msg string) { 71 | // as.ColorWRN.Printf("SKIPPED ") 72 | // as.ColorNFO.Printf("%s", name) 73 | // if msg != "" { 74 | // fmt.Printf(" (%s)", msg) 75 | // } 76 | // fmt.Println() 77 | } 78 | 79 | // CheckFailed may be called many times during testing 80 | func (p *Progresser) CheckFailed(name string, ss []string) { 81 | if len(ss) > 0 { 82 | as.ColorERR.Printf("FAILED ") 83 | as.ColorNFO.Println(ss[0]) 84 | } 85 | if len(ss) > 1 { 86 | for _, s := range ss[1:] { 87 | as.ColorERR.Println(s) 88 | } 89 | } 90 | as.ColorNFO.Println(" Found a bug!") 91 | } 92 | -------------------------------------------------------------------------------- /pkg/progresser/dots/progresser.go: -------------------------------------------------------------------------------- 1 | package dots 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/FuzzyMonkeyCo/monkey/pkg/as" 8 | "github.com/FuzzyMonkeyCo/monkey/pkg/progresser" 9 | ) 10 | 11 | var _ progresser.Interface = (*Progresser)(nil) 12 | 13 | // Progresser implements progresser.Interface 14 | type Progresser struct { 15 | totalTestsCount, totalCallsCount uint32 16 | dotting bool 17 | } 18 | 19 | func (p *Progresser) dot(n uint32, o *uint32, f, c string) { 20 | if *o != n { 21 | p.dotting = true 22 | if *o == 0 { 23 | fmt.Printf(f) 24 | } else { 25 | fmt.Printf(c) 26 | } 27 | *o = n 28 | } 29 | } 30 | 31 | // WithContext sets ctx of a progresser.Interface implementation 32 | func (p *Progresser) WithContext(ctx context.Context) {} 33 | 34 | // MaxTestsCount sets an upper bound before testing starts 35 | func (p *Progresser) MaxTestsCount(v uint32) {} 36 | 37 | // Terminate cleans up after a progresser.Interface implementation instance 38 | func (p *Progresser) Terminate() error { 39 | p.dotting = false 40 | fmt.Printf(">") 41 | return nil 42 | } 43 | 44 | // TotalTestsCount may be called many times during testing 45 | func (p *Progresser) TotalTestsCount(v uint32) { p.dot(v, &p.totalTestsCount, "<", "> <") } 46 | 47 | // TotalCallsCount may be called many times during testing 48 | func (p *Progresser) TotalCallsCount(v uint32) { p.dot(v, &p.totalCallsCount, ".", ".") } 49 | 50 | // TotalChecksCount may be called many times during testing 51 | func (p *Progresser) TotalChecksCount(v uint32) {} 52 | 53 | // TestCallsCount may be called many times during testing 54 | func (p *Progresser) TestCallsCount(v uint32) {} 55 | 56 | // CallChecksCount may be called many times during testing 57 | func (p *Progresser) CallChecksCount(v uint32) {} 58 | 59 | // Printf formats informational data 60 | func (p *Progresser) Printf(format string, s ...interface{}) { 61 | switch format { 62 | case " --seed=%s": 63 | fmt.Printf("\nseed: %s\n", s...) 64 | } 65 | } 66 | 67 | // Errorf formats error messages 68 | func (p *Progresser) Errorf(format string, s ...interface{}) { 69 | if p.dotting { 70 | fmt.Println() 71 | p.dotting = false 72 | } 73 | if format[len(format)-1] != '\n' { 74 | format = format + "\n" 75 | } 76 | fmt.Printf(format, s...) 77 | } 78 | 79 | // ChecksPassed may be called many times during testing 80 | func (p *Progresser) ChecksPassed() {} 81 | 82 | // CheckPassed may be called many times during testing 83 | func (p *Progresser) CheckPassed(name, msg string) { 84 | p.dotting = true 85 | as.ColorOK.Printf("|") 86 | } 87 | 88 | // CheckSkipped may be called many times during testing 89 | func (p *Progresser) CheckSkipped(name, msg string) {} 90 | 91 | // CheckFailed may be called many times during testing 92 | func (p *Progresser) CheckFailed(name string, ss []string) { 93 | as.ColorERR.Println("x") 94 | as.ColorNFO.Printf("Check failed: ") 95 | fmt.Println(name) 96 | for i, s := range ss { 97 | if i != 0 { 98 | as.ColorERR.Println(s) 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /pkg/progresser/interface.go: -------------------------------------------------------------------------------- 1 | package progresser 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // Shower displays informational data 8 | type Shower interface { 9 | // Printf formats informational data 10 | Printf(string, ...interface{}) 11 | // Errorf formats error messages 12 | Errorf(string, ...interface{}) 13 | } 14 | 15 | // Interface displays calls, resets and checks progression 16 | type Interface interface { 17 | Shower 18 | 19 | // WithContext sets ctx of a progresser.Interface implementation 20 | WithContext(context.Context) 21 | // MaxTestsCount sets an upper bound before testing starts 22 | MaxTestsCount(uint32) 23 | 24 | // TotalTestsCount may be called many times during testing 25 | TotalTestsCount(uint32) 26 | // TotalCallsCount may be called many times during testing 27 | TotalCallsCount(uint32) 28 | // TotalChecksCount may be called many times during testing 29 | TotalChecksCount(uint32) 30 | // TestCallsCount may be called many times during testing 31 | TestCallsCount(uint32) 32 | // CallChecksCount may be called many times during testing 33 | CallChecksCount(uint32) 34 | 35 | // CheckFailed may be called many times during testing 36 | CheckFailed(string, []string) 37 | // CheckSkipped may be called many times during testing 38 | CheckSkipped(string, string) 39 | // CheckPassed may be called many times during testing 40 | CheckPassed(string, string) 41 | // ChecksPassed may be called many times during testing 42 | ChecksPassed() 43 | 44 | // Terminate cleans up after a progresser.Interface implementation instance 45 | Terminate() error 46 | } 47 | -------------------------------------------------------------------------------- /pkg/protovalue/from.go: -------------------------------------------------------------------------------- 1 | package protovalue 2 | 3 | import ( 4 | "fmt" 5 | 6 | "google.golang.org/protobuf/types/known/structpb" 7 | ) 8 | 9 | // FromGo compiles a Go value to google.protobuf.Value. 10 | // It panics on unexpected types. 11 | // See https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Value 12 | func FromGo(value interface{}) *structpb.Value { 13 | if value == nil { 14 | return &structpb.Value{Kind: &structpb.Value_NullValue{ 15 | NullValue: structpb.NullValue_NULL_VALUE}} 16 | } 17 | switch val := value.(type) { 18 | case bool: 19 | return &structpb.Value{Kind: &structpb.Value_BoolValue{ 20 | BoolValue: val}} 21 | 22 | case uint8: 23 | return &structpb.Value{Kind: &structpb.Value_NumberValue{ 24 | NumberValue: float64(val)}} 25 | case int8: 26 | return &structpb.Value{Kind: &structpb.Value_NumberValue{ 27 | NumberValue: float64(val)}} 28 | case uint16: 29 | return &structpb.Value{Kind: &structpb.Value_NumberValue{ 30 | NumberValue: float64(val)}} 31 | case int16: 32 | return &structpb.Value{Kind: &structpb.Value_NumberValue{ 33 | NumberValue: float64(val)}} 34 | case uint32: 35 | return &structpb.Value{Kind: &structpb.Value_NumberValue{ 36 | NumberValue: float64(val)}} 37 | case int32: 38 | return &structpb.Value{Kind: &structpb.Value_NumberValue{ 39 | NumberValue: float64(val)}} 40 | case uint64: 41 | return &structpb.Value{Kind: &structpb.Value_NumberValue{ 42 | NumberValue: float64(val)}} 43 | case int64: 44 | return &structpb.Value{Kind: &structpb.Value_NumberValue{ 45 | NumberValue: float64(val)}} 46 | 47 | case float32: 48 | return &structpb.Value{Kind: &structpb.Value_NumberValue{ 49 | NumberValue: float64(val)}} 50 | case float64: 51 | return &structpb.Value{Kind: &structpb.Value_NumberValue{ 52 | NumberValue: val}} 53 | 54 | case string: 55 | return &structpb.Value{Kind: &structpb.Value_StringValue{ 56 | StringValue: val}} 57 | 58 | case []interface{}: 59 | vs := make([]*structpb.Value, 0, len(val)) 60 | for _, v := range val { 61 | vs = append(vs, FromGo(v)) 62 | } 63 | return &structpb.Value{Kind: &structpb.Value_ListValue{ListValue: &structpb.ListValue{Values: vs}}} 64 | 65 | case map[string]interface{}: 66 | vs := make(map[string]*structpb.Value, len(val)) 67 | for n, v := range val { 68 | vs[n] = FromGo(v) 69 | } 70 | return &structpb.Value{Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{Fields: vs}}} 71 | 72 | default: 73 | panic(fmt.Errorf("cannot convert from value type: %T", val)) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pkg/protovalue/to.go: -------------------------------------------------------------------------------- 1 | package protovalue 2 | 3 | import ( 4 | "fmt" 5 | 6 | "google.golang.org/protobuf/types/known/structpb" 7 | ) 8 | 9 | // ToGo compiles a google.protobuf.Value value to Go. 10 | // It panics on unexpected types. 11 | // See https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Value 12 | func ToGo(value *structpb.Value) interface{} { 13 | switch value.GetKind().(type) { 14 | case *structpb.Value_NullValue: 15 | return nil 16 | case *structpb.Value_BoolValue: 17 | return value.GetBoolValue() 18 | case *structpb.Value_NumberValue: 19 | return value.GetNumberValue() 20 | case *structpb.Value_StringValue: 21 | return value.GetStringValue() 22 | case *structpb.Value_ListValue: 23 | val := value.GetListValue().GetValues() 24 | vs := make([]interface{}, 0, len(val)) 25 | for _, v := range val { 26 | vs = append(vs, ToGo(v)) 27 | } 28 | return vs 29 | case *structpb.Value_StructValue: 30 | val := value.GetStructValue().GetFields() 31 | vs := make(map[string]interface{}, len(val)) 32 | for n, v := range val { 33 | vs[n] = ToGo(v) 34 | } 35 | return vs 36 | default: 37 | panic(fmt.Errorf("cannot convert from type %T: %+v", value.GetKind(), value)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pkg/resetter/interface.go: -------------------------------------------------------------------------------- 1 | package resetter 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "strings" 8 | 9 | "go.starlark.net/starlark" 10 | 11 | "github.com/FuzzyMonkeyCo/monkey/pkg/internal/fm" 12 | "github.com/FuzzyMonkeyCo/monkey/pkg/progresser" 13 | ) 14 | 15 | // Maker types the New func that instanciates new resetters 16 | type Maker func(kwargs []starlark.Tuple) (Interface, error) 17 | 18 | // Interface describes ways to reset the system under test to a known initial state 19 | // A package defining a type that implements Interface also has to define: 20 | // * a non-empty const Name that names the Starlark builtin 21 | // * a func of type Maker named New that instanciates a new resetter 22 | type Interface interface { // TODO: initers.Initer 23 | // Name uniquely identifies this instance 24 | Name() string 25 | 26 | // Provides lists the models a resetter resets 27 | Provides() []string 28 | 29 | // ToProto marshals a resetter.Interface implementation into a *fm.Clt_Fuzz_Resetter 30 | ToProto() *fm.Clt_Fuzz_Resetter 31 | 32 | // ExecStart executes the setup phase of the System Under Test 33 | ExecStart(context.Context, progresser.Shower, bool, map[string]string) error 34 | // ExecReset resets the System Under Test to a state similar to a post-ExecStart state 35 | ExecReset(context.Context, progresser.Shower, bool, map[string]string) error 36 | // ExecStop executes the cleanup phase of the System Under Test 37 | ExecStop(context.Context, progresser.Shower, bool, map[string]string) error 38 | 39 | // Terminate cleans up after a resetter.Interface implementation instance 40 | Terminate(context.Context, progresser.Shower, map[string]string) error 41 | } 42 | 43 | var _ error = (*Error)(nil) 44 | 45 | // Error describes a resetter error 46 | type Error struct { 47 | bt [][]byte 48 | } 49 | 50 | // NewError returns a new empty resetter.Error 51 | func NewError(bt [][]byte) *Error { 52 | return &Error{ 53 | bt: bt, 54 | } 55 | } 56 | 57 | // Reason describes the error on multiple lines 58 | func (re *Error) Reason() []string { 59 | bt := make([]string, 0, len(re.bt)) 60 | for _, line := range re.bt { 61 | bt = append(bt, string(line)) 62 | } 63 | return bt 64 | } 65 | 66 | func rev(s string) string { 67 | n := len(s) 68 | runes := make([]rune /*n,*/, n) 69 | for _, rune := range s { 70 | n-- 71 | runes[n] = rune 72 | } 73 | return string(runes[n:]) 74 | } 75 | 76 | // Error returns the error string 77 | func (re *Error) Error() string { 78 | e := bytes.Join(re.bt, []byte(";")) 79 | ee := rev(fmt.Sprintf("%.280s", rev(string(e)))) 80 | 81 | var msg strings.Builder 82 | if len(e) != len(ee) { 83 | msg.WriteString("...") 84 | } 85 | msg.WriteString(ee) 86 | return msg.String() 87 | } 88 | -------------------------------------------------------------------------------- /pkg/resetter/shell/cmds.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | ) 7 | 8 | type shellCmd int 9 | 10 | const ( 11 | cmdStart shellCmd = iota 12 | cmdReset 13 | cmdStop 14 | ) 15 | 16 | func (cmd shellCmd) String() string { 17 | return map[shellCmd]string{ 18 | cmdStart: "Start", 19 | cmdReset: "Reset", 20 | cmdStop: "Stop", 21 | }[cmd] 22 | } 23 | 24 | func (s *Resetter) commands() (cmds []shellCmd, err error) { 25 | var ( 26 | hasStart = "" != s.Start 27 | hasReset = "" != s.Rst 28 | hasStop = "" != s.Stop 29 | ) 30 | switch { 31 | case !hasStart && hasReset && !hasStop: 32 | log.Println("[NFO] running Shell.Rst") 33 | cmds = []shellCmd{cmdReset} 34 | return 35 | 36 | case hasStart && hasReset && hasStop: 37 | if s.isNotFirstRun { 38 | log.Println("[NFO] running Shell.Rst") 39 | cmds = []shellCmd{cmdReset} 40 | return 41 | } 42 | 43 | log.Println("[NFO] running Shell.Start then Shell.Rst") 44 | cmds = []shellCmd{cmdStart, cmdReset} 45 | return 46 | 47 | case hasStart && !hasReset && hasStop: 48 | if s.isNotFirstRun { 49 | log.Println("[NFO] running Shell.Stop then Shell.Start") 50 | cmds = []shellCmd{cmdStop, cmdStart} 51 | return 52 | } 53 | 54 | log.Println("[NFO] running Shell.Start") 55 | cmds = []shellCmd{cmdStart} 56 | return 57 | 58 | default: 59 | err = errors.New("missing at least `shell( reset = \"...code...\" )`") 60 | log.Println("[ERR]", err) 61 | return 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /pkg/resetter/shell/lines_writer.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | ) 7 | 8 | var _ io.Writer = (*linesWriter)(nil) 9 | 10 | type linesWriter struct { 11 | cb lineWriter 12 | } 13 | 14 | type lineWriter func([]byte) 15 | 16 | func newlinesWriter(cb lineWriter) *linesWriter { 17 | return &linesWriter{cb: cb} 18 | } 19 | 20 | func (lw *linesWriter) Write(p []byte) (int, error) { 21 | for i := 0; ; { 22 | n := bytes.IndexAny(p[i:], "\n\r") 23 | if n < 0 { 24 | lw.cb(p[i:]) 25 | break 26 | } 27 | lw.cb(p[i : i+n]) 28 | i += n + 1 29 | } 30 | return len(p), nil 31 | } 32 | -------------------------------------------------------------------------------- /pkg/resetter/shell/shell.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log" 7 | "strings" 8 | "sync" 9 | "time" 10 | 11 | "go.starlark.net/starlark" 12 | 13 | "github.com/FuzzyMonkeyCo/monkey/pkg/internal/fm" 14 | "github.com/FuzzyMonkeyCo/monkey/pkg/progresser" 15 | "github.com/FuzzyMonkeyCo/monkey/pkg/resetter" 16 | "github.com/FuzzyMonkeyCo/monkey/pkg/tags" 17 | ) 18 | 19 | // Name names the Starlark builtin 20 | const Name = "shell" 21 | 22 | // TODO:{start,reset,strop}_file a la Bazel 23 | 24 | const ( 25 | shell = "/bin/bash" // TODO: use mentioned shell 26 | 27 | scriptTimeout = 2 * time.Minute // TODO: tune through kwargs 28 | ) 29 | 30 | // New instanciates a new resetter 31 | func New(kwargs []starlark.Tuple) (resetter.Interface, error) { 32 | var lot struct { 33 | name, start, reset, stop starlark.String 34 | provides tags.UniqueStringsNonEmpty 35 | } 36 | if err := starlark.UnpackArgs(Name, nil, kwargs, 37 | "name", &lot.name, 38 | "provides", &lot.provides, 39 | // NOTE: all args following an optional? are implicitly optional. 40 | "start??", &lot.start, 41 | "reset??", &lot.reset, 42 | "stop??", &lot.stop, 43 | // TODO: waiton = "tcp/4000", various recipes => 1 rsttr per service 44 | //TODO: tags 45 | ); err != nil { 46 | log.Println("[ERR]", err) 47 | return nil, err 48 | } 49 | log.Printf("[DBG] unpacked %+v", lot) 50 | 51 | // verify each 52 | 53 | name := lot.name.GoString() 54 | if err := tags.LegalName(name); err != nil { //TODO: newUserError 55 | log.Println("[ERR]", err) 56 | return nil, err 57 | } 58 | 59 | // verify all 60 | 61 | // assemble 62 | 63 | s := &Resetter{ 64 | name: name, 65 | provides: lot.provides.GoStrings(), 66 | } 67 | s.Start = strings.TrimSpace(lot.start.GoString()) 68 | s.Rst = strings.TrimSpace(lot.reset.GoString()) 69 | s.Stop = strings.TrimSpace(lot.stop.GoString()) 70 | return s, nil 71 | } 72 | 73 | var _ resetter.Interface = (*Resetter)(nil) 74 | 75 | // Resetter implements resetter.Interface 76 | type Resetter struct { 77 | name string 78 | provides []string 79 | fm.Clt_Fuzz_Resetter_Shell 80 | 81 | isNotFirstRun bool 82 | 83 | scriptsCreator sync.Once 84 | scriptsPaths map[shellCmd]string 85 | stdin io.WriteCloser 86 | sherr chan error 87 | rcoms chan uint8 88 | } 89 | 90 | // Name uniquely identifies this instance 91 | func (s *Resetter) Name() string { return s.name } 92 | 93 | // Provides lists the models a resetter resets 94 | func (s *Resetter) Provides() []string { return s.provides } 95 | 96 | // ToProto marshals a resetter.Interface implementation into a *fm.Clt_Fuzz_Resetter 97 | func (s *Resetter) ToProto() *fm.Clt_Fuzz_Resetter { 98 | return &fm.Clt_Fuzz_Resetter{ 99 | Name: s.name, 100 | Provides: s.provides, 101 | Resetter: &fm.Clt_Fuzz_Resetter_Shell_{ 102 | Shell: &s.Clt_Fuzz_Resetter_Shell, 103 | }} 104 | } 105 | 106 | // ExecStart executes the setup phase of the System Under Test 107 | func (s *Resetter) ExecStart(ctx context.Context, shower progresser.Shower, only bool, envRead map[string]string) error { 108 | return s.exec(ctx, shower, envRead, cmdStart) 109 | } 110 | 111 | // ExecReset resets the System Under Test to a state similar to a post-ExecStart state 112 | func (s *Resetter) ExecReset(ctx context.Context, shower progresser.Shower, only bool, envRead map[string]string) error { 113 | if only { 114 | // Makes `monkey exec reset` run as if in between tests 115 | s.isNotFirstRun = true 116 | } 117 | 118 | cmds, err := s.commands() 119 | if err != nil { 120 | return err 121 | } 122 | 123 | if !s.isNotFirstRun { 124 | s.isNotFirstRun = true 125 | } 126 | 127 | return s.exec(ctx, shower, envRead, cmds...) 128 | } 129 | 130 | // ExecStop executes the cleanup phase of the System Under Test 131 | func (s *Resetter) ExecStop(ctx context.Context, shower progresser.Shower, only bool, envRead map[string]string) error { 132 | return s.exec(ctx, shower, envRead, cmdStop) 133 | } 134 | 135 | // Terminate cleans up after a resetter.Interface implementation instance 136 | func (s *Resetter) Terminate(ctx context.Context, shower progresser.Shower, envRead map[string]string) (err error) { 137 | if hasStop := s.Stop != ""; hasStop { 138 | if err = s.ExecStop(ctx, shower, true, envRead); err != nil { 139 | log.Println("[ERR]", err) 140 | return 141 | } 142 | } 143 | log.Println("[NFO] exiting shell singleton") 144 | s.signal(comExit, "") 145 | return 146 | } 147 | -------------------------------------------------------------------------------- /pkg/runtime/call_before_request.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | 8 | "go.starlark.net/starlark" 9 | "golang.org/x/sync/errgroup" 10 | 11 | "github.com/FuzzyMonkeyCo/monkey/pkg/starlarktruth" 12 | ) 13 | 14 | func (chk *check) tryBeforeRequest( 15 | ctx context.Context, 16 | name string, 17 | cx *cxModBeforeRequest, 18 | print func(string), 19 | maxSteps uint64, 20 | maxDuration time.Duration, 21 | ) (err error) { 22 | ctxG, cancel := context.WithTimeout(ctx, maxDuration) 23 | defer cancel() 24 | 25 | g, ctxG := errgroup.WithContext(ctxG) 26 | g.Go(func() (err error) { 27 | th := &starlark.Thread{ 28 | Name: name, 29 | Load: loadDisabled, 30 | Print: func(_ *starlark.Thread, msg string) { print(msg) }, 31 | } 32 | 33 | th.SetMaxExecutionSteps(maxSteps) // Upper bound on computation 34 | var hookRet starlark.Value 35 | start := time.Now() 36 | defer func() { 37 | log.Printf("[DBG] check %q ran in %s (%d steps)", name, time.Since(start), th.ExecutionSteps()) 38 | }() 39 | if hookRet, err = starlark.Call(th, chk.beforeRequest, starlark.Tuple{cx}, nil); err != nil { 40 | log.Println("[ERR]", err) 41 | // Check failed or an error happened 42 | return 43 | } 44 | if err = starlarktruth.Close(th); err != nil { 45 | log.Println("[ERR]", err) 46 | // Incomplete `assert that()` call 47 | return 48 | } 49 | if hookRet != starlark.None { 50 | err = newUserError("check(name = %q) should return None, got: %s", name, hookRet.String()) 51 | log.Println("[ERR]", err) 52 | return 53 | } 54 | // Check passed 55 | 56 | return 57 | }) 58 | err = g.Wait() 59 | return 60 | } 61 | -------------------------------------------------------------------------------- /pkg/runtime/ctx_modules.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "go.starlark.net/starlark" 5 | 6 | "github.com/FuzzyMonkeyCo/monkey/pkg/internal/fm" 7 | ) 8 | 9 | // TODO: rename `ctx` to `cx` 10 | 11 | func headerPairs(protoHeaders []*fm.HeaderPair) (starlark.Value, error) { 12 | d := starlark.NewDict(len(protoHeaders)) //fixme: dont make a dict out of repeated HeaderPair.s 13 | 14 | for _, kvs := range protoHeaders { 15 | values := kvs.GetValues() 16 | vs := make([]starlark.Value, 0, len(values)) 17 | for _, value := range values { 18 | vs = append(vs, starlark.String(value)) 19 | } 20 | if err := d.SetKey(starlark.String(kvs.GetKey()), starlark.NewList(vs)); err != nil { 21 | return nil, err 22 | } 23 | } 24 | return d, nil 25 | } 26 | -------------------------------------------------------------------------------- /pkg/runtime/ctxvalues/ctxvalues.go: -------------------------------------------------------------------------------- 1 | // Values set by monkey's runtime to be accessible through 2 | // context in other packages 3 | 4 | package ctxvalues 5 | 6 | type xUserAgent struct{} 7 | 8 | // XUserAgent key's associated value contains monkey version and platform information 9 | var XUserAgent = xUserAgent{} 10 | -------------------------------------------------------------------------------- /pkg/runtime/cx_mod_after_response.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "go.starlark.net/starlark" 8 | 9 | "github.com/FuzzyMonkeyCo/monkey/pkg/internal/fm" 10 | ) 11 | 12 | // TODO? easy access to generated parameters. For instance: 13 | // post_id = ctx.request["parameters"]["path"]["{id}"] (note decoded int) 14 | 15 | // cxModAfterResponse is the `ctx` starlark value accessible after executing a call 16 | type cxModAfterResponse struct { 17 | accessedState bool 18 | request *cxRequestAfterResponse 19 | response *cxResponseAfterResponse 20 | state *starlark.Dict 21 | //TODO: specs starlark.Value => provide models as JSON for now until we find a suitable Python-ish API 22 | //TODO: CLI filter `--only="starlark.expr(ctx.specs)"` 23 | //TODO: ctx.specs stops being accessible on first ctx.state access 24 | } 25 | 26 | type ( 27 | ctxctor2 func(*fm.Clt_CallResponseRaw_Output) ctxctor1 28 | ctxctor1 func(*starlark.Dict) *cxModAfterResponse 29 | ) 30 | 31 | func ctxCurry(callInput *fm.Clt_CallRequestRaw_Input) ctxctor2 { 32 | request := newCxRequestAfterResponse(callInput) 33 | request.Freeze() 34 | return func(callOutput *fm.Clt_CallResponseRaw_Output) ctxctor1 { 35 | response := newCxResponseAfterResponse(callOutput) 36 | response.Freeze() 37 | return func(state *starlark.Dict) *cxModAfterResponse { 38 | // state is mutated through checks 39 | return &cxModAfterResponse{ 40 | request: request, 41 | response: response, 42 | state: state, 43 | } 44 | } 45 | } 46 | } 47 | 48 | var _ starlark.HasAttrs = (*cxModAfterResponse)(nil) 49 | 50 | func (m *cxModAfterResponse) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: %s", m.Type()) } 51 | func (m *cxModAfterResponse) String() string { return "ctx_after_response" } 52 | func (m *cxModAfterResponse) Truth() starlark.Bool { return true } 53 | func (m *cxModAfterResponse) Type() string { return "ctx" } 54 | func (m *cxModAfterResponse) AttrNames() []string { return []string{"request", "response", "state"} } 55 | 56 | func (m *cxModAfterResponse) Freeze() { 57 | m.request.Freeze() 58 | m.response.Freeze() 59 | m.state.Freeze() 60 | } 61 | 62 | func (m *cxModAfterResponse) Attr(name string) (starlark.Value, error) { 63 | switch name { 64 | case "request": 65 | if m.accessedState { 66 | return nil, errors.New("cannot access ctx.request after accessing ctx.state") 67 | } 68 | return m.request, nil 69 | case "response": 70 | if m.accessedState { 71 | return nil, errors.New("cannot access ctx.response after accessing ctx.state") 72 | } 73 | return m.response, nil 74 | case "state": 75 | m.accessedState = true 76 | return m.state, nil 77 | default: 78 | return nil, nil // no such method 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /pkg/runtime/cx_mod_before_request.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "fmt" 5 | 6 | "go.starlark.net/starlark" 7 | ) 8 | 9 | // cxModBeforeRequest is the `ctx` starlark value accessible before executing a call 10 | type cxModBeforeRequest struct { 11 | request *cxRequestBeforeRequest 12 | // No response: this lives only before the request is attempted 13 | // No state: disallowed for now 14 | //TODO: specs 15 | } 16 | 17 | func newCxModBeforeRequest(req *cxRequestBeforeRequest) *cxModBeforeRequest { 18 | return &cxModBeforeRequest{ 19 | request: req, 20 | } 21 | } 22 | 23 | var _ starlark.HasAttrs = (*cxModBeforeRequest)(nil) 24 | 25 | // For `cx` values and subvalues everywhere: 26 | // * String() MUST roughly match Go type name 27 | // * Type() MUST be closer to Starlark land (shorter, more vague) 28 | 29 | func (m *cxModBeforeRequest) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: %s", m.Type()) } 30 | func (m *cxModBeforeRequest) String() string { return "ctx_before_request" } 31 | func (m *cxModBeforeRequest) Truth() starlark.Bool { return true } 32 | func (m *cxModBeforeRequest) Type() string { return "ctx" } 33 | func (m *cxModBeforeRequest) AttrNames() []string { return []string{"request"} } 34 | func (m *cxModBeforeRequest) Freeze() { m.request.Freeze() } 35 | 36 | func (m *cxModBeforeRequest) Attr(name string) (starlark.Value, error) { 37 | switch name { 38 | case "request": 39 | return m.request, nil 40 | default: 41 | return nil, nil // no such method 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pkg/runtime/cx_request_after_response.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "go.starlark.net/starlark" 8 | "google.golang.org/protobuf/types/known/structpb" 9 | 10 | "github.com/FuzzyMonkeyCo/monkey/pkg/internal/fm" 11 | "github.com/FuzzyMonkeyCo/monkey/pkg/starlarkvalue" 12 | ) 13 | 14 | // cxRequestAfterResponse is the `ctx.request` starlark value accessible after executing a call 15 | type cxRequestAfterResponse struct { 16 | ty string 17 | 18 | attrs starlark.StringDict 19 | attrnames []string 20 | 21 | protoBodyDecoded *structpb.Value 22 | body starlark.Value 23 | } 24 | 25 | func newCxRequestAfterResponse(i *fm.Clt_CallRequestRaw_Input) (cr *cxRequestAfterResponse) { 26 | switch x := i.GetInput().(type) { 27 | 28 | case *fm.Clt_CallRequestRaw_Input_HttpRequest_: 29 | cr = &cxRequestAfterResponse{ 30 | ty: cxRequestHttp, 31 | attrs: make(starlark.StringDict, 4), 32 | } 33 | 34 | reqProto := i.GetHttpRequest() 35 | cr.attrs["method"] = starlark.String(reqProto.Method) 36 | cr.attrs["url"] = starlark.String(reqProto.Url) 37 | cr.attrs["content"] = starlark.String(reqProto.Body) 38 | headers := newcxHead(reqProto.Headers) 39 | headers.Freeze() 40 | cr.attrs["headers"] = headers 41 | if reqProto.Body != nil { 42 | cr.protoBodyDecoded = reqProto.BodyDecoded 43 | } 44 | 45 | default: 46 | panic(fmt.Errorf("unhandled output %T: %+v", x, i)) 47 | } 48 | return 49 | } 50 | 51 | var _ starlark.HasAttrs = (*cxRequestAfterResponse)(nil) 52 | 53 | func (m *cxRequestAfterResponse) Hash() (uint32, error) { 54 | return 0, fmt.Errorf("unhashable: %s", m.Type()) 55 | } 56 | func (m *cxRequestAfterResponse) String() string { return "request_after_response" } 57 | func (m *cxRequestAfterResponse) Truth() starlark.Bool { return true } 58 | func (m *cxRequestAfterResponse) Type() string { return m.ty } 59 | 60 | func (m *cxRequestAfterResponse) Freeze() { 61 | m.attrs.Freeze() 62 | if m.body != nil { 63 | m.body.Freeze() 64 | } 65 | } 66 | 67 | func (m *cxRequestAfterResponse) AttrNames() []string { 68 | if m.attrnames == nil { 69 | names := m.attrs.Keys() 70 | if m.protoBodyDecoded != nil { 71 | names = append(names, "body") 72 | } 73 | sort.Strings(names) 74 | m.attrnames = names 75 | } 76 | return m.attrnames 77 | } 78 | 79 | func (m *cxRequestAfterResponse) Attr(name string) (starlark.Value, error) { 80 | switch { 81 | case name == "body" && m.protoBodyDecoded != nil: 82 | if m.body == nil { 83 | m.body = starlarkvalue.FromProtoValue(m.protoBodyDecoded) 84 | m.body.Freeze() 85 | } 86 | return m.body, nil 87 | default: 88 | return m.attrs[name], nil 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /pkg/runtime/cx_request_before_request.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | 8 | "go.starlark.net/starlark" 9 | "google.golang.org/protobuf/encoding/protojson" 10 | "google.golang.org/protobuf/types/known/structpb" 11 | 12 | "github.com/FuzzyMonkeyCo/monkey/pkg/internal/fm" 13 | "github.com/FuzzyMonkeyCo/monkey/pkg/starlarkvalue" 14 | ) 15 | 16 | const ( 17 | cxRequestHttp = "http_request" 18 | ) 19 | 20 | // cxRequestBeforeRequest is the `ctx.request` starlark value accessible before executing a call 21 | type cxRequestBeforeRequest struct { 22 | ty string 23 | 24 | method, url starlark.String 25 | headers *cxHead 26 | body starlark.Value 27 | } 28 | 29 | func newCxRequestBeforeRequest(input *fm.Srv_Call_Input) *cxRequestBeforeRequest { 30 | switch x := input.GetInput().(type) { 31 | 32 | case *fm.Srv_Call_Input_HttpRequest_: 33 | r := input.GetHttpRequest() 34 | // var bodyValue starlark.Value = starlark.None 35 | var bodyValue starlark.Value = nil 36 | if body := r.GetBody(); body != nil { 37 | bodyValue = starlarkvalue.FromProtoValue(body) 38 | } 39 | return &cxRequestBeforeRequest{ 40 | ty: cxRequestHttp, 41 | method: starlark.String(r.GetMethod()), 42 | url: starlark.String(r.GetUrl()), 43 | headers: newcxHead(r.GetHeaders()), 44 | //content: absent as encoding will only happen later 45 | body: bodyValue, 46 | } 47 | 48 | default: 49 | panic(fmt.Errorf("unhandled input %T: %+v", x, input)) 50 | } 51 | } 52 | 53 | func (cr *cxRequestBeforeRequest) IntoProto(err error) *fm.Clt_CallRequestRaw { 54 | var reason []string 55 | if err != nil { 56 | reason = strings.Split(err.Error(), "\n") 57 | } 58 | 59 | input := func() *fm.Clt_CallRequestRaw_Input { 60 | switch cr.ty { 61 | case cxRequestHttp: 62 | var body []byte 63 | var bodyDecoded *structpb.Value 64 | if cr.body != nil { 65 | bodyDecoded = starlarkvalue.ToProtoValue(cr.body) 66 | if body, err = protojson.Marshal(bodyDecoded); err != nil { 67 | log.Println("[ERR]", err) 68 | // return after sending msg 69 | if len(reason) == 0 && err != nil { 70 | reason = strings.Split(err.Error(), "\n") 71 | } 72 | } 73 | } 74 | return &fm.Clt_CallRequestRaw_Input{ 75 | Input: &fm.Clt_CallRequestRaw_Input_HttpRequest_{ 76 | HttpRequest: &fm.Clt_CallRequestRaw_Input_HttpRequest{ 77 | Method: string(cr.method), 78 | Url: string(cr.url), 79 | Headers: cr.headers.IntoProto(), 80 | Body: body, 81 | BodyDecoded: bodyDecoded, 82 | }}} 83 | 84 | default: 85 | panic(fmt.Errorf("unhandled input %s: %+v", cr.ty, cr)) 86 | 87 | } 88 | }() 89 | 90 | return &fm.Clt_CallRequestRaw{ 91 | Input: input, 92 | Reason: reason, 93 | } 94 | } 95 | 96 | var _ starlark.HasAttrs = (*cxRequestBeforeRequest)(nil) 97 | 98 | func (cr *cxRequestBeforeRequest) Hash() (uint32, error) { 99 | return 0, fmt.Errorf("unhashable: %s", cr.Type()) 100 | } 101 | func (cr *cxRequestBeforeRequest) String() string { return "request_before_request" } 102 | func (cr *cxRequestBeforeRequest) Truth() starlark.Bool { return true } 103 | func (cr *cxRequestBeforeRequest) Type() string { return cr.ty } 104 | 105 | func (cr *cxRequestBeforeRequest) Freeze() { 106 | if cr.body != nil { 107 | cr.body.Freeze() 108 | } 109 | cr.headers.Freeze() 110 | cr.method.Freeze() 111 | cr.url.Freeze() 112 | } 113 | 114 | func (cr *cxRequestBeforeRequest) AttrNames() []string { 115 | return []string{ // Keep 'em sorted 116 | "body", 117 | "headers", 118 | "method", 119 | "url", 120 | } 121 | } 122 | 123 | func (cr *cxRequestBeforeRequest) Attr(name string) (starlark.Value, error) { 124 | switch name { 125 | case "body": 126 | if cr.body != nil { 127 | return cr.body, nil 128 | } 129 | return starlark.None, nil 130 | case "headers": 131 | return cr.headers, nil 132 | case "method": 133 | return cr.method, nil 134 | case "url": 135 | return cr.url, nil 136 | default: 137 | return nil, nil // no such method 138 | } 139 | } 140 | 141 | var _ starlark.HasSetField = (*cxRequestBeforeRequest)(nil) 142 | 143 | func (cr *cxRequestBeforeRequest) SetField(name string, val starlark.Value) error { 144 | if name == "body" && cr.body == nil { 145 | cr.body = val 146 | } 147 | return nil 148 | } 149 | -------------------------------------------------------------------------------- /pkg/runtime/cx_response_after_response.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "go.starlark.net/starlark" 8 | "google.golang.org/protobuf/types/known/structpb" 9 | 10 | "github.com/FuzzyMonkeyCo/monkey/pkg/internal/fm" 11 | "github.com/FuzzyMonkeyCo/monkey/pkg/starlarkvalue" 12 | ) 13 | 14 | const ( 15 | cxResponseHttp = "http_response" 16 | ) 17 | 18 | // cxResponseAfterResponse is the `ctx.response` starlark value accessible after executing a call 19 | type cxResponseAfterResponse struct { 20 | ty string 21 | 22 | attrs starlark.StringDict 23 | attrnames []string 24 | 25 | protoBodyDecoded *structpb.Value 26 | body starlark.Value 27 | } 28 | 29 | func newCxResponseAfterResponse(o *fm.Clt_CallResponseRaw_Output) (cr *cxResponseAfterResponse) { 30 | switch x := o.GetOutput().(type) { 31 | 32 | case *fm.Clt_CallResponseRaw_Output_HttpResponse_: 33 | cr = &cxResponseAfterResponse{ 34 | ty: cxResponseHttp, 35 | attrs: make(starlark.StringDict, 6), 36 | } 37 | 38 | repProto := o.GetHttpResponse() 39 | cr.attrs["status_code"] = starlark.MakeUint(uint(repProto.StatusCode)) 40 | cr.attrs["reason"] = starlark.String(repProto.Reason) 41 | cr.attrs["content"] = starlark.String(repProto.Body) 42 | cr.attrs["elapsed_ns"] = starlark.MakeInt64(repProto.ElapsedNs) 43 | cr.attrs["elapsed_ms"] = starlark.MakeInt64(repProto.ElapsedNs / 1.e6) 44 | // "error": repProto.Error Checks make this unreachable 45 | // "history" :: []Rep (redirects)? 46 | headers := newcxHead(repProto.Headers) 47 | headers.Freeze() 48 | cr.attrs["headers"] = headers 49 | if repProto.Body != nil { 50 | cr.protoBodyDecoded = repProto.BodyDecoded 51 | } 52 | 53 | default: 54 | panic(fmt.Errorf("unhandled output %T: %+v", x, o)) 55 | } 56 | return 57 | } 58 | 59 | var _ starlark.HasAttrs = (*cxResponseAfterResponse)(nil) 60 | 61 | func (m *cxResponseAfterResponse) Hash() (uint32, error) { 62 | return 0, fmt.Errorf("unhashable: %s", m.Type()) 63 | } 64 | func (m *cxResponseAfterResponse) String() string { return "response_after_response" } 65 | func (m *cxResponseAfterResponse) Truth() starlark.Bool { return true } 66 | func (m *cxResponseAfterResponse) Type() string { return m.ty } 67 | 68 | func (m *cxResponseAfterResponse) Freeze() { 69 | m.attrs.Freeze() 70 | if m.body != nil { 71 | m.body.Freeze() 72 | } 73 | } 74 | 75 | func (m *cxResponseAfterResponse) AttrNames() []string { 76 | if m.attrnames == nil { 77 | names := m.attrs.Keys() 78 | if m.protoBodyDecoded != nil { 79 | names = append(names, "body") 80 | } 81 | sort.Strings(names) 82 | m.attrnames = names 83 | } 84 | return m.attrnames 85 | } 86 | 87 | func (m *cxResponseAfterResponse) Attr(name string) (starlark.Value, error) { 88 | switch { 89 | case name == "body" && m.protoBodyDecoded != nil: 90 | if m.body == nil { 91 | m.body = starlarkvalue.FromProtoValue(m.protoBodyDecoded) 92 | m.body.Freeze() 93 | } 94 | return m.body, nil 95 | default: 96 | return m.attrs[name], nil 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /pkg/runtime/endpoints.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "github.com/FuzzyMonkeyCo/monkey/pkg/internal/fm" 5 | "github.com/FuzzyMonkeyCo/monkey/pkg/modeler" 6 | ) 7 | 8 | // FilterEndpoints restricts which API endpoints are considered 9 | func (rt *Runtime) FilterEndpoints(criteria []string) error { 10 | return rt.forEachModel(func(name string, mdl modeler.Interface) (err error) { 11 | var eids []uint32 12 | if eids, err = mdl.FilterEndpoints(criteria); err != nil || len(eids) == 0 { 13 | return 14 | } 15 | 16 | if rt.selectedEIDs == nil { 17 | rt.selectedEIDs = make(map[string]*fm.Uint32S) 18 | } 19 | rt.selectedEIDs[name] = &fm.Uint32S{Values: eids} 20 | return 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /pkg/runtime/exec.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/FuzzyMonkeyCo/monkey/pkg/resetter" 7 | ) 8 | 9 | // JustExecStart only executes SUT 'start' 10 | func (rt *Runtime) JustExecStart(ctx context.Context) error { 11 | return rt.forEachResetter(func(name string, rsttr resetter.Interface) error { 12 | return rsttr.ExecStart(ctx, &osShower{}, true, rt.envRead) 13 | }) 14 | } 15 | 16 | // JustExecReset only executes SUT 'reset' which may be 'stop' followed by 'start' 17 | func (rt *Runtime) JustExecReset(ctx context.Context) error { 18 | return rt.forEachResetter(func(name string, rsttr resetter.Interface) error { 19 | return rsttr.ExecReset(ctx, &osShower{}, true, rt.envRead) 20 | }) 21 | } 22 | 23 | // JustExecStop only executes SUT 'stop' 24 | func (rt *Runtime) JustExecStop(ctx context.Context) error { 25 | return rt.forEachResetter(func(name string, rsttr resetter.Interface) error { 26 | return rsttr.ExecStop(ctx, &osShower{}, true, rt.envRead) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/runtime/exec_init.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os/user" 7 | "path" 8 | 9 | "github.com/chzyer/readline" 10 | "go.starlark.net/resolve" 11 | "go.starlark.net/starlark" 12 | 13 | "github.com/FuzzyMonkeyCo/monkey/pkg/starlarktruth" 14 | ) 15 | 16 | func loadDisabled(_th *starlark.Thread, _module string) (starlark.StringDict, error) { 17 | return nil, errors.New("load() disabled") 18 | } 19 | 20 | func initExec() { 21 | resolve.AllowSet = true // set([]) (no proto representation) 22 | resolve.AllowGlobalReassign = true // reassignment to top-level names 23 | //> Starlark programs cannot be Turing complete 24 | //> unless the -recursion flag is specified. 25 | resolve.AllowRecursion = false 26 | 27 | starlark.CompareLimit = 10 // Depth for (Equal|Compare)-ing things 28 | if starlarkCompareLimit > 0 { 29 | starlark.CompareLimit = starlarkCompareLimit // For tests 30 | } 31 | 32 | allow := map[string]struct{}{ 33 | "abs": {}, 34 | "all": {}, 35 | "any": {}, 36 | "bool": {}, 37 | "bytes": {}, 38 | "chr": {}, 39 | "dict": {}, 40 | "dir": {}, 41 | "enumerate": {}, 42 | "False": {}, 43 | "float": {}, 44 | "getattr": {}, 45 | "hasattr": {}, 46 | "hash": {}, 47 | "int": {}, 48 | "len": {}, 49 | "list": {}, 50 | "max": {}, 51 | "min": {}, 52 | "None": {}, 53 | "ord": {}, 54 | "print": {}, 55 | "range": {}, 56 | "repr": {}, 57 | "reversed": {}, 58 | "set": {}, 59 | "sorted": {}, 60 | "str": {}, 61 | "True": {}, 62 | "tuple": {}, 63 | "type": {}, 64 | "zip": {}, 65 | } 66 | deny := map[string]struct{}{ 67 | "fail": {}, 68 | } 69 | 70 | // Shortcut if already ran 71 | for denied := range deny { 72 | if _, ok := starlark.Universe[denied]; !ok { 73 | return 74 | } 75 | break 76 | } 77 | 78 | starlarktruth.NewModule(starlark.Universe) // Adds `assert that()` 79 | 80 | for f := range starlark.Universe { 81 | _, allowed := allow[f] 82 | _, denied := deny[f] 83 | switch { 84 | case allowed || f == starlarktruth.Module: 85 | case denied: 86 | delete(starlark.Universe, f) 87 | default: 88 | panic(fmt.Sprintf("unexpected builtin %q", f)) 89 | } 90 | } 91 | } 92 | 93 | // https://github.com/google/starlark-go/blob/243c74974e97462c5df21338e182470391748b04/doc/spec.md#built-in-methods 94 | 95 | var starlarkExtendedUniverse = map[string][]string{ 96 | "bytes": { 97 | "elems", 98 | }, 99 | 100 | "dict": { 101 | "clear", 102 | "get", 103 | "items", 104 | "keys", 105 | "pop", 106 | "popitem", 107 | "setdefault", 108 | "update", 109 | "values", 110 | }, 111 | 112 | "list": { 113 | "append", 114 | "clear", 115 | "extend", 116 | "index", 117 | "insert", 118 | "pop", 119 | "remove", 120 | }, 121 | 122 | "string": { 123 | "capitalize", 124 | "codepoint_ords", 125 | "codepoints", 126 | "count", 127 | "elem_ords", 128 | "elems", 129 | "endswith", 130 | "find", 131 | "format", 132 | "index", 133 | "isalnum", 134 | "isalpha", 135 | "isdigit", 136 | "islower", 137 | "isspace", 138 | "istitle", 139 | "isupper", 140 | "join", 141 | "lower", 142 | "lstrip", 143 | "partition", 144 | "removeprefix", 145 | "removesuffix", 146 | "replace", 147 | "rfind", 148 | "rindex", 149 | "rpartition", 150 | "rsplit", 151 | "rstrip", 152 | "split", 153 | "splitlines", 154 | "startswith", 155 | "strip", 156 | "title", 157 | "upper", 158 | }, 159 | 160 | "set": { 161 | "add", 162 | "clear", 163 | "difference", 164 | "discard", 165 | "intersection", 166 | "issubset", 167 | "issuperset", 168 | "pop", 169 | "remove", 170 | "symmetric_difference", 171 | "union", 172 | }, 173 | } 174 | 175 | const ( 176 | replPrompt = ">>> " 177 | replPromptSub = "... " 178 | replPromptFile = "" 179 | ) 180 | 181 | func newREPLConfig() (*readline.Config, error) { 182 | whoami, err := user.Current() 183 | if err != nil { 184 | return nil, err 185 | } 186 | 187 | // TODO: completer for methods of types + taylored for Python (not for a CLI) 188 | // (use starlarkExtendedUniverse) 189 | 190 | prefixes := make([]readline.PrefixCompleterInterface, 0, len(starlark.Universe)) 191 | prefixes = append(prefixes, readline.PcItem("assert")) 192 | for p := range starlark.Universe { 193 | if p == "assert" { 194 | continue 195 | } 196 | item := readline.PcItem(p + "(") 197 | if p == "True" || p == "False" { 198 | item = readline.PcItem(p) 199 | } 200 | prefixes = append(prefixes, item) 201 | } 202 | 203 | cfg := &readline.Config{ 204 | Prompt: replPrompt, 205 | HistoryFile: path.Join(whoami.HomeDir, ".fuzzymonkey_starlark_history"), 206 | HistorySearchFold: true, 207 | InterruptPrompt: "^C", 208 | EOFPrompt: "exit", 209 | FuncFilterInputRune: filterInput, 210 | AutoComplete: readline.NewPrefixCompleter(prefixes...), 211 | } 212 | return cfg, nil 213 | } 214 | -------------------------------------------------------------------------------- /pkg/runtime/exec_test.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "go.starlark.net/starlark" 8 | ) 9 | 10 | func TestOurStarlarkVerbs(t *testing.T) { 11 | for category, names := range starlarkExtendedUniverse { 12 | var got []string 13 | switch category { 14 | case "bytes": 15 | var x starlark.Bytes 16 | got = x.AttrNames() 17 | case "dict": 18 | var x *starlark.Dict 19 | got = x.AttrNames() 20 | case "list": 21 | var x *starlark.List 22 | got = x.AttrNames() 23 | case "string": 24 | var x starlark.String 25 | got = x.AttrNames() 26 | case "set": 27 | var x *starlark.Set 28 | got = x.AttrNames() 29 | default: 30 | panic(category) 31 | } 32 | require.EqualValues(t, names, got, category) 33 | } 34 | } 35 | 36 | func TestCompareLimit(t *testing.T) { 37 | defer func(prev int) { starlarkCompareLimit = prev }(starlarkCompareLimit) 38 | starlarkCompareLimit = 1 39 | 40 | _, err := newFakeMonkey(t, ` 41 | x = [[37]] 42 | assert that(x).is_equal_to(x) 43 | `[1:]+someOpenAPI3Model) 44 | 45 | require.Equal(t, 1, starlark.CompareLimit) 46 | 47 | require.EqualError(t, err, ` 48 | Traceback (most recent call last): 49 | fuzzymonkey.star:2:27: in 50 | Error in is_equal_to: comparison exceeded maximum recursion depth`[1:]) 51 | 52 | } 53 | -------------------------------------------------------------------------------- /pkg/runtime/fmt_test.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func ensureFormattedAs(t *testing.T, pre, post string) { 10 | t.Helper() 11 | 12 | starfileData = []byte(pre + " ") // trigger need for fmt 13 | err := Format(starFile, true) 14 | require.NoError(t, err) 15 | 16 | require.Equal(t, post, string(starfileData)) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/runtime/fmt_warnings_test.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/bazelbuild/buildtools/warn" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | // https://github.com/bazelbuild/buildtools/blob/f1ead6bc540dfa6ed95dfb4fe5f0c0bfc9243370/WARNINGS.md#buildifier-warnings 11 | 12 | func TestFmtWarnings(t *testing.T) { 13 | require.Subset(t, warn.AllWarnings, warn.DefaultWarnings) 14 | require.Subset(t, warn.AllWarnings, fmtWarningsList) 15 | require.IsIncreasing(t, fmtWarningsList) 16 | 17 | deny := []string{ 18 | "attr-cfg", 19 | "attr-license", 20 | "attr-non-empty", 21 | "attr-output-default", 22 | "attr-single-file", 23 | "ctx-actions", 24 | "ctx-args", 25 | "filetype", 26 | "git-repository", 27 | "http-archive", 28 | "load", 29 | "module-docstring", 30 | "native-android", 31 | "native-build", 32 | "native-cc", 33 | "native-java", 34 | "native-package", 35 | "native-proto", 36 | "native-py", 37 | "output-group", 38 | "package-name", 39 | "package-on-top", 40 | "provider-params", 41 | "repository-name", 42 | "rule-impl-return", 43 | } 44 | require.Subset(t, warn.AllWarnings, deny) 45 | require.IsIncreasing(t, deny) 46 | 47 | for _, w := range warn.AllWarnings { 48 | for _, d := range deny { 49 | if d == w { 50 | require.NotContains(t, fmtWarningsList, w) 51 | break 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pkg/runtime/for_eachs.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "sort" 7 | 8 | "go.starlark.net/starlark" 9 | "golang.org/x/sync/errgroup" 10 | 11 | "github.com/FuzzyMonkeyCo/monkey/pkg/modeler" 12 | "github.com/FuzzyMonkeyCo/monkey/pkg/resetter" 13 | ) 14 | 15 | func (rt *Runtime) forEachOfAnyCheck(f func(name string, chk *check) error) error { 16 | for _, name := range rt.checksNames { 17 | name, chk := name, rt.checks[name] 18 | if err := f(name, chk); err != nil { 19 | return err 20 | } 21 | } 22 | return nil 23 | } 24 | 25 | func (rt *Runtime) forEachBeforeRequestCheck(f func(name string, chk *check) error) error { 26 | return rt.forEachOfAnyCheck(func(name string, chk *check) (err error) { 27 | if chk.beforeRequest != nil { 28 | err = f(name, chk) 29 | } 30 | return 31 | }) 32 | } 33 | 34 | func (rt *Runtime) forEachAfterResponseCheck(f func(name string, chk *check) error) error { 35 | return rt.forEachOfAnyCheck(func(name string, chk *check) (err error) { 36 | if chk.afterResponse != nil { 37 | err = f(name, chk) 38 | } 39 | return 40 | }) 41 | } 42 | 43 | func (rt *Runtime) forEachModel(f func(name string, mdl modeler.Interface) error) error { 44 | for _, name := range rt.modelsNames { 45 | name, mdl := name, rt.models[name] 46 | if err := f(name, mdl); err != nil { 47 | return err 48 | } 49 | } 50 | return nil 51 | } 52 | 53 | func (rt *Runtime) forEachSelectedModel(f func(string, modeler.Interface) error) error { 54 | return rt.forEachModel(func(name string, mdl modeler.Interface) error { 55 | if _, ok := rt.selectedEIDs[name]; ok { 56 | return f(name, mdl) 57 | } 58 | return nil 59 | }) 60 | } 61 | 62 | func (rt *Runtime) forEachResetter(f func(name string, rsttr resetter.Interface) error) error { 63 | for _, name := range rt.resettersNames { 64 | name, rsttr := name, rt.resetters[name] 65 | if err := f(name, rsttr); err != nil { 66 | return err 67 | } 68 | } 69 | return nil 70 | } 71 | 72 | func (rt *Runtime) forEachSelectedResetter(ctx context.Context, f func(string, resetter.Interface) error) error { 73 | if rt.selectedResetters == nil { 74 | rt.selectedResetters = make(map[string]struct{}, len(rt.resetters)) 75 | _ = rt.forEachResetter(func(name string, rsttr resetter.Interface) error { 76 | for _, modelName := range rsttr.Provides() { 77 | if _, ok := rt.selectedEIDs[modelName]; ok { 78 | rt.selectedResetters[name] = struct{}{} 79 | break 80 | } 81 | } 82 | return nil 83 | }) 84 | } 85 | if len(rt.selectedResetters) == 0 { 86 | return errors.New("no resetter selected") 87 | } 88 | 89 | g, _ := errgroup.WithContext(ctx) 90 | _ = rt.forEachResetter(func(name string, rsttr resetter.Interface) error { 91 | if _, ok := rt.selectedResetters[name]; ok { 92 | g.Go(func() error { 93 | return f(name, rsttr) 94 | }) 95 | } 96 | return nil 97 | }) 98 | return g.Wait() 99 | } 100 | 101 | func (rt *Runtime) forEachGlobal(f func(name string, value starlark.Value) error) error { 102 | names := make([]string, 0, len(rt.globals)) 103 | for name := range rt.globals { 104 | names = append(names, name) 105 | } 106 | sort.Strings(names) 107 | for _, name := range names { 108 | name, value := name, rt.globals[name] 109 | if err := f(name, value); err != nil { 110 | return err 111 | } 112 | } 113 | return nil 114 | } 115 | 116 | func (rt *Runtime) forEachEnvRead(f func(name string, value string) error) error { 117 | names := make([]string, 0, len(rt.envRead)) 118 | for name := range rt.envRead { 119 | names = append(names, name) 120 | } 121 | sort.Strings(names) 122 | for _, name := range names { 123 | name, value := name, rt.envRead[name] 124 | if err := f(name, value); err != nil { 125 | return err 126 | } 127 | } 128 | return nil 129 | } 130 | -------------------------------------------------------------------------------- /pkg/runtime/inputs.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/FuzzyMonkeyCo/monkey/pkg/modeler" 7 | ) 8 | 9 | // InputsCount sums the amount of named schemas or types APIs define 10 | func (rt *Runtime) InputsCount() int { 11 | count := 0 12 | 13 | _ = rt.forEachModel(func(name string, mdl modeler.Interface) error { 14 | count += mdl.InputsCount() 15 | return nil 16 | }) 17 | 18 | return count 19 | } 20 | 21 | // WriteAbsoluteReferences pretty-prints the API's named types 22 | func (rt *Runtime) WriteAbsoluteReferences(w io.Writer) { 23 | _ = rt.forEachModel(func(name string, mdl modeler.Interface) error { 24 | mdl.WriteAbsoluteReferences(w) 25 | return nil 26 | }) 27 | } 28 | 29 | // ValidateAgainstSchema tries to smash the data through the given keyhole 30 | func (rt *Runtime) ValidateAgainstSchema(absRef string, data []byte) (err error) { 31 | count := 0 32 | 33 | err = rt.forEachModel(func(name string, mdl modeler.Interface) error { 34 | err := mdl.ValidateAgainstSchema(absRef, data) 35 | // TODO: support >1 models (MAY validate against schema of wrong mdl) 36 | if _, ok := err.(*modeler.NoSuchRefError); ok { 37 | count++ 38 | err = nil 39 | } 40 | return err 41 | }) 42 | 43 | if count == len(rt.modelsNames) { 44 | err = modeler.NewNoSuchRefError(absRef) 45 | } 46 | return 47 | } 48 | -------------------------------------------------------------------------------- /pkg/runtime/lint.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/FuzzyMonkeyCo/monkey/pkg/modeler" 7 | ) 8 | 9 | // Lint goes through specs and unsures they are valid 10 | func (rt *Runtime) Lint(ctx context.Context, showSpec bool) error { 11 | return rt.forEachModel(func(name string, mdl modeler.Interface) error { 12 | return mdl.Lint(ctx, showSpec) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /pkg/runtime/module.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | 8 | "go.starlark.net/starlark" 9 | 10 | "github.com/FuzzyMonkeyCo/monkey/pkg/modeler" 11 | openapi3 "github.com/FuzzyMonkeyCo/monkey/pkg/modeler/openapiv3" 12 | "github.com/FuzzyMonkeyCo/monkey/pkg/resetter" 13 | "github.com/FuzzyMonkeyCo/monkey/pkg/resetter/shell" 14 | "github.com/FuzzyMonkeyCo/monkey/pkg/tags" 15 | ) 16 | 17 | const ( 18 | moduleBuiltins = 2 19 | moduleModelers = 1 20 | moduleResetters = 1 21 | 22 | moduleAttrs = moduleBuiltins + moduleModelers + moduleResetters 23 | ) 24 | 25 | type module struct { 26 | attrs map[string]*starlark.Builtin 27 | } 28 | 29 | var _ starlark.HasAttrs = (*module)(nil) 30 | 31 | func (rt *Runtime) newModule() (m *module) { 32 | m = &module{ 33 | attrs: make(map[string]*starlark.Builtin, moduleAttrs), 34 | } 35 | 36 | modelMaker := func(modelerName string, maker modeler.Maker) *starlark.Builtin { 37 | f := rt.modelMakerBuiltin(modelerName, maker) 38 | b := starlark.NewBuiltin(modelerName, f) 39 | return b.BindReceiver(m) 40 | } 41 | m.attrs["openapi3"] = modelMaker(openapi3.Name, openapi3.New) 42 | 43 | resetterMaker := func(resetterName string, maker resetter.Maker) *starlark.Builtin { 44 | f := rt.resetterMakerBuiltin(resetterName, maker) 45 | b := starlark.NewBuiltin(resetterName, f) 46 | return b.BindReceiver(m) 47 | } 48 | m.attrs["shell"] = resetterMaker(shell.Name, shell.New) 49 | 50 | m.attrs["check"] = starlark.NewBuiltin("check", rt.bCheck).BindReceiver(m) 51 | m.attrs["env"] = starlark.NewBuiltin("env", rt.bEnv).BindReceiver(m) 52 | return 53 | } 54 | 55 | func (m *module) AttrNames() []string { 56 | return []string{ 57 | "check", 58 | "env", 59 | "openapi3", 60 | "shell", 61 | } 62 | } 63 | 64 | func (m *module) Attr(name string) (starlark.Value, error) { 65 | if v := m.attrs[name]; v != nil { 66 | return v, nil 67 | } 68 | return nil, nil // no such method 69 | } 70 | 71 | func (m *module) String() string { return "monkey" } 72 | func (m *module) Type() string { return "monkey" } 73 | func (m *module) Freeze() {} 74 | func (m *module) Truth() starlark.Bool { return true } 75 | func (m *module) Hash() (uint32, error) { return 0, errors.New("unhashable type: monkey") } 76 | 77 | type builtin func(th *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (ret starlark.Value, err error) 78 | 79 | func (rt *Runtime) modelMakerBuiltin(modelerName string, maker modeler.Maker) builtin { 80 | return func(th *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (ret starlark.Value, err error) { 81 | log.Printf("[DBG] registering new %s model", modelerName) 82 | ret = starlark.None 83 | 84 | if len(args) != 0 { 85 | err = fmt.Errorf("%s(...) does not take positional arguments, only named ones", b.Name()) 86 | log.Println("[ERR]", err) 87 | return 88 | } 89 | 90 | var model modeler.Interface 91 | if model, err = maker(kwargs); err != nil { 92 | log.Println("[ERR]", b.Name(), err) 93 | return 94 | } 95 | 96 | modelName := model.Name() 97 | 98 | if err = tags.LegalName(modelName); err != nil { 99 | log.Println("[ERR]", b.Name(), err) 100 | return 101 | } 102 | 103 | if _, ok := rt.models[modelName]; ok { 104 | err = fmt.Errorf("a model named %s already exists", modelName) 105 | log.Println("[ERR]", err) 106 | return 107 | } 108 | rt.models[modelName] = model 109 | log.Printf("[NFO] registered %s: %q", b.Name(), modelName) 110 | if len(rt.modelsNames) == 1 { // TODO: support >1 models 111 | err = fmt.Errorf("cannot define model %s as another (%s) already exists", modelName, rt.modelsNames[0]) 112 | log.Println("[ERR]", err) 113 | return 114 | } 115 | rt.modelsNames = append(rt.modelsNames, modelName) 116 | return 117 | } 118 | } 119 | 120 | func (rt *Runtime) resetterMakerBuiltin(resetterName string, maker resetter.Maker) builtin { 121 | return func(th *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (ret starlark.Value, err error) { 122 | log.Printf("[DBG] registering new %s resetter", resetterName) 123 | ret = starlark.None 124 | 125 | if len(args) != 0 { 126 | err = fmt.Errorf("%s(...) does not take positional arguments, only named ones", b.Name()) 127 | log.Println("[ERR]", err) 128 | return 129 | } 130 | 131 | var rsttr resetter.Interface 132 | if rsttr, err = maker(kwargs); err != nil { 133 | log.Println("[ERR]", b.Name(), err) 134 | return 135 | } 136 | 137 | rsttrName := rsttr.Name() 138 | 139 | if err = tags.LegalName(rsttrName); err != nil { 140 | log.Println("[ERR]", b.Name(), err) 141 | return 142 | } 143 | 144 | if _, ok := rt.resetters[rsttrName]; ok { 145 | err = fmt.Errorf("a resetter named %s already exists", rsttrName) 146 | log.Println("[ERR]", err) 147 | return 148 | } 149 | rt.resetters[rsttrName] = rsttr 150 | log.Printf("[NFO] registered %s: %q", b.Name(), rsttrName) 151 | rt.resettersNames = append(rt.resettersNames, rsttrName) 152 | return 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /pkg/runtime/module_test.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestMonkeyModuleAttrsCount(t *testing.T) { 10 | names := (*module)(nil).AttrNames() 11 | require.Len(t, names, moduleAttrs) 12 | } 13 | 14 | func TestMonkeyModuleAttrsNamesAreInOrder(t *testing.T) { 15 | names := (*module)(nil).AttrNames() 16 | require.IsIncreasing(t, names) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/runtime/monkey_check_cancellation_test.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestCheckCancellationLongtime(t *testing.T) { 12 | rt, err := newFakeMonkey(t, ` 13 | monkey.check( 14 | name = "takes_a_very_long_time", 15 | after_response = lambda ctx: monkeh_sleep(10), 16 | ) 17 | `[1:]+someOpenAPI3Model) 18 | require.NoError(t, err) 19 | require.Len(t, rt.checks, 1) 20 | 21 | ctx := context.Background() 22 | 23 | start := time.Now() 24 | passed, err := rt.fakeUserChecks(ctx, t) 25 | require.GreaterOrEqual(t, time.Since(start), 10*time.Millisecond) 26 | 27 | require.NoError(t, err) 28 | require.True(t, passed) 29 | } 30 | 31 | func TestCheckCancellationLongtimeOut(t *testing.T) { 32 | rt, err := newFakeMonkey(t, ` 33 | monkey.check( 34 | name = "takes_a_very_long_time", 35 | after_response = lambda ctx: monkeh_sleep(10), 36 | ) 37 | `[1:]+someOpenAPI3Model) 38 | require.NoError(t, err) 39 | require.Len(t, rt.checks, 1) 40 | 41 | ctx := context.Background() 42 | ctx, cancel := context.WithTimeout(ctx, 5*time.Millisecond) 43 | defer cancel() 44 | 45 | start := time.Now() 46 | passed, err := rt.fakeUserChecks(ctx, t) 47 | require.Less(t, time.Since(start), 10*time.Millisecond) 48 | 49 | require.Error(t, err, context.DeadlineExceeded) 50 | require.False(t, passed) 51 | } 52 | 53 | func TestCheckCancellationRunsAllChecksToCompletion(t *testing.T) { 54 | rt, err := newFakeMonkey(t, ` 55 | monkey.check( 56 | name = "always_fails", 57 | after_response = lambda ctx: assert that(42).is_none(), 58 | ) 59 | monkey.check( 60 | name = "takes_a_very_long_time", 61 | after_response = lambda ctx: monkeh_sleep(10), 62 | ) 63 | `[1:]+someOpenAPI3Model) 64 | require.NoError(t, err) 65 | require.Len(t, rt.checks, 2) 66 | 67 | ctx := context.Background() 68 | 69 | start := time.Now() 70 | passed, err := rt.fakeUserChecks(ctx, t) 71 | require.GreaterOrEqual(t, time.Since(start), 10*time.Millisecond) 72 | 73 | require.NoError(t, err) 74 | require.False(t, passed) 75 | } 76 | 77 | func TestCheckCancellationToCompletionButWithinTimeout(t *testing.T) { 78 | rt, err := newFakeMonkey(t, ` 79 | monkey.check( 80 | name = "always_fails", 81 | after_response = lambda ctx: assert that(42).is_none(), 82 | ) 83 | monkey.check( 84 | name = "takes_a_very_long_time", 85 | after_response = lambda ctx: monkeh_sleep(10), 86 | ) 87 | `[1:]+someOpenAPI3Model) 88 | require.NoError(t, err) 89 | require.Len(t, rt.checks, 2) 90 | 91 | ctx := context.Background() 92 | ctx, cancel := context.WithTimeout(ctx, 5*time.Millisecond) 93 | defer cancel() 94 | 95 | start := time.Now() 96 | passed, err := rt.fakeUserChecks(ctx, t) 97 | require.Less(t, time.Since(start), 10*time.Millisecond) 98 | 99 | require.Error(t, err, context.DeadlineExceeded) 100 | require.False(t, passed) 101 | } 102 | -------------------------------------------------------------------------------- /pkg/runtime/monkey_env.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "go.starlark.net/starlark" 9 | ) 10 | 11 | func (rt *Runtime) bEnv(th *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 12 | var env starlark.String 13 | var def starlark.Value 14 | if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &env, &def); err != nil { 15 | return nil, err 16 | } 17 | 18 | var defStr string 19 | if def != nil { 20 | if d, ok := def.(starlark.String); ok { 21 | defStr = d.GoString() 22 | } else { 23 | return nil, fmt.Errorf("expected string, got %s: %s", def.Type(), def.String()) 24 | } 25 | } 26 | envStr := env.GoString() 27 | 28 | if cachedStr, ok := rt.envRead[envStr]; ok { 29 | log.Printf("[NFO] read (cached) env %q", envStr) 30 | return starlark.String(cachedStr), nil 31 | } 32 | 33 | if read, ok := os.LookupEnv(envStr); ok { 34 | rt.envRead[envStr] = read 35 | log.Printf("[NFO] read env %q: %q", envStr, read) 36 | return starlark.String(read), nil 37 | } 38 | 39 | if def == nil { 40 | return nil, fmt.Errorf("unset environment variable: %q", envStr) 41 | } 42 | 43 | rt.envRead[envStr] = defStr 44 | log.Printf("[NFO] read (unset) env %q: %q", envStr, defStr) 45 | return def, nil 46 | } 47 | -------------------------------------------------------------------------------- /pkg/runtime/monkey_env_test.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | "go.starlark.net/starlark" 9 | ) 10 | 11 | func TestEnvReadsVar(t *testing.T) { 12 | err := os.Setenv("SOME_VAR", "42") 13 | require.NoError(t, err) 14 | defer func() { 15 | err := os.Unsetenv("SOME_VAR") 16 | require.NoError(t, err) 17 | }() 18 | 19 | rt, err := newFakeMonkey(t, ` 20 | value1 = monkey.env("SOME_VAR") 21 | value2 = monkey.env("SOME_VAR") 22 | `[1:]+someOpenAPI3Model) 23 | require.NoError(t, err) 24 | require.Equal(t, rt.globals["value1"], starlark.String("42")) 25 | require.Equal(t, rt.globals["value2"], starlark.String("42")) 26 | } 27 | 28 | func TestEnvReadsVarButIncorrectDefault(t *testing.T) { 29 | err := os.Setenv("SOME_VAR", "42") 30 | require.NoError(t, err) 31 | defer func() { 32 | err := os.Unsetenv("SOME_VAR") 33 | require.NoError(t, err) 34 | }() 35 | 36 | rt, err := newFakeMonkey(t, ` 37 | value = monkey.env("SOME_VAR", None) 38 | `[1:]+someOpenAPI3Model) 39 | require.EqualError(t, err, ` 40 | Traceback (most recent call last): 41 | fuzzymonkey.star:1:19: in 42 | Error in env: expected string, got NoneType: None`[1:]) 43 | require.Nil(t, rt) 44 | } 45 | 46 | func TestEnvReadsVarGoodDefault(t *testing.T) { 47 | err := os.Setenv("SOME_VAR", "42") 48 | require.NoError(t, err) 49 | defer func() { 50 | err := os.Unsetenv("SOME_VAR") 51 | require.NoError(t, err) 52 | }() 53 | 54 | rt, err := newFakeMonkey(t, ` 55 | value = monkey.env("SOME_VAR", "") 56 | `[1:]+someOpenAPI3Model) 57 | require.NoError(t, err) 58 | require.Equal(t, rt.globals["value"], starlark.String("42")) 59 | } 60 | 61 | func TestEnvUnsetVarNoDefault(t *testing.T) { 62 | rt, err := newFakeMonkey(t, ` 63 | value = monkey.env("SOME_VAR") 64 | `[1:]+someOpenAPI3Model) 65 | require.EqualError(t, err, ` 66 | Traceback (most recent call last): 67 | fuzzymonkey.star:1:19: in 68 | Error in env: unset environment variable: "SOME_VAR"`[1:]) 69 | require.Nil(t, rt) 70 | } 71 | 72 | func TestEnvUnsetVarWithDefault(t *testing.T) { 73 | rt, err := newFakeMonkey(t, ` 74 | value = monkey.env("SOME_VAR", "orelse") 75 | `[1:]+someOpenAPI3Model) 76 | require.NoError(t, err) 77 | require.Equal(t, rt.globals["value"], starlark.String("orelse")) 78 | } 79 | -------------------------------------------------------------------------------- /pkg/runtime/monkey_openapi3_test.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/FuzzyMonkeyCo/monkey/pkg/tags" 9 | ) 10 | 11 | // generic over modelers 12 | 13 | func TestModelMissingIsForbidden(t *testing.T) { 14 | rt, err := newFakeMonkey(t, ` 15 | print("Hullo") 16 | `[1:]) 17 | require.EqualError(t, err, `no models registered`) 18 | require.Nil(t, rt) 19 | } 20 | 21 | func TestModelPositionalArgsAreForbidden(t *testing.T) { 22 | rt, err := newFakeMonkey(t, ` 23 | monkey.openapi3("hi", name = "bla") 24 | `[1:]) 25 | require.EqualError(t, err, ` 26 | Traceback (most recent call last): 27 | fuzzymonkey.star:1:16: in 28 | Error in openapi3: openapi3(...) does not take positional arguments, only named ones`[1:]) 29 | require.Nil(t, rt) 30 | } 31 | 32 | func TestModelNamesMustBeLegal(t *testing.T) { 33 | rt, err := newFakeMonkey(t, ` 34 | monkey.openapi3( 35 | name = "blip blop", 36 | file = "some/api_spec.yml", 37 | ) 38 | `[1:]) 39 | require.EqualError(t, err, ` 40 | Traceback (most recent call last): 41 | fuzzymonkey.star:1:16: in 42 | Error in openapi3: only characters from `[1:]+tags.Alphabet+` should be in "blip blop"`) 43 | require.Nil(t, rt) 44 | } 45 | 46 | func TestModelNamesMustBeUnique(t *testing.T) { 47 | rt, err := newFakeMonkey(t, ` 48 | monkey.openapi3( 49 | name = "blip", 50 | file = "some/api_spec.yml", 51 | ) 52 | monkey.openapi3( 53 | name = "blip", 54 | file = "some/api_spec.yml", 55 | ) 56 | `[1:]) 57 | require.EqualError(t, err, ` 58 | Traceback (most recent call last): 59 | fuzzymonkey.star:5:16: in 60 | Error in openapi3: a model named blip already exists`[1:]) 61 | require.Nil(t, rt) 62 | 63 | rt, err = newFakeMonkey(t, ` 64 | monkey.openapi3( 65 | name = "blip", 66 | file = "some/api_spec.yml", 67 | ) 68 | monkey.openapi3( 69 | name = "blop", 70 | file = "some/api_spec.yml", 71 | ) 72 | `[1:]) 73 | require.EqualError(t, err, ` 74 | Traceback (most recent call last): 75 | fuzzymonkey.star:5:16: in 76 | Error in openapi3: cannot define model blop as another (blip) already exists`[1:]) 77 | require.Nil(t, rt) 78 | } 79 | 80 | // name 81 | 82 | func TestOpenapi3NameIsRequired(t *testing.T) { 83 | rt, err := newFakeMonkey(t, ` 84 | monkey.openapi3( 85 | file = "some/api_spec.yml", 86 | ) 87 | `[1:]) 88 | require.EqualError(t, err, ` 89 | Traceback (most recent call last): 90 | fuzzymonkey.star:1:16: in 91 | Error in openapi3: openapi3: missing argument for name`[1:]) 92 | require.Nil(t, rt) 93 | } 94 | 95 | func TestOpenapi3NameTyping(t *testing.T) { 96 | rt, err := newFakeMonkey(t, ` 97 | monkey.openapi3( 98 | name = 42.1337, 99 | ) 100 | `[1:]) 101 | require.EqualError(t, err, ` 102 | Traceback (most recent call last): 103 | fuzzymonkey.star:1:16: in 104 | Error in openapi3: openapi3: for parameter "name": got float, want string`[1:]) 105 | require.Nil(t, rt) 106 | } 107 | 108 | // kwargs 109 | 110 | func TestOpenapi3AdditionalKwardsForbidden(t *testing.T) { 111 | rt, err := newFakeMonkey(t, ` 112 | monkey.openapi3( 113 | name = "mything", 114 | wef = "bla", 115 | ) 116 | `[1:]) 117 | require.EqualError(t, err, ` 118 | Traceback (most recent call last): 119 | fuzzymonkey.star:1:16: in 120 | Error in openapi3: openapi3: unexpected keyword argument "wef"`[1:]) 121 | require.Nil(t, rt) 122 | } 123 | 124 | // kwarg: file 125 | 126 | func TestOpenapi3FileIsRequired(t *testing.T) { 127 | rt, err := newFakeMonkey(t, ` 128 | monkey.openapi3( 129 | name = "some_name", 130 | ) 131 | `[1:]) 132 | require.EqualError(t, err, ` 133 | Traceback (most recent call last): 134 | fuzzymonkey.star:1:16: in 135 | Error in openapi3: openapi3: missing argument for file`[1:]) 136 | require.Nil(t, rt) 137 | } 138 | 139 | func TestOpenapi3FileTyping(t *testing.T) { 140 | rt, err := newFakeMonkey(t, ` 141 | monkey.openapi3( 142 | name = "some_name", 143 | file = 42.1337, 144 | ) 145 | `[1:]) 146 | require.EqualError(t, err, ` 147 | Traceback (most recent call last): 148 | fuzzymonkey.star:1:16: in 149 | Error in openapi3: openapi3: for parameter "file": got float, want string`[1:]) 150 | require.Nil(t, rt) 151 | } 152 | 153 | // kwarg: host 154 | 155 | func TestOpenapi3HostTyping(t *testing.T) { 156 | rt, err := newFakeMonkey(t, ` 157 | monkey.openapi3( 158 | name = "some_name", 159 | file = "some/api_spec.yml", 160 | host = 42.1337, 161 | ) 162 | `[1:]) 163 | require.EqualError(t, err, ` 164 | Traceback (most recent call last): 165 | fuzzymonkey.star:1:16: in 166 | Error in openapi3: openapi3: for parameter "host": got float, want string`[1:]) 167 | require.Nil(t, rt) 168 | } 169 | 170 | // kwarg: header_authorization 171 | 172 | func TestOpenapi3HeaderAuthorizationTyping(t *testing.T) { 173 | rt, err := newFakeMonkey(t, ` 174 | monkey.openapi3( 175 | name = "some_name", 176 | file = "some/api_spec.yml", 177 | header_authorization = 42.1337, 178 | ) 179 | `[1:]) 180 | require.EqualError(t, err, ` 181 | Traceback (most recent call last): 182 | fuzzymonkey.star:1:16: in 183 | Error in openapi3: openapi3: for parameter "header_authorization": got float, want string`[1:]) 184 | require.Nil(t, rt) 185 | } 186 | -------------------------------------------------------------------------------- /pkg/runtime/monkey_starfile_test.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestStarfileCanBeChanged(t *testing.T) { 11 | defer func(prev string) { starFile = prev }(starFile) 12 | starFile = "fm.star" 13 | 14 | code := someOpenAPI3Model[1:] 15 | rt, err := newFakeMonkey(t, code) 16 | require.NoError(t, err) 17 | 18 | ctx := context.Background() 19 | passed, err := rt.fakeUserChecks(ctx, t) 20 | require.NoError(t, err) 21 | require.True(t, passed) 22 | 23 | ensureFormattedAs(t, code, code) 24 | } 25 | 26 | func TestStarfileCanBeChangedAndShowsUpInBT(t *testing.T) { 27 | defer func(prev string) { starFile = prev }(starFile) 28 | starFile = "bla.star" 29 | 30 | code := ` 31 | monkey.check( 32 | name = "some_check_has_typo", 33 | after_reponse = lambda ctx: 42, 34 | ) 35 | `[1:] + someOpenAPI3Model 36 | 37 | _, err := newFakeMonkey(t, code) 38 | require.EqualError(t, err, ` 39 | Traceback (most recent call last): 40 | bla.star:1:13: in 41 | Error in check: check: unexpected keyword argument "after_reponse" (did you mean after_response?)`[1:]) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/runtime/os_shower.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/FuzzyMonkeyCo/monkey/pkg/progresser" 8 | ) 9 | 10 | var _ progresser.Shower = &osShower{} 11 | 12 | type osShower struct{} 13 | 14 | // Printf formats informational data 15 | func (p *osShower) Printf(format string, s ...interface{}) { 16 | _, _ = fmt.Fprintf(os.Stdout, format+"\n", s...) 17 | } 18 | 19 | // Errorf formats error messages 20 | func (p *osShower) Errorf(format string, s ...interface{}) { 21 | _, _ = fmt.Fprintf(os.Stderr, format+"\n", s...) 22 | } 23 | -------------------------------------------------------------------------------- /pkg/runtime/progress.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/FuzzyMonkeyCo/monkey/pkg/internal/fm" 9 | "github.com/FuzzyMonkeyCo/monkey/pkg/progresser/bar" 10 | "github.com/FuzzyMonkeyCo/monkey/pkg/progresser/ci" 11 | "github.com/FuzzyMonkeyCo/monkey/pkg/progresser/dots" 12 | ) 13 | 14 | func (rt *Runtime) newProgress(ctx context.Context, max uint32, vvv uint8, ptype string) (err error) { 15 | if ptype == "" { 16 | if vvv != 0 { 17 | ptype = "ci" 18 | } else { 19 | ptype = "dots" 20 | } 21 | log.Printf("[NFO] using --progress=%s", ptype) 22 | } 23 | switch ptype { 24 | case "bar": 25 | rt.progress = &bar.Progresser{} 26 | case "ci": 27 | rt.progress = &ci.Progresser{} 28 | if vvv == 0 { 29 | vvv = 3 // lowest level: DBG 30 | } 31 | case "dots": 32 | rt.progress = &dots.Progresser{} 33 | default: 34 | err = fmt.Errorf("unexpected progresser %q", ptype) 35 | log.Println("[ERR]", err) 36 | return 37 | } 38 | rt.progress.WithContext(ctx) 39 | rt.progress.MaxTestsCount(max) 40 | return 41 | } 42 | 43 | func (rt *Runtime) recvFuzzingProgress(ctx context.Context) (err error) { 44 | log.Println("[DBG] receiving fm.Srv_FuzzingProgress...") 45 | var srv *fm.Srv 46 | if srv, err = rt.client.Receive(ctx); err != nil { 47 | log.Println("[ERR]", err) 48 | return 49 | } 50 | fp := srv.GetFuzzingProgress() 51 | if fp == nil { 52 | err = fmt.Errorf("empty Srv_FuzzingProgress: %+v", srv) 53 | log.Println("[ERR]", err) 54 | return 55 | } 56 | rt.fuzzingProgress(fp) 57 | return 58 | } 59 | 60 | func (rt *Runtime) fuzzingProgress(fp *fm.Srv_FuzzingProgress) { 61 | log.Println("[DBG] srvprogress:", fp) 62 | rt.progress.TotalTestsCount(fp.GetTotalTestsCount()) 63 | rt.progress.TotalCallsCount(fp.GetTotalCallsCount()) 64 | rt.progress.TotalChecksCount(fp.GetTotalChecksCount()) 65 | rt.progress.TestCallsCount(fp.GetTestCallsCount()) 66 | rt.progress.CallChecksCount(fp.GetCallChecksCount()) 67 | rt.lastFuzzingProgress = fp 68 | } 69 | -------------------------------------------------------------------------------- /pkg/runtime/reset.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "strings" 7 | "time" 8 | 9 | "github.com/FuzzyMonkeyCo/monkey/pkg/as" 10 | "github.com/FuzzyMonkeyCo/monkey/pkg/internal/fm" 11 | "github.com/FuzzyMonkeyCo/monkey/pkg/resetter" 12 | ) 13 | 14 | // Cleanup ensures that resetters are terminated 15 | func (rt *Runtime) Cleanup(ctx context.Context) (err error) { 16 | as.ColorNFO.Println() 17 | as.ColorWRN.Printf("Ran for %s.\n", time.Since(rt.fuzzingStartedAt)) 18 | if rt.cleanedup { 19 | return 20 | } 21 | as.ColorNFO.Println("Cleaning up...") 22 | 23 | log.Println("[NFO] terminating resetter") 24 | if errR := rt.forEachSelectedResetter(ctx, func(name string, rsttr resetter.Interface) error { 25 | return rsttr.Terminate(ctx, &osShower{}, rt.envRead) 26 | }); errR != nil { 27 | err = errR 28 | // Keep going 29 | } 30 | 31 | rt.cleanedup = true 32 | return 33 | } 34 | 35 | func (rt *Runtime) reset(ctx context.Context) (errL, errT error) { 36 | const showp = "Resetting system under test..." 37 | rt.progress.Printf(showp + "\n") 38 | 39 | rp := func(msg *fm.Clt_ResetProgress) *fm.Clt { 40 | return &fm.Clt{Msg: &fm.Clt_ResetProgress_{ResetProgress: msg}} 41 | } 42 | 43 | if errT = rt.client.Send(ctx, rp(&fm.Clt_ResetProgress{ 44 | Status: fm.Clt_ResetProgress_started, 45 | })); errT != nil { 46 | log.Println("[ERR]", errT) 47 | return 48 | } 49 | 50 | start := time.Now() 51 | errL = rt.runReset(ctx) 52 | elapsed := time.Since(start).Nanoseconds() 53 | if errL != nil { 54 | log.Println("[ERR] exec'd:", errL) 55 | 56 | var reason []string 57 | if resetErr, ok := errL.(*resetter.Error); ok { 58 | reason = resetErr.Reason() 59 | } else { 60 | reason = strings.Split(errL.Error(), "\n") 61 | } 62 | 63 | if errT = rt.client.Send(ctx, rp(&fm.Clt_ResetProgress{ 64 | Status: fm.Clt_ResetProgress_failed, 65 | ElapsedNs: elapsed, 66 | Reason: reason, 67 | })); errT != nil { 68 | log.Println("[ERR]", errT) 69 | return 70 | } 71 | 72 | if strings.Contains(errL.Error(), context.Canceled.Error()) { 73 | rt.progress.Errorf(showp + " failed! (timed out)\n") 74 | } else { 75 | rt.progress.Errorf(showp + " failed!\n") 76 | } 77 | return 78 | } 79 | 80 | if errT = rt.client.Send(ctx, rp(&fm.Clt_ResetProgress{ 81 | Status: fm.Clt_ResetProgress_ended, 82 | ElapsedNs: elapsed, 83 | })); errT != nil { 84 | log.Println("[ERR]", errT) 85 | return 86 | } 87 | 88 | rt.progress.Printf(showp + " done.\n") 89 | return 90 | } 91 | 92 | func (rt *Runtime) runReset(ctx context.Context) (err error) { 93 | if err = rt.forEachAfterResponseCheck(func(name string, chk *check) error { 94 | if err := chk.reset(name); err != nil { 95 | log.Println("[ERR]", err) 96 | return err 97 | } 98 | return nil 99 | }); err != nil { 100 | return 101 | } 102 | log.Println("[NFO] re-initialized model state") 103 | 104 | return rt.forEachSelectedResetter(ctx, func(name string, rsttr resetter.Interface) error { 105 | return rsttr.ExecReset(ctx, rt.progress, false, rt.envRead) 106 | }) 107 | } 108 | -------------------------------------------------------------------------------- /pkg/runtime/starfiledata.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | var starlarkCompareLimit int 8 | 9 | var starfileData []byte 10 | 11 | func starfiledata(starfile string) (data []byte, err error) { 12 | data = starfileData 13 | if starfileData == nil { // When not mocking 14 | if data, err = os.ReadFile(starfile); err != nil { 15 | return 16 | } 17 | } 18 | 19 | data = starTrick(data) 20 | return 21 | } 22 | -------------------------------------------------------------------------------- /pkg/runtime/startrick.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | // The Starlark Trick is to allow using `assert that(x).y(z)` 4 | // when Starlark does not treat `assert` as a statement (a la Python) 5 | // but as a value: `assert.that(x).y(z)` 6 | 7 | import ( 8 | "bytes" 9 | "errors" 10 | "regexp" 11 | "strings" 12 | 13 | "go.starlark.net/starlark" 14 | 15 | "github.com/FuzzyMonkeyCo/monkey/pkg/starlarktruth" 16 | ) 17 | 18 | func starTrickError(err error) error { 19 | trick := func(data string) string { 20 | return strings.Replace(data, ": assert.that(", ": assert that(", 1) 21 | } 22 | switch e := err.(type) { 23 | case *starlark.EvalError: 24 | return errors.New(trick(e.Backtrace())) // No way to build an error of same type 25 | case *starlarktruth.UnresolvedError: 26 | const prefix = "Traceback (most recent call last):\n " 27 | return &starlarktruth.UnresolvedError{Msg: prefix + trick(e.Error())} 28 | default: 29 | return err 30 | } 31 | } 32 | 33 | var startrick = regexp.MustCompile(`(^|\n|[^"']+)\s*assert\s+that\s*`) 34 | 35 | func starTrick(data []byte) []byte { 36 | return startrick.ReplaceAllFunc(data, starTrickFunc) 37 | } 38 | 39 | func starTrickPerLine(data []byte) { 40 | if bytes.ContainsRune(data, '#') { 41 | return 42 | } 43 | 44 | fixes := []string{" ", "\t"} 45 | for _, prefix := range fixes { 46 | for _, suffix := range fixes { 47 | if bytes.HasPrefix(data, []byte("assert"+suffix)) { 48 | data[len("assert"+suffix)-1] = '.' 49 | } 50 | 51 | if k := bytes.Index(data, []byte(prefix+"assert"+suffix)); k > -1 { 52 | data[k+len(prefix+"assert"+suffix)-1] = '.' 53 | } 54 | } 55 | } 56 | } 57 | 58 | func starTrickFunc(data []byte) []byte { 59 | for i := 0; ; { 60 | n := bytes.IndexAny(data[i:], "\n\r") 61 | if n < 0 && len(data[i:]) > 0 { 62 | starTrickPerLine(data[i:]) 63 | break 64 | } 65 | starTrickPerLine(data[i : i+n]) 66 | i += n + 1 67 | } 68 | return data 69 | } 70 | 71 | var startrickdual = regexp.MustCompile(`(^|\n|[^"']+)\s*assert[.]that[(]`) 72 | 73 | func starTrickDual(data []byte) []byte { 74 | return startrickdual.ReplaceAllFunc(data, starTrickDualFunc) 75 | } 76 | 77 | func starTrickDualFunc(data []byte) []byte { 78 | return bytes.ReplaceAll(data, []byte("assert.that("), []byte("assert that(")) 79 | } 80 | -------------------------------------------------------------------------------- /pkg/runtime/startrick_test.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestAssertTrickAtRoot(t *testing.T) { 10 | code := ` 11 | assert that("this") 12 | assert that("that") 13 | assert that("too") 14 | `[1:] 15 | _, err := newFakeMonkey(nil, code) 16 | require.EqualError(t, err, ` 17 | Traceback (most recent call last): 18 | fuzzymonkey.star:2:15: in 19 | Error in that: fuzzymonkey.star:1:12: assert that(...) is missing an assertion`[1:]) 20 | ensureFormattedAs(t, code, ` 21 | assert that("this") 22 | assert that("that") 23 | assert that("too") 24 | `[1:]) 25 | 26 | _, err = newFakeMonkey(t, ` 27 | assert that("this").is_equal_to("that") 28 | `[1:]) 29 | require.EqualError(t, err, ` 30 | Traceback (most recent call last): 31 | fuzzymonkey.star:1:32: in 32 | Error in is_equal_to: Not true that <"this"> is equal to <"that">.`[1:]) 33 | } 34 | 35 | func TestAssertTrick(t *testing.T) { 36 | code := ` 37 | def some_check(ctx): 38 | assert that(ctx).is_not_none() 39 | 40 | monkey.check( 41 | name = "some_check", 42 | after_response = some_check, 43 | ) 44 | `[1:] + someOpenAPI3Model 45 | rt, err := newFakeMonkey(t, code) 46 | require.NoError(t, err) 47 | require.Len(t, rt.checks, 1) 48 | 49 | ensureFormattedAs(t, code, code) 50 | } 51 | 52 | func TestAssertTrickSpaces(t *testing.T) { 53 | ensureFormattedAs(t, ` 54 | assert that(42) 55 | assert that(42) 56 | if True: 57 | assert that(42) 58 | x = lambda y: assert that(42) 59 | 60 | # Well, assert that(42) 61 | # assert that("this").is_equal_to("that") 62 | p[42] = lambda q: assert that(42) 63 | `[1:], ` 64 | assert that(42) 65 | assert that(42) 66 | if True: 67 | assert that(42) 68 | x = lambda y: assert that(42) 69 | 70 | # Well, assert that(42) 71 | # assert that("this").is_equal_to("that") 72 | p[42] = lambda q: assert that(42) 73 | `[1:]) 74 | } 75 | 76 | func TestAssertTrickNaked(t *testing.T) { 77 | _, err := newFakeMonkey(nil, `assert 42`) 78 | require.EqualError(t, err, `fuzzymonkey.star:1:10: got int literal, want newline`) 79 | 80 | _, err = newFakeMonkey(nil, `assert True`) 81 | require.EqualError(t, err, `fuzzymonkey.star:1:13: got identifier, want newline`) 82 | } 83 | -------------------------------------------------------------------------------- /pkg/runtime/summary.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | // TestingCampaignOutcomer describes a testing campaign's results 4 | type TestingCampaignOutcomer interface { 5 | error 6 | isTestingCampaignOutcomer() 7 | } 8 | 9 | var _ TestingCampaignOutcomer = (*TestingCampaignSuccess)(nil) 10 | var _ TestingCampaignOutcomer = (*TestingCampaignFailure)(nil) 11 | var _ TestingCampaignOutcomer = (*TestingCampaignFailureDueToResetterError)(nil) 12 | 13 | // TestingCampaignSuccess indicates no bug was found during fuzzing. 14 | type TestingCampaignSuccess struct{} 15 | 16 | // TestingCampaignFailure indicates a bug was found during fuzzing. 17 | type TestingCampaignFailure struct{} 18 | 19 | // TestingCampaignFailureDueToResetterError indicates a bug was found during reset. 20 | type TestingCampaignFailureDueToResetterError struct{} 21 | 22 | func (tc *TestingCampaignSuccess) Error() string { return "Found no bug" } 23 | func (tc *TestingCampaignFailure) Error() string { return "Found a bug" } 24 | func (tc *TestingCampaignFailureDueToResetterError) Error() string { 25 | return "Something went wrong while resetting the system to a neutral state." 26 | } 27 | 28 | func (tc *TestingCampaignSuccess) isTestingCampaignOutcomer() {} 29 | func (tc *TestingCampaignFailure) isTestingCampaignOutcomer() {} 30 | func (tc *TestingCampaignFailureDueToResetterError) isTestingCampaignOutcomer() {} 31 | 32 | func plural(s string, n uint32) string { 33 | if n == 1 { 34 | return s 35 | } 36 | return s + "s" 37 | } 38 | -------------------------------------------------------------------------------- /pkg/runtime/user_error.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type userError string 8 | 9 | var _ error = userError("") 10 | 11 | func newUserError(f string, a ...interface{}) userError { return userError(fmt.Sprintf(f, a...)) } 12 | func (e userError) Error() string { return string(e) } 13 | -------------------------------------------------------------------------------- /pkg/starlarkclone/clone.go: -------------------------------------------------------------------------------- 1 | package starlarkclone 2 | 3 | import ( 4 | "fmt" 5 | 6 | "go.starlark.net/starlark" 7 | ) 8 | 9 | // A Cloner defines custom Value copying. 10 | // See Clone. 11 | type Cloner interface { 12 | Clone() (starlark.Value, error) 13 | } 14 | 15 | // Clone returns a copy of the given value. 16 | func Clone(value starlark.Value) (dst starlark.Value, err error) { 17 | switch src := value.(type) { 18 | case Cloner: 19 | return src.Clone() 20 | 21 | case starlark.NoneType, starlark.Bool, starlark.Float: 22 | dst = src 23 | return 24 | 25 | case starlark.Int: 26 | dst = starlark.MakeBigInt(src.BigInt()) 27 | return 28 | 29 | case starlark.String: 30 | dst = starlark.String(src.GoString()) 31 | return 32 | 33 | case *starlark.List: 34 | n := src.Len() 35 | vs := make([]starlark.Value, 0, n) 36 | for i := 0; i < n; i++ { 37 | var v starlark.Value 38 | if v, err = Clone(src.Index(i)); err != nil { 39 | return 40 | } 41 | vs = append(vs, v) 42 | } 43 | dst = starlark.NewList(vs) 44 | return 45 | 46 | case starlark.Tuple: 47 | n := src.Len() 48 | vs := make([]starlark.Value, 0, n) 49 | for i := 0; i < n; i++ { 50 | var v starlark.Value 51 | if v, err = Clone(src.Index(i)); err != nil { 52 | return 53 | } 54 | vs = append(vs, v) 55 | } 56 | dst = starlark.Tuple(vs) 57 | return 58 | 59 | case *starlark.Dict: 60 | vs := starlark.NewDict(src.Len()) 61 | for _, kv := range src.Items() { 62 | var k, v starlark.Value 63 | if k, err = Clone(kv.Index(0)); err != nil { 64 | return 65 | } 66 | if v, err = Clone(kv.Index(1)); err != nil { 67 | return 68 | } 69 | if err = vs.SetKey(k, v); err != nil { 70 | return 71 | } 72 | } 73 | dst = vs 74 | return 75 | 76 | default: 77 | err = fmt.Errorf("un-Clone-able value of type %s: %s", value.Type(), value.String()) 78 | return 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /pkg/starlarkclone/clone_test.go: -------------------------------------------------------------------------------- 1 | package starlarkclone 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "go.starlark.net/starlark" 8 | ) 9 | 10 | func TestStarlarkValueClone(t *testing.T) { 11 | type testcase struct { 12 | value starlark.Value 13 | edit func(starlark.Value) 14 | } 15 | 16 | for someCaseName, someCase := range map[string]*testcase{ 17 | "replace an item of a list within a tuple": { 18 | value: starlark.Tuple([]starlark.Value{ 19 | starlark.String("blip"), 20 | starlark.Tuple([]starlark.Value{starlark.String("blop")}), 21 | starlark.NewList([]starlark.Value{starlark.String("blap")}), 22 | }), 23 | edit: func(v starlark.Value) { 24 | t := v.(starlark.Tuple) 25 | vv := t[2] 26 | ll := vv.(*starlark.List) 27 | ll.SetIndex(0, starlark.Bool(true)) 28 | }, 29 | }, 30 | "replace an item of a list within a list": { 31 | value: starlark.NewList([]starlark.Value{ 32 | starlark.String("blip"), 33 | starlark.Tuple([]starlark.Value{starlark.String("blop")}), 34 | starlark.NewList([]starlark.Value{starlark.String("blap")}), 35 | }), 36 | edit: func(v starlark.Value) { 37 | l := v.(*starlark.List) 38 | vv := l.Index(2) 39 | ll := vv.(*starlark.List) 40 | err := ll.SetIndex(0, starlark.Bool(true)) 41 | require.NoError(t, err) 42 | }, 43 | }, 44 | "delete a value of a dict": { 45 | value: func() starlark.Value { 46 | someDict := starlark.NewDict(2) 47 | err := someDict.SetKey(starlark.String("key"), starlark.String("value")) 48 | require.NoError(t, err) 49 | someOtherDict := starlark.NewDict(3) 50 | err = someOtherDict.SetKey(starlark.String("a"), starlark.Bool(true)) 51 | require.NoError(t, err) 52 | err = someOtherDict.SetKey(starlark.String("b"), starlark.MakeInt(42)) 53 | require.NoError(t, err) 54 | err = someOtherDict.SetKey(starlark.String("c"), starlark.Float(4.2)) 55 | require.NoError(t, err) 56 | err = someDict.SetKey(starlark.String("k"), someOtherDict) 57 | require.NoError(t, err) 58 | return someDict 59 | }(), 60 | edit: func(v starlark.Value) { 61 | d := v.(*starlark.Dict) 62 | _, found, err := d.Delete(starlark.String("key")) 63 | require.NoError(t, err) 64 | require.True(t, found) 65 | }, 66 | }, 67 | "delete a value of a dict within a dict": { 68 | value: func() starlark.Value { 69 | someDict := starlark.NewDict(2) 70 | err := someDict.SetKey(starlark.String("key"), starlark.String("value")) 71 | require.NoError(t, err) 72 | someOtherDict := starlark.NewDict(3) 73 | err = someOtherDict.SetKey(starlark.String("a"), starlark.Bool(true)) 74 | require.NoError(t, err) 75 | err = someOtherDict.SetKey(starlark.String("b"), starlark.MakeInt(42)) 76 | require.NoError(t, err) 77 | err = someOtherDict.SetKey(starlark.String("c"), starlark.Float(4.2)) 78 | require.NoError(t, err) 79 | err = someDict.SetKey(starlark.String("k"), someOtherDict) 80 | require.NoError(t, err) 81 | return someDict 82 | }(), 83 | edit: func(v starlark.Value) { 84 | d := v.(*starlark.Dict) 85 | vv, found, err := d.Get(starlark.String("k")) 86 | require.NoError(t, err) 87 | require.True(t, found) 88 | dd := vv.(*starlark.Dict) 89 | _, found, err = dd.Delete(starlark.String("c")) 90 | require.NoError(t, err) 91 | require.True(t, found) 92 | }, 93 | }, 94 | } { 95 | t.Run(someCaseName, func(t *testing.T) { 96 | repr := someCase.value.String() 97 | require.NotEmpty(t, repr) 98 | t.Logf("repr: %s", repr) 99 | cloned, err := Clone(someCase.value) 100 | require.NoError(t, err) 101 | t.Logf("A cloned value shares its representation") 102 | require.Equal(t, repr, cloned.String()) 103 | 104 | someCase.edit(cloned) 105 | t.Logf("Editing a cloned value changes its representation...") 106 | require.NotEqual(t, repr, cloned.String()) 107 | t.Logf("... but does not change the original value's representation.") 108 | require.Equal(t, repr, someCase.value.String()) 109 | }) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /pkg/starlarktruth/README.md: -------------------------------------------------------------------------------- 1 | # [package starlarktruth](github.com/FuzzyMonkeyCo/monkey/pkg/starlarktruth) 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/FuzzyMonkeyCo/monkey/pkg/starlarktruth.svg)](https://pkg.go.dev/github.com/FuzzyMonkeyCo/monkey/pkg/starlarktruth) 4 | 5 | Package starlarktruth defines builtins and methods to express test 6 | assertions within Starlark programs in the fashion of https://truth.dev 7 | 8 | This package is a Starlark [port of PyTruth](https://github.com/google/pytruth/compare/master...2c3717ddad) 9 | -------------------------------------------------------------------------------- /pkg/starlarktruth/bools_test.go: -------------------------------------------------------------------------------- 1 | package starlarktruth 2 | 3 | import "testing" 4 | 5 | func TestTrue(t *testing.T) { 6 | testEach(t, map[string]error{ 7 | `that(True).is_true()`: nil, 8 | `that(True).is_false()`: fail("True", "is False"), 9 | }) 10 | } 11 | 12 | func TestFalse(t *testing.T) { 13 | testEach(t, map[string]error{ 14 | `that(False).is_false()`: nil, 15 | `that(False).is_true()`: fail("False", "is True"), 16 | }) 17 | } 18 | 19 | func TestTruthyThings(t *testing.T) { 20 | values := []string{ 21 | `1`, 22 | `True`, 23 | `2.5`, 24 | `"Hi"`, 25 | `[3]`, 26 | `{4: "four"}`, 27 | `("my", "tuple")`, 28 | `set([5])`, 29 | `-1`, 30 | } 31 | m := make(map[string]error, 4*len(values)) 32 | for _, v := range values { 33 | m[`that(`+v+`).is_truthy()`] = nil 34 | m[`that(`+v+`).is_falsy()`] = fail(v, "is falsy") 35 | m[`that(`+v+`).is_false()`] = fail(v, "is False") 36 | if v != `True` { 37 | m[`that(`+v+`).is_true()`] = fail(v, "is True", 38 | " However, it is truthy. Did you mean to call .is_truthy() instead?") 39 | } 40 | } 41 | testEach(t, m) 42 | } 43 | 44 | func TestFalsyThings(t *testing.T) { 45 | values := []string{ 46 | `None`, 47 | `False`, 48 | `0`, 49 | `0.0`, 50 | `""`, 51 | `()`, // tuple 52 | `[]`, 53 | `{}`, 54 | `set()`, 55 | } 56 | m := make(map[string]error, 4*len(values)) 57 | for _, v := range values { 58 | vv := v 59 | if v == `set()` { 60 | vv = `set([])` 61 | } 62 | m[`that(`+v+`).is_falsy()`] = nil 63 | m[`that(`+v+`).is_truthy()`] = fail(vv, "is truthy") 64 | m[`that(`+v+`).is_true()`] = fail(vv, "is True") 65 | if v != `False` { 66 | m[`that(`+v+`).is_false()`] = fail(vv, "is False", 67 | " However, it is falsy. Did you mean to call .is_falsy() instead?") 68 | } 69 | } 70 | testEach(t, m) 71 | } 72 | -------------------------------------------------------------------------------- /pkg/starlarktruth/cmp_test.go: -------------------------------------------------------------------------------- 1 | package starlarktruth 2 | 3 | import "testing" 4 | 5 | func TestIsAtLeast(t *testing.T) { 6 | testEach(t, map[string]error{ 7 | `that(5).is_at_least(3)`: nil, 8 | `that(5).is_at_least(5)`: nil, 9 | `that(5).is_at_least(8)`: fail("5", "is at least <8>"), 10 | }) 11 | } 12 | 13 | func TestIsAtMost(t *testing.T) { 14 | testEach(t, map[string]error{ 15 | `that(5).is_at_most(5)`: nil, 16 | `that(5).is_at_most(8)`: nil, 17 | `that(5).is_at_most(3)`: fail("5", "is at most <3>"), 18 | }) 19 | } 20 | 21 | func TestIsGreaterThan(t *testing.T) { 22 | testEach(t, map[string]error{ 23 | `that(5).is_greater_than(3)`: nil, 24 | `that(5).is_greater_than(5)`: fail("5", "is greater than <5>"), 25 | `that(5).is_greater_than(8)`: fail("5", "is greater than <8>"), 26 | }) 27 | } 28 | 29 | func TestIsLessThan(t *testing.T) { 30 | testEach(t, map[string]error{ 31 | `that(5).is_less_than(8)`: nil, 32 | `that(5).is_less_than(5)`: fail("5", "is less than <5>"), 33 | `that(5).is_less_than(3)`: fail("5", "is less than <3>"), 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/starlarktruth/dupe_counter.go: -------------------------------------------------------------------------------- 1 | package starlarktruth 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "go.starlark.net/starlark" 8 | ) 9 | 10 | var _ fmt.Stringer = (*duplicateCounter)(nil) 11 | 12 | // duplicateCounter is a synchronized collection of counters for tracking duplicates. 13 | // 14 | // The count values may be modified only through Increment() and Decrement(), 15 | // which increment and decrement by 1 (only). If a count ever becomes 0, the item 16 | // is immediately expunged from the dictionary. Counts can never be negative; 17 | // attempting to Decrement an absent key has no effect. 18 | // 19 | // Order is preserved so that error messages containing expected values match. 20 | // 21 | // Supports counting values based on their (starlark.Value).String() representation. 22 | // TODO: track hashable objects in a hashmap. Use a slice and equality for non-hashables. 23 | type duplicateCounter struct { 24 | m map[string]uint 25 | s []string 26 | d uint 27 | } 28 | 29 | func newDuplicateCounter() *duplicateCounter { 30 | return &duplicateCounter{ 31 | m: make(map[string]uint), 32 | } 33 | } 34 | 35 | // HasDupes indicates whether there are values that appears > 1 times. 36 | func (dc *duplicateCounter) HasDupes() bool { return dc.d != 0 } 37 | 38 | func (dc *duplicateCounter) Empty() bool { return len(dc.m) == 0 } 39 | 40 | func (dc *duplicateCounter) Len() int { return len(dc.m) } 41 | 42 | func (dc *duplicateCounter) Contains(v starlark.Value) bool { 43 | return dc.contains(v.String()) 44 | } 45 | 46 | func (dc *duplicateCounter) contains(v string) bool { 47 | _, ok := dc.m[v] 48 | return ok 49 | } 50 | 51 | // Increment increments a count by 1. Inserts the item if not present. 52 | func (dc *duplicateCounter) Increment(v starlark.Value) { 53 | dc.increment(v.String()) 54 | } 55 | 56 | func (dc *duplicateCounter) increment(v string) { 57 | if _, ok := dc.m[v]; !ok { 58 | dc.m[v] = 0 59 | dc.s = append(dc.s, v) 60 | } 61 | dc.m[v]++ 62 | if dc.m[v] == 2 { 63 | dc.d++ 64 | } 65 | } 66 | 67 | // Decrement decrements a count by 1. Expunges the item if the count is 0. 68 | // If the item is not present, has no effect. 69 | func (dc *duplicateCounter) Decrement(v starlark.Value) { 70 | dc.decrement(v.String()) 71 | } 72 | 73 | func (dc *duplicateCounter) decrement(v string) { 74 | if count, ok := dc.m[v]; ok { 75 | if count != 1 { 76 | dc.m[v]-- 77 | if dc.m[v] == 1 { 78 | dc.d-- 79 | } 80 | return 81 | } 82 | delete(dc.m, v) 83 | if sz := len(dc.s); sz != 1 { 84 | s := make([]string, 0, len(dc.s)-1) 85 | for _, vv := range dc.s { 86 | if vv != v { 87 | s = append(s, vv) 88 | } 89 | } 90 | dc.s = s 91 | } else { 92 | dc.s = nil 93 | } 94 | } 95 | } 96 | 97 | // Returns the string representation of the duplicate counts. 98 | // 99 | // Items occurring more than once are accompanied by their count. 100 | // Otherwise the count is implied to be 1. 101 | // 102 | // For example, if the internal dict is `{2: 1, 3: 4, "abc": 1}`, this returns 103 | // the string `2, 3 [4 copies], "abc"`. 104 | func (dc *duplicateCounter) String() string { 105 | var b strings.Builder 106 | for i, vv := range dc.s { 107 | if i != 0 { 108 | b.WriteString(", ") 109 | } 110 | 111 | b.WriteString(vv) 112 | if count := dc.m[vv]; count != 1 { 113 | b.WriteString(" [") 114 | b.WriteString(fmt.Sprintf("%d", count)) 115 | b.WriteString(" copies]") 116 | } 117 | } 118 | return b.String() 119 | } 120 | 121 | // Dupes shows only items whose count > 1. 122 | func (dc *duplicateCounter) Dupes() string { 123 | var b strings.Builder 124 | first := true 125 | for _, vv := range dc.s { 126 | if count := dc.m[vv]; count != 1 { 127 | if !first { 128 | b.WriteString(", ") 129 | } 130 | first = false 131 | 132 | b.WriteString(vv) 133 | b.WriteString(" [") 134 | b.WriteString(fmt.Sprintf("%d", count)) 135 | b.WriteString(" copies]") 136 | } 137 | } 138 | return b.String() 139 | } 140 | -------------------------------------------------------------------------------- /pkg/starlarktruth/dupe_counter_test.go: -------------------------------------------------------------------------------- 1 | package starlarktruth 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "go.starlark.net/starlark" 8 | ) 9 | 10 | var ( 11 | a = starlark.String("a") 12 | b = starlark.String("b") 13 | 14 | alist = starlark.NewList([]starlark.Value{a}) 15 | emptylist = starlark.NewList([]starlark.Value{}) 16 | ) 17 | 18 | func maker(t *testing.T) starlark.Value { 19 | t.Helper() 20 | d := starlark.NewDict(1) 21 | err := d.SetKey(starlark.String("a"), starlark.NewList([]starlark.Value{ 22 | starlark.String("b"), 23 | starlark.String("c"), 24 | })) 25 | require.NoError(t, err) 26 | return d 27 | } 28 | 29 | func TestDupeCounterContains(t *testing.T) { 30 | d := newDuplicateCounter() // {} 31 | require.False(t, d.Contains(a)) 32 | require.False(t, d.Contains(b)) 33 | 34 | d.Increment(a) // {'a': 1} 35 | require.True(t, d.Contains(a)) 36 | require.False(t, d.Contains(b)) 37 | 38 | d.Decrement(a) // {} 39 | require.False(t, d.Contains(a)) 40 | require.False(t, d.Contains(b)) 41 | } 42 | 43 | func TestDupeCounterLen(t *testing.T) { 44 | d := newDuplicateCounter() 45 | require.True(t, d.Empty()) 46 | d.Increment(a) 47 | require.Equal(t, 1, d.Len()) 48 | d.Increment(alist) 49 | require.Equal(t, 2, d.Len()) 50 | } 51 | 52 | func TestDupeCounterEverything(t *testing.T) { 53 | d := newDuplicateCounter() // {} 54 | require.True(t, d.Empty()) 55 | require.Equal(t, ``, d.String()) 56 | 57 | d.Increment(a) // {'a': 1} 58 | require.Equal(t, 1, d.Len()) 59 | require.Equal(t, `"a"`, d.String()) 60 | 61 | d.Increment(a) // {'a': 2} 62 | require.Equal(t, 1, d.Len()) 63 | require.Equal(t, `"a" [2 copies]`, d.String()) 64 | 65 | d.Increment(b) // {'a': 2, 'b': 1} 66 | require.Equal(t, 2, d.Len()) 67 | require.Equal(t, `"a" [2 copies], "b"`, d.String()) 68 | 69 | d.Decrement(a) // {'a': 1, 'b': 1} 70 | require.Equal(t, 2, d.Len()) 71 | require.Equal(t, `"a", "b"`, d.String()) 72 | 73 | d.Decrement(a) // {'b': 1} 74 | require.Equal(t, 1, d.Len()) 75 | require.Equal(t, `"b"`, d.String()) 76 | 77 | d.Increment(a) // {'b': 1, 'a': 1} 78 | require.Equal(t, 2, d.Len()) 79 | require.Equal(t, `"b", "a"`, d.String()) 80 | 81 | d.Decrement(a) // {'b': 1} 82 | require.Equal(t, 1, d.Len()) 83 | require.Equal(t, `"b"`, d.String()) 84 | 85 | d.Decrement(b) // {} 86 | require.True(t, d.Empty()) 87 | require.Equal(t, ``, d.String()) 88 | 89 | d.Decrement(a) // {} 90 | require.True(t, d.Empty()) 91 | require.Equal(t, ``, d.String()) 92 | } 93 | 94 | func TestDupeCounterUnhashableKeys(t *testing.T) { 95 | d := newDuplicateCounter() // {} 96 | require.False(t, d.Contains(emptylist)) 97 | 98 | d.Increment(alist) // {['a']: 1} 99 | require.True(t, d.Contains(alist)) 100 | require.Equal(t, 1, d.Len()) 101 | require.Equal(t, `["a"]`, d.String()) 102 | 103 | d.Decrement(alist) // {} 104 | require.False(t, d.Contains(alist)) 105 | require.True(t, d.Empty()) 106 | require.Equal(t, ``, d.String()) 107 | } 108 | 109 | func TestDupeCounterIncrementEquivalentDictionaries(t *testing.T) { 110 | d := newDuplicateCounter() 111 | d.Increment(maker(t)) 112 | d.Increment(maker(t)) 113 | d.Increment(maker(t)) 114 | d.Increment(maker(t)) 115 | require.Equal(t, 1, d.Len()) 116 | require.Equal(t, `{"a": ["b", "c"]} [4 copies]`, d.String()) 117 | } 118 | 119 | func TestDupeCounterDecrementEquivalentDictionaries(t *testing.T) { 120 | d := newDuplicateCounter() 121 | d.Increment(maker(t)) 122 | d.Increment(maker(t)) 123 | require.Equal(t, 1, d.Len()) 124 | d.Decrement(maker(t)) 125 | require.Equal(t, 1, d.Len()) 126 | d.Decrement(maker(t)) 127 | require.True(t, d.Empty()) 128 | d.Decrement(maker(t)) 129 | require.True(t, d.Empty()) 130 | } 131 | -------------------------------------------------------------------------------- /pkg/starlarktruth/errors.go: -------------------------------------------------------------------------------- 1 | package starlarktruth 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "go.starlark.net/starlark" 8 | ) 9 | 10 | // InvalidAssertion signifies an invalid assertion was attempted 11 | // such as comparing with None. 12 | type InvalidAssertion string 13 | 14 | var _ error = InvalidAssertion("") 15 | 16 | func newInvalidAssertion(prop string) InvalidAssertion { return InvalidAssertion(prop) } 17 | func (e InvalidAssertion) Error() string { return string(e) } 18 | 19 | // TruthAssertion signifies an assertion predicate was invalidated. 20 | type TruthAssertion string 21 | 22 | var _ error = TruthAssertion("") 23 | 24 | func newTruthAssertion(msg string) TruthAssertion { return TruthAssertion(msg) } 25 | func (e TruthAssertion) Error() string { return string(e) } 26 | 27 | // unhandled internal & public errors 28 | 29 | const errUnhandled = unhandledError(0) 30 | 31 | type unhandledError int 32 | 33 | var _ error = errUnhandled 34 | 35 | func (e unhandledError) Error() string { return "unhandled" } 36 | 37 | // UnhandledError appears when an operation on an incompatible type is attempted. 38 | type UnhandledError struct { 39 | name string 40 | actual starlark.Value 41 | args starlark.Tuple 42 | } 43 | 44 | var _ error = (*UnhandledError)(nil) 45 | 46 | func (t *T) unhandled(name string, args ...starlark.Value) *UnhandledError { 47 | return &UnhandledError{ 48 | name: name, 49 | actual: t.actual, 50 | args: args, 51 | } 52 | } 53 | 54 | func (e UnhandledError) Error() string { 55 | var b strings.Builder 56 | b.WriteString("Invalid assertion .") 57 | b.WriteString(e.name) 58 | b.WriteByte('(') 59 | for i, arg := range e.args { 60 | if i != 0 { 61 | b.WriteString(", ") 62 | } 63 | b.WriteString(arg.String()) 64 | } 65 | b.WriteString(") on value of type ") 66 | b.WriteString(e.actual.Type()) 67 | return b.String() 68 | } 69 | 70 | // UnresolvedError describes that an `assert.that(actual)` was called but never any of its `.truth_methods(subject)`. 71 | // At the exception of (as each by themselves this still require an assertion): 72 | // * `.named(name)` 73 | // * `.is_within(tolerance)` 74 | // * `.is_not_within(tolerance)` 75 | type UnresolvedError struct { 76 | // Msg is the error string 77 | Msg string 78 | } 79 | 80 | var _ error = &UnresolvedError{} 81 | 82 | func (e *UnresolvedError) Error() string { return e.Msg } 83 | 84 | func newUnresolvedError(e string) *UnresolvedError { 85 | return &UnresolvedError{ 86 | Msg: fmt.Sprintf("%s: %s.%s(...) is missing an assertion", e, Module, Method), 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /pkg/starlarktruth/example_test.go: -------------------------------------------------------------------------------- 1 | package starlarktruth_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "go.starlark.net/starlark" 7 | 8 | "github.com/FuzzyMonkeyCo/monkey/pkg/starlarktruth" 9 | ) 10 | 11 | func Example() { 12 | starlarktruth.Module = "should" 13 | starlarktruth.Method = "verify" 14 | 15 | starlarktruth.NewModule(starlark.Universe) 16 | 17 | data := ` 18 | squares = [x*x for x in range(10)] 19 | should.verify(squares).has_size(10) 20 | 21 | print(greeting + ", world") 22 | `[1:] 23 | 24 | th := &starlark.Thread{ 25 | Name: "truth", 26 | Print: func(_ *starlark.Thread, msg string) { fmt.Println(msg) }, 27 | } 28 | 29 | _, err := starlark.ExecFile(th, "some/file.star", data, starlark.StringDict{ 30 | "greeting": starlark.String("hello"), 31 | }) 32 | if err != nil { 33 | if evalErr, ok := err.(*starlark.EvalError); ok { 34 | panic(evalErr.Backtrace()) 35 | } 36 | panic(err) 37 | } 38 | 39 | // Fail if any subject wasn't applied a property. 40 | if err := starlarktruth.Close(th); err != nil { 41 | panic(err) 42 | } 43 | 44 | // Output: 45 | // hello, world 46 | } 47 | -------------------------------------------------------------------------------- /pkg/starlarktruth/fail.go: -------------------------------------------------------------------------------- 1 | package starlarktruth 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "go.starlark.net/starlark" 8 | ) 9 | 10 | const warnContainsExactlySingleIterable = "" + 11 | " Passing a single iterable to .contains_exactly(*expected) is often" + 12 | " not the correct thing to do. Did you mean to call" + 13 | " .contains_exactly_elements_in(some_iterable) instead?" 14 | 15 | func errMustBeEqualNumberOfKVPairs(count int) error { 16 | return newInvalidAssertion( 17 | fmt.Sprintf("There must be an equal number of key/value pairs"+ 18 | " (i.e., the number of key/value parameters (%d) must be even).", count)) 19 | } 20 | 21 | func (t *T) failNone(check string, other starlark.Value) error { 22 | if other == starlark.None { 23 | msg := fmt.Sprintf("It is illegal to compare using .%s(None)", check) 24 | return newInvalidAssertion(msg) 25 | } 26 | return nil 27 | } 28 | 29 | func (t *T) failIterable() (starlark.Iterable, error) { 30 | itermap, ok := t.actual.(starlark.IterableMapping) 31 | if ok { 32 | iter := newTupleSlice(itermap.Items()) 33 | return iter, nil 34 | } 35 | 36 | iter, ok := t.actual.(starlark.Iterable) 37 | if !ok { 38 | msg := fmt.Sprintf("Cannot use %s as Iterable.", t.subject()) 39 | return nil, newInvalidAssertion(msg) 40 | } 41 | return iter, nil 42 | } 43 | 44 | func (t *T) failComparingValues(verb string, other starlark.Value, suffix string) error { 45 | proposition := fmt.Sprintf("%s <%s>", verb, other.String()) 46 | return t.failWithProposition(proposition, suffix) 47 | } 48 | 49 | func (t *T) failWithProposition(proposition, suffix string) error { 50 | msg := fmt.Sprintf("Not true that %s %s.%s", t.subject(), proposition, suffix) 51 | return newTruthAssertion(msg) 52 | } 53 | 54 | func (t *T) failWithBadResults( 55 | verb string, other starlark.Value, 56 | failVerb string, actual fmt.Stringer, 57 | suffix string, 58 | ) error { 59 | msg := fmt.Sprintf("%s <%s>. It %s <%s>", 60 | verb, other.String(), 61 | failVerb, actual.String()) 62 | return t.failWithProposition(msg, suffix) 63 | } 64 | 65 | func (t *T) failWithSubject(verb string) error { 66 | msg := fmt.Sprintf("%s %s.", t.subject(), verb) 67 | return newTruthAssertion(msg) 68 | } 69 | 70 | func (t *T) subject() string { 71 | str := "" 72 | switch actual := t.actual.(type) { 73 | case starlark.String: 74 | if strings.Contains(actual.GoString(), "\n") { 75 | if t.name == "" { 76 | return "actual" 77 | } 78 | return fmt.Sprintf("actual %s", t.name) 79 | } 80 | case starlark.Callable: 81 | str = t.actual.String() 82 | case starlark.Tuple: 83 | if t.actualIsIterableFromString { 84 | var b strings.Builder 85 | b.WriteString(`<"`) 86 | for _, v := range actual { 87 | b.WriteString(v.(starlark.String).GoString()) 88 | } 89 | b.WriteString(`">`) 90 | str = b.String() 91 | } 92 | default: 93 | } 94 | if str == "" { 95 | str = fmt.Sprintf("<%s>", t.actual.String()) 96 | } 97 | if t.name == "" { 98 | return str 99 | } 100 | return fmt.Sprintf("%s(%s)", t.name, str) 101 | } 102 | -------------------------------------------------------------------------------- /pkg/starlarktruth/func_test.go: -------------------------------------------------------------------------------- 1 | package starlarktruth 2 | 3 | import "testing" 4 | 5 | func TestHasAttribute(t *testing.T) { 6 | s := func(x string) string { 7 | return `that("my str").has_attribute(` + x + `)` 8 | } 9 | testEach(t, map[string]error{ 10 | s(`"elems"`): nil, 11 | s(`"index"`): nil, 12 | s(`"isdigit"`): nil, 13 | s(`""`): fail(`"my str"`, `has attribute <"">`), 14 | s(`"ermagerd"`): fail(`"my str"`, `has attribute <"ermagerd">`), 15 | }) 16 | } 17 | 18 | func TestDoesNotHaveAttribute(t *testing.T) { 19 | s := func(x string) string { 20 | return `that({1: ()}).does_not_have_attribute(` + x + `)` 21 | } 22 | testEach(t, map[string]error{ 23 | s(`"other_attribute"`): nil, 24 | s(`""`): nil, 25 | s(`"keys"`): fail(`{1: ()}`, `does not have attribute <"keys">`), 26 | s(`"values"`): fail(`{1: ()}`, `does not have attribute <"values">`), 27 | s(`"setdefault"`): fail(`{1: ()}`, `does not have attribute <"setdefault">`), 28 | }) 29 | } 30 | 31 | func TestIsCallable(t *testing.T) { 32 | s := func(t string) string { 33 | return `that(` + t + `).is_callable()` 34 | } 35 | testEach(t, map[string]error{ 36 | s(`lambda x: x`): nil, 37 | s(`"str".endswith`): nil, 38 | s(`that`): nil, 39 | s(`None`): fail(`None`, `is callable`), 40 | s(abc): fail(abc, `is callable`), 41 | }) 42 | } 43 | 44 | func TestIsNotCallable(t *testing.T) { 45 | testEach(t, map[string]error{ 46 | `assert.that(assert.that).is_not_callable()`: fail(`built-in method `+Method+` of `+Module+` value`, `is not callable`), 47 | }, asModule) 48 | s := func(t string) string { 49 | return `that(` + t + `).is_not_callable()` 50 | } 51 | testEach(t, map[string]error{ 52 | s(`None`): nil, 53 | s(abc): nil, 54 | s(`lambda x: x`): fail(`function lambda`, `is not callable`), 55 | s(`"str".endswith`): fail(`built-in method endswith of string value`, `is not callable`), 56 | s(`that`): fail(`built-in function that`, `is not callable`), 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /pkg/starlarktruth/is_in_test.go: -------------------------------------------------------------------------------- 1 | package starlarktruth 2 | 3 | import "testing" 4 | 5 | func TestIsIn(t *testing.T) { 6 | s := func(x string) string { 7 | return `that(3).is_in(` + x + `)` 8 | } 9 | testEach(t, map[string]error{ 10 | `that("a").is_in("abc")`: nil, 11 | `that("d").is_in("abc")`: fail(`"d"`, `is equal to any of <"abc">`), 12 | s(`(3,)`): nil, 13 | s(`(3, 5)`): nil, 14 | s(`(1, 3, 5)`): nil, 15 | s(`{3: "three"}`): nil, 16 | s(`set([3, 5])`): nil, 17 | s(`()`): fail(`3`, `is equal to any of <()>`), 18 | s(`(2,)`): fail(`3`, `is equal to any of <(2,)>`), 19 | }) 20 | } 21 | 22 | func TestIsNotIn(t *testing.T) { 23 | s := func(x string) string { 24 | return `that(3).is_not_in(` + x + `)` 25 | } 26 | testEach(t, map[string]error{ 27 | `that("a").is_not_in("abc")`: fail(`"a"`, `is not in "abc". It was found at index 0`), 28 | `that("d").is_not_in("abc")`: nil, 29 | s(`(5,)`): nil, 30 | s(`set([5])`): nil, 31 | s(`("3",)`): nil, 32 | s(`(3,)`): fail(`3`, `is not in (3,). It was found at index 0`), 33 | s(`(1, 3)`): fail(`3`, `is not in (1, 3). It was found at index 1`), 34 | s(`set([3])`): fail(`3`, `is not in set([3])`), 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /pkg/starlarktruth/is_of_test.go: -------------------------------------------------------------------------------- 1 | package starlarktruth 2 | 3 | import "testing" 4 | 5 | func TestIsAnyOf(t *testing.T) { 6 | s := func(x string) string { 7 | return `that(3).is_any_of(` + x + `)` 8 | } 9 | testEach(t, map[string]error{ 10 | s(`3`): nil, 11 | s(`3, 5`): nil, 12 | s(`1, 3, 5`): nil, 13 | s(``): fail(`3`, `is equal to any of <()>`), 14 | s(`2`): fail(`3`, `is equal to any of <(2,)>`), 15 | }) 16 | } 17 | 18 | func TestIsNoneOf(t *testing.T) { 19 | s := func(x string) string { 20 | return `that(3).is_none_of(` + x + `)` 21 | } 22 | testEach(t, map[string]error{ 23 | s(`5`): nil, 24 | s(`"3"`): nil, 25 | s(`3`): fail(`3`, `is not in (3,). It was found at index 0`), 26 | s(`1, 3`): fail(`3`, `is not in (1, 3). It was found at index 1`), 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/starlarktruth/iteritems.go: -------------------------------------------------------------------------------- 1 | package starlarktruth 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "go.starlark.net/starlark" 8 | ) 9 | 10 | const tupleSliceType = "items" 11 | 12 | var _ starlark.Iterable = (tupleSlice)(nil) 13 | 14 | // tupleSlice is used to iterate on starlark.Dict's key-values, not its keys. 15 | // From starlark-go docs: 16 | // > If a type satisfies both Mapping and Iterable, the iterator yields 17 | // > the keys of the mapping. 18 | type tupleSlice []starlark.Tuple 19 | 20 | func newTupleSlice(ts []starlark.Tuple) tupleSlice { return tupleSlice(ts) } 21 | func (ts tupleSlice) String() string { 22 | var b strings.Builder 23 | b.WriteString(tupleSliceType) 24 | b.WriteString("([") 25 | for i, v := range ts { 26 | if i != 0 { 27 | b.WriteString(", ") 28 | } 29 | b.WriteString(v.String()) 30 | } 31 | b.WriteString("])") 32 | return b.String() 33 | } 34 | func (ts tupleSlice) Type() string { return tupleSliceType } 35 | func (ts tupleSlice) Freeze() { 36 | for _, v := range ts { 37 | v.Freeze() 38 | } 39 | } 40 | func (ts tupleSlice) Truth() starlark.Bool { return len(ts) > 0 } 41 | func (ts tupleSlice) Hash() (uint32, error) { 42 | return 0, fmt.Errorf("unhashable type: %s", tupleSliceType) 43 | } 44 | 45 | func (ts tupleSlice) Values() []starlark.Value { 46 | vs := make([]starlark.Value, 0, len(ts)) 47 | for _, v := range ts { 48 | vs = append(vs, v) 49 | } 50 | return vs 51 | } 52 | 53 | func (ts tupleSlice) Iterate() starlark.Iterator { return newTupleSliceIterator(ts) } 54 | 55 | var _ starlark.Iterator = (*tupleSliceIterator)(nil) 56 | 57 | type tupleSliceIterator struct { 58 | s tupleSlice 59 | i int 60 | } 61 | 62 | func newTupleSliceIterator(ts tupleSlice) *tupleSliceIterator { return &tupleSliceIterator{s: ts} } 63 | func (tsi *tupleSliceIterator) Done() {} 64 | func (tsi *tupleSliceIterator) Next(v *starlark.Value) bool { 65 | if tsi.i < len(tsi.s) { 66 | *v = tsi.s[tsi.i] 67 | tsi.i++ 68 | return true 69 | } 70 | return false 71 | } 72 | -------------------------------------------------------------------------------- /pkg/starlarktruth/module.go: -------------------------------------------------------------------------------- 1 | package starlarktruth 2 | 3 | import ( 4 | "fmt" 5 | 6 | "go.starlark.net/starlark" 7 | ) 8 | 9 | var ( 10 | // Module is the module name used by default. 11 | Module = "assert" 12 | 13 | // Method is the attribute name used by default. 14 | Method = "that" 15 | ) 16 | 17 | type module struct{} 18 | 19 | var _ starlark.HasAttrs = (*module)(nil) 20 | 21 | // NewModule registers a Starlark module of https://truth.dev/ 22 | func NewModule(predeclared starlark.StringDict) { predeclared[Module] = &module{} } 23 | 24 | func (m *module) String() string { return Module } 25 | func (m *module) Type() string { return Module } 26 | func (m *module) Freeze() {} 27 | func (m *module) Truth() starlark.Bool { return true } 28 | func (m *module) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable type: %s", Module) } 29 | func (m *module) AttrNames() []string { return []string{Method} } 30 | func (m *module) Attr(name string) (starlark.Value, error) { 31 | if name != Method { 32 | return nil, nil // no such method 33 | } 34 | b := starlark.NewBuiltin(name, That) 35 | return b.BindReceiver(m), nil 36 | } 37 | 38 | // That implements the `.that(target)` builtin 39 | func That(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 40 | var target starlark.Value 41 | if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &target); err != nil { 42 | return nil, err 43 | } 44 | 45 | if err := Close(thread); err != nil { 46 | return nil, err 47 | } 48 | thread.SetLocal(LocalThreadKeyForClose, thread.CallFrame(1)) 49 | 50 | return newT(target), nil 51 | } 52 | 53 | var _ starlark.HasAttrs = (*T)(nil) 54 | 55 | func newT(target starlark.Value) *T { return &T{actual: target} } 56 | 57 | func (t *T) String() string { return fmt.Sprintf("%s.%s(%s)", Module, Method, t.actual.String()) } 58 | func (t *T) Type() string { return Module } 59 | func (t *T) Freeze() { t.actual.Freeze() } 60 | func (t *T) Truth() starlark.Bool { return true } 61 | func (t *T) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: %s", t.Type()) } 62 | func (t *T) Attr(name string) (starlark.Value, error) { return builtinAttr(t, name) } 63 | func (t *T) AttrNames() []string { return attrNames } 64 | -------------------------------------------------------------------------------- /pkg/starlarktruth/ordered_test.go: -------------------------------------------------------------------------------- 1 | package starlarktruth 2 | 3 | import "testing" 4 | 5 | func TestIsOrdered(t *testing.T) { 6 | s := func(t string) string { 7 | return `that(` + t + `).is_ordered()` 8 | } 9 | testEach(t, map[string]error{ 10 | s(`()`): nil, 11 | s(`(3,)`): nil, 12 | s(`(3, 5, 8)`): nil, 13 | s(`(3, 5, 5)`): nil, 14 | s(`"abcdef"`): nil, 15 | s(`"fedcba"`): newTruthAssertion(`Not true that <"fedcba"> is ordered <("f", "e")>.`), 16 | s(`{5: 4}`): errDictOrdering, 17 | s(`(5, 4)`): newTruthAssertion(`Not true that <(5, 4)> is ordered <(5, 4)>.`), 18 | s(`(3, 5, 4)`): newTruthAssertion(`Not true that <(3, 5, 4)> is ordered <(5, 4)>.`), 19 | }) 20 | } 21 | 22 | func TestIsOrderedAccordingTo(t *testing.T) { 23 | s := func(t string) string { 24 | return `that(` + t + `).is_ordered_according_to(someCmp)` 25 | } 26 | testEach(t, map[string]error{ 27 | s(`()`): nil, 28 | s(`(3,)`): nil, 29 | s(`(8, 5, 3)`): nil, 30 | s(`(5, 5, 3)`): nil, 31 | s(`"fedcba"`): nil, 32 | s(`"abcdef"`): newTruthAssertion(`Not true that <"abcdef"> is ordered <("a", "b")>.`), 33 | s(`{5: 4}`): errDictOrdering, 34 | s(`(4, 5)`): newTruthAssertion(`Not true that <(4, 5)> is ordered <(4, 5)>.`), 35 | s(`(3, 5, 4)`): newTruthAssertion(`Not true that <(3, 5, 4)> is ordered <(3, 5)>.`), 36 | }) 37 | } 38 | 39 | func TestIsStrictlyOrdered(t *testing.T) { 40 | s := func(t string) string { 41 | return `that(` + t + `).is_strictly_ordered()` 42 | } 43 | testEach(t, map[string]error{ 44 | s(`()`): nil, 45 | s(`(3,)`): nil, 46 | s(`(3, 5, 8)`): nil, 47 | s(`"abcdef"`): nil, 48 | s(`"abcdee"`): newTruthAssertion(`Not true that <"abcdee"> is strictly ordered <("e", "e")>.`), 49 | s(`"fedcba"`): newTruthAssertion(`Not true that <"fedcba"> is strictly ordered <("f", "e")>.`), 50 | s(`{5: 4}`): errDictOrdering, 51 | s(`(5, 4)`): newTruthAssertion(`Not true that <(5, 4)> is strictly ordered <(5, 4)>.`), 52 | s(`(3, 5, 5)`): newTruthAssertion(`Not true that <(3, 5, 5)> is strictly ordered <(5, 5)>.`), 53 | }) 54 | } 55 | 56 | func TestIsStrictlyOrderedAccordingTo(t *testing.T) { 57 | s := func(t string) string { 58 | return `that(` + t + `).is_strictly_ordered_according_to(someCmp)` 59 | } 60 | testEach(t, map[string]error{ 61 | s(`()`): nil, 62 | s(`(3,)`): nil, 63 | s(`(8, 5, 3)`): nil, 64 | s(`"fedcba"`): nil, 65 | s(`"fedcbb"`): newTruthAssertion(`Not true that <"fedcbb"> is strictly ordered <("b", "b")>.`), 66 | s(`"abcdef"`): newTruthAssertion(`Not true that <"abcdef"> is strictly ordered <("a", "b")>.`), 67 | s(`{5: 4}`): errDictOrdering, 68 | s(`(4, 5)`): newTruthAssertion(`Not true that <(4, 5)> is strictly ordered <(4, 5)>.`), 69 | s(`(5, 5, 3)`): newTruthAssertion(`Not true that <(5, 5, 3)> is strictly ordered <(5, 5)>.`), 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /pkg/starlarktruth/size_test.go: -------------------------------------------------------------------------------- 1 | package starlarktruth 2 | 3 | import "testing" 4 | 5 | func TestHasSize(t *testing.T) { 6 | s := func(x string) string { 7 | return `that((2, 5, 8)).has_size(` + x + `)` 8 | } 9 | testEach(t, map[string]error{ 10 | s(`3`): nil, 11 | s(`-1`): fail(`(2, 5, 8)`, `has a size of <-1>. It is <3>`), 12 | s(`2`): fail(`(2, 5, 8)`, `has a size of <2>. It is <3>`), 13 | }) 14 | } 15 | 16 | func TestIsEmpty(t *testing.T) { 17 | s := func(t string) string { 18 | return `that(` + t + `).is_empty()` 19 | } 20 | testEach(t, map[string]error{ 21 | s(`()`): nil, 22 | s(`[]`): nil, 23 | s(`{}`): nil, 24 | s(`set([])`): nil, 25 | s(`""`): nil, 26 | s(`(3,)`): fail(`(3,)`, `is empty`), 27 | s(`[4]`): fail(`[4]`, `is empty`), 28 | s(`{5: 6}`): fail(`{5: 6}`, `is empty`), 29 | s(`set([7])`): fail(`set([7])`, `is empty`), 30 | s(`"height"`): fail(`"height"`, `is empty`), 31 | }) 32 | } 33 | 34 | func TestIsNotEmpty(t *testing.T) { 35 | s := func(t string) string { 36 | return `that(` + t + `).is_not_empty()` 37 | } 38 | testEach(t, map[string]error{ 39 | s(`(3,)`): nil, 40 | s(`[4]`): nil, 41 | s(`{5: 6}`): nil, 42 | s(`set([7])`): nil, 43 | s(`"height"`): nil, 44 | s(`()`): fail(`()`, `is not empty`), 45 | s(`[]`): fail(`[]`, `is not empty`), 46 | s(`{}`): fail(`{}`, `is not empty`), 47 | s(`set([])`): fail(`set([])`, `is not empty`), 48 | s(`""`): fail(`""`, `is not empty`), 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /pkg/starlarktruth/strings_test.go: -------------------------------------------------------------------------------- 1 | package starlarktruth 2 | 3 | import "testing" 4 | 5 | func TestHasLength(t *testing.T) { 6 | ss := abc 7 | s := func(x string) string { 8 | return `that(` + ss + `).has_length(` + x + `)` 9 | } 10 | testEach(t, map[string]error{ 11 | s(`3`): nil, 12 | s(`4`): fail(ss, `has a length of 4. It is 3`), 13 | s(`2`): fail(ss, `has a length of 2. It is 3`), 14 | }) 15 | } 16 | 17 | func TestStartsWith(t *testing.T) { 18 | ss := abc 19 | s := func(x string) string { 20 | return `that(` + ss + `).starts_with(` + x + `)` 21 | } 22 | testEach(t, map[string]error{ 23 | s(`""`): nil, 24 | s(`"a"`): nil, 25 | s(`"ab"`): nil, 26 | s(abc): nil, 27 | s(`"b"`): fail(ss, `starts with <"b">`), 28 | }) 29 | } 30 | 31 | func TestEndsWith(t *testing.T) { 32 | ss := abc 33 | s := func(x string) string { 34 | return `that(` + ss + `).ends_with(` + x + `)` 35 | } 36 | testEach(t, map[string]error{ 37 | s(`""`): nil, 38 | s(`"c"`): nil, 39 | s(`"bc"`): nil, 40 | s(abc): nil, 41 | s(`"b"`): fail(ss, `ends with <"b">`), 42 | }) 43 | } 44 | 45 | func TestMatches(t *testing.T) { 46 | ss := abc 47 | s := func(x string) string { 48 | return `that(` + ss + `).matches(` + x + `)` 49 | } 50 | testEach(t, map[string]error{ 51 | s(`"a"`): nil, 52 | s(`r".b"`): nil, 53 | s(`r"[Aa]"`): nil, 54 | s(`"d"`): fail(ss, `matches <"d">`), 55 | s(`"b"`): fail(ss, `matches <"b">`), 56 | }) 57 | } 58 | 59 | func TestDoesNotMatch(t *testing.T) { 60 | ss := abc 61 | s := func(x string) string { 62 | return `that(` + ss + `).does_not_match(` + x + `)` 63 | } 64 | testEach(t, map[string]error{ 65 | s(`"b"`): nil, 66 | s(`"d"`): nil, 67 | s(`"a"`): fail(ss, `fails to match <"a">`), 68 | s(`r".b"`): fail(ss, `fails to match <".b">`), 69 | s(`r"[Aa]"`): fail(ss, `fails to match <"[Aa]">`), 70 | }) 71 | } 72 | 73 | func TestContainsMatch(t *testing.T) { 74 | ss := abc 75 | s := func(x string) string { 76 | return `that(` + ss + `).contains_match(` + x + `)` 77 | } 78 | testEach(t, map[string]error{ 79 | s(`"a"`): nil, 80 | s(`r".b"`): nil, 81 | s(`r"[Aa]"`): nil, 82 | s(`"b"`): nil, 83 | s(`"d"`): fail(ss, `should have contained a match for <"d">`), 84 | }) 85 | } 86 | 87 | func TestDoesNotContainMatch(t *testing.T) { 88 | ss := abc 89 | s := func(x string) string { 90 | return `that(` + ss + `).does_not_contain_match(` + x + `)` 91 | } 92 | testEach(t, map[string]error{ 93 | s(`"d"`): nil, 94 | s(`"a"`): fail(ss, `should not have contained a match for <"a">`), 95 | s(`"b"`): fail(ss, `should not have contained a match for <"b">`), 96 | s(`r".b"`): fail(ss, `should not have contained a match for <".b">`), 97 | s(`r"[Aa]"`): fail(ss, `should not have contained a match for <"[Aa]">`), 98 | }) 99 | } 100 | -------------------------------------------------------------------------------- /pkg/starlarktruth/t.go: -------------------------------------------------------------------------------- 1 | package starlarktruth 2 | 3 | import ( 4 | "math/big" 5 | 6 | "go.starlark.net/starlark" 7 | ) 8 | 9 | // T wraps an assert target 10 | type T struct { 11 | // Target in assert.that(target) 12 | actual starlark.Value 13 | 14 | // Readable optional prefix with .named(name) 15 | name string 16 | 17 | // True when actual was a String and was made into an iterable. 18 | // Helps when pretty printing. 19 | actualIsIterableFromString bool 20 | 21 | // forOrdering is relevant to .in_order() assertions 22 | forOrdering *forOrdering 23 | 24 | // registered holds the compiled default compare function 25 | registered *registeredValues 26 | 27 | // withinTolerance is used to delta-compare numbers 28 | withinTolerance *withinTolerance 29 | } 30 | 31 | type forOrdering struct { 32 | inOrderError error 33 | } 34 | 35 | type withinTolerance struct { 36 | within bool 37 | actual *big.Rat 38 | tolerance *big.Rat 39 | toleranceAsValue starlark.Value 40 | } 41 | 42 | func (t *T) turnActualIntoIterableFromString() { 43 | s := t.actual.(starlark.String).GoString() 44 | vs := make([]starlark.Value, 0, len(s)) 45 | for _, c := range s { 46 | vs = append(vs, starlark.String(c)) 47 | } 48 | t.actual = starlark.Tuple(vs) 49 | t.actualIsIterableFromString = true 50 | } 51 | 52 | type registeredValues struct { 53 | Cmp starlark.Value 54 | Apply func(f *starlark.Function, args starlark.Tuple) (starlark.Value, error) 55 | } 56 | 57 | const cmpSrc = `lambda a, b: int(a > b) - int(a < b)` 58 | 59 | func (t *T) registerValues(thread *starlark.Thread) error { 60 | if t.registered == nil { 61 | cmp, err := starlark.Eval(thread, "", cmpSrc, starlark.StringDict{}) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | apply := func(f *starlark.Function, args starlark.Tuple) (starlark.Value, error) { 67 | return starlark.Call(thread, f, args, nil) 68 | } 69 | 70 | t.registered = ®isteredValues{ 71 | Cmp: cmp, 72 | Apply: apply, 73 | } 74 | } 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /pkg/starlarktruth/truth_test.go: -------------------------------------------------------------------------------- 1 | package starlarktruth 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | "go.starlark.net/resolve" 11 | "go.starlark.net/starlark" 12 | ) 13 | 14 | type asWhat int 15 | 16 | const ( 17 | asFunc asWhat = iota 18 | asModule 19 | ) 20 | 21 | const abc = `"abc"` // Please linter 22 | 23 | func helper(t *testing.T, as asWhat, program string) (starlark.StringDict, error) { 24 | t.Helper() 25 | 26 | // Enabled so they can be tested 27 | resolve.AllowFloat = true 28 | resolve.AllowSet = true 29 | resolve.AllowLambda = true 30 | 31 | predeclared := starlark.StringDict{} 32 | if as == asModule { 33 | NewModule(predeclared) 34 | } else { 35 | starlark.Universe["that"] = starlark.NewBuiltin("that", That) 36 | } 37 | 38 | thread := &starlark.Thread{ 39 | Name: t.Name(), 40 | Print: func(_ *starlark.Thread, msg string) { 41 | t.Logf("--> %s", msg) 42 | }, 43 | Load: func(_ *starlark.Thread, module string) (starlark.StringDict, error) { 44 | return nil, errors.New("load() disabled") 45 | }, 46 | } 47 | 48 | script := strings.Join([]string{ 49 | `dfltCmp = ` + cmpSrc, 50 | `someCmp = lambda a, b: dfltCmp(b, a)`, 51 | program, 52 | }, "\n") 53 | 54 | d, err := starlark.ExecFile(thread, t.Name()+".star", script, predeclared) 55 | if err != nil { 56 | return nil, err 57 | } 58 | if err := Close(thread); err != nil { 59 | return nil, err 60 | } 61 | return d, nil 62 | } 63 | 64 | func testEach(t *testing.T, m map[string]error, asSlice ...asWhat) { 65 | if len(asSlice) > 1 { 66 | panic("too many items in slice") 67 | } 68 | as := asFunc 69 | for _, as = range asSlice { 70 | break 71 | } 72 | for code, expectedErr := range m { 73 | t.Run(code, func(t *testing.T) { 74 | globals, err := helper(t, as, code) 75 | delete(globals, "dfltCmp") 76 | delete(globals, "someCmp") 77 | delete(globals, "fortytwo") 78 | require.Empty(t, globals) 79 | if expectedErr == nil { 80 | require.NoError(t, err) 81 | } else { 82 | require.Error(t, err) 83 | require.EqualError(t, err, expectedErr.Error()) 84 | switch err := err.(type) { 85 | case *starlark.EvalError: 86 | e := err.Unwrap() 87 | if _, ok := e.(*UnhandledError); !ok { 88 | require.Exactly(t, expectedErr, e) 89 | } 90 | case *UnresolvedError: 91 | require.Exactly(t, expectedErr, err) 92 | default: 93 | panic("unreachable") 94 | } 95 | } 96 | }) 97 | } 98 | } 99 | 100 | func fail(value, expected string, suffixes ...string) error { 101 | var suffix string 102 | switch len(suffixes) { 103 | case 0: 104 | case 1: 105 | suffix = suffixes[0] 106 | default: 107 | panic(`There must be only one suffix`) 108 | } 109 | msg := "Not true that <" + value + "> " + expected + "." + suffix 110 | return newTruthAssertion(msg) 111 | } 112 | 113 | func TestClosedness(t *testing.T) { 114 | testEach(t, map[string]error{ 115 | ` 116 | fortytwo = that(True) 117 | that(False).is_false() 118 | `: newUnresolvedError("TestClosedness/_fortytwo_=_that(True)_that(False).is_false()_.star:4:16"), 119 | ` 120 | fortytwo = that(True) 121 | fortytwo.is_true() 122 | that(False).is_false() 123 | `: nil, 124 | }) 125 | testEach(t, map[string]error{ 126 | `assert.that(True)`: newUnresolvedError("TestClosedness/assert.that(True).star:3:12"), 127 | `assert.that(True).is_true()`: nil, 128 | 129 | `assert.that(True).named("eh")`: newUnresolvedError(`TestClosedness/assert.that(True).named("eh").star:3:12`), 130 | `assert.that(True).named("eh").is_true()`: nil, 131 | 132 | `assert.that(10).is_within(0.1)`: newUnresolvedError("TestClosedness/assert.that(10).is_within(0.1).star:3:12"), 133 | `assert.that(10).is_within(0.1).of(10)`: nil, 134 | `assert.that(10).is_not_within(0.1)`: newUnresolvedError("TestClosedness/assert.that(10).is_not_within(0.1).star:3:12"), 135 | `assert.that(10).is_not_within(0.1).of(42)`: nil, 136 | }, asModule) 137 | } 138 | 139 | func TestAsValue(t *testing.T) { 140 | testEach(t, map[string]error{ 141 | ` 142 | fortytwo = that(42) 143 | fortytwo.is_equal_to(42.0) 144 | fortytwo.is_not_callable() 145 | fortytwo.is_at_least(42) 146 | `: nil, 147 | 148 | ` 149 | fortytwo = that([1,2,3]) 150 | fortytwo.contains(2) 151 | fortytwo.contains_exactly(1,2,3) 152 | fortytwo.contains_exactly(1,2,3).in_order() 153 | fortytwo.contains_all_of(2,3).in_order() 154 | `: nil, 155 | }) 156 | } 157 | 158 | func TestImpossibleInOrder(t *testing.T) { 159 | testEach(t, map[string]error{ 160 | `that([1,2,3]).is_ordered()`: nil, 161 | `that([1,2,3]).in_order()`: fmt.Errorf(`Invalid assertion .in_order() on value of type list`), 162 | }) 163 | } 164 | 165 | func TestKwargsForbidden(t *testing.T) { 166 | tests := make(map[string]error, len(methods)) 167 | for _, method := range methods { 168 | for m := range method { 169 | tests[`that([42]).`+m+`(a="blip", b="blap", c="blop")`] = fmt.Errorf("%s: unexpected keyword arguments", m) 170 | break 171 | } 172 | } 173 | testEach(t, tests) 174 | } 175 | -------------------------------------------------------------------------------- /pkg/starlarktruth/type_test.go: -------------------------------------------------------------------------------- 1 | package starlarktruth 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestIsOfType(t *testing.T) { 9 | testEach(t, map[string]error{ 10 | `that(None).is_of_type("NoneType")`: nil, 11 | `that("").is_of_type("string")`: nil, 12 | `that(0).is_of_type("int")`: nil, 13 | `that(0.0).is_of_type("float")`: nil, 14 | `that(set([])).is_of_type("set")`: nil, 15 | `that(someCmp).is_of_type(type(lambda _: 42))`: nil, 16 | `that([]).is_of_type("list")`: nil, 17 | `that(()).is_of_type("tuple")`: nil, 18 | `that({}).is_of_type("dict")`: nil, 19 | `that({}).is_of_type("int")`: fail(`{}`, `is of type <"int">`, ` However, it is of type <"dict">`), 20 | `that({}).is_of_type(type({}))`: nil, 21 | `that({}).is_of_type({})`: fmt.Errorf(`Invalid assertion .is_of_type({}) on value of type dict`), 22 | }) 23 | } 24 | 25 | func TestIsNotOfType(t *testing.T) { 26 | testEach(t, map[string]error{ 27 | `that(None).is_not_of_type("int")`: nil, 28 | `that("").is_not_of_type("int")`: nil, 29 | `that(0).is_not_of_type("dict")`: nil, 30 | `that(0.0).is_not_of_type("int")`: nil, 31 | `that(set([])).is_not_of_type("int")`: nil, 32 | `that(someCmp).is_not_of_type("int")`: nil, 33 | `that([]).is_not_of_type("int")`: nil, 34 | `that(()).is_not_of_type("int")`: nil, 35 | `that({}).is_not_of_type("int")`: nil, 36 | `that({}).is_not_of_type("dict")`: fail(`{}`, `is not of type <"dict">`, ` However, it is of type <"dict">`), 37 | `that({}).is_not_of_type(type([{}]))`: nil, 38 | `that([]).is_not_of_type({})`: fmt.Errorf(`Invalid assertion .is_not_of_type({}) on value of type list`), 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/starlarktruth/unicodes_test.go: -------------------------------------------------------------------------------- 1 | package starlarktruth 2 | 3 | import "testing" 4 | 5 | func TestContainsExactlyHandlesStringsAsCodepoints(t *testing.T) { 6 | const ( 7 | // multiple bytes codepoint 8 | u1 = `Й` 9 | // more multiple bytes codepoint 10 | u2 = `😿` 11 | // concats 12 | full = `"abc` + u1 + u2 + `"` 13 | tuple = `("a", "` + u1 + `", "c")` 14 | elput = `("c", "` + u1 + `", "a")` 15 | ) 16 | testEach(t, map[string]error{ 17 | `that("abc").contains_exactly("abc")`: fail(abc, 18 | `contains exactly <("abc",)>. It is missing <"abc"> and has unexpected items <"a", "b", "c">`), 19 | 20 | `that("abc").contains_exactly("a", "b", "c")`: nil, 21 | `that("abc").contains_exactly("a", "b", "c").in_order()`: nil, 22 | `that("abc").contains_exactly("c", "b", "a")`: nil, 23 | `that("abc").contains_exactly("c", "b", "a").in_order()`: fail(abc, 24 | `contains exactly these elements in order <("c", "b", "a")>`), 25 | 26 | `that("abc").contains_exactly("a", "bc")`: fail(abc, 27 | `contains exactly <("a", "bc")>. It is missing <"bc"> and has unexpected items <"b", "c">`), 28 | 29 | `that(` + tuple + `).contains_exactly` + tuple + ``: nil, 30 | `that(` + tuple + `).contains_exactly` + elput + ``: nil, 31 | `that(` + tuple + `).contains_exactly` + tuple + `.in_order()`: nil, 32 | `that(` + tuple + `).contains_exactly` + elput + `.in_order()`: fail(tuple, 33 | `contains exactly these elements in order <`+elput+`>`), 34 | 35 | `that(` + full + `).contains_exactly("a", "` + u1 + `")`: fail(full, 36 | `contains exactly <("a", "`+u1+`")>. It has unexpected items <"b", "c", "`+u2+`">`), 37 | 38 | `that(` + full + `).contains_exactly("a` + u1 + `")`: fail(full, 39 | `contains exactly <("a`+u1+`",)>. It is missing <"a`+u1+`"> and has unexpected items <"a", "b", "c", "`+u1+`", "`+u2+`">`), 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/starlarkunpacked/strings.go: -------------------------------------------------------------------------------- 1 | package starlarkunpacked 2 | 3 | import ( 4 | "fmt" 5 | 6 | "go.starlark.net/starlark" 7 | "go.starlark.net/syntax" 8 | ) 9 | 10 | // Strings is a subtype of *starlark.List containing only starlark.String.s 11 | type Strings struct { 12 | l *starlark.List 13 | } 14 | 15 | // *Strings implements everything that *starlark.List implements... 16 | var ( 17 | _ starlark.Comparable = (*Strings)(nil) 18 | _ starlark.HasSetIndex = (*Strings)(nil) 19 | _ starlark.Sliceable = (*Strings)(nil) 20 | _ starlark.HasAttrs = (*Strings)(nil) 21 | ) 22 | 23 | // ...and more! 24 | var _ starlark.Unpacker = (*Strings)(nil) 25 | 26 | func (sl *Strings) Unpack(v starlark.Value) error { 27 | list, ok := v.(*starlark.List) 28 | if !ok { 29 | return fmt.Errorf("got %s, want list", v.Type()) 30 | } 31 | 32 | for i, l := 0, list.Len(); i < l; i++ { 33 | x := list.Index(i) 34 | if _, isString := x.(starlark.String); !isString { 35 | return fmt.Errorf("got %s, want string", x.Type()) 36 | } 37 | } 38 | sl.l = list 39 | return nil 40 | } 41 | 42 | // GoStrings panics if any item is not a starlark.String 43 | func (sl *Strings) GoStrings() (xs []string) { 44 | l := sl.l.Len() 45 | xs = make([]string, 0, l) 46 | for i := 0; i < l; i++ { 47 | xs = append(xs, sl.l.Index(i).(starlark.String).GoString()) 48 | } 49 | return 50 | } 51 | 52 | func (sl *Strings) Append(v starlark.Value) error { return sl.l.Append(v) } 53 | func (sl *Strings) Attr(name string) (starlark.Value, error) { return sl.l.Attr(name) } 54 | func (sl *Strings) AttrNames() []string { return sl.l.AttrNames() } 55 | func (sl *Strings) Clear() error { return sl.l.Clear() } 56 | func (sl *Strings) CompareSameType(op syntax.Token, y starlark.Value, depth int) (bool, error) { 57 | return sl.l.CompareSameType(op, y, depth) 58 | } 59 | func (sl *Strings) Freeze() { sl.l.Freeze() } 60 | func (sl *Strings) Hash() (uint32, error) { return sl.l.Hash() } 61 | func (sl *Strings) Index(i int) starlark.Value { return sl.l.Index(i) } 62 | func (sl *Strings) Iterate() starlark.Iterator { return sl.l.Iterate() } 63 | func (sl *Strings) Len() int { return sl.l.Len() } 64 | func (sl *Strings) SetIndex(i int, v starlark.Value) error { return sl.l.SetIndex(i, v) } 65 | func (sl *Strings) Slice(start, end, step int) starlark.Value { return sl.l.Slice(start, end, step) } 66 | func (sl *Strings) String() string { return sl.l.String() } 67 | func (sl *Strings) Truth() starlark.Bool { return sl.l.Truth() } 68 | func (sl *Strings) Type() string { return sl.l.Type() } 69 | -------------------------------------------------------------------------------- /pkg/starlarkunpacked/strings_test.go: -------------------------------------------------------------------------------- 1 | package starlarkunpacked 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "go.starlark.net/starlark" 8 | ) 9 | 10 | func TestUnpackingStrings(t *testing.T) { 11 | want := starlark.NewList([]starlark.Value{starlark.String("Oh"), starlark.String("nice!")}) 12 | 13 | var sl Strings 14 | err := starlark.UnpackArgs("unpack", starlark.Tuple{want}, nil, "sl", &sl) 15 | require.NoError(t, err) 16 | 17 | require.Equal(t, []string{"Oh", "nice!"}, sl.GoStrings()) 18 | } 19 | 20 | func TestUnpackingStringsWithBadItem(t *testing.T) { 21 | want := starlark.NewList([]starlark.Value{starlark.String("Oh"), starlark.MakeInt(42)}) 22 | var sl Strings 23 | err := starlark.UnpackArgs("nope", starlark.Tuple{want}, nil, "sl", &sl) 24 | require.EqualError(t, err, `nope: for parameter sl: got int, want string`) 25 | } 26 | -------------------------------------------------------------------------------- /pkg/starlarkunpacked/unique_strings.go: -------------------------------------------------------------------------------- 1 | package starlarkunpacked 2 | 3 | import ( 4 | "fmt" 5 | 6 | "go.starlark.net/starlark" 7 | ) 8 | 9 | // UniqueStrings implements starlark.Unpacker 10 | type UniqueStrings struct { 11 | strings []string 12 | } 13 | 14 | var _ starlark.Unpacker = (*UniqueStrings)(nil) 15 | 16 | // Unpack unmarshals UniqueStrings from a starlark.Value 17 | func (us *UniqueStrings) Unpack(v starlark.Value) error { 18 | list, ok := v.(*starlark.List) 19 | if !ok { 20 | return fmt.Errorf("got %s, want list", v.Type()) 21 | } 22 | 23 | l := list.Len() 24 | m := make(map[uint32]struct{}, l) 25 | us.strings = make([]string, 0, l) 26 | for i := 0; i < l; i++ { 27 | x := list.Index(i) 28 | 29 | s, isString := x.(starlark.String) 30 | if !isString { 31 | return fmt.Errorf("got %s, want string", x.Type()) 32 | } 33 | 34 | h, err := s.Hash() 35 | if err != nil { 36 | panic("unreachable") 37 | } 38 | if _, isDupe := m[h]; isDupe { 39 | return fmt.Errorf("%s appears more than once", s) 40 | } 41 | m[h] = struct{}{} 42 | 43 | us.strings = append(us.strings, s.GoString()) 44 | } 45 | return nil 46 | } 47 | 48 | // GoStrings returns the list of unique strings as a Go slice 49 | func (us *UniqueStrings) GoStrings() []string { 50 | return us.strings 51 | } 52 | -------------------------------------------------------------------------------- /pkg/starlarkunpacked/unique_strings_test.go: -------------------------------------------------------------------------------- 1 | package starlarkunpacked 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "go.starlark.net/starlark" 8 | ) 9 | 10 | func TestUnpackingUniqueStrings(t *testing.T) { 11 | want := starlark.NewList([]starlark.Value{starlark.String("Oh"), starlark.String("nice!")}) 12 | 13 | var sl UniqueStrings 14 | err := starlark.UnpackArgs("unpack", starlark.Tuple{want}, nil, "sl", &sl) 15 | require.NoError(t, err) 16 | 17 | require.Equal(t, []string{"Oh", "nice!"}, sl.GoStrings()) 18 | } 19 | 20 | func TestUnpackingUniqueStringsWithBadItem(t *testing.T) { 21 | want := starlark.NewList([]starlark.Value{starlark.String("Oh"), starlark.MakeInt(42)}) 22 | var sl UniqueStrings 23 | err := starlark.UnpackArgs("nope", starlark.Tuple{want}, nil, "sl", &sl) 24 | require.EqualError(t, err, `nope: for parameter sl: got int, want string`) 25 | } 26 | 27 | func TestUnpackingUniqueStringsWithDuplicateItem(t *testing.T) { 28 | want := starlark.NewList([]starlark.Value{starlark.String("Oh"), starlark.String("Oh")}) 29 | var sl UniqueStrings 30 | err := starlark.UnpackArgs("dupe", starlark.Tuple{want}, nil, "sl", &sl) 31 | require.EqualError(t, err, `dupe: for parameter sl: "Oh" appears more than once`) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/starlarkvalue/from_protovalue.go: -------------------------------------------------------------------------------- 1 | package starlarkvalue 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | 7 | "go.starlark.net/starlark" 8 | "google.golang.org/protobuf/types/known/structpb" 9 | ) 10 | 11 | // ProtoCompatible returns a non-nil error when the Starlark value 12 | // has no trivial Protocol Buffers Well-Known-Types representation. 13 | func ProtoCompatible(value starlark.Value) (err error) { 14 | switch v := value.(type) { 15 | case starlark.NoneType: 16 | return 17 | case starlark.Bool: 18 | return 19 | case starlark.Int: 20 | return 21 | case starlark.Float: 22 | if !isFinite(float64(v)) { 23 | return fmt.Errorf("non-finite float: %v", v) 24 | } 25 | return 26 | case starlark.String: 27 | return 28 | case *starlark.List: 29 | for i := 0; i < v.Len(); i++ { 30 | if err = ProtoCompatible(v.Index(i)); err != nil { 31 | return 32 | } 33 | } 34 | return 35 | case starlark.Tuple: 36 | for i := 0; i < v.Len(); i++ { 37 | if err = ProtoCompatible(v.Index(i)); err != nil { 38 | return 39 | } 40 | } 41 | return 42 | case *starlark.Dict: 43 | for _, kv := range v.Items() { 44 | if _, ok := kv.Index(0).(starlark.String); !ok { 45 | err = fmt.Errorf("want string key, got: (%s) %s", value.Type(), value.String()) 46 | return 47 | } 48 | if err = ProtoCompatible(kv.Index(1)); err != nil { 49 | return 50 | } 51 | } 52 | return 53 | default: 54 | err = fmt.Errorf("incompatible value (%s): %s", value.Type(), value.String()) 55 | return 56 | } 57 | } 58 | 59 | // FromProtoValue converts a Google Well-Known-Type Value to a Starlark value. 60 | // Panics on unexpected proto value. 61 | func FromProtoValue(x *structpb.Value) starlark.Value { 62 | switch x.GetKind().(type) { 63 | 64 | case *structpb.Value_NullValue: 65 | return starlark.None 66 | 67 | case *structpb.Value_BoolValue: 68 | return starlark.Bool(x.GetBoolValue()) 69 | 70 | case *structpb.Value_NumberValue: 71 | return starlark.Float(x.GetNumberValue()) 72 | 73 | case *structpb.Value_StringValue: 74 | return starlark.String(x.GetStringValue()) 75 | 76 | case *structpb.Value_ListValue: 77 | xs := x.GetListValue().GetValues() 78 | values := make([]starlark.Value, 0, len(xs)) 79 | for _, x := range xs { 80 | value := FromProtoValue(x) 81 | values = append(values, value) 82 | } 83 | return starlark.NewList(values) 84 | 85 | case *structpb.Value_StructValue: 86 | kvs := x.GetStructValue().GetFields() 87 | values := starlark.NewDict(len(kvs)) 88 | for k, v := range kvs { 89 | value := FromProtoValue(v) 90 | key := starlark.String(k) 91 | if err := values.SetKey(key, value); err != nil { 92 | panic(err) // unreachable: hashable key, not iterating, not frozen. 93 | } 94 | } 95 | return values 96 | 97 | default: 98 | panic(fmt.Errorf("unhandled: %T %+v", x.GetKind(), x)) // unreachable: only proto values. 99 | } 100 | } 101 | 102 | // isFinite reports whether f represents a finite rational value. 103 | // It is equivalent to !math.IsNan(f) && !math.IsInf(f, 0). 104 | func isFinite(f float64) bool { 105 | return math.Abs(f) <= math.MaxFloat64 106 | } 107 | -------------------------------------------------------------------------------- /pkg/starlarkvalue/to_protovalue.go: -------------------------------------------------------------------------------- 1 | package starlarkvalue 2 | 3 | import ( 4 | "fmt" 5 | 6 | "go.starlark.net/starlark" 7 | "google.golang.org/protobuf/types/known/structpb" 8 | ) 9 | 10 | // ToProtoValue converts a Starlark value to a Google Well-Known-Type value. 11 | // Panics on unexpected starlark value. 12 | func ToProtoValue(x starlark.Value) *structpb.Value { 13 | switch x := x.(type) { 14 | 15 | case starlark.NoneType: 16 | return &structpb.Value{Kind: &structpb.Value_NullValue{ 17 | NullValue: structpb.NullValue_NULL_VALUE}} 18 | 19 | case starlark.Bool: 20 | return &structpb.Value{Kind: &structpb.Value_BoolValue{ 21 | BoolValue: bool(x)}} 22 | 23 | case starlark.Float: 24 | return &structpb.Value{Kind: &structpb.Value_NumberValue{ 25 | NumberValue: float64(x)}} 26 | 27 | case starlark.Int: 28 | if x, ok := x.Int64(); ok { 29 | return &structpb.Value{Kind: &structpb.Value_NumberValue{ 30 | NumberValue: float64(x)}} 31 | } 32 | panic(fmt.Errorf("unexpected value of type %s: %s", x.Type(), x.String())) 33 | 34 | case starlark.String: 35 | return &structpb.Value{Kind: &structpb.Value_StringValue{ 36 | StringValue: x.GoString()}} 37 | 38 | case *starlark.List: 39 | n := x.Len() 40 | vs := make([]*structpb.Value, 0, n) 41 | for i := 0; i < n; i++ { 42 | vs = append(vs, ToProtoValue(x.Index(i))) 43 | } 44 | return &structpb.Value{Kind: &structpb.Value_ListValue{ListValue: &structpb.ListValue{ 45 | Values: vs}}} 46 | 47 | // case starlark.Tuple: // Encodes starlark.Tuple as array. 48 | // n := x.Len() 49 | // vs := make([]*structpb.Value, 0, n) 50 | // for i := 0; i < n; i++ { 51 | // vs = append(vs, ToProtoValue(x.Index(i))) 52 | // } 53 | // return &structpb.Value{Kind: &structpb.Value_ListValue{ListValue: &structpb.ListValue{ 54 | // Values: vs}}} 55 | 56 | case *starlark.Dict: 57 | vs := make(map[string]*structpb.Value, x.Len()) 58 | for _, kv := range x.Items() { 59 | k := kv.Index(0).(starlark.String).GoString() 60 | v := ToProtoValue(kv.Index(1)) 61 | vs[k] = v 62 | } 63 | return &structpb.Value{Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{ 64 | Fields: vs}}} 65 | 66 | default: 67 | panic(fmt.Errorf("unexpected value of type %s: %s", x.Type(), x.String())) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pkg/starlarkvalue/value_test.go: -------------------------------------------------------------------------------- 1 | package starlarkvalue 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "go.starlark.net/starlark" 8 | ) 9 | 10 | func TestNonJSONLikeFails(t *testing.T) { 11 | err := ProtoCompatible(starlark.None) 12 | require.NoError(t, err) 13 | 14 | k := starlark.MakeInt(42) 15 | err = ProtoCompatible(k) 16 | require.NoError(t, err) 17 | 18 | v := starlark.String("bla") 19 | err = ProtoCompatible(v) 20 | require.NoError(t, err) 21 | 22 | d := starlark.NewDict(1) 23 | err = d.SetKey(k, v) 24 | require.NoError(t, err) 25 | 26 | err = ProtoCompatible(d) 27 | require.EqualError(t, err, `want string key, got: (dict) {42: "bla"}`) 28 | 29 | s := starlark.NewSet(1) 30 | err = ProtoCompatible(s) 31 | require.EqualError(t, err, `incompatible value (set): set([])`) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/tags/name_test.go: -------------------------------------------------------------------------------- 1 | package tags 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestLegalName(t *testing.T) { 10 | require.NoError(t, LegalName("some_name")) 11 | require.NoError(t, LegalName("92")) 12 | require.NoError(t, LegalName("_ah")) 13 | require.NoError(t, LegalName("ah")) 14 | require.Error(t, LegalName("Ah")) 15 | require.Error(t, LegalName("ah ah")) 16 | require.Error(t, LegalName("ah-ah")) 17 | require.Error(t, LegalName("!!")) 18 | } 19 | -------------------------------------------------------------------------------- /pkg/tags/tags.go: -------------------------------------------------------------------------------- 1 | package tags 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | // Alphabet lists the only characters allowed in LegalName 11 | const Alphabet = "abcdefghijklmnopqrstuvwxyz_1234567890" 12 | 13 | // Tags represent a set of legal (per LegalName) tags. 14 | type Tags = map[string]struct{} 15 | 16 | // Filter is used to activate or deactivate a monkey.check during Fuzz. 17 | type Filter struct { 18 | excludeAll, includeAll bool 19 | include, exclude Tags 20 | } 21 | 22 | // NewFilter attempts to build a tags filter from CLI-ish arguments. 23 | func NewFilter(includeSetButZero, excludeSetButZero bool, i, o []string) (r *Filter, err error) { 24 | if includeSetButZero && excludeSetButZero || 25 | len(i) != 0 && len(o) != 0 || 26 | includeSetButZero && len(o) != 0 || 27 | excludeSetButZero && len(i) != 0 { 28 | err = errors.New("filtering tags with both inclusion and exclusion lists is unsupported") 29 | return 30 | } 31 | f := &Filter{excludeAll: includeSetButZero, includeAll: excludeSetButZero} 32 | if f.include, err = fromSlice(i); err != nil { 33 | return 34 | } 35 | if f.exclude, err = fromSlice(o); err != nil { 36 | return 37 | } 38 | r = f 39 | return 40 | } 41 | 42 | // Excludes applies the filter to a monkey.check's tags. 43 | func (f *Filter) Excludes(checking Tags) bool { 44 | if f.includeAll { 45 | return false 46 | } 47 | if f.excludeAll { 48 | return true 49 | } 50 | for tag := range checking { 51 | if _, ok := f.include[tag]; ok { 52 | return !ok 53 | } 54 | if _, ok := f.exclude[tag]; ok { 55 | return ok 56 | } 57 | } 58 | return false 59 | } 60 | 61 | func fromSlice(xs []string) (r Tags, err error) { 62 | m := make(Tags, len(xs)) 63 | for _, x := range xs { 64 | if err = LegalName(x); err != nil { 65 | return 66 | } 67 | if _, ok := m[x]; ok { 68 | err = fmt.Errorf("tag %q appears more than once in filter list", x) 69 | return 70 | } 71 | m[x] = struct{}{} 72 | } 73 | r = m 74 | return 75 | } 76 | 77 | // LegalName fails when string isn't the right format. 78 | func LegalName(name string) error { 79 | if len(name) == 0 { 80 | return errors.New("string is empty") 81 | } 82 | if len(name) > 255 { 83 | return fmt.Errorf("string is too long: %q", name) 84 | } 85 | for _, c := range name { 86 | if !strings.ContainsRune(Alphabet, c) { 87 | return fmt.Errorf("only characters from %s should be in %q", Alphabet, name) 88 | } 89 | } 90 | return nil 91 | } 92 | 93 | var isFullCapsRegexp = regexp.MustCompile(`^[A-Z]+[A-Z0-9_]*$`) 94 | 95 | // IsFullCaps returns whether a name would represent a Pythonic constant 96 | func IsFullCaps(name string) bool { 97 | return isFullCapsRegexp.MatchString(name) 98 | } 99 | -------------------------------------------------------------------------------- /pkg/tags/unpack.go: -------------------------------------------------------------------------------- 1 | package tags 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "go.starlark.net/starlark" 8 | ) 9 | 10 | var _ starlark.Unpacker = (*UniqueStrings)(nil) 11 | 12 | // UniqueStrings implements starlark.Unpacker 13 | type UniqueStrings struct { 14 | strings []string 15 | } 16 | 17 | // Unpack unmarshals UniqueStrings from a starlark.Value 18 | func (us *UniqueStrings) Unpack(v starlark.Value) (err error) { 19 | us.strings, err = unpack(v) 20 | return 21 | } 22 | 23 | // GoStringsMap returns the unique strings as a Go map 24 | func (us *UniqueStrings) GoStringsMap() (m Tags) { 25 | m = make(Tags, len(us.strings)) 26 | for _, s := range us.strings { 27 | m[s] = struct{}{} 28 | } 29 | return 30 | } 31 | 32 | // GoStrings returns the list of unique strings as a Go slice 33 | func (us *UniqueStrings) GoStrings() []string { return us.strings } 34 | 35 | var _ starlark.Unpacker = (*UniqueStringsNonEmpty)(nil) 36 | 37 | // UniqueStringsNonEmpty implements starlark.Unpacker 38 | type UniqueStringsNonEmpty struct { 39 | strings []string 40 | } 41 | 42 | // Unpack unmarshals UniqueStringsNonEmpty from a starlark.Value 43 | func (us *UniqueStringsNonEmpty) Unpack(v starlark.Value) (err error) { 44 | if us.strings, err = unpack(v); err != nil { 45 | return 46 | } 47 | if len(us.strings) == 0 { 48 | err = errors.New("must not be empty") 49 | } 50 | return 51 | } 52 | 53 | // GoStrings returns the list of unique strings as a Go slice 54 | func (us *UniqueStringsNonEmpty) GoStrings() []string { return us.strings } 55 | 56 | func unpack(v starlark.Value) ([]string, error) { 57 | list, ok := v.(*starlark.List) 58 | if !ok { 59 | return nil, fmt.Errorf("got %s, want list", v.Type()) 60 | } 61 | 62 | l := list.Len() 63 | m := make(map[uint32]struct{}, l) 64 | strs := make([]string, 0, l) 65 | for i := 0; i < l; i++ { 66 | x := list.Index(i) 67 | 68 | s, isString := x.(starlark.String) 69 | if !isString { 70 | return nil, fmt.Errorf("got %s, want string", x.Type()) 71 | } 72 | 73 | h, err := s.Hash() 74 | if err != nil { 75 | panic("unreachable") 76 | } 77 | if _, isDupe := m[h]; isDupe { 78 | return nil, fmt.Errorf("%s appears more than once", s) 79 | } 80 | m[h] = struct{}{} 81 | 82 | strs = append(strs, s.GoString()) 83 | } 84 | return strs, nil 85 | } 86 | -------------------------------------------------------------------------------- /pkg/tags/unpack_test.go: -------------------------------------------------------------------------------- 1 | package tags 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "go.starlark.net/starlark" 8 | ) 9 | 10 | func TestUnpackingUniqueStrings(t *testing.T) { 11 | want := starlark.NewList([]starlark.Value{starlark.String("Oh"), starlark.String("nice!")}) 12 | 13 | var sl UniqueStrings 14 | err := starlark.UnpackArgs("unpack", starlark.Tuple{want}, nil, "sl", &sl) 15 | require.NoError(t, err) 16 | 17 | require.Equal(t, []string{"Oh", "nice!"}, sl.GoStrings()) 18 | require.Equal(t, map[string]struct{}{"Oh": {}, "nice!": {}}, sl.GoStringsMap()) 19 | } 20 | 21 | func TestUnpackingUniqueStringsWithBadItem(t *testing.T) { 22 | want := starlark.NewList([]starlark.Value{starlark.String("Oh"), starlark.MakeInt(42)}) 23 | var sl UniqueStrings 24 | err := starlark.UnpackArgs("nope", starlark.Tuple{want}, nil, "sl", &sl) 25 | require.EqualError(t, err, `nope: for parameter sl: got int, want string`) 26 | } 27 | 28 | func TestUnpackingUniqueStringsWithDuplicateItem(t *testing.T) { 29 | want := starlark.NewList([]starlark.Value{starlark.String("Oh"), starlark.String("Oh")}) 30 | var sl UniqueStrings 31 | err := starlark.UnpackArgs("dupe", starlark.Tuple{want}, nil, "sl", &sl) 32 | require.EqualError(t, err, `dupe: for parameter sl: "Oh" appears more than once`) 33 | } 34 | --------------------------------------------------------------------------------