├── .github └── workflows │ └── action.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── README.md ├── benchmark_test.go ├── component_manager.go ├── component_manager_test.go ├── doc.go ├── eventbus.go ├── eventbus_test.go ├── framework.go ├── framework_test.go ├── go.mod ├── go.sum ├── injection.go ├── injection_config_test.go ├── injection_test.go ├── injection_wire_test.go ├── registry.go ├── registry_test.go ├── runtime.go ├── runtime_test.go ├── session.go ├── session_test.go ├── tester.go ├── util.go └── util_test.go /.github/workflows/action.yml: -------------------------------------------------------------------------------- 1 | name: action 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v3 17 | with: 18 | go-version: 1.18 19 | 20 | - name: Build 21 | run: go build -v ./... 22 | 23 | - name: Lint 24 | uses: golangci/golangci-lint-action@v3 25 | 26 | - name: Test 27 | run: | 28 | n=0 29 | until go test -cpu 2 -timeout 2m -race -v ./...; do 30 | if [ $n -ge 3 ]; then 31 | exit 1 32 | fi 33 | echo Test coverage failed, retrying in 3 seconds... 34 | n=$(($n+1)) 35 | sleep 3 36 | done 37 | 38 | - name: Perform benchmark testing 39 | run: go test -bench=Benchmark . -run Benchmark 40 | 41 | - name: Fuzz testing 42 | run: | 43 | files=$(grep -r --include='**_test.go' --files-with-matches 'func Fuzz' .) 44 | 45 | for file in ${files}; do 46 | funcs=$(awk '/^func Fuzz/ {print $2}' $file) 47 | for func in ${funcs} 48 | do 49 | func=${func::-2} 50 | echo "Fuzzing $func in $file" 51 | parentDir=$(dirname $file) 52 | go test $parentDir -run=$func -fuzz=$func -fuzztime=30s 53 | done 54 | done 55 | 56 | - name: Test coverage 57 | run: | 58 | until go test -v -p 1 -covermode=count -coverprofile=coverage.out ./...; do 59 | echo Test coverage failed, retrying in 3 seconds... 60 | sleep 3 61 | done 62 | 63 | - name: Tool cover to func 64 | run: go tool cover -func=coverage.out -o=cover.out 65 | 66 | - name: Obtain coverage 67 | run: echo "COVERAGE=$(grep -e 'total' cover.out | awk '{print $3;}')" >> $GITHUB_ENV 68 | 69 | - name: Create coverage badge 70 | uses: schneegans/dynamic-badges-action@v1.6.0 71 | with: 72 | auth: ${{ secrets.GIST_SECRET }} 73 | gistID: c77b22000b3e249510dfb4542847c708 74 | filename: test_coverage.json 75 | label: coverage 76 | message: ${{ env.COVERAGE }} 77 | valColorRange: ${{ env.COVERAGE }} 78 | maxColorRange: 100 79 | minColorRange: 0 80 | 81 | - name: Tool cover to html 82 | run: go tool cover -html=coverage.out -o=cover.html 83 | 84 | - name: Upload html coverage 85 | uses: exuanbo/actions-deploy-gist@v1 86 | with: 87 | token: ${{ secrets.GIST_SECRET }} 88 | gist_id: c77b22000b3e249510dfb4542847c708 89 | file_path: cover.html 90 | file_type: text 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | *.iml 17 | *.prof 18 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | # include test files or not 3 | tests: true 4 | 5 | linters: 6 | enable: 7 | # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string 8 | - govet 9 | # find unused code 10 | # - deadcode => The owner seems to have abandoned the linter. Replaced by unused. 11 | # simplify source code: 12 | - gosimple 13 | # govet "on steroids" 14 | - staticcheck 15 | # Detects when assignments to existing variables are not used 16 | - ineffassign 17 | # diagnostics for bugs, performance and style issues 18 | - gocritic 19 | # checks whether HTTP response body is closed successfully 20 | # - bodyclose => not supported by go 1.18 21 | # finds repeated strings that could be replaced by a constant 22 | - goconst 23 | # checks the cyclomatic complexity of functions 24 | - gocyclo 25 | # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases 26 | - errcheck 27 | # Like the front-end of a Go compiler, parses and type-checks Go code 28 | - typecheck 29 | # Checks Go code for unused constants, variables, functions and types 30 | - unused 31 | # bidichk ⚙️ Checks for dangerous unicode character sequences 32 | - bidichk 33 | # containedctx is a linter that detects struct contained context.Context field 34 | - containedctx 35 | # contextcheck 36 | # - contextcheck // go1.18 issue 37 | # Checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error. 38 | - errname 39 | # checks function and package cyclomatic complexity 40 | - cyclop 41 | # Checks assignments with too many blank identifiers 42 | - dogsled 43 | # Tool for code clone detection 44 | - dupl 45 | # check for two durations multiplied together 46 | - durationcheck 47 | # Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted. 48 | - errchkjson 49 | # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. 50 | - errorlint 51 | # ⚙️ check exhaustiveness of enum switch statements 52 | - exhaustive 53 | # checks for pointers to enclosing loop variables 54 | - exportloopref 55 | # finds forced type assertions 56 | - forcetypeassert 57 | # Tool for detection of long functions 58 | - funlen 59 | # Computes and checks the cognitive complexity of functions 60 | - gocognit 61 | # Finds repeated strings that could be replaced by a constant 62 | - goconst 63 | # Checks is file header matches to pattern 64 | - goheader 65 | # An analyzer to detect magic numbers. 66 | - gomnd 67 | # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. 68 | - gomoddirectives 69 | # Checks that printf-like functions are named with f at the end 70 | - goprintffuncname 71 | # Inspects source code for security problems 72 | - gosec 73 | # Enforces consistent import aliases 74 | - importas 75 | # Accept Interfaces, Return Concrete Types 76 | - ireturn 77 | # Finds slice declarations with non-zero initial length 78 | - makezero 79 | # Finds commonly misspelled English words in comments 80 | - misspell 81 | # Finds naked returns in functions greater than a specified function length 82 | - nakedret 83 | # Reports deeply nested if statements 84 | # - nestif => TODO: fix this 85 | # Checks that there is no simultaneous return of nil error and an invalid value. 86 | - nilnil 87 | # Reports ill-formed or insufficient nolint directives 88 | - nolintlint 89 | # Checks for misuse of Sprintf to construct a host with port in a URL. 90 | - nosprintfhostport 91 | # find code that shadows one of Go's predeclared identifiers 92 | - predeclared 93 | # Checks the struct tags. 94 | - tagliatelle 95 | # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 96 | - tenv 97 | # Remove unnecessary type conversions 98 | - unconvert 99 | # checks that the length of a variable's name matches its scope 100 | - varnamelen 101 | # Tool for detection of leading and trailing whitespace 102 | - whitespace 103 | disable: 104 | # Finds unused struct fields 105 | - structcheck 106 | 107 | 108 | linters-settings: 109 | gosimple: 110 | go: "1.18" 111 | staticcheck: 112 | go: "1.18" 113 | decorder: 114 | # Required order of `type`, `const`, `var` and `func` declarations inside a file. 115 | # Default: types before constants before variables before functions. 116 | dec-order: 117 | - type 118 | - const 119 | - var 120 | - func 121 | # If true, order of declarations is not checked at all. 122 | # Default: true (disabled) 123 | disable-dec-order-check: false 124 | # If true, `init` func can be anywhere in file (does not have to be declared before all other functions). 125 | # Default: true (disabled) 126 | disable-init-func-first-check: false 127 | # If true, multiple global `type`, `const` and `var` declarations are allowed. 128 | # Default: true (disabled) 129 | disable-dec-num-check: false 130 | funlen: 131 | # Checks the number of lines in a function. 132 | # If lower than 0, disable the check. 133 | # Default: 60 134 | lines: 120 135 | # Checks the number of statements in a function. 136 | # If lower than 0, disable the check. 137 | # Default: 40 138 | statements: 60 139 | goheader: 140 | # The template use for checking. 141 | # Default: "" 142 | template: "" 143 | nolintlint: 144 | # Disable to ensure that all nolint directives actually have an effect. 145 | # Default: false 146 | allow-unused: false 147 | # Disable to ensure that nolint directives don't have a leading space. 148 | # Default: true 149 | allow-leading-space: false 150 | # Exclude following linters from requiring an explanation. 151 | # Default: [] 152 | allow-no-explanation: [] 153 | # Enable to require an explanation of nonzero length after each nolint directive. 154 | # Default: false 155 | require-explanation: true 156 | # Enable to require nolint directives to mention the specific linter being suppressed. 157 | # Default: false 158 | require-specific: true 159 | varnamelen: 160 | # The longest distance, in source lines, that is being considered a "small scope". 161 | # Variables used in at most this many lines will be ignored. 162 | # Default: 5 163 | max-distance: 20 164 | # The minimum length of a variable's name that is considered "long". 165 | # Variable names that are at least this long will be ignored. 166 | # Default: 3 167 | min-name-length: 2 168 | # Check method receivers. 169 | # Default: false 170 | check-receiver: true 171 | # Check named return values. 172 | # Default: false 173 | check-return: true 174 | # Check type parameters. 175 | # Default: false 176 | check-type-param: true 177 | # Ignore "ok" variables that hold the bool return value of a type assertion. 178 | # Default: false 179 | ignore-type-assert-ok: true 180 | # Ignore "ok" variables that hold the bool return value of a map index. 181 | # Default: false 182 | ignore-map-index-ok: true 183 | # Ignore "ok" variables that hold the bool return value of a channel receive. 184 | # Default: false 185 | ignore-chan-recv-ok: true 186 | # Optional list of variable names that should be ignored completely. 187 | # Default: [] 188 | ignore-names: 189 | - err 190 | # Optional list of variable declarations that should be ignored completely. 191 | # Entries must be in one of the following forms (see below for examples): 192 | # - for variables, parameters, named return values, method receivers, or type parameters: 193 | # ( can also be a pointer/slice/map/chan/...) 194 | # - for constants: const 195 | # 196 | # Default: [] 197 | ignore-decls: 198 | - c echo.Context 199 | - t testing.T 200 | - f *foo.Bar 201 | - e error 202 | - i int 203 | - const C 204 | - T any 205 | - m map[string]int 206 | cyclop: 207 | # The maximal code complexity to report. 208 | # Default: 10 209 | max-complexity: 15 210 | # The maximal average package complexity. 211 | # If it's higher than 0.0 (float) the check is enabled 212 | # Default: 0.0 213 | package-average: 2.5 214 | # Should ignore tests. 215 | # Default: false 216 | skip-tests: true 217 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2023 boot-go 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | Commit tag 5 | github action 6 | test coverage 7 | go report 8 | Go Reference 9 | License: MIT 10 |
11 |
12 | 13 | **boot-go** accentuate [component-based development](https://en.wikipedia.org/wiki/Component-based_software_engineering) (CBD). 14 | 15 | This is an opinionated view of writing modular and cohesive [Go](https://github.com/golang/go) code. It emphasizes the separation of concerns by loosely coupled components, which communicate with each other via methods and events. The goal is to support writing maintainable code on the long run by leveraging the well-defined standard library. 16 | 17 | **boot-go** provided key features are: 18 | - dependency injection 19 | - configuration handling 20 | - code decoupling 21 | 22 | ### Development characteristic 23 | **boot-go** supports two different development characteristic. For simplicity reason, use the functions ```Register```, ```RegisterName```, ```Override```, ```OverrideName```, ```Shutdown``` and ```Go``` to register components and start **boot-go**. This is the recommended way, despite the fact that one global session is used. 24 | 25 | But **boot-go** supports also creating new sessions, so that no global variable is required. In this case, the methods ```Register```, ```RegisterName```, ```Override```, ```OverrideName```, ```Shutdown``` and ```Go``` are provided to register components and start **boot-go**. 26 | 27 | ### Simple Example 28 | The **hello** component is a very basic example. It contains no fields or provides any interface to interact with other components. The component will just print the _'Hello World'_ message to the console. 29 | ```go 30 | package main 31 | 32 | import ( 33 | "github.com/boot-go/boot" 34 | "log" 35 | ) 36 | 37 | // init() registers a factory method, which creates a hello component. 38 | func init() { 39 | boot.Register(func() boot.Component { 40 | return &hello{} 41 | }) 42 | } 43 | 44 | // hello is the simplest component. 45 | type hello struct{} 46 | 47 | // Init is the initializer of the component. 48 | func (c *hello) Init() error { 49 | log.Printf("boot-go says > 'Hello World'\n") 50 | return nil 51 | } 52 | 53 | // Start the example and exit after the component was completed. 54 | func main() { 55 | boot.Go() 56 | } 57 | ``` 58 | 59 | The same example using a new session, which don't need any global variables. 60 | ```Go 61 | package main 62 | 63 | import ( 64 | "github.com/boot-go/boot" 65 | "log" 66 | ) 67 | 68 | // hello is the simplest component. 69 | type hello struct{} 70 | 71 | // Init is the initializer of the component. 72 | func (c *hello) Init() error { 73 | log.Printf("boot-go says > 'Hello World'\n") 74 | return nil 75 | } 76 | 77 | // Start the example and exit after the component was completed. 78 | func main() { 79 | s := boot.NewSession() 80 | s.Register(func() boot.Component { 81 | return &hello{} 82 | }) 83 | s.Go() 84 | } 85 | ``` 86 | 87 | ### Component wiring 88 | This example shows how components get wired automatically with dependency injection. The server component starts at ```:8080``` by default, but the port is configurable by setting the environment variable ```HTTP_SERVER_PORT```. 89 | ```go 90 | package main 91 | 92 | import ( 93 | "github.com/boot-go/boot" 94 | "github.com/boot-go/stack/server/httpcmp" 95 | "io" 96 | "net/http" 97 | ) 98 | 99 | // init() registers a factory method, which creates a hello component 100 | func init() { 101 | boot.Register(func() boot.Component { 102 | return &hello{} 103 | }) 104 | } 105 | 106 | // hello is a very simple http server example. 107 | // It requires the Eventbus and the chi.Server component. Both components 108 | // are injected by the boot framework automatically 109 | type hello struct { 110 | Eventbus boot.EventBus `boot:"wire"` 111 | Server httpcmp.Server `boot:"wire"` 112 | } 113 | 114 | // Init is the constructor of the component. The handler registration takes place here. 115 | func (h *hello) Init() error { 116 | // Subscribe to the registration event 117 | h.Eventbus.Subscribe(func(event httpcmp.InitializedEvent) { 118 | h.Server.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { 119 | io.WriteString(writer, "boot-go says: 'Hello World'\n") 120 | }) 121 | }) 122 | return nil 123 | } 124 | 125 | // Start the example and test with 'curl localhost:8080' 126 | func main() { 127 | boot.Go() 128 | } 129 | ``` 130 | 131 | ### Component 132 | Everything in **boot-go** starts with a component. They are key fundamental in the development and can be considered as an elementary build block. The essential concept is to get all the necessary components functioning with as less effort as possible. Therefore, components must always provide a default configuration, which uses the most common settings. As an example, a **http server** should always start using port **8080**, unless the developer specifies it. Or a postgres component should try to connect to **localhost:5432** when there is no database url provided. 133 | 134 | A component should be _fail tolerant_, _recoverable_, _agnostic_ and _decent_. 135 | 136 | | Facet | Meaning | Example | 137 | |-----------------|-----------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 138 | | _fail tolerant_ | Don't stop processing on errors. | A http request can still be processed, even when the metrics server is not available anymore. | 139 | | _recoverable_ | Try to recover from errors. | A database component should try to reconnect after losing the connection. | 140 | | _agnostic_ | Behave the same in any environment. | A key-value store component should work on a local development machine the same way as in a containerized environment. | 141 | | _decent_ | Don't overload the developer with complexity. | Keep the interface and events as simple as possible. It's better to build three smaller but specific components then one general with increased complexity. Less is often more. | 142 | 143 | ### Configuration 144 | Configuration values can also be automatically injected with arguments or environment variables at start time. The value from ```USER``` will be used in this example. If the argument ```--USER madpax``` is not set and the environment variable is not defined, it is possible to specify the reaction whether the execution should stop with a panic or continue with a warning. 145 | ```go 146 | package main 147 | 148 | import ( 149 | "github.com/boot-go/boot" 150 | "log" 151 | ) 152 | 153 | // hello is still a simple component. 154 | type hello struct{ 155 | Out string `boot:"config,key:USER,default:madjax"` // get the value from the argument list or environment variable. If no value could be determined, then use the default value `madjax`. 156 | } 157 | 158 | // init() registers a factory method, which creates a hello component and returns a reference to it. 159 | func init() { 160 | boot.Register(func() boot.Component { 161 | return &hello{} 162 | }) 163 | } 164 | 165 | // Init is the initializer of the component. 166 | func (c *hello) Init() error { 167 | log.Printf("boot-go says > 'Hello %s'\n", c.Out) 168 | return nil 169 | } 170 | 171 | // Start the example and exit after the component was completed 172 | func main() { 173 | boot.Go() 174 | } 175 | 176 | ``` 177 | 178 | 179 | ### boot stack 180 | **boot-go** was primarily designed to build opinionated frameworks and bundle them as a stack. So every developer or company can choose to use the [default stack](https://github.com/boot-go/stack), a shared stack or rather create a new one. Stacks should be build with one specific purpose in mind for building a **microservice**, **ui application**, **web application**, **data analytics application** and so on. As an example, a **web application boot stack** could contain a http server component, a sql database component, a logging and a web application framework. 181 | 182 | 183 | ### Examples 184 | More examples can be found in the [tutorial repository](https://github.com/boot-go/tutorial). 185 | -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2023 boot-go 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | * 22 | */ 23 | 24 | package boot 25 | 26 | import ( 27 | "fmt" 28 | "testing" 29 | "time" 30 | ) 31 | 32 | const ( 33 | events = 100000 34 | 35 | receivers = 1000 36 | 37 | referenceTime = 35 38 | 39 | maxRetries = 5 40 | ) 41 | 42 | type IncrementEvent struct { 43 | value int 44 | } 45 | 46 | type testReceiverComponent struct { 47 | EventBus EventBus `boot:"wire"` 48 | id int 49 | count int 50 | } 51 | 52 | func (t *testReceiverComponent) Init() error { 53 | t.count = 0 54 | err := t.EventBus.Subscribe(func(e IncrementEvent) { 55 | t.count = e.value 56 | }) 57 | if err != nil { 58 | return err 59 | } 60 | return nil 61 | } 62 | 63 | type testSenderComponent struct { 64 | EventBus EventBus `boot:"wire"` 65 | } 66 | 67 | func (t *testSenderComponent) Init() error { 68 | return nil 69 | } 70 | 71 | func (t *testSenderComponent) Start() error { 72 | for i := 0; i < events; i++ { 73 | err := t.EventBus.Publish(IncrementEvent{ 74 | value: i, 75 | }) 76 | if err != nil { 77 | return err 78 | } 79 | } 80 | return nil 81 | } 82 | 83 | func (t *testSenderComponent) Stop() error { 84 | return nil 85 | } 86 | 87 | func BenchmarkEventBus_Publish(b *testing.B) { 88 | success := false 89 | for try := 1; try < maxRetries+1; try++ { 90 | if success { 91 | break 92 | } 93 | fmt.Printf("run : %v\n", try) 94 | startTime := time.Now() 95 | s := NewSession(UnitTestFlag) 96 | for i := 0; i < receivers; i++ { 97 | err := s.RegisterName(fmt.Sprintf("receiver-%d", i), func() Component { 98 | return &testReceiverComponent{ 99 | id: i, 100 | } 101 | }) 102 | if err != nil { 103 | b.Fatal(err) 104 | } 105 | } 106 | err := s.Register(func() Component { 107 | return &testSenderComponent{} 108 | }) 109 | if err != nil { 110 | b.Fatal(err) 111 | } 112 | b.ReportAllocs() 113 | b.ResetTimer() 114 | err = s.Go() 115 | if err != nil { 116 | b.Fatal(err) 117 | } 118 | b.StopTimer() 119 | endTime := time.Now() 120 | duration := endTime.Sub(startTime) 121 | relative := referenceTime / duration.Seconds() * 100 122 | eventRate := events / duration.Seconds() 123 | fmt.Printf("reference : %vs\n", referenceTime) 124 | fmt.Printf("events : %v\n", events) 125 | fmt.Printf("receivers : %v\n", receivers) 126 | fmt.Printf("reference : %vs\n", referenceTime) 127 | fmt.Printf("duration : %v\n", duration) 128 | fmt.Printf("result : %.1f%%\n", relative) 129 | fmt.Printf("event rate: %.1f/s\n", eventRate) 130 | if duration.Seconds() <= referenceTime { 131 | success = true 132 | } else { 133 | fmt.Printf("insufficient performance - retrying...\n") 134 | } 135 | } 136 | if !success { 137 | b.Fatal("Benchmark failed") 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /component_manager.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2023 boot-go 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | * 22 | */ 23 | 24 | package boot 25 | 26 | import "sync" 27 | 28 | // componentManager represents a registry entity containing the component with its metadata. 29 | type componentManager struct { 30 | // component is the running global componentManager. 31 | component Component 32 | // state contains the component state. 33 | state componentState 34 | // name is used to identify the component. 35 | name string 36 | // stateChangeMutex to prevent race conditions 37 | stateChangeMutex *sync.Mutex 38 | // waitGroup is used to block the main process until all Processes are stopped 39 | waitGroup *sync.WaitGroup 40 | } 41 | 42 | // componentState is used to describe the current state of a component componentManager 43 | type componentState int 44 | 45 | const ( 46 | // Created is set directly after the component was successfully created by the provided factory 47 | Created componentState = iota 48 | // Initialized is set after the component Init() function was called 49 | Initialized 50 | // Started is set after the component Start() function was called 51 | Started 52 | // Stopped is set after the component Stop() function was called 53 | Stopped 54 | // Failed is set when the component couldn't be initialized 55 | Failed 56 | ) 57 | 58 | func newComponentManager(name string, cmp Component, wg *sync.WaitGroup) *componentManager { 59 | return &componentManager{ 60 | name: name, 61 | component: cmp, 62 | state: Created, 63 | stateChangeMutex: &sync.Mutex{}, 64 | waitGroup: wg, 65 | } 66 | } 67 | 68 | // getFullName() return the componentManager name with name of the component separated by a colon. 69 | // E.g. default:github.com/boot-go/boot/boot/runtime 70 | func (cm *componentManager) getFullName() string { 71 | return cm.name + ":" + QualifiedName(cm.component) 72 | } 73 | 74 | // getName returns the qualified name 75 | func (cm *componentManager) getName() string { 76 | return QualifiedName(cm.component) 77 | } 78 | 79 | // start will call the start function inside Component, if it is not nil 80 | func (cm *componentManager) start() { 81 | if process, ok := cm.component.(Process); ok { 82 | cm.stateChangeMutex.Lock() 83 | if cm.state == Initialized { 84 | cm.waitGroup.Add(1) 85 | go func() { 86 | cm.stateChangeMutex.Lock() 87 | cm.state = Started 88 | cm.stateChangeMutex.Unlock() 89 | Logger.Debug.Printf("starting %s", cm.getFullName()) 90 | err := process.Start() 91 | cm.stateChangeMutex.Lock() 92 | if cm.state == Started { 93 | cm.waitGroup.Done() 94 | if err == nil { 95 | cm.state = Stopped 96 | } else { 97 | cm.state = Failed 98 | Logger.Error.Printf("process.Start() failed: %v", err) 99 | } 100 | } 101 | cm.stateChangeMutex.Unlock() 102 | }() 103 | } 104 | cm.stateChangeMutex.Unlock() 105 | } 106 | } 107 | 108 | // stop will call the stop function inside Component, if it is not nil 109 | func (cm *componentManager) stop() { 110 | if process, ok := cm.component.(Process); ok { 111 | cm.stateChangeMutex.Lock() 112 | if cm.state == Started { 113 | Logger.Debug.Printf("stopping %s", cm.getFullName()) 114 | err := process.Stop() 115 | if err != nil { 116 | Logger.Error.Printf("process.Stop() failed: %v", err) 117 | } 118 | cm.state = Stopped 119 | cm.waitGroup.Done() 120 | } 121 | cm.stateChangeMutex.Unlock() 122 | } 123 | } 124 | 125 | type componentManagers []*componentManager 126 | 127 | func (e componentManagers) stopComponents() { 128 | for i := range e { 129 | e := e[len(e)-i-1] 130 | e.stop() 131 | } 132 | } 133 | 134 | func (e componentManagers) startComponents() { 135 | for _, e := range e { 136 | e.start() 137 | } 138 | } 139 | 140 | func (e componentManagers) count() int { 141 | return len(e) 142 | } 143 | -------------------------------------------------------------------------------- /component_manager_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2023 boot-go 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | * 22 | */ 23 | 24 | package boot 25 | 26 | import ( 27 | "errors" 28 | "sync" 29 | "testing" 30 | ) 31 | 32 | type componentManagerTest struct{} 33 | 34 | func (c *componentManagerTest) Init() error { 35 | return nil 36 | } 37 | 38 | func (c *componentManagerTest) Start() error { 39 | return errors.New("fail") 40 | } 41 | 42 | func (c *componentManagerTest) Stop() error { 43 | return errors.New("fail") 44 | } 45 | 46 | func TestComponentManagerStart(t *testing.T) { 47 | type fields struct { 48 | component Component 49 | state componentState 50 | name string 51 | stateChangeMutex *sync.Mutex 52 | waitGroup *sync.WaitGroup 53 | } 54 | tests := []struct { 55 | name string 56 | fields fields 57 | }{ 58 | {name: "with error", fields: struct { 59 | component Component 60 | state componentState 61 | name string 62 | stateChangeMutex *sync.Mutex 63 | waitGroup *sync.WaitGroup 64 | }{component: &componentManagerTest{}, state: Initialized, name: DefaultName, stateChangeMutex: &sync.Mutex{}, waitGroup: &sync.WaitGroup{}}}, 65 | } 66 | for _, tt := range tests { 67 | t.Run(tt.name, func(t *testing.T) { 68 | e := &componentManager{ 69 | component: tt.fields.component, 70 | state: tt.fields.state, 71 | name: tt.fields.name, 72 | stateChangeMutex: tt.fields.stateChangeMutex, 73 | waitGroup: tt.fields.waitGroup, 74 | } 75 | e.start() 76 | }) 77 | } 78 | } 79 | 80 | func TestComponentManagerStop(t *testing.T) { 81 | type fields struct { 82 | component Component 83 | state componentState 84 | name string 85 | stateChangeMutex *sync.Mutex 86 | waitGroup *sync.WaitGroup 87 | } 88 | tests := []struct { 89 | name string 90 | fields fields 91 | }{ 92 | {name: "with error", fields: struct { 93 | component Component 94 | state componentState 95 | name string 96 | stateChangeMutex *sync.Mutex 97 | waitGroup *sync.WaitGroup 98 | }{component: &componentManagerTest{}, state: Started, name: DefaultName, stateChangeMutex: &sync.Mutex{}, waitGroup: &sync.WaitGroup{}}}, 99 | } 100 | for _, tt := range tests { 101 | t.Run(tt.name, func(t *testing.T) { 102 | e := &componentManager{ 103 | component: tt.fields.component, 104 | state: tt.fields.state, 105 | name: tt.fields.name, 106 | stateChangeMutex: tt.fields.stateChangeMutex, 107 | waitGroup: tt.fields.waitGroup, 108 | } 109 | e.waitGroup.Add(1) 110 | e.stop() 111 | }) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2023 boot-go 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | * 22 | */ 23 | 24 | package boot 25 | 26 | const version = "v1.2.1" 27 | 28 | // boot-go is a framework to support component-based development (CBD) using the Go programming language. 29 | -------------------------------------------------------------------------------- /eventbus.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2023 boot-go 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | * 22 | */ 23 | 24 | package boot 25 | 26 | import ( 27 | "errors" 28 | "fmt" 29 | "reflect" 30 | "sync" 31 | ) 32 | 33 | // eventBus internal implementation 34 | type eventBus struct { 35 | Runtime Runtime `boot:"wire"` 36 | handlers map[string][]*busListener // contains all handler for a given type 37 | lock sync.RWMutex // a mutex for the handlers map and the isStarted flag 38 | isStarted bool 39 | queue []any // the queue which will receive the events until the init phase is changed 40 | queueLock sync.RWMutex // a mutex for the queue of events 41 | } 42 | 43 | // Handler is a function which has one argument. This argument is usually a published event. An error 44 | // may be optional provided. E.g. func(e MyEvent) err 45 | type Handler any 46 | 47 | // Event is published and can be any type 48 | type Event any 49 | 50 | // EventBus provides the ability to decouple components. It is designed as a replacement for direct 51 | // methode calls, so components can subscribe for messages produced by other components. 52 | type EventBus interface { 53 | // Subscribe subscribes to a message type. 54 | // Returns error if handler fails. 55 | Subscribe(handler Handler) error 56 | // Unsubscribe removes handler defined for a message type. 57 | // Returns error if there are no handlers subscribed to the message type. 58 | Unsubscribe(handler Handler) error 59 | // Publish executes handler defined for a message type. 60 | Publish(event Event) (err error) 61 | // HasHandler returns true if exists any handler subscribed to the message type. 62 | HasHandler(event Handler) bool 63 | } 64 | 65 | var _ Component = (*eventBus)(nil) // Verify conformity to Component 66 | 67 | var ( 68 | // ErrHandlerMustNotBeNil is returned if the handler is nil, which is not allowed 69 | ErrHandlerMustNotBeNil = errors.New("handler must not be nil") 70 | // ErrEventMustNotBeNil is returned if the event is nil, which is not sensible 71 | ErrEventMustNotBeNil = errors.New("event must not be nil") 72 | // ErrUnknownEventType is returned if the event type could not be determined 73 | ErrUnknownEventType = errors.New("couldn't determiner the message type") 74 | // ErrHandlerNotFound is returned if the handler to be removed could not be found 75 | ErrHandlerNotFound = errors.New("handler not found to remove") 76 | ) 77 | 78 | // Init is described in the Component interface 79 | func (bus *eventBus) Init() error { 80 | bus.lock.Lock() 81 | defer bus.lock.Unlock() 82 | bus.isStarted = false 83 | return nil 84 | } 85 | 86 | func (bus *eventBus) activate() error { 87 | bus.lock.Lock() 88 | bus.isStarted = true 89 | bus.lock.Unlock() 90 | 91 | // republishing queued events 92 | bus.queueLock.RLock() 93 | defer bus.queueLock.RUnlock() 94 | Logger.Debug.Printf("eventbus started with %d queued events\n", len(bus.queue)) 95 | pubErr := newPublicError() 96 | for _, event := range bus.queue { 97 | err := bus.Publish(event) 98 | if err != nil { 99 | Logger.Error.Printf("publishing queued event failed on eventbus start: %v", err.Error()) 100 | if p, ok := err.(*PublishError); ok { //nolint:errorlint // casting required 101 | pubErr.addPublishError(p) 102 | } else { 103 | Logger.Error.Printf("unrecoverable error occurred while activating eventbus %v", err) 104 | return err 105 | } 106 | } 107 | } 108 | if pubErr.hasErrors() { 109 | return pubErr 110 | } 111 | return nil 112 | } 113 | 114 | // busListener contains the reference to one subscribed member 115 | type busListener struct { 116 | handler reflect.Value 117 | qualifiedName string 118 | eventTypeName string 119 | } 120 | 121 | // newBusListener will validate the handler and return the name of the event type, which is provided 122 | // as an argument to the handler 123 | func newBusListener(handler Handler) (*busListener, error) { 124 | if handler == nil { 125 | return nil, ErrHandlerMustNotBeNil 126 | } 127 | if reflect.TypeOf(handler).Kind() != reflect.Func { 128 | return nil, fmt.Errorf("handler is not a function - detail: %s is not of type reflect.Func", reflect.TypeOf(handler).Kind()) 129 | } 130 | // validate argument 131 | argValue := reflect.ValueOf(handler) 132 | if argValue.Type().NumIn() != 1 { 133 | return nil, fmt.Errorf("handler function has unsupported argument, found %d but requires 1" + fmt.Sprintf("%v", argValue.Type().NumIn())) 134 | } 135 | // validate return value type 136 | switch argValue.Type().NumOut() { 137 | case 0: 138 | case 1: 139 | retType := argValue.Type().Out(0) 140 | if _, ok := reflect.New(retType).Interface().(*error); !ok { 141 | return nil, fmt.Errorf("handler function return type is not an error") 142 | } 143 | default: 144 | return nil, fmt.Errorf("handler function has more than one return value, found %d but requires 1" + fmt.Sprintf("%v", argValue.Type().NumOut())) 145 | } 146 | path := argValue.Type().In(0).PkgPath() 147 | name := argValue.Type().In(0).Name() 148 | if len(path) == 0 && len(name) == 0 { 149 | return nil, ErrUnknownEventType 150 | } 151 | eventTypeName := path + "/" + name 152 | 153 | return &busListener{ 154 | handler: argValue, 155 | qualifiedName: QualifiedName(handler), 156 | eventTypeName: eventTypeName, 157 | }, nil 158 | } 159 | 160 | // newEventbus returns new an eventBus. 161 | func newEventbus() *eventBus { 162 | return &eventBus{ 163 | handlers: make(map[string][]*busListener), 164 | lock: sync.RWMutex{}, 165 | queue: nil, 166 | } 167 | } 168 | 169 | // Subscribe subscribes to a message type. 170 | // Returns error if `fn` is not a function. 171 | func (bus *eventBus) Subscribe(handler Handler) error { 172 | eventHandler, err := newBusListener(handler) 173 | if err != nil { 174 | errRet := fmt.Errorf("%s seems not to be a regular handler function: %w", QualifiedName(handler), err) 175 | Logger.Error.Printf(errRet.Error()) 176 | return errRet 177 | } 178 | defer bus.lock.Unlock() 179 | bus.lock.Lock() 180 | bus.handlers[eventHandler.eventTypeName] = append(bus.handlers[eventHandler.eventTypeName], eventHandler) 181 | Logger.Debug.Printf("handler %s subscribed\n", eventHandler.qualifiedName) 182 | return nil 183 | } 184 | 185 | // HasHandler returns true if exists any subscribed message handler. 186 | func (bus *eventBus) HasHandler(handler Handler) bool { 187 | bus.lock.Lock() 188 | defer bus.lock.Unlock() 189 | eventType := QualifiedName(handler) 190 | _, ok := bus.handlers[eventType] 191 | if ok { 192 | return len(bus.handlers[eventType]) > 0 193 | } 194 | return false 195 | } 196 | 197 | // Unsubscribe removes handler defined for a message type. 198 | // Returns error if there are no handlers subscribed to the message type. 199 | func (bus *eventBus) Unsubscribe(handler Handler) error { 200 | eventHandler, err := newBusListener(handler) 201 | if err != nil { 202 | errRet := fmt.Errorf("%s seems not to be an regular handler function: %w", QualifiedName(handler), err) 203 | Logger.Error.Printf(errRet.Error()) 204 | return errRet 205 | } 206 | bus.lock.Lock() 207 | defer bus.lock.Unlock() 208 | if _, ok := bus.handlers[eventHandler.eventTypeName]; ok && len(bus.handlers[eventHandler.eventTypeName]) > 0 { 209 | if ok := bus.removeHandler(eventHandler.eventTypeName, bus.findHandler(eventHandler.eventTypeName, handler)); !ok { 210 | return ErrHandlerNotFound 211 | } 212 | Logger.Debug.Printf("handler %s unsubscribed \n", eventHandler.qualifiedName) 213 | return nil 214 | } 215 | return fmt.Errorf("eventType %s doesn't exist", QualifiedName(handler)) 216 | } 217 | 218 | // PublishError will be provided by the 219 | type PublishError struct { 220 | failedListeners map[Event]map[*busListener]error 221 | } 222 | 223 | func newPublicError() *PublishError { 224 | return &PublishError{ 225 | failedListeners: make(map[Event]map[*busListener]error), 226 | } 227 | } 228 | 229 | func (err *PublishError) hasErrors() bool { 230 | return len(err.failedListeners) > 0 231 | } 232 | 233 | func (err *PublishError) addPublishError(pubErr *PublishError) { 234 | for event, m := range pubErr.failedListeners { 235 | for key, pErr := range m { 236 | err.addError(event, key, pErr) 237 | } 238 | } 239 | } 240 | 241 | func (err *PublishError) addError(e Event, bl *busListener, pubErr error) { 242 | m := err.failedListeners[e] 243 | if m == nil { 244 | m = make(map[*busListener]error) 245 | err.failedListeners[e] = m 246 | } 247 | m[bl] = pubErr 248 | } 249 | 250 | // Error is used to confirm to the error interface 251 | func (err *PublishError) Error() string { 252 | str := "publish failed for " 253 | for event, m := range err.failedListeners { 254 | str += fmt.Sprintf("event: %s", QualifiedName(event)) 255 | for key, pErr := range m { 256 | str = fmt.Sprintf("%s [%s: %s]", str, key.qualifiedName, pErr.Error()) 257 | } 258 | } 259 | 260 | return str 261 | } 262 | 263 | var _ error = (*PublishError)(nil) // force error to confirm to error interface 264 | 265 | // Publish executes handler defined for a message type. Any additional argument will be transferred to the handler. 266 | func (bus *eventBus) Publish(event Event) (err error) { 267 | // if the bus is processing already, the upcoming messages will be queued 268 | var eventType string 269 | defer func() { 270 | if r := recover(); r != nil { 271 | switch v := r.(type) { 272 | case error: 273 | err = v 274 | case string: 275 | err = errors.New(v) 276 | default: 277 | err = fmt.Errorf("unsupported error type found %s", QualifiedName(v)) 278 | } 279 | } 280 | if err != nil { 281 | Logger.Error.Printf("publishing event %s failed: %s\n", eventType, err.Error()) 282 | } 283 | }() 284 | if event == nil { 285 | return ErrEventMustNotBeNil 286 | } 287 | eventType = QualifiedName(event) 288 | bus.lock.RLock() 289 | started := bus.isStarted 290 | bus.lock.RUnlock() 291 | if !started { 292 | Logger.Debug.Printf("queuing event %s\n", eventType) 293 | bus.queueLock.Lock() 294 | defer bus.queueLock.Unlock() 295 | bus.queue = append(bus.queue, event) 296 | return nil 297 | } 298 | Logger.Debug.Printf("publishing event %s\n", eventType) 299 | if handlers, ok := bus.handlers[eventType]; ok && 0 < len(handlers) { 300 | // Handlers slice may be changed by removeHandler and Unsubscribe during iteration, 301 | // so make a copy and iterate the copied slice. 302 | bus.lock.RLock() 303 | copyHandlers := make([]*busListener, len(handlers)) 304 | copy(copyHandlers, handlers) 305 | bus.lock.RUnlock() 306 | pErr := bus.publish(event, copyHandlers) 307 | if pErr != nil { 308 | return pErr 309 | } 310 | } 311 | return err 312 | } 313 | 314 | // publish the event to all provided bus listeners 315 | func (bus *eventBus) publish(event Event, listeners []*busListener) *PublishError { 316 | errPublish := newPublicError() 317 | for _, listener := range listeners { 318 | passedArguments := bus.prepare(event) 319 | ret := listener.handler.Call(passedArguments) 320 | // a handler may return an error... validation will verify 321 | if len(ret) == 1 { 322 | err, ok := ret[0].Interface().(error) 323 | if ok && err != nil { 324 | errPublish.addError(event, listener, err) 325 | } 326 | } 327 | } 328 | if len(errPublish.failedListeners) > 0 { 329 | return errPublish 330 | } 331 | return nil 332 | } 333 | 334 | func (bus *eventBus) removeHandler(eventTypeName string, index int) bool { 335 | l := len(bus.handlers[eventTypeName]) 336 | 337 | if !(0 <= index && index < l) { 338 | return false 339 | } 340 | 341 | copy(bus.handlers[eventTypeName][index:], bus.handlers[eventTypeName][index+1:]) 342 | bus.handlers[eventTypeName][l-1] = nil // or the zero value of T 343 | bus.handlers[eventTypeName] = bus.handlers[eventTypeName][:l-1] 344 | return true 345 | } 346 | 347 | func (bus *eventBus) findHandler(eventTypeName string, handler any) int { 348 | if _, ok := bus.handlers[eventTypeName]; ok { 349 | for index, h := range bus.handlers[eventTypeName] { 350 | if h.qualifiedName == QualifiedName(handler) { 351 | return index 352 | } 353 | } 354 | } 355 | return -1 356 | } 357 | 358 | func (bus *eventBus) prepare(event any) []reflect.Value { 359 | callArgs := make([]reflect.Value, 1) 360 | callArgs[0] = reflect.ValueOf(event) 361 | return callArgs 362 | } 363 | 364 | // testableEventBus box for handlers and callbacks. 365 | type testableEventBus struct { 366 | eventBus 367 | } 368 | 369 | // newTestableEventBus can be used for unit testing. 370 | func newTestableEventBus() *testableEventBus { 371 | return &testableEventBus{eventBus{ 372 | Runtime: &runtime{ 373 | modes: []Flag{UnitTestFlag}, 374 | }, 375 | handlers: make(map[string][]*busListener), 376 | lock: sync.RWMutex{}, 377 | queue: nil, 378 | }} 379 | } 380 | -------------------------------------------------------------------------------- /eventbus_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2023 boot-go 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | * 22 | */ 23 | 24 | package boot 25 | 26 | import ( 27 | "errors" 28 | "math/rand" 29 | "sync/atomic" 30 | "testing" 31 | "time" 32 | ) 33 | 34 | type testEvent struct { 35 | } 36 | 37 | func TestEventbusHasCallback(t *testing.T) { 38 | testcases := []struct { 39 | name string 40 | topic any 41 | wantErr bool 42 | }{ 43 | { 44 | name: "success", 45 | topic: testEvent{}, 46 | wantErr: false, 47 | }, { 48 | name: "callback does not exist", 49 | topic: "test", 50 | wantErr: true, 51 | }, 52 | } 53 | for _, tc := range testcases { 54 | t.Run(tc.name, func(t *testing.T) { 55 | bus := newEventbus() 56 | if tc.topic != nil { 57 | err := bus.Subscribe(func(event testEvent) {}) 58 | if err != nil { 59 | t.Errorf("eventBus.Subscribe() failed with %v", err) 60 | } 61 | } 62 | hasCallback := bus.HasHandler(tc.topic) 63 | if hasCallback != !tc.wantErr { 64 | t.Errorf("eventBus.subscribe() , wantErr %v", tc.wantErr) 65 | } 66 | }) 67 | } 68 | } 69 | 70 | func TestEventbusSubscribe(t *testing.T) { 71 | testcases := []struct { 72 | name string 73 | topic any 74 | wantErr bool 75 | }{ 76 | { 77 | name: "success", 78 | topic: func(event testEvent) {}, 79 | wantErr: false, 80 | }, { 81 | name: "success with return", 82 | topic: func(event testEvent) error { return nil }, 83 | wantErr: false, 84 | }, { 85 | name: "wrong topic type string", 86 | topic: "test", 87 | wantErr: true, 88 | }, { 89 | name: "wrong topic type int", 90 | topic: "test", 91 | wantErr: true, 92 | }, 93 | { 94 | name: "irregular handler", 95 | topic: func(event []string) {}, 96 | wantErr: true, 97 | }, 98 | { 99 | name: "irregular handler with return", 100 | topic: func(event []string) string { return "" }, 101 | wantErr: true, 102 | }, 103 | { 104 | name: "irregular handler with multiple return values", 105 | topic: func(event []string) (error, string) { return nil, "" }, 106 | wantErr: true, 107 | }, 108 | { 109 | name: "multiple param event", 110 | topic: func(event1 testEvent, event2 testEvent) {}, 111 | wantErr: true, 112 | }, 113 | { 114 | name: "nil error", 115 | topic: nil, 116 | wantErr: true, 117 | }, 118 | } 119 | for _, tc := range testcases { 120 | t.Run(tc.name, func(t *testing.T) { 121 | bus := newTestableEventBus() 122 | err := bus.Subscribe(tc.topic) 123 | if (err != nil) != tc.wantErr { 124 | t.Errorf("eventBus.subscribe() error = %v, wantErr %v", err, tc.wantErr) 125 | } 126 | }) 127 | } 128 | } 129 | 130 | func TestEventbusPublish(t *testing.T) { 131 | testcases := []struct { 132 | name string 133 | event any 134 | topic any 135 | wantErr bool 136 | }{ 137 | { 138 | name: "success", 139 | event: testEvent{}, 140 | topic: func(event testEvent) {}, 141 | wantErr: false, 142 | }, 143 | { 144 | name: "panic with string", 145 | event: testEvent{}, 146 | topic: func(event testEvent) { 147 | panic("Test panic") 148 | }, 149 | wantErr: true, 150 | }, 151 | { 152 | name: "panic with error", 153 | event: testEvent{}, 154 | topic: func(event testEvent) { 155 | panic(errors.New("test error")) 156 | }, 157 | wantErr: true, 158 | }, 159 | { 160 | name: "panic with int", 161 | event: testEvent{}, 162 | topic: func(event testEvent) { 163 | panic(0) 164 | }, 165 | wantErr: true, 166 | }, 167 | { 168 | name: "missing event", 169 | event: nil, 170 | topic: func(event testEvent) {}, 171 | wantErr: true, 172 | }, 173 | { 174 | name: "successfully event processing with error return", 175 | event: testEvent{}, 176 | topic: func(event testEvent) error { return nil }, 177 | wantErr: false, 178 | }, 179 | { 180 | name: "event processing failed", 181 | event: nil, 182 | topic: func(event testEvent) error { return errors.New("test fail") }, 183 | wantErr: true, 184 | }, 185 | } 186 | for _, tc := range testcases { 187 | t.Run(tc.name, func(t *testing.T) { 188 | bus := newEventbus() 189 | err := bus.activate() 190 | if err != nil { 191 | t.Errorf("failed to start event bus: %v", err) 192 | } 193 | err = bus.Subscribe(tc.topic) 194 | if err != nil { 195 | t.Errorf("eventBus.subscribe() error = %v", err) 196 | } 197 | err = bus.Publish(tc.event) 198 | if (err != nil) != tc.wantErr { 199 | t.Errorf("eventBus.publish() , wantErr %v", tc.wantErr) 200 | } 201 | }) 202 | } 203 | } 204 | 205 | func TestEventbusNotActivatedPublish(t *testing.T) { 206 | eventTriggered := false 207 | testcases := []struct { 208 | name string 209 | event any 210 | topic any 211 | wantPublishErr bool 212 | wantActivateErr bool 213 | }{ 214 | { 215 | name: "success", 216 | event: testEvent{}, 217 | topic: func(event testEvent) { 218 | eventTriggered = true 219 | }, 220 | wantPublishErr: false, 221 | wantActivateErr: false, 222 | }, 223 | { 224 | name: "error", 225 | event: testEvent{}, 226 | topic: func(event testEvent) error { 227 | eventTriggered = true 228 | return errors.New("fail") 229 | }, 230 | wantPublishErr: false, 231 | wantActivateErr: true, 232 | }, 233 | { 234 | name: "panic", 235 | event: testEvent{}, 236 | topic: func(event testEvent) error { 237 | eventTriggered = true 238 | panic("test") 239 | }, 240 | wantPublishErr: false, 241 | wantActivateErr: true, 242 | }, 243 | } 244 | for _, tc := range testcases { 245 | t.Run(tc.name, func(t *testing.T) { 246 | eventTriggered = false 247 | bus := newEventbus() 248 | err := bus.Subscribe(tc.topic) 249 | if err != nil { 250 | t.Errorf("eventBus.subscribe() error = %v", err) 251 | } 252 | err = bus.Publish(tc.event) 253 | if (err != nil) != tc.wantPublishErr { 254 | t.Errorf("eventBus.publish() , wantErr %v", tc.wantPublishErr) 255 | } 256 | err = bus.activate() 257 | if (err != nil) != tc.wantActivateErr { 258 | t.Errorf("failed to start event bus: %v", err) 259 | } 260 | if !eventTriggered { 261 | t.Fatal("event not published after activation") 262 | } 263 | }) 264 | } 265 | } 266 | 267 | func TestEventbusPublishError(t *testing.T) { 268 | testcases := []struct { 269 | name string 270 | event any 271 | topics []any 272 | wantErr string 273 | }{ 274 | { 275 | name: "one topic fail", 276 | event: testEvent{}, 277 | topics: []any{func(event testEvent) error { return errors.New("fail1") }}, 278 | wantErr: "publish failed for event: github.com/boot-go/boot/testEvent [github.com/boot-go/boot.TestEventbusPublishError.func1: fail1]", 279 | }, 280 | { 281 | name: "two topics fail", 282 | event: testEvent{}, 283 | topics: []any{func(event testEvent) error { return errors.New("fail1") }, func(event testEvent) error { return errors.New("fail2") }}, 284 | wantErr: "publish failed for event: github.com/boot-go/boot/testEvent [github.com/boot-go/boot.TestEventbusPublishError.func2: fail1] [github.com/boot-go/boot.TestEventbusPublishError.func3: fail2]", 285 | }, 286 | { 287 | name: "two topics fail, one succeed", 288 | event: testEvent{}, 289 | topics: []any{func(event testEvent) error { return errors.New("fail1") }, func(event testEvent) error { return nil }, func(event testEvent) error { return errors.New("fail2") }}, 290 | wantErr: "publish failed for event: github.com/boot-go/boot/testEvent [github.com/boot-go/boot.TestEventbusPublishError.func4: fail1] [github.com/boot-go/boot.TestEventbusPublishError.func6: fail2]", 291 | }, 292 | } 293 | for _, tc := range testcases { 294 | t.Run(tc.name, func(t *testing.T) { 295 | bus := newEventbus() 296 | err := bus.activate() 297 | if err != nil { 298 | t.Errorf("failed to start event bus: %v", err) 299 | } 300 | for _, topic := range tc.topics { 301 | err = bus.Subscribe(topic) 302 | if err != nil { 303 | t.Errorf("eventBus.subscribe() error = %v", err) 304 | } 305 | } 306 | err = bus.Publish(tc.event) 307 | pErr, ok := err.(*PublishError) //nolint:errorlint // casting required 308 | if !ok { 309 | t.Errorf("expected PublishError() , but found %v", err) 310 | } 311 | if pErr.Error() != tc.wantErr { 312 | t.Errorf("eventBus.publish()\nwant: %v\n got: %v", tc.wantErr, pErr) 313 | } 314 | }) 315 | } 316 | } 317 | 318 | func TestEventbusPublishMultipleEvents(t *testing.T) { 319 | const events = 100 320 | testcases := []struct { 321 | name string 322 | topic any 323 | wantErr bool 324 | }{ 325 | { 326 | name: "success", 327 | wantErr: false, 328 | }, 329 | } 330 | for _, tc := range testcases { 331 | t.Run(tc.name, func(t *testing.T) { 332 | bus := newEventbus() 333 | err := bus.activate() 334 | if err != nil { 335 | t.Errorf("failed to start event bus: %v", err) 336 | } 337 | var counter int32 = 0 338 | err = bus.Subscribe(func(event testEvent) { 339 | time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond) //nolint:gosec // sec for this test is irrelevant 340 | atomic.AddInt32(&counter, 1) 341 | }) 342 | if (err != nil) != tc.wantErr { 343 | t.Errorf("eventBus.subscribe() error = %v, wantErr %v", err, tc.wantErr) 344 | } 345 | 346 | for i := 0; i < events; i++ { 347 | go func() { 348 | err := bus.Publish(testEvent{}) 349 | if err != nil { 350 | t.Errorf("bus.Publish() failed with %v", err) 351 | } 352 | }() 353 | } 354 | 355 | hasCallback := bus.HasHandler(testEvent{}) 356 | if !hasCallback != tc.wantErr { 357 | t.Errorf("eventBus.subscribe() , wantErr %v", tc.wantErr) 358 | } 359 | time.Sleep(time.Second) 360 | if atomic.LoadInt32(&counter) != events { 361 | t.Fail() 362 | } 363 | }) 364 | } 365 | } 366 | 367 | func TestEventbusUnsubscribe(t *testing.T) { 368 | testEventFunction := func(event testEvent) {} 369 | testcases := []struct { 370 | name string 371 | event any 372 | eventHandler any 373 | wantErr bool 374 | }{ 375 | { 376 | name: "success", 377 | event: testEventFunction, 378 | eventHandler: testEventFunction, 379 | wantErr: false, 380 | }, 381 | { 382 | name: "non existing subscription", 383 | event: testEventFunction, 384 | eventHandler: func(event testEvent) {}, 385 | wantErr: true, 386 | }, 387 | { 388 | name: "int subscription", 389 | event: testEventFunction, 390 | eventHandler: func(event int) {}, 391 | wantErr: true, 392 | }, 393 | { 394 | name: "wrong number arguments", 395 | event: testEventFunction, 396 | eventHandler: func(event1 int, event2 int) {}, 397 | wantErr: true, 398 | }, 399 | { 400 | name: "non Function", 401 | event: testEventFunction, 402 | eventHandler: "", 403 | wantErr: true, 404 | }, 405 | { 406 | name: "no Arg Function", 407 | event: testEventFunction, 408 | eventHandler: func() {}, 409 | wantErr: true, 410 | }, 411 | { 412 | name: "irregular handler", 413 | event: testEventFunction, 414 | eventHandler: func(event []string) {}, 415 | wantErr: true, 416 | }, 417 | { 418 | name: "nil error", 419 | event: testEventFunction, 420 | eventHandler: nil, 421 | wantErr: true, 422 | }, 423 | } 424 | for _, tc := range testcases { 425 | t.Run(tc.name, func(t *testing.T) { 426 | bus := newTestableEventBus() 427 | err := bus.Subscribe(tc.event) 428 | if err != nil { 429 | t.Errorf("bus.Subscribe() failed: %v", err) 430 | } 431 | err = bus.Unsubscribe(tc.eventHandler) 432 | if (err != nil) != tc.wantErr { 433 | t.Fail() 434 | } 435 | }) 436 | } 437 | } 438 | 439 | func TestNewTestableEventBus(t *testing.T) { 440 | t.Run("new testable event bus", func(t *testing.T) { 441 | bus := newTestableEventBus() 442 | if bus == nil { 443 | t.FailNow() 444 | } 445 | }) 446 | } 447 | -------------------------------------------------------------------------------- /framework.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2023 boot-go 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | * 22 | */ 23 | 24 | package boot 25 | 26 | import ( 27 | gort "runtime" 28 | "time" 29 | ) 30 | 31 | // Component represent functional building blocks, which solve one specific purpose. 32 | // They should be fail tolerant, recoverable, agnostic and decent. 33 | // 34 | // fail tolerant: Don't stop processing on errors. 35 | // Example: A http request can still be processed, even when the logging server is not available anymore. 36 | // 37 | // recoverable: Try to recover from errors. 38 | // E.g. a database component should try to reconnect after lost connection. 39 | // 40 | // agnostic: Behave the same in any environment. 41 | // E.g. a key-value store component should work on a local development Session the same way as in a containerized environment. 42 | // 43 | // decent: Don't overload the developer with complexity. 44 | // E.g. keep the interface and events as simple as possible. Less is often more. 45 | type Component interface { 46 | // Init initializes data, set the default configuration, subscribe to events 47 | // or performs other kind of configuration. 48 | Init() error 49 | } 50 | 51 | // Process is a Component which has a processing functionality. This can be anything like a server, 52 | // cron job or long-running process. 53 | type Process interface { 54 | Component 55 | // Start is called as soon as all boot.Component components are initialized. The call should be 56 | // blocking until all processing is completed. 57 | Start() error 58 | // Stop is called to abort the processing and clean up resources. Pay attention that the 59 | // processing may already be stopped. 60 | Stop() error 61 | } 62 | 63 | const ( 64 | // DefaultName is used when registering components without an explicit name. 65 | DefaultName = "default" 66 | ) 67 | 68 | // globalSession is the one and only global variable 69 | var globalSession *Session 70 | 71 | func init() { 72 | globalSession = NewSession(StandardFlag) 73 | } 74 | 75 | // Register a default factory function. 76 | func Register(create func() Component) { 77 | RegisterName(DefaultName, create) 78 | } 79 | 80 | // RegisterName registers a factory function with the given name. 81 | func RegisterName(name string, create func() Component) { 82 | err := globalSession.RegisterName(name, create) 83 | if err != nil { 84 | panic(err) 85 | } 86 | } 87 | 88 | // Override a default factory function. 89 | func Override(create func() Component) { 90 | OverrideName(DefaultName, create) 91 | } 92 | 93 | // OverrideName overrides a factory function with the given name. 94 | func OverrideName(name string, create func() Component) { 95 | err := globalSession.OverrideName(name, create) 96 | if err != nil { 97 | panic(err) 98 | } 99 | } 100 | 101 | // Go the boot component framework. This starts the execution process. 102 | func Go() error { 103 | startTime := time.Now() 104 | s := new(gort.MemStats) 105 | gort.ReadMemStats(s) 106 | // output some basic info 107 | const kilobyte = 1024 108 | const megabyte = kilobyte * 2 109 | Logger.Info.Printf("booting `boot-go %s` /// %s OS/%s ARCH/%s CPU/%d MEM/%dMB SYS/%dMB\n", version, gort.Version(), gort.GOOS, gort.GOARCH, gort.NumCPU(), s.Alloc/megabyte, s.Sys/megabyte) 110 | err := globalSession.Go() 111 | if err == nil { 112 | Logger.Info.Printf("exited after %s\n", time.Since(startTime).String()) 113 | } else { 114 | Logger.Error.Printf("exited after %s with: %s\n", time.Since(startTime).String(), err.Error()) 115 | } 116 | return err 117 | } 118 | 119 | // Shutdown boot-go componentManager. All components will be stopped. With standard options, this is equivalent with 120 | // issuing a SIGTERM on process level. 121 | func Shutdown() error { 122 | err := globalSession.Shutdown() 123 | if err != nil { 124 | return err 125 | } 126 | return nil 127 | } 128 | -------------------------------------------------------------------------------- /framework_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2023 boot-go 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | * 22 | */ 23 | 24 | package boot 25 | 26 | import ( 27 | "errors" 28 | "math" 29 | "sync" 30 | "testing" 31 | "time" 32 | ) 33 | 34 | type bootTestComponent struct { 35 | initCalled bool 36 | startCalled bool 37 | stopCalled bool 38 | } 39 | 40 | func (t *bootTestComponent) Init() error { 41 | t.initCalled = true 42 | return nil 43 | } 44 | 45 | func (t *bootTestComponent) Start() error { 46 | t.startCalled = true 47 | return nil 48 | } 49 | 50 | func (t *bootTestComponent) Stop() error { 51 | t.stopCalled = true 52 | return nil 53 | } 54 | 55 | type bootProcessesComponent struct { 56 | block chan bool 57 | stopped bool 58 | mutex sync.Mutex 59 | } 60 | 61 | func (t *bootProcessesComponent) Init() error { 62 | t.block = make(chan bool, 1) 63 | t.mutex = sync.Mutex{} 64 | defer t.mutex.Unlock() 65 | t.mutex.Lock() 66 | t.stopped = false 67 | return nil 68 | } 69 | 70 | func (t *bootProcessesComponent) Start() error { 71 | <-t.block 72 | defer t.mutex.Unlock() 73 | t.mutex.Lock() 74 | t.stopped = true 75 | return nil 76 | } 77 | 78 | func (t *bootProcessesComponent) Stop() error { 79 | t.block <- true 80 | close(t.block) 81 | return nil 82 | } 83 | 84 | type bootMissingDependencyComponent struct { 85 | WireFails string `boot:"wire"` 86 | } 87 | 88 | func (t *bootMissingDependencyComponent) Init() error { 89 | return nil 90 | } 91 | 92 | type bootPanicComponent struct { 93 | content any 94 | } 95 | 96 | func (t *bootPanicComponent) Init() error { 97 | panic(t.content) 98 | } 99 | 100 | func TestBootGo(t *testing.T) { 101 | testStruct := &bootTestComponent{} 102 | ts := newTestSession(testStruct) 103 | 104 | err := ts.Go() 105 | if err != nil { 106 | t.FailNow() 107 | } 108 | 109 | if !testStruct.initCalled || 110 | !testStruct.startCalled || 111 | testStruct.stopCalled { 112 | t.Fail() 113 | } 114 | } 115 | 116 | func TestBootAlreadyRegisteredComponent(t *testing.T) { 117 | testStruct := &bootTestComponent{} 118 | ts := newTestSession() 119 | err := ts.registerTestComponent(testStruct, testStruct) 120 | if err != nil { 121 | t.Failed() 122 | } 123 | 124 | err = ts.Go() 125 | if err != nil && err.Error() == "go aborted because component github.com/boot-go/boot/bootTestComponent already registered under the name 'default'" { 126 | return 127 | } 128 | t.Fatal("error expected on already registered component") 129 | } 130 | 131 | func TestBootFactoryFail(t *testing.T) { 132 | err := newTestSession() 133 | if err == nil { 134 | t.FailNow() 135 | } 136 | } 137 | 138 | func TestBootWithErrorComponent(t *testing.T) { 139 | tests := []struct { 140 | name string 141 | content any 142 | err error 143 | }{ 144 | {name: "string content", content: "string content", err: errors.New("initializing default:github.com/boot-go/boot/bootPanicComponent panicked with message: string content")}, 145 | {name: "error content", content: errors.New("error content"), err: errors.New("initializing default:github.com/boot-go/boot/bootPanicComponent panicked with error: error content")}, 146 | {name: "other content", content: 0, err: errors.New("initializing default:github.com/boot-go/boot/bootPanicComponent panicked")}, 147 | } 148 | 149 | for _, tt := range tests { 150 | t.Run(tt.name, func(t *testing.T) { 151 | testStruct := &bootPanicComponent{content: tt.content} 152 | ts := newTestSession() 153 | err := ts.overrideTestComponent(testStruct) 154 | if err != nil { 155 | t.Failed() 156 | } 157 | err = ts.Go() 158 | if err == nil || err.Error() != tt.err.Error() { 159 | t.Errorf("Expected '%s' but found '%s'", tt.err.Error(), err.Error()) 160 | } 161 | }) 162 | } 163 | } 164 | 165 | func TestBootShutdown(t *testing.T) { 166 | testStruct := &bootProcessesComponent{} 167 | ts := newTestSession(testStruct) 168 | 169 | go func() { 170 | time.Sleep(5 * time.Second) 171 | err := ts.Shutdown() 172 | if err != nil { 173 | t.Fail() 174 | } 175 | }() 176 | 177 | err := ts.Go() 178 | if err != nil { 179 | t.FailNow() 180 | } 181 | 182 | time.Sleep(2 * time.Second) 183 | defer testStruct.mutex.Unlock() 184 | testStruct.mutex.Lock() 185 | if !testStruct.stopped { 186 | t.Fatal("Component not stopped") 187 | } 188 | } 189 | 190 | func TestBootShutdownFails(t *testing.T) { 191 | globalSession = NewSession(UnitTestFlag) 192 | Register(func() Component { 193 | return &bootTestComponent{} 194 | }) 195 | globalSession.option.DoShutdown = func() error { 196 | globalSession.option.shutdownChannel <- shutdownSignal 197 | return errors.New("shutdown failed") 198 | } 199 | 200 | testSucceeded := false 201 | mutex := sync.Mutex{} 202 | 203 | go func() { 204 | time.Sleep(2 * time.Second) 205 | err := Shutdown() 206 | if err == nil || err.Error() != "shutdown failed" { 207 | t.Fail() 208 | } else { 209 | mutex.Lock() 210 | testSucceeded = true 211 | mutex.Unlock() 212 | } 213 | }() 214 | 215 | err := Go() 216 | if err != nil { 217 | t.FailNow() 218 | } 219 | 220 | time.Sleep(5 * time.Second) 221 | defer mutex.Unlock() 222 | mutex.Lock() 223 | if !testSucceeded { 224 | t.Fatal("shutdown test failed") 225 | } 226 | } 227 | 228 | func TestBootAlreadyRunningThenRegister(t *testing.T) { 229 | testStruct := &bootTestComponent{} 230 | ts := newTestSession() 231 | err := ts.registerTestComponent(testStruct) 232 | if err != nil { 233 | t.Fatal("first registration should not fail") 234 | } 235 | 236 | err = ts.Go() 237 | if err != nil { 238 | t.Fatal("boot should not fail to start") 239 | } 240 | err = ts.registerTestComponent(testStruct) 241 | if err == nil { 242 | t.Fatal("error expected on boot started but registering component") 243 | } 244 | } 245 | 246 | func TestBootAlreadyRunningThenOverride(t *testing.T) { 247 | testStruct := &bootTestComponent{} 248 | ts := newTestSession() 249 | err := ts.registerTestComponent(testStruct) 250 | if err != nil { 251 | t.Fatal("first registration should not fail") 252 | } 253 | 254 | err = ts.Go() 255 | if err != nil { 256 | t.Fatal("boot should not fail to start") 257 | } 258 | err = ts.overrideTestComponent(testStruct) 259 | if err == nil { 260 | t.Fatal("error expected on boot started but overriding component") 261 | } 262 | } 263 | 264 | func TestShutdownByOsSignal(t *testing.T) { 265 | testStruct := &bootProcessesComponent{} 266 | ts := newTestSession(testStruct) 267 | 268 | go func() { 269 | time.Sleep(2 * time.Second) 270 | ts.Session.option.shutdownChannel <- shutdownSignal 271 | }() 272 | 273 | err := ts.Go() 274 | if err != nil { 275 | t.FailNow() 276 | } 277 | 278 | time.Sleep(1 * time.Second) 279 | defer testStruct.mutex.Unlock() 280 | testStruct.mutex.Lock() 281 | if !testStruct.stopped { 282 | t.Fatal("component not stopped") 283 | } 284 | } 285 | 286 | func TestInterruptByOsSignal(t *testing.T) { 287 | testStruct := &bootProcessesComponent{} 288 | ts := newTestSession(testStruct) 289 | 290 | go func() { 291 | time.Sleep(2 * time.Second) 292 | ts.changeMutex.Lock() 293 | defer ts.changeMutex.Unlock() 294 | ts.Session.option.shutdownChannel <- interruptSignal 295 | }() 296 | 297 | err := ts.Go() 298 | if err != nil { 299 | t.FailNow() 300 | } 301 | 302 | time.Sleep(1 * time.Second) 303 | defer testStruct.mutex.Unlock() 304 | testStruct.mutex.Lock() 305 | if !testStruct.stopped { 306 | t.Fatal("component not stopped") 307 | } 308 | } 309 | 310 | func TestResolveComponentError(t *testing.T) { 311 | testStruct := &bootMissingDependencyComponent{} 312 | ts := newTestSession(testStruct) 313 | 314 | err := ts.Go() 315 | if err == nil || err.Error() != "Error dependency field is not a pointer receiver " { 316 | t.Fatal("resolve dependency error must result in an exit with proper error message") 317 | } 318 | } 319 | 320 | func TestRegister(t *testing.T) { 321 | type args struct { 322 | create func() Component 323 | } 324 | tests := []struct { 325 | name string 326 | args args 327 | }{ 328 | { 329 | name: "Success", 330 | args: args{ 331 | create: func() Component { 332 | return &bootTestComponent{} 333 | }, 334 | }, 335 | }, 336 | } 337 | for _, tt := range tests { 338 | t.Run(tt.name, func(t *testing.T) { 339 | ts := newTestSession() 340 | err := ts.register(DefaultName, tt.args.create, false) 341 | if err != nil { 342 | t.Error(err) 343 | } 344 | }) 345 | } 346 | } 347 | 348 | //nolint:dupl // duplication okay 349 | func TestRegisterWithPanic(t *testing.T) { 350 | type args struct { 351 | name string 352 | create func() Component 353 | started bool 354 | } 355 | tests := []struct { 356 | name string 357 | args args 358 | err error 359 | }{ 360 | { 361 | name: "WithNoName", 362 | args: args{ 363 | name: "", 364 | create: func() Component { 365 | return &bootTestComponent{} 366 | }, 367 | }, 368 | err: errSessionRegisterNameOrFunction, 369 | }, 370 | { 371 | name: "WithoutFactoryFunction", 372 | args: args{ 373 | name: "Test", 374 | create: nil, 375 | }, 376 | err: errSessionRegisterNameOrFunction, 377 | }, 378 | { 379 | name: "BootAlreadyStarted", 380 | args: args{ 381 | name: "Test", 382 | create: func() Component { 383 | return &bootProcessesComponent{} 384 | }, 385 | started: true, 386 | }, 387 | err: errSessionRegisterComponentOutsideInitialize, 388 | }, 389 | } 390 | for _, tt := range tests { 391 | t.Run(tt.name, func(t *testing.T) { 392 | ts := newTestSession(&bootProcessesComponent{}) 393 | if tt.args.started { 394 | go func() { 395 | err := ts.Go() 396 | if err != nil { 397 | t.Error("Component test failed") 398 | return 399 | } 400 | }() 401 | time.Sleep(2 * time.Second) 402 | } 403 | err := ts.register(tt.args.name, tt.args.create, false) 404 | if !errors.Is(err, tt.err) { 405 | t.Error(err) 406 | } 407 | }) 408 | } 409 | } 410 | 411 | func TestOverride(t *testing.T) { 412 | type args struct { 413 | create func() Component 414 | } 415 | tests := []struct { 416 | name string 417 | args args 418 | }{ 419 | { 420 | name: "Success", 421 | args: args{ 422 | create: func() Component { 423 | return &bootTestComponent{} 424 | }, 425 | }, 426 | }, 427 | } 428 | for _, tt := range tests { 429 | t.Run(tt.name, func(t *testing.T) { 430 | ts := newTestSession() 431 | err := ts.register(DefaultName, tt.args.create, true) 432 | if err != nil { 433 | t.Error(err) 434 | } 435 | }) 436 | } 437 | } 438 | 439 | //nolint:dupl // duplication okay 440 | func TestOverrideWithPanic(t *testing.T) { 441 | type args struct { 442 | name string 443 | create func() Component 444 | started bool 445 | } 446 | tests := []struct { 447 | name string 448 | args args 449 | err error 450 | }{ 451 | { 452 | name: "WithNoName", 453 | args: args{ 454 | name: "", 455 | create: func() Component { 456 | return &bootTestComponent{} 457 | }, 458 | }, 459 | err: errSessionRegisterNameOrFunction, 460 | }, 461 | { 462 | name: "WithoutFactoryFunction", 463 | args: args{ 464 | name: "Test", 465 | create: nil, 466 | }, 467 | err: errSessionRegisterNameOrFunction, 468 | }, 469 | { 470 | name: "BootAlreadyStarted", 471 | args: args{ 472 | name: "Test", 473 | create: func() Component { 474 | return &bootProcessesComponent{} 475 | }, 476 | started: true, 477 | }, 478 | err: errSessionRegisterComponentOutsideInitialize, 479 | }, 480 | } 481 | for _, tt := range tests { 482 | t.Run(tt.name, func(t *testing.T) { 483 | ts := newTestSession(&bootProcessesComponent{}) 484 | if tt.args.started { 485 | go func() { 486 | err := ts.Go() 487 | if err != nil { 488 | t.Error("Component test failed") 489 | return 490 | } 491 | }() 492 | time.Sleep(2 * time.Second) 493 | } 494 | err := ts.register(tt.args.name, tt.args.create, true) 495 | if !errors.Is(err, tt.err) { 496 | t.Error(err) 497 | } 498 | }) 499 | } 500 | } 501 | 502 | func TestPhaseString(t *testing.T) { 503 | tests := []struct { 504 | name string 505 | p phase 506 | want string 507 | }{ 508 | {name: "none", p: math.MaxUint8, want: "unknown"}, 509 | } 510 | for _, tt := range tests { 511 | t.Run(tt.name, func(t *testing.T) { 512 | if got := tt.p.String(); got != tt.want { 513 | t.Errorf("String() = %v, want %v", got, tt.want) 514 | } 515 | }) 516 | } 517 | } 518 | 519 | func TestBoot(t *testing.T) { 520 | globalSession = NewSession(UnitTestFlag) 521 | Register(func() Component { 522 | return &bootTestComponent{} 523 | }) 524 | Override(func() Component { 525 | return &bootProcessesComponent{} 526 | }) 527 | go func() { 528 | time.Sleep(time.Second) 529 | err := Shutdown() 530 | if err != nil { 531 | t.Fail() 532 | } 533 | }() 534 | err := Go() 535 | if err != nil { 536 | t.Fatal("boot failed") 537 | } 538 | } 539 | 540 | func TestBootFail(t *testing.T) { 541 | globalSession = NewSession(UnitTestFlag) 542 | Register(func() Component { 543 | return &bootMissingDependencyComponent{} 544 | }) 545 | err := Go() 546 | if err == nil { 547 | t.Failed() 548 | } 549 | } 550 | 551 | func TestRegisterFail(t *testing.T) { 552 | globalSession = NewSession(UnitTestFlag) 553 | Register(func() Component { 554 | return &bootTestComponent{} 555 | }) 556 | Override(func() Component { 557 | return &bootProcessesComponent{} 558 | }) 559 | go func() { 560 | defer func() { 561 | if r := recover(); r != nil { 562 | err := Shutdown() 563 | if err != nil { 564 | t.Fail() 565 | } 566 | } 567 | }() 568 | time.Sleep(time.Second) 569 | Register(func() Component { 570 | return &bootTestComponent{} 571 | }) 572 | t.Failed() 573 | }() 574 | err := Go() 575 | if err != nil { 576 | t.Fatal("boot failed") 577 | } 578 | } 579 | 580 | func TestOverrideFail(t *testing.T) { 581 | globalSession = NewSession(UnitTestFlag) 582 | Register(func() Component { 583 | return &bootTestComponent{} 584 | }) 585 | Override(func() Component { 586 | return &bootProcessesComponent{} 587 | }) 588 | go func() { 589 | defer func() { 590 | if r := recover(); r != nil { 591 | err := Shutdown() 592 | if err != nil { 593 | t.Fail() 594 | } 595 | } 596 | }() 597 | time.Sleep(time.Second) 598 | Override(func() Component { 599 | return &bootTestComponent{} 600 | }) 601 | t.Failed() 602 | }() 603 | err := Go() 604 | if err != nil { 605 | t.Fatal("boot failed") 606 | } 607 | } 608 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | ///* 2 | // * Copyright (c) 2021-2022 boot-go 3 | // * 4 | // * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // * of this software and associated documentation files (the "Software"), to deal 6 | // * in the Software without restriction, including without limitation the rights 7 | // * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // * copies of the Software, and to permit persons to whom the Software is 9 | // * furnished to do so, subject to the following conditions: 10 | // * 11 | // * The above copyright notice and this permission notice shall be included in all 12 | // * copies or substantial portions of the Software. 13 | // * 14 | // * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // * SOFTWARE. 21 | // * 22 | // */ 23 | 24 | module github.com/boot-go/boot 25 | 26 | go 1.18 27 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boot-go/boot/76f9cc6ef2930cdcd47c50a96e41c76408548e59/go.sum -------------------------------------------------------------------------------- /injection.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2023 boot-go 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | * 22 | */ 23 | 24 | package boot 25 | 26 | import ( 27 | "errors" 28 | "fmt" 29 | "os" 30 | "reflect" 31 | "strconv" 32 | "strings" 33 | ) 34 | 35 | // DependencyInjectionError contains a detail description for the cause of the injection failure 36 | type DependencyInjectionError struct { 37 | error 38 | detail string 39 | } 40 | 41 | const ( 42 | fieldTag = "boot" // this key should follow the package name 43 | fieldTagConfig = "config" 44 | fieldTagWire = "wire" 45 | fieldTagName = "name" 46 | fieldTagWireKey = "key" 47 | fieldTagWirePanic = "panic" 48 | fieldTagWireDefault = "default" 49 | ) 50 | 51 | const ( 52 | errorTextLoadConfiguration = "failed to load configuration value for " 53 | errorTextInitializing = "initializing " 54 | ) 55 | 56 | func (e *DependencyInjectionError) Error() string { 57 | return fmt.Sprintf("Error %s %s", e.error.Error(), e.detail) 58 | } 59 | 60 | func resolveDependency(regEntry *componentManager, reg *registry) (entries []*componentManager, err error) { 61 | // exit if this component is already initialized 62 | if regEntry.state != Created { 63 | return entries, nil 64 | } 65 | Logger.Debug.Printf("resolving dependencies for %s", regEntry.getFullName()) 66 | reflectedComponent := reflect.ValueOf(regEntry.component) 67 | if reflectedComponent.Kind() == reflect.Ptr { 68 | reflectedComponent = reflectedComponent.Elem() 69 | } 70 | for j := 0; j < reflectedComponent.Type().NumField(); j++ { 71 | field := reflectedComponent.Type().Field(j) 72 | fieldValue := reflectedComponent.Field(j) 73 | if tag, ok := field.Tag.Lookup(fieldTag); ok { 74 | parsedTag, ok := parseStructTag(tag) 75 | if !ok { 76 | return nil, &DependencyInjectionError{ 77 | error: errors.New("field contains unparsable tag"), 78 | detail: " <" + reflectedComponent.Type().Name() + "." + field.Name + 79 | " `" + tag + "`>", 80 | } 81 | } 82 | switch parsedTag.name { 83 | case fieldTagWire: 84 | regEntryName := parsedTag.options[fieldTagName] 85 | if regEntryName == "" { 86 | regEntryName = DefaultName 87 | } 88 | if resolvedEntries, err := processWiring(reg, reflectedComponent, field, fieldValue, regEntryName); err == nil { 89 | entries = append(entries, resolvedEntries...) 90 | } else { 91 | return nil, err 92 | } 93 | case fieldTagConfig: 94 | if err := processConfiguration(reflectedComponent, field, fieldValue, parsedTag); err != nil { 95 | return nil, err 96 | } 97 | default: 98 | return nil, &DependencyInjectionError{ 99 | error: errors.New("dependency field has unsupported tag"), 100 | detail: " <" + reflectedComponent.Type().Name() + "." + field.Name + 101 | " `" + tag + "`>", 102 | } 103 | } 104 | } 105 | } 106 | // initialize component 107 | Logger.Debug.Printf("initializing %s\n", regEntry.getFullName()) 108 | err = initComponent(regEntry) 109 | if err != nil { 110 | regEntry.state = Failed 111 | return nil, err 112 | } 113 | regEntry.state = Initialized 114 | entries = append(entries, regEntry) 115 | return entries, nil 116 | } 117 | 118 | func initComponent(resolveEntry *componentManager) (err error) { 119 | defer func() { 120 | if r := recover(); r != nil { 121 | switch v := r.(type) { 122 | case error: 123 | err = errors.New(errorTextInitializing + resolveEntry.getFullName() + " panicked with error: " + v.Error()) 124 | case string: 125 | err = errors.New(errorTextInitializing + resolveEntry.getFullName() + " panicked with message: " + v) 126 | default: 127 | err = errors.New(errorTextInitializing + resolveEntry.getFullName() + " panicked") 128 | } 129 | } 130 | }() 131 | err = resolveEntry.component.Init() 132 | if err != nil { 133 | return fmt.Errorf("failed to initialize component %s - reason: %w", resolveEntry.getFullName(), err) 134 | } 135 | return 136 | } 137 | 138 | func processWiring(reg *registry, reflectedComponent reflect.Value, field reflect.StructField, fieldValue reflect.Value, regEntryName string) ([]*componentManager, error) { 139 | if fieldValue.Kind() != reflect.Ptr && fieldValue.Kind() != reflect.Interface { 140 | return nil, &DependencyInjectionError{ 141 | error: errors.New("dependency field is not a pointer receiver"), 142 | detail: "<" + reflectedComponent.Type().Name() + "." + field.Name + ">", 143 | } 144 | } 145 | var matchingValues []reflect.Value 146 | for _, list := range reg.items { 147 | e := list[regEntryName] 148 | if e != nil { 149 | controlValue := reflect.ValueOf(e.component) 150 | if controlValue.Type().AssignableTo(field.Type) { 151 | if fieldValue.CanSet() { 152 | matchingValues = append(matchingValues, controlValue) 153 | } else { 154 | return nil, &DependencyInjectionError{ 155 | error: errors.New("dependency value cannot be set into"), 156 | detail: "<" + reflectedComponent.Type().Name() + "." + field.Name + ">", 157 | } 158 | } 159 | } 160 | } 161 | } 162 | switch len(matchingValues) { 163 | case 1: 164 | typeName := matchingValues[0].Elem().Type().PkgPath() + "/" + matchingValues[0].Elem().Type().Name() 165 | e := reg.items[typeName][regEntryName] 166 | if e.state == Created { 167 | entries, err := resolveDependency(e, reg) 168 | if err != nil { 169 | return nil, err 170 | } 171 | fieldValue.Set(reflect.ValueOf(e.component)) 172 | return entries, nil 173 | } 174 | fieldValue.Set(reflect.ValueOf(e.component)) 175 | case 0: 176 | return nil, &DependencyInjectionError{ 177 | error: errors.New("dependency value not found for"), 178 | detail: "<" + regEntryName + ":" + reflectedComponent.Type().Name() + "." + field.Name + ">", 179 | } 180 | default: 181 | detail := regEntryName + ":" + reflectedComponent.Type().Name() + "." + field.Name 182 | for _, v := range matchingValues { 183 | detail += "[" + QualifiedName(v) + "]" 184 | } 185 | return nil, &DependencyInjectionError{ 186 | error: errors.New("multiple dependency values found for"), 187 | detail: "<" + detail + ">", 188 | } 189 | } 190 | return []*componentManager{}, nil // this 191 | } 192 | 193 | func processConfiguration(reflectedComponent reflect.Value, field reflect.StructField, fieldValue reflect.Value, tag *tag) error { 194 | panicOnFail := false 195 | defaultCfg := "" 196 | hasDefault := false 197 | if tag.hasOption(fieldTagWirePanic) { 198 | panicOnFail = true 199 | } 200 | if tag.hasOption(fieldTagWireDefault) { 201 | defaultCfg = tag.options[fieldTagWireDefault] 202 | hasDefault = true 203 | } 204 | if tag.hasOption(fieldTagWireKey) { 205 | if cfgKey := tag.options[fieldTagWireKey]; len(cfgKey) > 0 { 206 | if cfgValue, ok := getConfig(cfgKey); ok || hasDefault { 207 | if !ok && hasDefault { 208 | cfgValue = defaultCfg 209 | } 210 | if fieldValue.CanSet() { 211 | err := processConfigValue(reflectedComponent, field, fieldValue, cfgValue, cfgKey, panicOnFail) 212 | if err != nil { 213 | return err 214 | } 215 | } 216 | } else { 217 | if panicOnFail { 218 | return &DependencyInjectionError{ 219 | error: errors.New(errorTextLoadConfiguration + cfgKey), 220 | detail: "<" + reflectedComponent.Type().Name() + "." + field.Name + ">", 221 | } 222 | } 223 | Logger.Warn.Printf("failed to parse configuration value %s for %s\n", cfgValue, "<"+reflectedComponent.Type().Name()+"."+field.Name+">") 224 | } 225 | } else { 226 | return fmt.Errorf("unsupported tag value %s", cfgKey) 227 | } 228 | } else { 229 | return &DependencyInjectionError{ 230 | error: errors.New("unsupported configuration options found"), 231 | detail: "<" + reflectedComponent.Type().Name() + "." + field.Name + ">", 232 | } 233 | } 234 | return nil 235 | } 236 | 237 | func processConfigValue(reflectedComponent reflect.Value, field reflect.StructField, fieldValue reflect.Value, cfgValue string, cfgKey string, panicOnFail bool) error { 238 | processConfigString(field, fieldValue, cfgValue, cfgKey) 239 | err := processConfigInt(field, reflectedComponent, fieldValue, cfgValue, panicOnFail, cfgKey) 240 | if err != nil { 241 | return err 242 | } 243 | err = processConfigBool(field, reflectedComponent, fieldValue, cfgValue, panicOnFail, cfgKey) 244 | if err != nil { 245 | return err 246 | } 247 | return nil 248 | } 249 | 250 | func getConfig(cfgKey string) (string, bool) { 251 | key := "" 252 | for _, arg := range os.Args { 253 | if strings.HasPrefix(arg, "--") && len(arg) > 2 { //nolint:gocritic // using switch is unsuitebale 254 | key = arg[2:] 255 | } else if key != "" { 256 | if cfgKey == key { 257 | return arg, true 258 | } 259 | } else { 260 | key = "" 261 | } 262 | } 263 | return os.LookupEnv(cfgKey) 264 | } 265 | 266 | func processConfigBool(field reflect.StructField, componentValue reflect.Value, fieldValue reflect.Value, cfgValue string, panicOnFail bool, cfg string) error { 267 | if field.Type.Name() == "bool" { 268 | if !fieldValue.Bool() { 269 | boolValue, err := strconv.ParseBool(cfgValue) 270 | if err != nil { 271 | if panicOnFail { 272 | return &DependencyInjectionError{ 273 | error: errors.New(errorTextLoadConfiguration + cfg), 274 | detail: "<" + componentValue.Type().Name() + "." + field.Name + ">", 275 | } 276 | } 277 | Logger.Warn.Printf("failed to parse configuration value %s as boolean: %s\n", cfgValue, err) 278 | } 279 | fieldValue.SetBool(boolValue) 280 | Logger.Debug.Printf("setting boolean configuration %s=%s\n", cfg, cfgValue) 281 | } 282 | } 283 | return nil 284 | } 285 | 286 | func processConfigInt(field reflect.StructField, componentValue reflect.Value, fieldValue reflect.Value, cfgValue string, panicOnFail bool, cfg string) error { 287 | if field.Type.Name() == "int" { 288 | if fieldValue.Int() == 0 { 289 | const bitSize = 64 290 | const base = 10 291 | intValue, err := strconv.ParseInt(cfgValue, base, bitSize) 292 | if err != nil { 293 | if panicOnFail { 294 | return &DependencyInjectionError{ 295 | error: errors.New(errorTextLoadConfiguration + cfg), 296 | detail: "<" + componentValue.Type().Name() + "." + field.Name + ">", 297 | } 298 | } 299 | Logger.Warn.Printf("failed to parse configuration value %s as integer: %s\n", cfgValue, err) 300 | } 301 | fieldValue.SetInt(intValue) 302 | Logger.Debug.Printf("setting integer configuration %s=%s\n", cfg, cfgValue) 303 | } 304 | } 305 | return nil 306 | } 307 | 308 | func processConfigString(field reflect.StructField, fieldValue reflect.Value, cfgValue string, cfg string) { 309 | if field.Type.Name() == "string" { 310 | if fieldValue.String() == "" { 311 | fieldValue.SetString(cfgValue) 312 | Logger.Debug.Printf("setting string configuration %s=%s\n", cfg, cfgValue) 313 | } 314 | } 315 | } 316 | 317 | type tag struct { 318 | name string 319 | options map[string]string 320 | } 321 | 322 | func (t *tag) hasOption(name string) bool { 323 | _, ok := t.options[name] 324 | return ok 325 | } 326 | 327 | // parseStructTag returns a reference to a Tag if the tagValue string 328 | // is successfully parsed, which indicated by the second bool return 329 | // value. 330 | func parseStructTag(tagValue string) (*tag, bool) { 331 | options := make(map[string]string) 332 | tokens, ok := Split(tagValue, ",", "'") 333 | if !ok { 334 | return nil, false 335 | } 336 | name := strings.TrimSpace(tokens[0]) 337 | for i, token := range tokens { 338 | if i > 0 { 339 | // the split can't fail because the previous split already validates the value 340 | subtokens, _ := Split(token, ":", "'") 341 | if len(subtokens) > 0 && len(subtokens) < 3 { 342 | key := subtokens[0] 343 | const tokenCut = 2 344 | if len(subtokens) == tokenCut { 345 | options[key] = strings.Trim(subtokens[1], " '") 346 | } else { 347 | options[key] = "" 348 | } 349 | } else { 350 | return nil, false 351 | } 352 | } 353 | } 354 | return &tag{ 355 | name: name, 356 | options: options, 357 | }, true 358 | } 359 | -------------------------------------------------------------------------------- /injection_config_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2023 boot-go 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | * 22 | */ 23 | 24 | package boot 25 | 26 | import ( 27 | "os" 28 | "reflect" 29 | "testing" 30 | ) 31 | 32 | //nolint:funlen,gocognit // Testdata 33 | func TestBootWithWireConfig(t *testing.T) { 34 | t1 := &envTestStruct1{} 35 | t2 := &envTestStruct2{} 36 | t3 := &envTestStruct3{} 37 | t4 := &envTestStruct4{} 38 | t5 := &envTestStruct5{} 39 | t6 := &envTestStruct6{} 40 | t7 := &envTestStruct7{} 41 | t8 := &envTestStruct8{} 42 | t9 := &envTestStruct9{} 43 | t10 := &envTestStruct10{} 44 | t11 := &envTestStruct11{} 45 | t12 := &envTestStruct12{} 46 | t13 := &envTestStruct13{} 47 | t14 := &envTestStruct14{} 48 | t15 := &envTestStruct15{} 49 | t16 := &envTestStruct16{} 50 | t17 := &envTestStruct17{} 51 | t18 := &envTestStruct18{} 52 | t19 := &envTestStruct19{} 53 | controls := []Component{ 54 | t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18, t19, 55 | } 56 | 57 | registry := newRegistry() 58 | for _, control := range controls { 59 | err := registry.addItem(DefaultName, false, control) 60 | if err != nil { 61 | Logger.Error.Printf("registry.addItem() failed: %v", err) 62 | } 63 | } 64 | 65 | getEntry := func(c *Component) *componentManager { 66 | cmpName := QualifiedName(*c) 67 | return registry.items[cmpName][DefaultName] 68 | } 69 | 70 | tests := []struct { 71 | name string 72 | controller Component 73 | setup func() 74 | expected Component 75 | err string 76 | }{ 77 | { 78 | name: "simple configuration", 79 | controller: t1, 80 | setup: func() { 81 | err := os.Setenv("t1", "v1") 82 | if err != nil { 83 | panic("failed to set environment variable") 84 | } 85 | }, 86 | expected: &envTestStruct1{ 87 | C: "v1", 88 | }, 89 | }, 90 | { 91 | name: "missing environment variable", 92 | controller: t2, 93 | expected: &envTestStruct2{ 94 | C: "", 95 | }, 96 | }, 97 | { 98 | name: "missing environment variable will panic", 99 | controller: t3, 100 | err: "Error failed to load configuration value for t3 ", 101 | }, 102 | { 103 | name: "misconfigured tag", 104 | controller: t4, 105 | expected: &envTestStruct4{ 106 | C: "", 107 | }, 108 | }, 109 | { 110 | name: "misconfigured tag name", 111 | controller: t5, 112 | err: "Error dependency field has unsupported tag ", 113 | }, 114 | { 115 | name: "missing tag value", 116 | controller: t6, 117 | err: "Error unsupported configuration options found ", 118 | }, 119 | { 120 | name: "simple int configuration", 121 | controller: t7, 122 | setup: func() { 123 | err := os.Setenv("t7", "100") 124 | if err != nil { 125 | panic("failed to set environment variable") 126 | } 127 | }, 128 | expected: &envTestStruct7{ 129 | B: 100, 130 | }, 131 | }, 132 | { 133 | name: "wrong int configuration", 134 | controller: t8, 135 | setup: func() { 136 | err := os.Setenv("t8", "XYZ") 137 | if err != nil { 138 | panic("failed to set environment variable") 139 | } 140 | }, 141 | err: "Error failed to load configuration value for t8 ", 142 | }, 143 | { 144 | name: "simple bool configuration", 145 | controller: t9, 146 | setup: func() { 147 | err := os.Setenv("t9", "true") 148 | if err != nil { 149 | panic("failed to set environment variable") 150 | } 151 | }, 152 | expected: &envTestStruct9{ 153 | F: true, 154 | }, 155 | }, 156 | { 157 | name: "wrong bool configuration", 158 | controller: t10, 159 | setup: func() { 160 | err := os.Setenv("t10", "xyz") 161 | if err != nil { 162 | panic("failed to set environment variable") 163 | } 164 | }, 165 | err: "Error failed to load configuration value for t10 ", 166 | }, 167 | { 168 | name: "bool invalid syntax", 169 | controller: t11, 170 | setup: func() { 171 | err := os.Setenv("t11", "xyz") 172 | if err != nil { 173 | panic("failed to set environment variable") 174 | } 175 | }, 176 | err: " ", 177 | }, 178 | { 179 | name: "int invalid syntax", 180 | controller: t12, 181 | setup: func() { 182 | err := os.Setenv("t12", "xyz") 183 | if err != nil { 184 | panic("failed to set environment variable") 185 | } 186 | }, 187 | err: " ", 188 | }, 189 | { 190 | name: "unsupported tag value", 191 | controller: t13, 192 | setup: func() { 193 | err := os.Setenv("t13", "xyz") 194 | if err != nil { 195 | panic("failed to set environment variable") 196 | } 197 | }, 198 | err: "unsupported tag value ", 199 | }, 200 | { 201 | name: "default config value", 202 | controller: t14, 203 | setup: func() {}, 204 | expected: &envTestStruct14{ 205 | B: 42, 206 | }, 207 | }, 208 | { 209 | name: "default config value with string", 210 | controller: t15, 211 | setup: func() {}, 212 | expected: &envTestStruct15{ 213 | C: "Hello world", 214 | }, 215 | }, 216 | { 217 | name: "default config value with special string", 218 | controller: t16, 219 | setup: func() {}, 220 | expected: &envTestStruct16{ 221 | C: "Hello:world", 222 | }, 223 | }, 224 | { 225 | name: "default config value with special another string", 226 | controller: t17, 227 | setup: func() {}, 228 | expected: &envTestStruct17{ 229 | C: "Hello:world:again", 230 | }, 231 | }, 232 | { 233 | name: "default config value with empty string", 234 | controller: t18, 235 | setup: func() {}, 236 | expected: &envTestStruct18{ 237 | C: "", 238 | }, 239 | }, 240 | { 241 | name: "wrong unparsable tag", 242 | controller: t19, 243 | setup: func() {}, 244 | err: "Error field contains unparsable tag ", 245 | }, 246 | } 247 | for _, test := range tests { 248 | t.Run(test.name, func(t *testing.T) { 249 | if test.setup != nil { 250 | test.setup() 251 | } 252 | _, err := resolveDependency(getEntry(&test.controller), registry) //nolint:gosec // sec for this test is irrelevant 253 | if test.err == "" { 254 | if err != nil { 255 | t.Fail() 256 | } 257 | if !reflect.DeepEqual(test.controller, test.expected) { 258 | t.Fail() 259 | } 260 | } else if err != nil && err.Error() != test.err { 261 | t.Fatal(err.Error()) 262 | } 263 | }) 264 | } 265 | } 266 | 267 | func TestGetConfig(t *testing.T) { 268 | type args struct { 269 | cfgKey string 270 | cmdArgs []string 271 | } 272 | tests := []struct { 273 | name string 274 | args args 275 | wantValue string 276 | wantOk bool 277 | }{ 278 | {name: "Key found", args: args{cfgKey: "mytest", cmdArgs: []string{"--mytest", "Hello"}}, wantValue: "Hello", wantOk: true}, 279 | {name: "Key not found", args: args{cfgKey: "mytest"}, wantValue: "", wantOk: false}, 280 | } 281 | for _, tt := range tests { 282 | t.Run(tt.name, func(t *testing.T) { 283 | backupArgs := os.Args 284 | os.Args = append(os.Args, tt.args.cmdArgs...) 285 | got, got1 := getConfig(tt.args.cfgKey) 286 | os.Args = backupArgs 287 | if got != tt.wantValue { 288 | t.Errorf("getConfig() got = %v, want %v", got, tt.wantValue) 289 | } 290 | if got1 != tt.wantOk { 291 | t.Errorf("getConfig() got1 = %v, want %v", got1, tt.wantOk) 292 | } 293 | }) 294 | } 295 | } 296 | 297 | func FuzzGetConfig(f *testing.F) { 298 | f.Fuzz(func(t *testing.T, a string) { 299 | getConfig(a) 300 | }) 301 | } 302 | 303 | //nolint:unused // for testing purpose nolint:unused 304 | type envTestStruct1 struct { 305 | a int 306 | B int 307 | C string `boot:"config,key:t1"` 308 | d any 309 | e []any 310 | } 311 | 312 | func (t envTestStruct1) Init() error { return nil } 313 | 314 | func (t envTestStruct1) do1() {} 315 | 316 | //nolint:unused // for testing purpose nolint:unused 317 | type envTestStruct2 struct { 318 | a int 319 | B int 320 | C string `boot:"config,key:t2"` 321 | d any 322 | e []any 323 | } 324 | 325 | func (t envTestStruct2) do2() {} 326 | 327 | func (t envTestStruct2) Init() error { return nil } 328 | 329 | //nolint:unused // for testing purpose nolint:unused 330 | type envTestStruct3 struct { 331 | a int 332 | B int 333 | C string `boot:"config,key:t3,panic"` 334 | d any 335 | e []any 336 | } 337 | 338 | func (t envTestStruct3) Init() error { return nil } 339 | 340 | //nolint:unused // for testing purpose nolint:unused 341 | type envTestStruct4 struct { 342 | a int 343 | B int 344 | C string `bo-ot:"config,key:t1,panic"` 345 | d any 346 | e []any 347 | } 348 | 349 | func (t envTestStruct4) Init() error { return nil } 350 | 351 | //nolint:unused // for testing purpose nolint:unused 352 | type envTestStruct5 struct { 353 | a int 354 | B int 355 | C string `boot:"wi-re,key:t3,panic"` 356 | d any 357 | e []any 358 | } 359 | 360 | func (t envTestStruct5) Init() error { return nil } 361 | 362 | //nolint:unused // for testing purpose nolint:unused 363 | type envTestStruct6 struct { 364 | a int 365 | B int 366 | C string `boot:"config"` 367 | d any 368 | e []any 369 | } 370 | 371 | func (t envTestStruct6) Init() error { return nil } 372 | 373 | //nolint:unused // for testing purpose nolint:unused 374 | type envTestStruct7 struct { 375 | a int 376 | B int `boot:"config,key:t7"` 377 | C string 378 | d any 379 | e []any 380 | } 381 | 382 | func (t envTestStruct7) Init() error { return nil } 383 | 384 | //nolint:unused // for testing purpose nolint:unused 385 | type envTestStruct8 struct { 386 | a int 387 | B int `boot:"config,key:t8,panic"` 388 | C string 389 | d any 390 | e []any 391 | } 392 | 393 | func (t envTestStruct8) Init() error { return nil } 394 | 395 | //nolint:unused // for testing purpose nolint:unused 396 | type envTestStruct9 struct { 397 | a int 398 | B int 399 | C string 400 | d any 401 | e []any 402 | F bool `boot:"config,key:t9,panic"` 403 | } 404 | 405 | func (t envTestStruct9) Init() error { return nil } 406 | 407 | //nolint:unused // for testing purpose nolint:unused 408 | type envTestStruct10 struct { 409 | a int 410 | B int 411 | C string 412 | d any 413 | e []any 414 | F bool `boot:"config,key:t10,panic"` 415 | } 416 | 417 | func (t envTestStruct10) Init() error { return nil } 418 | 419 | //nolint:unused // for testing purpose nolint:unused 420 | type envTestStruct11 struct { 421 | a int 422 | B int 423 | C string 424 | d any 425 | e []any 426 | F bool `boot:"config,key:t11"` 427 | } 428 | 429 | func (t envTestStruct11) Init() error { return nil } 430 | 431 | //nolint:unused // for testing purpose nolint:unused 432 | type envTestStruct12 struct { 433 | a int 434 | B int `boot:"config,key:t12"` 435 | C string 436 | d any 437 | e []any 438 | F bool 439 | } 440 | 441 | func (t envTestStruct12) Init() error { return nil } 442 | 443 | //nolint:unused // for testing purpose nolint:unused 444 | type envTestStruct13 struct { 445 | a int 446 | B int `boot:"config,key"` 447 | C string 448 | d any 449 | e []any 450 | F bool 451 | } 452 | 453 | func (t envTestStruct13) Init() error { return nil } 454 | 455 | //nolint:unused // for testing purpose nolint:unused 456 | type envTestStruct14 struct { 457 | a int 458 | B int `boot:"config,key:UNKNOWN,default:42"` 459 | C string 460 | d any 461 | e []any 462 | F bool 463 | } 464 | 465 | func (t envTestStruct14) Init() error { return nil } 466 | 467 | //nolint:unused // for testing purpose nolint:unused 468 | type envTestStruct15 struct { 469 | a int 470 | B int 471 | C string `boot:"config,key:UNKNOWN,default:Hello world"` 472 | d any 473 | e []any 474 | F bool 475 | } 476 | 477 | func (t envTestStruct15) Init() error { return nil } 478 | 479 | //nolint:unused // for testing purpose nolint:unused 480 | type envTestStruct16 struct { 481 | a int 482 | B int 483 | C string `boot:"config,key:UNKNOWN,default:'Hello:world'"` 484 | d any 485 | e []any 486 | F bool 487 | } 488 | 489 | func (t envTestStruct16) Init() error { return nil } 490 | 491 | //nolint:unused // for testing purpose nolint:unused 492 | type envTestStruct17 struct { 493 | a int 494 | B int 495 | C string `boot:"config,key:UNKNOWN,default:'Hello:world:again'"` 496 | d any 497 | e []any 498 | F bool 499 | } 500 | 501 | func (t envTestStruct17) Init() error { return nil } 502 | 503 | //nolint:unused // for testing purpose nolint:unused 504 | type envTestStruct18 struct { 505 | a int 506 | B int 507 | C string `boot:"config,key:UNKNOWN,default:''"` 508 | d any 509 | e []any 510 | F bool 511 | } 512 | 513 | func (t envTestStruct18) Init() error { return nil } 514 | 515 | //nolint:unused // for testing purpose nolint:unused 516 | type envTestStruct19 struct { 517 | a int 518 | B int `boot:"config,key:UNKNOWN:unsupported"` 519 | C string 520 | d any 521 | e []any 522 | F bool 523 | } 524 | 525 | func (t envTestStruct19) Init() error { return nil } 526 | -------------------------------------------------------------------------------- /injection_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2023 boot-go 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | * 22 | */ 23 | 24 | package boot 25 | 26 | import ( 27 | "reflect" 28 | "testing" 29 | ) 30 | 31 | func TestResolveDependency(t *testing.T) { 32 | type args struct { 33 | resolveEntry *componentManager 34 | registry *registry 35 | } 36 | tests := []struct { 37 | name string 38 | args args 39 | wantEntries []*componentManager 40 | wantErr bool 41 | }{ 42 | {name: "component already created", args: struct { 43 | resolveEntry *componentManager 44 | registry *registry 45 | }{resolveEntry: &componentManager{ 46 | component: nil, 47 | state: Started, 48 | name: DefaultName, 49 | }, registry: newRegistry()}, wantEntries: nil, wantErr: false}, 50 | } 51 | for _, tt := range tests { 52 | t.Run(tt.name, func(t *testing.T) { 53 | gotEntries, err := resolveDependency(tt.args.resolveEntry, tt.args.registry) 54 | if (err != nil) != tt.wantErr { 55 | t.Errorf("resolveDependency() error = %v, wantErr %v", err, tt.wantErr) 56 | return 57 | } 58 | if !reflect.DeepEqual(gotEntries, tt.wantEntries) { 59 | t.Errorf("resolveDependency() gotEntries = %v, want %v", gotEntries, tt.wantEntries) 60 | } 61 | }) 62 | } 63 | } 64 | 65 | func TestParseStructTag(t *testing.T) { 66 | type args struct { 67 | tagValue string 68 | } 69 | tests := []struct { 70 | name string 71 | args args 72 | want *tag 73 | want1 bool 74 | }{ 75 | { 76 | name: "wrong amount of sub-tokens", 77 | args: args{tagValue: ",::::"}, 78 | want: nil, 79 | want1: false, 80 | }, 81 | { 82 | name: "wrong format of sub-tokens", 83 | args: args{tagValue: ",':"}, 84 | want: nil, 85 | want1: false, 86 | }, 87 | { 88 | name: "wrong nested format of sub-tokens", 89 | args: args{tagValue: ",':'':"}, 90 | want: nil, 91 | want1: false, 92 | }, 93 | } 94 | for _, tt := range tests { 95 | t.Run(tt.name, func(t *testing.T) { 96 | got, got1 := parseStructTag(tt.args.tagValue) 97 | if !reflect.DeepEqual(got, tt.want) { 98 | t.Errorf("parseStructTag() got = %v, want %v", got, tt.want) 99 | } 100 | if got1 != tt.want1 { 101 | t.Errorf("parseStructTag() got1 = %v, want %v", got1, tt.want1) 102 | } 103 | }) 104 | } 105 | } 106 | 107 | func FuzzParseStructTag(f *testing.F) { 108 | f.Fuzz(func(t *testing.T, a string) { 109 | parseStructTag(a) 110 | }) 111 | } 112 | 113 | type testerComponentOne struct{} 114 | 115 | func (t *testerComponentOne) Init() error { return nil } 116 | 117 | type testerComponentTwo struct { 118 | One *testerComponentOne `boot:"wire"` 119 | } 120 | 121 | func (t *testerComponentTwo) Init() error { return nil } 122 | 123 | func TestTesterWithMultipleTestComponents(t *testing.T) { 124 | ts := newTestSession(&testerComponentOne{}, &testerComponentTwo{}) 125 | err := ts.Go() 126 | if err != nil { 127 | t.Errorf("Test failed: %s", err.Error()) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /injection_wire_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2023 boot-go 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | * 22 | */ 23 | 24 | package boot 25 | 26 | import ( 27 | "errors" 28 | "reflect" 29 | "testing" 30 | ) 31 | 32 | //nolint:funlen // Testdata 33 | func TestBootWithWire(t *testing.T) { 34 | // ts := newTestSession() 35 | 36 | t1 := &testStruct1{} 37 | t2 := &testStruct2{} 38 | t3 := &testStruct3{} 39 | t4 := &testStruct4{} 40 | t5 := &testStruct5{} 41 | t6 := &testStruct6{} 42 | t7 := &testStruct7{} 43 | t8 := &testStruct8{} 44 | t9 := &testStruct9{} 45 | t10 := &testStruct10{} 46 | t11 := &testStruct11{} 47 | t12 := &testStruct12{} 48 | t13 := &testStruct13{} 49 | t14 := &testStruct14{} 50 | t15 := &testStruct15{} 51 | t16 := &testStruct16{} 52 | controls := []Component{t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16} 53 | 54 | registry := newRegistry() 55 | for _, control := range controls { 56 | err := registry.addItem(DefaultName, false, control) 57 | if err != nil { 58 | Logger.Error.Printf("registry.addItem() failed: %v", err) 59 | } 60 | } 61 | err := registry.addItem("test", false, t1) 62 | if err != nil { 63 | Logger.Error.Printf("registry.addItem() failed: %v", err) 64 | } 65 | 66 | getEntry := func(c *Component) *componentManager { 67 | cmpName := QualifiedName(*c) 68 | return registry.items[cmpName][DefaultName] 69 | } 70 | 71 | tests := []struct { 72 | name string 73 | controller Component 74 | expected Component 75 | err string 76 | }{ 77 | { 78 | name: "No injection", 79 | controller: t1, 80 | expected: &testStruct1{}, 81 | }, 82 | { 83 | name: "Single injection", 84 | controller: t2, 85 | expected: &testStruct2{ 86 | F: t1, 87 | }, 88 | }, 89 | { 90 | name: "Multiple injections", 91 | controller: t3, 92 | expected: &testStruct3{ 93 | F: t1, 94 | G: t2, 95 | }, 96 | }, 97 | { 98 | name: "Failed injection into unexported variable", 99 | controller: t4, 100 | err: "Error dependency value cannot be set into ", 101 | }, 102 | { 103 | name: "Injection into interface", 104 | controller: t5, 105 | expected: &testStruct5{ 106 | F: t1, 107 | }, 108 | }, 109 | { 110 | name: "Failed injection not unique", 111 | controller: t6, 112 | err: "Error multiple dependency values found for ", 113 | }, 114 | { 115 | name: "Failed injection for unrecognized component", 116 | controller: t7, 117 | err: "Error dependency value not found for ", 118 | }, 119 | { 120 | name: "Failed injection non pointer receiver", 121 | controller: t8, 122 | err: "Error dependency field is not a pointer receiver ", 123 | }, 124 | { 125 | name: "Single injection by name", 126 | controller: t9, 127 | expected: &testStruct9{ 128 | F: t1, 129 | }, 130 | }, 131 | { 132 | name: "Single injection by unknown name", 133 | controller: t10, 134 | err: "Error dependency value not found for ", 135 | }, 136 | { 137 | name: "Single injection with unparsable name", 138 | controller: t11, 139 | err: "Error field contains unparsable tag ", 140 | }, 141 | { 142 | name: "Traverse injection failed", 143 | controller: t12, 144 | err: "Error field contains unparsable tag ", 145 | }, 146 | { 147 | name: "Tag format error", 148 | controller: t13, 149 | err: "Error field contains unparsable tag ", 150 | }, 151 | { 152 | name: "Injection failed due tag format error", 153 | controller: t14, 154 | err: "Error field contains unparsable tag ", 155 | }, 156 | { 157 | name: "Injection failed due init error", 158 | controller: t15, 159 | err: "failed to initialize component default:github.com/boot-go/boot/testStruct15 - reason: fail-15", 160 | }, 161 | { 162 | name: "Injection failed due wired init error", 163 | controller: t16, 164 | err: "failed to initialize component default:github.com/boot-go/boot/testStruct15 - reason: fail-15", 165 | }, 166 | } 167 | for _, test := range tests { 168 | t.Run(test.name, func(t *testing.T) { 169 | _, err := resolveDependency(getEntry(&test.controller), registry) //nolint:gosec // sec for this test is irrelevant 170 | if test.err == "" { 171 | if err != nil { 172 | t.Fail() 173 | } 174 | if !reflect.DeepEqual(test.controller, test.expected) { 175 | t.Fail() 176 | } 177 | } else if err != nil && err.Error() != test.err { 178 | t.Errorf("error occurred\nexpected: %s\n got: %v", test.err, err.Error()) 179 | } 180 | }) 181 | } 182 | } 183 | 184 | type testInterface1 interface { 185 | do1() 186 | } 187 | 188 | type testInterface2 interface { 189 | do2() 190 | } 191 | 192 | //nolint:unused // for testing purpose nolint:unused 193 | type testStruct1 struct { 194 | a int 195 | B int 196 | c string 197 | d any 198 | e []any 199 | } 200 | 201 | func (t testStruct1) do1() {} 202 | 203 | func (t testStruct1) Init() error { return nil } 204 | 205 | func (t testStruct1) Start() error { return nil } 206 | 207 | func (t testStruct1) Stop() error { return nil } 208 | 209 | //nolint:unused // for testing purpose nolint:unused 210 | type testStruct2 struct { 211 | a int 212 | B int 213 | c string 214 | d any 215 | e []any 216 | F *testStruct1 `boot:"wire"` 217 | } 218 | 219 | func (t testStruct2) do2() {} 220 | 221 | func (t testStruct2) Init() error { return nil } 222 | 223 | func (t testStruct2) Start() error { return nil } 224 | 225 | func (t testStruct2) Stop() error { return nil } 226 | 227 | //nolint:unused // for testing purpose nolint:unused 228 | type testStruct3 struct { 229 | a int 230 | B int 231 | c string 232 | d any 233 | e []any //nolint:unused // for testing purpose nolint:unused 234 | F *testStruct1 `boot:"wire"` 235 | G *testStruct2 `boot:"wire"` 236 | } 237 | 238 | func (t testStruct3) Init() error { return nil } 239 | 240 | func (t testStruct3) Start() error { return nil } 241 | 242 | func (t testStruct3) Stop() error { return nil } 243 | 244 | //nolint:unused // for testing purpose nolint:unused 245 | type testStruct4 struct { 246 | a int //nolint:unused // for testing purpose nolint:unused 247 | B int 248 | c string 249 | d any 250 | e []any 251 | f *testStruct1 `boot:"wire"` //nolint:unused // for testing purpose nolint:unused 252 | } 253 | 254 | func (t testStruct4) Init() error { return nil } 255 | 256 | func (t testStruct4) Start() error { return nil } 257 | 258 | func (t testStruct4) Stop() error { return nil } 259 | 260 | //nolint:unused // for testing purpose nolint:unused 261 | type testStruct5 struct { 262 | a int 263 | B int 264 | c string //nolint:unused // for testing purpose nolint:unused 265 | d any 266 | e []any 267 | F testInterface1 `boot:"wire"` 268 | } 269 | 270 | func (t testStruct5) Init() error { return nil } 271 | 272 | func (t testStruct5) Start() error { return nil } 273 | 274 | func (t testStruct5) Stop() error { return nil } 275 | 276 | //nolint:unused // for testing purpose nolint:unused 277 | type testStruct6 struct { 278 | a int 279 | B int 280 | c string 281 | d any 282 | e []any 283 | F testInterface2 `boot:"wire"` 284 | } 285 | 286 | func (t testStruct6) do2() {} 287 | 288 | func (t testStruct6) Init() error { return nil } 289 | 290 | func (t testStruct6) Start() error { return nil } 291 | 292 | func (t testStruct6) Stop() error { return nil } 293 | 294 | //nolint:unused // for testing purpose nolint:unused 295 | type testStruct7 struct { 296 | a int 297 | B int 298 | c string 299 | d any 300 | e []any 301 | F *string `boot:"wire"` 302 | } 303 | 304 | func (t testStruct7) Init() error { return nil } 305 | 306 | func (t testStruct7) Start() error { return nil } 307 | 308 | func (t testStruct7) Stop() error { return nil } 309 | 310 | //nolint:unused // for testing purpose nolint:unused 311 | type testStruct8 struct { 312 | a int 313 | B int 314 | c string 315 | d any 316 | e []any 317 | F testStruct1 `boot:"wire"` 318 | } 319 | 320 | func (t testStruct8) Init() error { return nil } 321 | 322 | func (t testStruct8) Start() error { return nil } 323 | 324 | func (t testStruct8) Stop() error { return nil } 325 | 326 | //nolint:unused // for testing purpose nolint:unused 327 | type testStruct9 struct { 328 | a int 329 | B int 330 | c string 331 | d any 332 | e []any 333 | F *testStruct1 `boot:"wire,name:test"` 334 | } 335 | 336 | func (t testStruct9) Init() error { return nil } 337 | 338 | //nolint:unused // for testing purpose nolint:unused 339 | type testStruct10 struct { 340 | a int 341 | B int 342 | c string 343 | d any 344 | e []any 345 | F *testStruct1 `boot:"wire,name:unknown"` 346 | } 347 | 348 | func (t testStruct10) Init() error { return nil } 349 | 350 | //nolint:unused // for testing purpose nolint:unused 351 | type testStruct11 struct { 352 | a int //nolint:unused // for testing purpose nolint:unused 353 | B int 354 | c string 355 | d any 356 | e []any 357 | F *testStruct1 `boot:"wire,name:"` 358 | } 359 | 360 | func (t testStruct11) Init() error { return nil } 361 | 362 | //nolint:unused // for testing purpose nolint:unused 363 | type testStruct12 struct { 364 | a int //nolint:unused // for testing purpose nolint:unused 365 | B int 366 | c string 367 | d any 368 | e []any 369 | F *testStruct11 `boot:"wire,name:default"` 370 | } 371 | 372 | func (t testStruct12) Init() error { return nil } 373 | 374 | //nolint:unused // for testing purpose nolint:unused 375 | type testStruct13 struct { 376 | a int 377 | B int 378 | c string 379 | d any 380 | e []any 381 | F *testStruct11 `boot:"wire,name:default:unsupported"` 382 | } 383 | 384 | func (t testStruct13) Init() error { return nil } 385 | 386 | //nolint:unused // for testing purpose nolint:unused 387 | type testStruct14 struct { 388 | a int 389 | B int 390 | c string 391 | d any 392 | e []any //nolint:unused // for testing purpose nolint:unused 393 | F *testStruct13 `boot:"wire"` 394 | } 395 | 396 | func (t testStruct14) Init() error { return nil } 397 | 398 | //nolint:unused // for testing purpose nolint:unused 399 | type testStruct15 struct { 400 | a int 401 | B int 402 | c string 403 | d any 404 | e []any //nolint:unused // for testing purpose nolint:unused 405 | } 406 | 407 | func (t testStruct15) Init() error { return errors.New("fail-15") } 408 | 409 | //nolint:unused // for testing purpose nolint:unused 410 | type testStruct16 struct { 411 | a int 412 | B int 413 | c string 414 | d any 415 | e []any //nolint:unused // for testing purpose nolint:unused 416 | F *testStruct15 `boot:"wire"` 417 | } 418 | 419 | func (t testStruct16) Init() error { return nil } 420 | -------------------------------------------------------------------------------- /registry.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2023 boot-go 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | * 22 | */ 23 | 24 | package boot 25 | 26 | import ( 27 | "errors" 28 | "sync" 29 | ) 30 | 31 | // registry contains all created component componentManagers. 32 | type registry struct { 33 | // items are organized in hierarchy, using the component name, componentManager name and containing 34 | // the componentManager. 35 | items map[string]map[string]*componentManager 36 | // executionWaitGroup tracks the amount off active components 37 | executionWaitGroup sync.WaitGroup 38 | } 39 | 40 | // newRegistry creates a new component registry. 41 | func newRegistry() *registry { 42 | return ®istry{ 43 | items: make(map[string]map[string]*componentManager), 44 | executionWaitGroup: sync.WaitGroup{}, 45 | } 46 | } 47 | 48 | // addItem adds a component componentManager to the registry. 49 | func (reg *registry) addItem(name string, override bool, cmp Component) error { 50 | cmpMngr := newComponentManager(name, cmp, ®.executionWaitGroup) 51 | id := cmpMngr.getName() 52 | if reg.items[id] == nil { 53 | // enter first componentManager in registry 54 | v := make(map[string]*componentManager) 55 | v[name] = cmpMngr 56 | reg.items[id] = v 57 | Logger.Debug.Printf("creating %s", cmpMngr.getFullName()) 58 | } else { 59 | registeredComponent := reg.items[id][name] 60 | if registeredComponent == nil { 61 | // items already found, but not for given name 62 | reg.items[id][name] = cmpMngr 63 | Logger.Debug.Printf("creating %s\n", cmpMngr.getFullName()) 64 | } else { 65 | if override { 66 | Logger.Debug.Printf("overriding %s\n", cmpMngr.getFullName()) 67 | reg.items[id][name] = cmpMngr 68 | } else { 69 | // a component exists with the given name 70 | return errors.New("go aborted because component " + id + " already registered under the name '" + name + "'") 71 | } 72 | } 73 | } 74 | return nil 75 | } 76 | 77 | func (reg *registry) resolveComponentDependencies() (componentManagers, error) { 78 | var entries []*componentManager 79 | for _, cmpTypList := range reg.items { 80 | for _, entry := range cmpTypList { 81 | newEntries, err := resolveDependency(entry, reg) 82 | if err != nil { 83 | return nil, err 84 | } 85 | entries = append(entries, newEntries...) 86 | } 87 | } 88 | return entries, nil 89 | } 90 | -------------------------------------------------------------------------------- /registry_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2023 boot-go 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | * 22 | */ 23 | 24 | package boot 25 | 26 | import ( 27 | "testing" 28 | ) 29 | 30 | func TestBoot_WhenAddEntry(t *testing.T) { 31 | t1 := &testStruct1{} 32 | registry := newRegistry() 33 | err := registry.addItem("test", false, t1) 34 | if err != nil { 35 | t.Errorf("addItem failed: %v", err.Error()) 36 | } 37 | err = registry.addItem("test", false, t1) 38 | if err.Error() != "go aborted because component github.com/boot-go/boot/testStruct1 already registered under the name 'test'" { 39 | t.Fail() 40 | } 41 | } 42 | 43 | func TestBoot_WhenAddOverrideEntry(t *testing.T) { 44 | t1 := &testStruct1{} 45 | registry := newRegistry() 46 | err := registry.addItem("test", false, t1) 47 | if err != nil { 48 | t.Errorf("addItem failed: %v", err.Error()) 49 | } 50 | err = registry.addItem("test", true, t1) 51 | if err != nil { 52 | t.Fail() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /runtime.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2023 boot-go 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | * 22 | */ 23 | 24 | package boot 25 | 26 | // runtime contains the configuration settings, which must be available globally for all components 27 | // at the same time. Do not use this for component configurations. 28 | type runtime struct { 29 | modes []Flag 30 | } 31 | 32 | // Flag describes a special behaviour of a component 33 | type Flag string 34 | 35 | // Runtime is a standard component, which is used to alternate the component behaviour at runtime. 36 | type Runtime interface { 37 | HasFlag(flag Flag) bool 38 | } 39 | 40 | const ( 41 | // StandardFlag is set when the component is started in unit test mode. 42 | StandardFlag Flag = "standard" 43 | // UnitTestFlag is set when the component is started in unit test mode. 44 | UnitTestFlag Flag = "unitTest" 45 | // FunctionalTestFlag is set when the component is started in functional test mode. 46 | FunctionalTestFlag Flag = "functionalTest" 47 | ) 48 | 49 | var _ Component = (*runtime)(nil) // Verify conformity to Component 50 | 51 | func (r *runtime) Init() error { 52 | return nil 53 | } 54 | 55 | func (r *runtime) HasFlag(mode Flag) bool { 56 | for _, m := range r.modes { 57 | if m == mode { 58 | return true 59 | } 60 | } 61 | return false 62 | } 63 | -------------------------------------------------------------------------------- /runtime_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2023 boot-go 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | * 22 | */ 23 | 24 | package boot 25 | 26 | import "testing" 27 | 28 | func TestRuntimeHasMode(t *testing.T) { 29 | type fields struct { 30 | modes []Flag 31 | } 32 | type args struct { 33 | mode Flag 34 | } 35 | tests := []struct { 36 | name string 37 | fields fields 38 | args args 39 | want bool 40 | }{ 41 | { 42 | name: "Unit Test Flag", 43 | fields: fields{modes: []Flag{StandardFlag, UnitTestFlag}}, 44 | args: args{mode: UnitTestFlag}, 45 | want: true, 46 | }, 47 | { 48 | name: "Missing Unit Test Flag", 49 | fields: fields{modes: []Flag{StandardFlag, FunctionalTestFlag}}, 50 | args: args{mode: UnitTestFlag}, 51 | want: false, 52 | }, 53 | } 54 | for _, tt := range tests { 55 | t.Run(tt.name, func(t *testing.T) { 56 | r := &runtime{ 57 | modes: tt.fields.modes, 58 | } 59 | if got := r.HasFlag(tt.args.mode); got != tt.want { 60 | t.Errorf("HasFlag() = %v, want %v", got, tt.want) 61 | } 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2023 boot-go 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | * 22 | */ 23 | 24 | package boot 25 | 26 | import ( 27 | "errors" 28 | "fmt" 29 | "os" 30 | "os/signal" 31 | "sync" 32 | "syscall" 33 | ) 34 | 35 | // factory contains a name, some metadata and factory function for a given component. 36 | type factory struct { 37 | create func() Component 38 | name string 39 | override bool 40 | } 41 | 42 | // phase describes the status of the boot-go componentManager 43 | type phase uint8 44 | 45 | // String returns the name of the phase 46 | func (p phase) String() string { 47 | switch p { 48 | case initializing: 49 | return "initialization" 50 | case booting: 51 | return "booting" 52 | case running: 53 | return "running" 54 | case stopping: 55 | return "stopping" 56 | case exiting: 57 | return "exiting" 58 | } 59 | return "unknown" 60 | } 61 | 62 | const ( 63 | // shutdownSignal uses SIGTERM. The SIGTERM signal is sent to a process to request its 64 | // termination. It will also be used when Shutdown() is called. 65 | shutdownSignal = syscall.SIGTERM 66 | // interruptSignal will be used by the Session. 67 | interruptSignal = syscall.SIGINT 68 | ) 69 | 70 | var ( 71 | errSessionRegisterNameOrFunction = errors.New("name and function for component factory registration is required") 72 | errSessionRegisterComponentOutsideInitialize = errors.New("register component not allowed after boot has been started") 73 | ) 74 | 75 | const ( 76 | // initializing is set directly after the application started. 77 | // In this phase it is safe to subscribe to events. 78 | initializing phase = iota 79 | // booting is set when starting the boot framework. 80 | booting 81 | // running is set when all components were initialized and started 82 | running 83 | // stopping is set when all components are requested to stop their service. 84 | stopping 85 | // exiting is set when all components were stopped. 86 | exiting 87 | ) 88 | 89 | // Session is the main struct for the boot-go application framework 90 | type Session struct { 91 | factories []factory 92 | changeMutex sync.Mutex 93 | phase phase 94 | runtime *runtime 95 | eventbus *eventBus 96 | option Options 97 | } 98 | 99 | // Options contains the options for the boot-go Session 100 | type Options struct { 101 | // Mode is a list of flags to be used for the application. 102 | Mode []Flag 103 | // DoMain is called when the application is requested to start and blocks until shutdown is requested. 104 | DoMain func() error 105 | // DoShutdown is called when the application is requested to shutdown. 106 | DoShutdown func() error 107 | // channel to receive shutdown or interrupt signal - this is used for testing 108 | shutdownChannel chan os.Signal 109 | } 110 | 111 | // NewSession will create a new Session with default options 112 | func NewSession(mode ...Flag) *Session { 113 | localShutdownChannel := make(chan os.Signal, 1) 114 | return NewSessionWithOptions(Options{ 115 | Mode: mode, 116 | DoMain: func() error { 117 | signal.Notify(localShutdownChannel, interruptSignal, shutdownSignal) 118 | sig := <-localShutdownChannel 119 | switch { 120 | case sig == interruptSignal: 121 | Logger.Warn.Printf("caught interrupt signal %s\n", sig.String()) 122 | Logger.Debug.Printf("shutdown gracefully initiated...\n") 123 | case sig == shutdownSignal: 124 | Logger.Debug.Printf("shutdown requested...\n") 125 | } 126 | return nil 127 | }, 128 | DoShutdown: func() error { 129 | localShutdownChannel <- shutdownSignal 130 | return nil 131 | }, 132 | shutdownChannel: localShutdownChannel, 133 | }) 134 | } 135 | 136 | // NewSessionWithOptions will create a new Session with given options 137 | func NewSessionWithOptions(options Options) *Session { 138 | s := &Session{ 139 | factories: []factory{}, 140 | changeMutex: sync.Mutex{}, 141 | phase: initializing, 142 | option: options, 143 | } 144 | // register default components... errors not possible, so they are ignored 145 | s.runtime = &runtime{ 146 | modes: options.Mode, 147 | } 148 | _ = s.register(DefaultName, func() Component { 149 | return s.runtime 150 | }, false) 151 | s.eventbus = newEventbus() 152 | _ = s.register(DefaultName, func() Component { 153 | return s.eventbus 154 | }, false) 155 | return s 156 | } 157 | 158 | // nextPhaseAfter will change the current phase to the next phase. If the current phase is not the expected phase, an error will be returned. 159 | func (s *Session) nextPhaseAfter(expected phase) error { 160 | defer s.changeMutex.Unlock() 161 | s.changeMutex.Lock() 162 | newPhase := initializing 163 | switch s.phase { 164 | case initializing: 165 | newPhase = booting 166 | case booting: 167 | newPhase = running 168 | case running: 169 | newPhase = stopping 170 | case stopping: 171 | newPhase = exiting 172 | case exiting: 173 | // there is no new phase, because it would be exited 174 | } 175 | if expected != s.phase { 176 | return errors.New("current boot phase " + s.phase.String() + " doesn't match expected boot phase " + expected.String()) 177 | } 178 | Logger.Debug.Printf("boot phase changed from " + s.phase.String() + " to " + newPhase.String()) 179 | s.phase = newPhase 180 | return nil 181 | } 182 | 183 | // register a factory function for a component. These functions will be called on boot to create the components. 184 | func (s *Session) register(name string, create func() Component, override bool) error { 185 | if name == "" || create == nil { 186 | return errSessionRegisterNameOrFunction 187 | } 188 | defer s.changeMutex.Unlock() 189 | s.changeMutex.Lock() 190 | if s.phase != initializing { 191 | return errSessionRegisterComponentOutsideInitialize 192 | } 193 | s.factories = append(s.factories, factory{ 194 | create: create, 195 | name: name, 196 | override: override, 197 | }) 198 | return nil 199 | } 200 | 201 | // Register a factory function for a component. The component will be created on boot. 202 | func (s *Session) Register(create func() Component) error { 203 | return s.register(DefaultName, create, false) 204 | } 205 | 206 | // Override a factory function for a component. The component will be created on boot. 207 | func (s *Session) Override(create func() Component) error { 208 | return s.register(DefaultName, create, true) 209 | } 210 | 211 | // RegisterName registers a factory function with the given name. The component will be created on boot. 212 | func (s *Session) RegisterName(name string, create func() Component) error { 213 | return s.register(name, create, false) 214 | } 215 | 216 | // OverrideName overrides a factory function with the given name. The component will be created on boot. 217 | func (s *Session) OverrideName(name string, create func() Component) error { 218 | return s.register(name, create, true) 219 | } 220 | 221 | // Go the boot component framework. This starts the execution process. 222 | func (s *Session) Go() error { //nolint:varnamelen // s is fine for method 223 | if err := s.nextPhaseAfter(initializing); err != nil { 224 | return err 225 | } 226 | 227 | registry, err := s.createComponents() 228 | if err != nil { 229 | return err 230 | } 231 | instances, err := registry.resolveComponentDependencies() 232 | if err != nil { 233 | return err 234 | } 235 | 236 | if err := s.nextPhaseAfter(booting); err != nil { 237 | return err 238 | } 239 | instances.startComponents() 240 | Logger.Debug.Printf("%d components started", instances.count()) 241 | 242 | go func() { 243 | err := s.waitUntilAllComponentsStopped(registry) 244 | if err != nil { 245 | Logger.Error.Printf("shutdown failed with error: %v", err) 246 | } 247 | }() 248 | 249 | // activate eventbus to process alle queued events 250 | err = s.eventbus.activate() 251 | if err == nil { 252 | // blocking here until Shutdown 253 | err = s.option.DoMain() 254 | if err != nil { 255 | Logger.Error.Printf("processing until shutdown failed with error: %v", err) 256 | return err 257 | } 258 | } else { 259 | Logger.Error.Printf("going down - eventbus activation failed: %v", err) 260 | } 261 | 262 | if err := s.nextPhaseAfter(running); err != nil { 263 | Logger.Error.Printf("component stop error: %v", err) 264 | } 265 | instances.stopComponents() 266 | Logger.Debug.Printf("%d components stopped", instances.count()) 267 | 268 | if err := s.nextPhaseAfter(stopping); err != nil { 269 | return err 270 | } 271 | 272 | Logger.Debug.Printf("boot done") 273 | return nil 274 | } 275 | 276 | // Shutdown initiates the shutdown process. All components will be stopped. 277 | func (s *Session) Shutdown() error { 278 | Logger.Debug.Printf("shutdown initiated...") 279 | if s.option.DoShutdown != nil { 280 | err := s.option.DoShutdown() 281 | if err != nil { 282 | return err 283 | } 284 | } 285 | return nil 286 | } 287 | 288 | // createComponents() will create all registered components 289 | func (s *Session) createComponents() (*registry, error) { 290 | registry := newRegistry() 291 | for _, factory := range s.factories { 292 | component := factory.create() 293 | if component == nil { 294 | return nil, fmt.Errorf("factory %s failed to create a component", QualifiedName(factory)) 295 | } 296 | err := registry.addItem(factory.name, factory.override, component) 297 | if err != nil { 298 | return registry, err 299 | } 300 | } 301 | return registry, nil 302 | } 303 | 304 | // waitUntilAllComponentsStopped() will wait until all components have stopped processing 305 | func (s *Session) waitUntilAllComponentsStopped(reg *registry) error { 306 | Logger.Debug.Printf("wait until all components are stopped...") 307 | reg.executionWaitGroup.Wait() 308 | err := s.Shutdown() 309 | if err != nil { 310 | return err 311 | } 312 | return err 313 | } 314 | -------------------------------------------------------------------------------- /session_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2023 boot-go 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | * 22 | */ 23 | 24 | package boot 25 | 26 | import ( 27 | "errors" 28 | "testing" 29 | ) 30 | 31 | func TestSessionNextPhaseAfter(t *testing.T) { 32 | type fields struct { 33 | phase phase 34 | } 35 | type args struct { 36 | expected phase 37 | } 38 | tests := []struct { 39 | name string 40 | fields fields 41 | args args 42 | wantErr bool 43 | }{ 44 | { 45 | name: "next phase after initializing", 46 | fields: fields{ 47 | phase: initializing, 48 | }, 49 | args: args{initializing}, 50 | wantErr: false, 51 | }, 52 | { 53 | name: "next phase after booting", 54 | fields: fields{ 55 | phase: booting, 56 | }, 57 | args: args{booting}, 58 | wantErr: false, 59 | }, 60 | { 61 | name: "next phase after running", 62 | fields: fields{ 63 | phase: running, 64 | }, 65 | args: args{running}, 66 | wantErr: false, 67 | }, 68 | { 69 | name: "next phase after stopping", 70 | fields: fields{ 71 | phase: stopping, 72 | }, 73 | args: args{stopping}, 74 | wantErr: false, 75 | }, 76 | { 77 | name: "next phase after stopping", 78 | fields: fields{ 79 | phase: exiting, 80 | }, 81 | args: args{exiting}, 82 | wantErr: false, 83 | }, 84 | { 85 | name: "fail next phase after stopping", 86 | fields: fields{ 87 | phase: exiting, 88 | }, 89 | args: args{initializing}, 90 | wantErr: true, 91 | }, 92 | { 93 | name: "fail with unknown next phase after exiting", 94 | fields: fields{ 95 | phase: 100, 96 | }, 97 | args: args{101}, 98 | wantErr: true, 99 | }, 100 | } 101 | for _, tt := range tests { 102 | t.Run(tt.name, func(t *testing.T) { 103 | s := NewSession(UnitTestFlag) 104 | s.phase = tt.fields.phase 105 | if err := s.nextPhaseAfter(tt.args.expected); (err != nil) != tt.wantErr { 106 | t.Errorf("nextPhaseAfter() error = %v, wantErr %v", err, tt.wantErr) 107 | } 108 | }) 109 | } 110 | } 111 | 112 | func TestSessionRegister(t *testing.T) { //nolint:dupl // duplication accepted 113 | type fields struct { 114 | phase phase 115 | } 116 | type args struct { 117 | name string 118 | create func() Component 119 | } 120 | tests := []struct { 121 | name string 122 | fields fields 123 | args args 124 | wantErr error 125 | }{ 126 | { 127 | name: "successful", 128 | fields: fields{ 129 | phase: initializing, 130 | }, 131 | args: args{ 132 | name: DefaultName, 133 | create: func() Component { 134 | return nil 135 | }, 136 | }, 137 | wantErr: nil, 138 | }, 139 | { 140 | name: "error when no name or func ", 141 | fields: fields{ 142 | phase: initializing, 143 | }, 144 | args: args{ 145 | name: "", 146 | create: nil, 147 | }, 148 | wantErr: errSessionRegisterNameOrFunction, 149 | }, 150 | { 151 | name: "error when no name or func ", 152 | fields: fields{ 153 | phase: initializing, 154 | }, 155 | args: args{ 156 | name: "", 157 | create: nil, 158 | }, 159 | wantErr: errSessionRegisterNameOrFunction, 160 | }, 161 | { 162 | name: "error when phase is not initializing", 163 | fields: fields{ 164 | phase: running, 165 | }, 166 | args: args{ 167 | name: DefaultName, 168 | create: func() Component { 169 | return nil 170 | }, 171 | }, 172 | wantErr: errSessionRegisterComponentOutsideInitialize, 173 | }, 174 | } 175 | for _, tt := range tests { 176 | t.Run(tt.name, func(t *testing.T) { 177 | s := NewSession(UnitTestFlag) 178 | s.phase = tt.fields.phase 179 | if err := s.RegisterName(tt.args.name, tt.args.create); err != tt.wantErr { //nolint:errorlint // using errors.Is(..) will fail test 180 | t.Errorf("register() error = %v, wantErr %v", err, tt.wantErr) 181 | } 182 | }) 183 | } 184 | } 185 | 186 | func TestSessionRegisterDefault(t *testing.T) { 187 | type fields struct { 188 | phase phase 189 | } 190 | type args struct { 191 | create func() Component 192 | } 193 | tests := []struct { 194 | name string 195 | fields fields 196 | args args 197 | wantErr error 198 | }{ 199 | { 200 | name: "successful", 201 | fields: fields{ 202 | phase: initializing, 203 | }, 204 | args: args{ 205 | create: func() Component { 206 | return nil 207 | }, 208 | }, 209 | wantErr: nil, 210 | }, 211 | } 212 | for _, tt := range tests { 213 | t.Run(tt.name, func(t *testing.T) { 214 | s := NewSession(UnitTestFlag) 215 | s.phase = tt.fields.phase 216 | if err := s.Register(tt.args.create); err != tt.wantErr { //nolint:errorlint // errors.Is(...) will fail 217 | t.Errorf("register() error = %v, wantErr %v", err, tt.wantErr) 218 | } 219 | }) 220 | } 221 | } 222 | 223 | func TestSessionOverride(t *testing.T) { //nolint:dupl // duplication accepted 224 | type fields struct { 225 | phase phase 226 | } 227 | type args struct { 228 | name string 229 | create func() Component 230 | } 231 | tests := []struct { 232 | name string 233 | fields fields 234 | args args 235 | wantErr error 236 | }{ 237 | { 238 | name: "successful", 239 | fields: fields{ 240 | phase: initializing, 241 | }, 242 | args: args{ 243 | name: DefaultName, 244 | create: func() Component { 245 | return nil 246 | }, 247 | }, 248 | wantErr: nil, 249 | }, 250 | { 251 | name: "error when no name or func ", 252 | fields: fields{ 253 | phase: initializing, 254 | }, 255 | args: args{ 256 | name: "", 257 | create: nil, 258 | }, 259 | wantErr: errSessionRegisterNameOrFunction, 260 | }, 261 | { 262 | name: "error when no name or func ", 263 | fields: fields{ 264 | phase: initializing, 265 | }, 266 | args: args{ 267 | name: "", 268 | create: nil, 269 | }, 270 | wantErr: errSessionRegisterNameOrFunction, 271 | }, 272 | { 273 | name: "error when phase is not initializing", 274 | fields: fields{ 275 | phase: running, 276 | }, 277 | args: args{ 278 | name: DefaultName, 279 | create: func() Component { 280 | return nil 281 | }, 282 | }, 283 | wantErr: errSessionRegisterComponentOutsideInitialize, 284 | }, 285 | } 286 | for _, tt := range tests { 287 | t.Run(tt.name, func(t *testing.T) { 288 | s := NewSession(UnitTestFlag) 289 | s.phase = tt.fields.phase 290 | if err := s.OverrideName(tt.args.name, tt.args.create); err != tt.wantErr { //nolint:errorlint // errors.Is(...) will fail 291 | t.Errorf("register() error = %v, wantErr %v", err, tt.wantErr) 292 | } 293 | }) 294 | } 295 | } 296 | 297 | func TestSessionOverrideDefault(t *testing.T) { 298 | type fields struct { 299 | phase phase 300 | } 301 | type args struct { 302 | create func() Component 303 | } 304 | tests := []struct { 305 | name string 306 | fields fields 307 | args args 308 | wantErr error 309 | }{ 310 | { 311 | name: "successful", 312 | fields: fields{ 313 | phase: initializing, 314 | }, 315 | args: args{ 316 | create: func() Component { 317 | return nil 318 | }, 319 | }, 320 | wantErr: nil, 321 | }, 322 | } 323 | for _, tt := range tests { 324 | t.Run(tt.name, func(t *testing.T) { 325 | s := NewSession(UnitTestFlag) 326 | s.phase = tt.fields.phase 327 | if err := s.Override(tt.args.create); err != tt.wantErr { //nolint:errorlint // using errors.Is(..) will fail test 328 | t.Errorf("register() error = %v, wantErr %v", err, tt.wantErr) 329 | } 330 | }) 331 | } 332 | } 333 | 334 | type eventbusActivationTest struct { 335 | Eventbus EventBus `boot:"wire"` 336 | initSubscribeReturnErr error 337 | } 338 | 339 | func (e *eventbusActivationTest) Init() error { 340 | _ = e.Eventbus.Subscribe(func(t testEvent) error { 341 | return e.initSubscribeReturnErr 342 | }) 343 | return e.Eventbus.Publish(testEvent{}) 344 | } 345 | 346 | type eventbusActivationProcessTest struct { 347 | Eventbus EventBus `boot:"wire"` 348 | initSubscribeReturnErr error 349 | } 350 | 351 | func (e *eventbusActivationProcessTest) Init() error { return nil } 352 | 353 | func (e *eventbusActivationProcessTest) Start() error { 354 | return e.Eventbus.Publish(testEvent{}) 355 | } 356 | 357 | func (e *eventbusActivationProcessTest) Stop() error { return nil } 358 | 359 | func TestSessionRunEventbusActivationFail(t *testing.T) { 360 | type args struct { 361 | create func() Component 362 | } 363 | tests := []struct { 364 | name string 365 | args args 366 | wantErr error 367 | }{ 368 | { 369 | name: "successful", 370 | args: args{ 371 | create: func() Component { 372 | return &eventbusActivationTest{ 373 | initSubscribeReturnErr: nil, 374 | } 375 | }, 376 | }, 377 | wantErr: nil, 378 | }, 379 | { 380 | name: "successful process component", 381 | args: args{ 382 | create: func() Component { 383 | return &eventbusActivationProcessTest{ 384 | initSubscribeReturnErr: nil, 385 | } 386 | }, 387 | }, 388 | wantErr: nil, 389 | }, 390 | { 391 | name: "unsuccessful", 392 | args: args{ 393 | create: func() Component { 394 | return &eventbusActivationTest{ 395 | initSubscribeReturnErr: errors.New("fail"), 396 | } 397 | }, 398 | }, 399 | wantErr: nil, 400 | }, 401 | } 402 | for _, tt := range tests { 403 | t.Run(tt.name, func(t *testing.T) { 404 | s := NewSession(UnitTestFlag) 405 | err := s.Register(tt.args.create) 406 | if err != nil { 407 | t.Fail() 408 | } 409 | err = s.Go() 410 | if err != tt.wantErr { //nolint:errorlint // using errors.Is(..) will fail test 411 | t.Errorf("Go() error = %v, wantErr %v", err, tt.wantErr) 412 | } 413 | }) 414 | } 415 | } 416 | 417 | type testPhase struct { 418 | s *testSession 419 | init phase 420 | start phase 421 | stop phase 422 | } 423 | 424 | func (t *testPhase) Init() error { 425 | t.s.phase = t.init 426 | return nil 427 | } 428 | 429 | func (t *testPhase) Start() error { 430 | t.s.phase = t.start 431 | return nil 432 | } 433 | 434 | func (t *testPhase) Stop() error { 435 | t.s.phase = t.stop 436 | return nil 437 | } 438 | 439 | func TestSessionRun(t *testing.T) { 440 | tests := []struct { 441 | name string 442 | initPhase phase 443 | bootPhase phase 444 | runningPhase phase 445 | wantErr string 446 | }{ 447 | { 448 | name: "fail boot", 449 | initPhase: exiting, 450 | wantErr: "current boot phase exiting doesn't match expected boot phase initialization", 451 | }, 452 | { 453 | name: "fail init", 454 | initPhase: initializing, 455 | bootPhase: exiting, 456 | wantErr: "current boot phase exiting doesn't match expected boot phase booting", 457 | }, 458 | { 459 | name: "fail start", 460 | initPhase: initializing, 461 | bootPhase: booting, 462 | runningPhase: exiting, 463 | wantErr: "current boot phase exiting doesn't match expected boot phase stopping", 464 | }, 465 | } 466 | for _, tt := range tests { 467 | t.Run(tt.name, func(t *testing.T) { 468 | s := newTestSession() 469 | _ = s.registerTestComponent(&testPhase{ 470 | s: s, 471 | init: tt.bootPhase, 472 | start: tt.runningPhase, 473 | }) 474 | s.phase = tt.initPhase 475 | err := s.Go() 476 | if err == nil || err.Error() != tt.wantErr { 477 | t.Errorf("Go() error = %v, wantErr %v", err, tt.wantErr) 478 | return 479 | } 480 | }) 481 | } 482 | } 483 | 484 | func TestSessionFailOnCreateNilComponents(t *testing.T) { 485 | t.Run("nil component", func(t *testing.T) { 486 | s := newTestSession(nil) 487 | _, err := s.createComponents() 488 | if err == nil { 489 | t.Errorf("createComponents() must throw an error") 490 | return 491 | } 492 | }) 493 | } 494 | 495 | func TestSessionWithOptionsFailOnDoShutdown(t *testing.T) { 496 | t.Run("doShutdown fails", func(t *testing.T) { 497 | s := newTestSessionWithOptions(Options{ 498 | DoShutdown: func() error { 499 | return errors.New("fail") 500 | }, 501 | }) 502 | err := s.Shutdown() 503 | if err == nil || err.Error() != "fail" { 504 | t.Fail() 505 | } 506 | }) 507 | } 508 | 509 | func TestSessionWithOptionsFailOnDoMain(t *testing.T) { 510 | t.Run("doMain fails", func(t *testing.T) { 511 | s := newTestSessionWithOptions(Options{ 512 | DoMain: func() error { 513 | return errors.New("fail") 514 | }, 515 | }) 516 | err := s.Go() 517 | if err == nil || err.Error() != "fail" { 518 | t.Fail() 519 | } 520 | }) 521 | } 522 | -------------------------------------------------------------------------------- /tester.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2023 boot-go 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | * 22 | */ 23 | 24 | package boot 25 | 26 | import ( 27 | "os" 28 | ) 29 | 30 | type testSession struct { 31 | *Session 32 | } 33 | 34 | func newTestSessionWithOptions(options Options, mocks ...Component) *testSession { 35 | ts := &testSession{ 36 | Session: NewSessionWithOptions(options), 37 | } 38 | Logger.Debug.SetOutput(os.Stdout) 39 | // ignore error because it can't fail 40 | _ = ts.overrideTestComponent(mocks...) 41 | return ts 42 | } 43 | 44 | // newTestSession creates a new session with one or more specific components within a unit test. 45 | func newTestSession(mocks ...Component) *testSession { 46 | ts := &testSession{ 47 | Session: NewSession(UnitTestFlag), 48 | } 49 | Logger.Debug.SetOutput(os.Stdout) 50 | // ignore error because it can't fail 51 | _ = ts.overrideTestComponent(mocks...) 52 | return ts 53 | } 54 | 55 | func (ts *testSession) overrideTestComponent(mocks ...Component) error { 56 | for _, mock := range mocks { 57 | mock := mock 58 | err := ts.register(DefaultName, func() Component { 59 | return mock 60 | }, true) 61 | if err != nil { 62 | return err 63 | } 64 | } 65 | return nil 66 | } 67 | 68 | func (ts *testSession) registerTestComponent(mocks ...Component) error { 69 | for _, mock := range mocks { 70 | mock := mock 71 | err := ts.register(DefaultName, func() Component { 72 | return mock 73 | }, false) 74 | if err != nil { 75 | return err 76 | } 77 | } 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2023 boot-go 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | * 22 | */ 23 | 24 | package boot 25 | 26 | import ( 27 | "io" 28 | "log" 29 | "os" 30 | "reflect" 31 | gort "runtime" 32 | "strings" 33 | ) 34 | 35 | // QualifiedName returns the full name of a struct, function or a simple name of a primitive. 36 | func QualifiedName(v any) string { 37 | t := reflect.TypeOf(v) 38 | if t != nil { 39 | switch t.Kind() { //nolint:exhaustive,nolintlint 40 | case reflect.Ptr: 41 | return t.Elem().PkgPath() + "/" + t.Elem().Name() 42 | case reflect.Func: 43 | return gort.FuncForPC(reflect.ValueOf(v).Pointer()).Name() 44 | default: 45 | pkg := t.PkgPath() 46 | if pkg != "" { 47 | pkg += "/" 48 | } 49 | return pkg + reflect.TypeOf(v).Name() 50 | } 51 | } else { 52 | return "nil" 53 | } 54 | } 55 | 56 | // Split returns the tokens separated by sep and ignores content in the quotes. If the parsing is okay, the bool return value will be true. 57 | // Unfortunately, the regex can't be used to split the string, because Go has limitations. 58 | func Split(s, sep, quote string) ([]string, bool) { 59 | var result []string 60 | tokens := strings.Split(s, sep) 61 | summarizedToken := "" 62 | summarizing := false 63 | for _, token := range tokens { 64 | count := len(strings.Split(token, quote)) - 1 65 | if count == 1 || count%2 != 0 { 66 | if summarizing { 67 | // summarizing completed 68 | summarizing = false 69 | summarizedToken += sep + token 70 | } else { 71 | // summarizing started 72 | summarizing = true 73 | summarizedToken = token 74 | } 75 | } else { 76 | if summarizing { 77 | // summarizing ongoing 78 | summarizedToken += sep + token 79 | } else { 80 | // no summarizing at all 81 | summarizedToken = token 82 | } 83 | } 84 | if !summarizing { 85 | result = append(result, summarizedToken) 86 | summarizedToken = "" 87 | } 88 | } 89 | if summarizedToken == "" { 90 | return result, true 91 | } else { 92 | return nil, false 93 | } 94 | } 95 | 96 | // logger contains multiple loggers which can be seen as different 97 | // log levels 98 | type logger struct { 99 | Debug *log.Logger 100 | Info *log.Logger 101 | Warn *log.Logger 102 | Error *log.Logger 103 | } 104 | 105 | // Mute will mute the provided logger. 106 | func (l logger) Mute(logger *log.Logger) { 107 | logger.SetOutput(io.Discard) 108 | } 109 | 110 | // Unmute will unmute the provided logger. 111 | func (l logger) Unmute(logger *log.Logger) { 112 | logger.SetOutput(os.Stdout) 113 | } 114 | 115 | var ( 116 | // Logger contains a debug, info, warning and error logger, which is used for fine-grained log 117 | // output. Every logger can be muted or unmuted separately. 118 | // e.g. Logger.Unmute(Logger.Debug) 119 | Logger logger 120 | ) 121 | 122 | func init() { 123 | Logger.Debug = log.New(os.Stdout, "boot.debug ", log.LstdFlags|log.Lmsgprefix) 124 | Logger.Info = log.New(os.Stdout, "boot.info ", log.LstdFlags|log.Lmsgprefix) 125 | Logger.Warn = log.New(os.Stdout, "boot.warn ", log.LstdFlags|log.Lmsgprefix) 126 | Logger.Error = log.New(os.Stdout, "boot.error ", log.LstdFlags|log.Lmsgprefix) 127 | // default is to mute the debug logger 128 | Logger.Mute(Logger.Debug) 129 | } 130 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2023 boot-go 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | * 22 | */ 23 | 24 | package boot 25 | 26 | import ( 27 | "reflect" 28 | "testing" 29 | ) 30 | 31 | type qualifiedNameComponent struct{} 32 | 33 | func TestQualifiedName(t *testing.T) { 34 | type args struct { 35 | v any 36 | } 37 | tests := []struct { 38 | name string 39 | args args 40 | want string 41 | }{ 42 | { 43 | name: "struct", 44 | args: args{v: qualifiedNameComponent{}}, 45 | want: "github.com/boot-go/boot/qualifiedNameComponent", 46 | }, 47 | { 48 | name: "struct pointer", 49 | args: args{v: &qualifiedNameComponent{}}, 50 | want: "github.com/boot-go/boot/qualifiedNameComponent", 51 | }, 52 | { 53 | name: "string", 54 | args: args{v: ""}, 55 | want: "string", 56 | }, 57 | { 58 | name: "byte a.k.a uint8", 59 | args: args{v: byte(0)}, 60 | want: "uint8", 61 | }, 62 | { 63 | name: "int", 64 | args: args{v: 0}, 65 | want: "int", 66 | }, 67 | { 68 | name: "int64", 69 | args: args{v: int64(0)}, 70 | want: "int64", 71 | }, 72 | { 73 | name: "bool", 74 | args: args{v: true}, 75 | want: "bool", 76 | }, 77 | { 78 | name: "nil", 79 | args: args{v: nil}, 80 | want: "nil", 81 | }, 82 | } 83 | for _, tt := range tests { 84 | t.Run(tt.name, func(t *testing.T) { 85 | if got := QualifiedName(tt.args.v); got != tt.want { 86 | t.Errorf("QualifiedName() = %v, want %v", got, tt.want) 87 | } 88 | }) 89 | } 90 | } 91 | 92 | func FuzzQualifiedName(f *testing.F) { 93 | f.Fuzz(func(t *testing.T, a string) { 94 | QualifiedName(a) 95 | }) 96 | } 97 | 98 | func TestSplit(t *testing.T) { 99 | type args struct { 100 | s string 101 | sep string 102 | quote string 103 | } 104 | tests := []struct { 105 | name string 106 | args args 107 | want []string 108 | want1 bool 109 | }{ 110 | { 111 | name: "simple", 112 | args: args{ 113 | s: "'hello,world'", 114 | sep: ",", 115 | quote: "'", 116 | }, 117 | want: []string{"'hello,world'"}, 118 | want1: true, 119 | }, 120 | { 121 | name: "none", 122 | args: args{ 123 | s: "hello,world", 124 | sep: ",", 125 | quote: "'", 126 | }, 127 | want: []string{"hello", "world"}, 128 | want1: true, 129 | }, 130 | { 131 | name: "two", 132 | args: args{ 133 | s: "'hello','world'", 134 | sep: ",", 135 | quote: "'", 136 | }, 137 | want: []string{"'hello'", "'world'"}, 138 | want1: true, 139 | }, 140 | { 141 | name: "both", 142 | args: args{ 143 | s: "'hello,world','here'", 144 | sep: ",", 145 | quote: "'", 146 | }, 147 | want: []string{"'hello,world'", "'here'"}, 148 | want1: true, 149 | }, 150 | { 151 | name: "failure_missing_quote", 152 | args: args{ 153 | s: "'hello,world,here", 154 | sep: ",", 155 | quote: "'", 156 | }, 157 | want: nil, 158 | want1: false, 159 | }, 160 | { 161 | name: "failure", 162 | args: args{ 163 | s: "hello,world,here'", 164 | sep: ",", 165 | quote: "'", 166 | }, 167 | want: nil, 168 | want1: false, 169 | }, 170 | } 171 | for _, tt := range tests { 172 | t.Run(tt.name, func(t *testing.T) { 173 | got, got1 := Split(tt.args.s, tt.args.sep, tt.args.quote) 174 | if !reflect.DeepEqual(got, tt.want) { 175 | t.Errorf("Split() got = %v, want %v", got, tt.want) 176 | } 177 | if got1 != tt.want1 { 178 | t.Errorf("Split() got1 = %v, want %v", got1, tt.want1) 179 | } 180 | }) 181 | } 182 | } 183 | 184 | func FuzzSplit(f *testing.F) { 185 | f.Fuzz(func(t *testing.T, a string, b string, c string) { 186 | Split(a, b, c) 187 | }) 188 | } 189 | 190 | func TestLoggerMute(t *testing.T) { 191 | t.Run("mute test", func(t *testing.T) { 192 | Logger.Mute(Logger.Error) 193 | Logger.Mute(Logger.Warn) 194 | Logger.Mute(Logger.Info) 195 | Logger.Mute(Logger.Debug) 196 | }) 197 | } 198 | 199 | func TestLoggerUnmute(t *testing.T) { 200 | t.Run("unmute test", func(t *testing.T) { 201 | Logger.Unmute(Logger.Error) 202 | Logger.Unmute(Logger.Warn) 203 | Logger.Unmute(Logger.Info) 204 | Logger.Unmute(Logger.Debug) 205 | }) 206 | } 207 | --------------------------------------------------------------------------------