From 2177e155c2e35bc8b5e5bf48bc0d3fdc869a0458 Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Wed, 13 Jan 2021 11:03:02 +0000 Subject: [PATCH] feat(climatecontrol): initial working version --- .gitignore | 4 + .golangci.yml | 78 +++++++++++++++ Makefile | 175 ++++++++++++++++++++++++++++++++ climatecontrol.go | 77 ++++++++++++++ climatecontrol_example_test.go | 108 ++++++++++++++++++++ climatecontrol_test.go | 178 +++++++++++++++++++++++++++++++++ go.mod | 5 + go.sum | 10 ++ 8 files changed, 635 insertions(+) create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 Makefile create mode 100644 climatecontrol.go create mode 100644 climatecontrol_example_test.go create mode 100644 climatecontrol_test.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2663299 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/*.tidy-check +/bin/* +/coverage.out +/output.txt diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..b5356f5 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,78 @@ +linters-settings: + funlen: + lines: 100 + statements: 150 + gocyclo: + min-complexity: 20 + golint: + min-confidence: 0 + govet: + check-shadowing: true + enable-all: true + lll: + line-length: 80 + tab-width: 4 + maligned: + suggest-new: true + misspell: + locale: US + +linters: + disable-all: true + enable: + - bodyclose + - deadcode + - depguard + - dupl + - errcheck + - funlen + - gochecknoinits + - goconst + - gocritic + - gocyclo + - goerr113 + - goimports + - golint + - goprintffuncname + - gosec + - gosimple + - govet + - ineffassign + - lll + - misspell + - nakedret + - nlreturn + - noctx + - nolintlint + - scopelint + - sqlclosecheck + - staticcheck + - structcheck + - typecheck + - unconvert + - unused + - varcheck + - whitespace + +issues: + include: + # - EXC0002 # disable excluding of issues about comments from golint + exclude: + - Using the variable on range scope `tt` in function literal + - Using the variable on range scope `tc` in function literal + exclude-rules: + - path: "_test\\.go" + linters: + - funlen + - dupl + - source: "^//go:generate " + linters: + - lll + - source: "`json:" + linters: + - lll + +run: + timeout: 2m + allow-parallel-runners: true + modules-download-mode: readonly diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..73300aa --- /dev/null +++ b/Makefile @@ -0,0 +1,175 @@ +GOMODNAME := $(shell grep 'module' go.mod | sed -e 's/^module //') +SOURCES := $(shell find . -name "*.go" -or -name "go.mod" -or -name "go.sum" \ + -or -name "Makefile") + +# Verbose output +ifdef VERBOSE +V = -v +endif + +# +# Environment +# + +BINDIR := bin +TOOLDIR := $(BINDIR)/tools + +# Global environment variables for all targets +SHELL ?= /bin/bash +SHELL := env \ + GO111MODULE=on \ + GOBIN=$(CURDIR)/$(TOOLDIR) \ + CGO_ENABLED=1 \ + PATH='$(CURDIR)/$(BINDIR):$(CURDIR)/$(TOOLDIR):$(PATH)' \ + $(SHELL) + +# +# Defaults +# + +# Default target +.DEFAULT_GOAL := test + +# +# Tools +# + +TOOLS += $(TOOLDIR)/gobin +gobin: $(TOOLDIR)/gobin +$(TOOLDIR)/gobin: + GO111MODULE=off go get -u github.com/myitcv/gobin + +# external tool +define tool # 1: binary-name, 2: go-import-path +TOOLS += $(TOOLDIR)/$(1) + +.PHONY: $(1) +$(1): $(TOOLDIR)/$(1) + +$(TOOLDIR)/$(1): $(TOOLDIR)/gobin Makefile + gobin $(V) "$(2)" +endef + +$(eval $(call tool,godoc,golang.org/x/tools/cmd/godoc)) +$(eval $(call tool,gofumports,mvdan.cc/gofumpt/gofumports)) +$(eval $(call tool,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.35)) + +.PHONY: tools +tools: $(TOOLS) + +# +# Development +# + +TEST ?= $$(go list ./... | grep -v 'vendor') +BENCH ?= . + +.PHONY: clean +clean: + rm -f $(TOOLS) + rm -f ./coverage.out ./go.mod.tidy-check ./go.sum.tidy-check + +.PHONY: clean-golden +clean-golden: + rm -f $(shell find * -path '*/testdata/*' -name "*.golden" \ + -exec echo "'{}'" \;) + +.PHONY: test +test: + go test $(V) -count=1 -race $(TESTARGS) $(TEST) + +.PHONY: test-update-golden +test-update-golden: + @$(MAKE) test UPDATE_GOLDEN=1 + +.PHONY: regen-golden +regen-golden: clean-golden test-update-golden + +.PHONY: test-deps +test-deps: + go test all + +.PHONY: lint +lint: golangci-lint + GOGC=off golangci-lint $(V) run + +.PHONY: format +format: gofumports + gofumports -w . + +.SILENT: bench +.PHONY: bench +bench: + go test $(V) -count=1 -bench=$(BENCH) $(TESTARGS) $(TEST) + +# +# Coverage +# + +.PHONY: cov +cov: coverage.out + +.PHONY: cov-html +cov-html: coverage.out + go tool cover -html=./coverage.out + +.PHONY: cov-func +cov-func: coverage.out + go tool cover -func=./coverage.out + +coverage.out: $(SOURCES) + go test $(V) -covermode=count -coverprofile=./coverage.out ./... + +# +# Dependencies +# + +.PHONY: deps +deps: + $(info Downloading dependencies) + go mod download + +.PHONY: tidy +tidy: + go mod tidy $(V) + +.PHONY: verify +verify: + go mod verify + +.SILENT: check-tidy +.PHONY: check-tidy +check-tidy: + cp go.mod go.mod.tidy-check + cp go.sum go.sum.tidy-check + go mod tidy + ( \ + diff go.mod go.mod.tidy-check && \ + diff go.sum go.sum.tidy-check && \ + rm -f go.mod go.sum && \ + mv go.mod.tidy-check go.mod && \ + mv go.sum.tidy-check go.sum \ + ) || ( \ + rm -f go.mod go.sum && \ + mv go.mod.tidy-check go.mod && \ + mv go.sum.tidy-check go.sum; \ + exit 1 \ + ) + +# +# Documentation +# + +# Serve docs +.PHONY: docs +docs: godoc + $(info serviing docs on http://127.0.0.1:6060/pkg/$(GOMODNAME)/) + @godoc -http=127.0.0.1:6060 + +# +# Release +# + +.PHONY: new-version +new-version: + npx standard-version diff --git a/climatecontrol.go b/climatecontrol.go new file mode 100644 index 0000000..743b686 --- /dev/null +++ b/climatecontrol.go @@ -0,0 +1,77 @@ +// Package climatecontrol provides test helper functions which temporary changes +// environment variables accessible via os.Getenv and os.LookupEnv. After they +// are done, all changes to environment variables are reset. +package climatecontrol + +import ( + "os" + "strings" + "sync" +) + +var mux = &sync.Mutex{} + +// WithEnv temporarily sets all given vars as environment variables during the +// execution of f function. Existing environment variables are also available +// within f. Any overridden environment variables will contain the overridden +// value.. +// +// After f execution completes all changes to environment variables are reset, +// including manual changes within the f function. +func WithEnv(vars map[string]string, f func()) { + mux.Lock() + defer mux.Unlock() + + undo := parseEnviron(os.Environ()) + + apply(vars) + defer func() { + os.Clearenv() + apply(undo) + }() + + f() +} + +// WithCleanEnv temporarily changes all environment variables available within f +// function to only be those provided. Existing environment variables are not +// available within f. +// +// After f execution completes all changes to environment variables are reset, +// including manual changes within the f function. +func WithCleanEnv(vars map[string]string, f func()) { + mux.Lock() + defer mux.Unlock() + + undo := parseEnviron(os.Environ()) + + os.Clearenv() + apply(vars) + defer func() { + os.Clearenv() + apply(undo) + }() + + f() +} + +func apply(vars map[string]string) { + for k, v := range vars { + os.Setenv(k, v) + } +} + +func parseEnviron(vars []string) map[string]string { + r := map[string]string{} + + for _, v := range vars { + i := strings.Index(v, "=") + if i < 1 { + continue + } + + r[v[0:i]] = v[i+1:] + } + + return r +} diff --git a/climatecontrol_example_test.go b/climatecontrol_example_test.go new file mode 100644 index 0000000..cfde412 --- /dev/null +++ b/climatecontrol_example_test.go @@ -0,0 +1,108 @@ +package climatecontrol_test + +import ( + "fmt" + "os" + + cc "github.com/jimeh/climatecontrol" +) + +func ExampleWithEnv() { + // existing environment variables + os.Setenv("MYAPP_HOSTNAME", "myapp.com") + os.Setenv("MYAPP_PORT", "80") + + fmt.Println("Before:") + fmt.Printf(" - MYAPP_HOSTNAME=%s\n", os.Getenv("MYAPP_HOSTNAME")) + fmt.Printf(" - MYAPP_PORT=%s\n", os.Getenv("MYAPP_PORT")) + fmt.Printf(" - MYAPP_THEME=%s\n", os.Getenv("MYAPP_THEME")) + fmt.Printf(" - MYAPP_TESTING=%s\n", os.Getenv("MYAPP_TESTING")) + + // temporary environment variables + env := map[string]string{ + "MYAPP_HOSTNAME": "testing-myapp.test", + "MYAPP_TESTING": "unit", + } + cc.WithEnv(env, func() { + os.Setenv("MYAPP_THEME", "dark") + + fmt.Println("Inside func:") + fmt.Printf(" - MYAPP_HOSTNAME=%s\n", os.Getenv("MYAPP_HOSTNAME")) + fmt.Printf(" - MYAPP_PORT=%s\n", os.Getenv("MYAPP_PORT")) + fmt.Printf(" - MYAPP_THEME=%s\n", os.Getenv("MYAPP_THEME")) + fmt.Printf(" - MYAPP_TESTING=%s\n", os.Getenv("MYAPP_TESTING")) + }) + + // original environment variables restored + fmt.Println("After:") + fmt.Printf(" - MYAPP_HOSTNAME=%s\n", os.Getenv("MYAPP_HOSTNAME")) + fmt.Printf(" - MYAPP_PORT=%s\n", os.Getenv("MYAPP_PORT")) + fmt.Printf(" - MYAPP_THEME=%s\n", os.Getenv("MYAPP_THEME")) + fmt.Printf(" - MYAPP_TESTING=%s\n", os.Getenv("MYAPP_TESTING")) + // Output: + // Before: + // - MYAPP_HOSTNAME=myapp.com + // - MYAPP_PORT=80 + // - MYAPP_THEME= + // - MYAPP_TESTING= + // Inside func: + // - MYAPP_HOSTNAME=testing-myapp.test + // - MYAPP_PORT=80 + // - MYAPP_THEME=dark + // - MYAPP_TESTING=unit + // After: + // - MYAPP_HOSTNAME=myapp.com + // - MYAPP_PORT=80 + // - MYAPP_THEME= + // - MYAPP_TESTING= +} + +func ExampleWithCleanEnv() { + // existing environment variables + os.Setenv("MYAPP_HOSTNAME", "myapp.com") + os.Setenv("MYAPP_PORT", "80") + + fmt.Println("Before:") + fmt.Printf(" - MYAPP_HOSTNAME=%s\n", os.Getenv("MYAPP_HOSTNAME")) + fmt.Printf(" - MYAPP_PORT=%s\n", os.Getenv("MYAPP_PORT")) + fmt.Printf(" - MYAPP_THEME=%s\n", os.Getenv("MYAPP_THEME")) + fmt.Printf(" - MYAPP_TESTING=%s\n", os.Getenv("MYAPP_TESTING")) + + // temporary environment variables + env := map[string]string{ + "MYAPP_HOSTNAME": "testing-myapp.test", + "MYAPP_TESTING": "unit", + } + cc.WithCleanEnv(env, func() { + os.Setenv("MYAPP_THEME", "dark") + + fmt.Println("Inside func:") + fmt.Printf(" - MYAPP_HOSTNAME=%s\n", os.Getenv("MYAPP_HOSTNAME")) + fmt.Printf(" - MYAPP_PORT=%s\n", os.Getenv("MYAPP_PORT")) + fmt.Printf(" - MYAPP_THEME=%s\n", os.Getenv("MYAPP_THEME")) + fmt.Printf(" - MYAPP_TESTING=%s\n", os.Getenv("MYAPP_TESTING")) + }) + + // original environme + fmt.Println("After:") + fmt.Printf(" - MYAPP_HOSTNAME=%s\n", os.Getenv("MYAPP_HOSTNAME")) + fmt.Printf(" - MYAPP_PORT=%s\n", os.Getenv("MYAPP_PORT")) + fmt.Printf(" - MYAPP_THEME=%s\n", os.Getenv("MYAPP_THEME")) + fmt.Printf(" - MYAPP_TESTING=%s\n", os.Getenv("MYAPP_TESTING")) + // Output: + // Before: + // - MYAPP_HOSTNAME=myapp.com + // - MYAPP_PORT=80 + // - MYAPP_THEME= + // - MYAPP_TESTING= + // Inside func: + // - MYAPP_HOSTNAME=testing-myapp.test + // - MYAPP_PORT= + // - MYAPP_THEME=dark + // - MYAPP_TESTING=unit + // After: + // - MYAPP_HOSTNAME=myapp.com + // - MYAPP_PORT=80 + // - MYAPP_THEME= + // - MYAPP_TESTING= +} diff --git a/climatecontrol_test.go b/climatecontrol_test.go new file mode 100644 index 0000000..e4e66ce --- /dev/null +++ b/climatecontrol_test.go @@ -0,0 +1,178 @@ +package climatecontrol + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestWithEnv(t *testing.T) { + tests := []struct { + name string + env map[string]string + }{ + { + name: "empty", + env: map[string]string{}, + }, + { + name: "new vars", + env: map[string]string{ + "CC_TEST_INSIDE": "new var is here", + "CC_TEST_FOO": "bar", + }, + }, + { + name: "set existing", + env: map[string]string{ + "CC_TEST_INSIDE": "new var is here", + "CC_TEST_OUTSIDE": "this is not the same", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os.Setenv("CC_TEST_OUTSIDE", t.Name()) + outside := parseEnviron(os.Environ()) + + WithEnv(tt.env, func() { + os.Setenv("CC_TEST_SET_INSIDE", t.Name()+"-manual") + + for k, v := range tt.env { + got := os.Getenv(k) + assert.Equal(t, v, got) + } + + for k, v := range outside { + if _, ok := tt.env[k]; !ok { + assert.Equal(t, v, os.Getenv(k)) + } + } + }) + + _, exists := os.LookupEnv("CC_TEST_SET_INSIDE") + assert.Equal(t, false, exists) + + for k := range tt.env { + if _, ok := outside[k]; !ok { + _, exists := os.LookupEnv(k) + assert.Equal(t, false, exists) + } + } + + for k, v := range outside { + assert.Equal(t, v, os.Getenv(k)) + } + + os.Unsetenv("CC_TEST_OUTSIDE") + }) + } +} + +func TestWithCleanEnv(t *testing.T) { + tests := []struct { + name string + env map[string]string + }{ + { + name: "empty", + env: map[string]string{}, + }, + { + name: "new vars", + env: map[string]string{ + "CC_TEST_INSIDE": "new var is here", + "CC_TEST_FOO": "bar", + }, + }, + { + name: "set existing", + env: map[string]string{ + "CC_TEST_INSIDE": "new var is here", + "CC_TEST_OUTSIDE": "is is not the same", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os.Setenv("CC_TEST_OUTSIDE", t.Name()) + outside := parseEnviron(os.Environ()) + + WithCleanEnv(tt.env, func() { + os.Setenv("CC_TEST_SET_INSIDE", t.Name()+"-manual") + + for k, v := range tt.env { + got := os.Getenv(k) + assert.Equal(t, v, got) + } + + for k := range outside { + if _, ok := tt.env[k]; !ok { + _, exists := os.LookupEnv(k) + assert.Equal(t, false, exists) + } + } + }) + + _, exists := os.LookupEnv("CC_TEST_SET_INSIDE") + assert.Equal(t, false, exists) + + for k := range tt.env { + if _, ok := outside[k]; !ok { + _, exists := os.LookupEnv(k) + assert.Equal(t, false, exists) + } + } + + for k, v := range outside { + assert.Equal(t, v, os.Getenv(k)) + } + + os.Unsetenv("CC_TEST_OUTSIDE") + }) + } +} + +func Test_parseEnviron(t *testing.T) { + tests := []struct { + name string + vars []string + want map[string]string + }{ + { + name: "empty", + vars: []string{}, + want: map[string]string{}, + }, + { + name: "various", + vars: []string{ + "USER=john", + "NAME=John Doe", + "SHELL=/bin/bash", + "GOPRIVATE=", + "X=11", + "TAGS=foo=bar,hello=world", + "_=go", + "INVALID-var", + }, + want: map[string]string{ + "USER": "john", + "NAME": "John Doe", + "SHELL": "/bin/bash", + "GOPRIVATE": "", + "X": "11", + "TAGS": "foo=bar,hello=world", + "_": "go", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := parseEnviron(tt.vars) + + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..30b6b62 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/jimeh/climatecontrol + +go 1.15 + +require github.com/stretchr/testify v1.6.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..56d62e7 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=