├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── actionseq.go ├── actionseq_test.go ├── examples └── bolt │ ├── README.md │ ├── boltsmat.go │ ├── crash_test.go │ └── fuzzdata_test.go ├── go.mod ├── smat.go └── smat_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | #* 2 | *.sublime-* 3 | *~ 4 | .#* 5 | .project 6 | .settings 7 | **/.idea/ 8 | **/*.iml 9 | /examples/bolt/boltsmat-fuzz.zip 10 | /examples/bolt/workdir/ 11 | .DS_Store 12 | coverage.out 13 | *.test 14 | tags 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | go: 4 | - 1.6 5 | script: 6 | - go get golang.org/x/tools/cmd/cover 7 | - go get github.com/mattn/goveralls 8 | - go get github.com/kisielk/errcheck 9 | - go test -v -race 10 | - go vet 11 | - errcheck ./... 12 | - go test -coverprofile=profile.out -covermode=count 13 | - goveralls -service=travis-ci -coverprofile=profile.out -repotoken $COVERALLS 14 | notifications: 15 | email: 16 | - marty.schoch@gmail.com 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # smat – State Machine Assisted Testing 2 | 3 | The concept is simple, describe valid uses of your library as states and actions. States describe which actions are possible, and with what probability they should occur. Actions mutate the context and transition to another state. 4 | 5 | By doing this, two things are possible: 6 | 7 | 1. Use [go-fuzz](https://github.com/dvyukov/go-fuzz) to find/test interesting sequences of operations on your library. 8 | 9 | 2. Automate longevity testing of your application by performing long sequences of valid operations. 10 | 11 | **NOTE**: both of these can also incorporate validation logic (not just failure detection by building validation into the state machine) 12 | 13 | ## Status 14 | 15 | The API is still not stable. This is brand new and we'll probably change things we don't like... 16 | 17 | [![Build Status](https://travis-ci.org/mschoch/smat.svg?branch=master)](https://travis-ci.org/mschoch/smat) 18 | [![Coverage Status](https://coveralls.io/repos/github/mschoch/smat/badge.svg?branch=master)](https://coveralls.io/github/mschoch/smat?branch=master) 19 | [![GoDoc](https://godoc.org/github.com/mschoch/smat?status.svg)](https://godoc.org/github.com/mschoch/smat) 20 | [![codebeat badge](https://codebeat.co/badges/c3ff6180-a241-4128-97f0-fa6bf6f48752)](https://codebeat.co/projects/github-com-mschoch-smat) 21 | [![Go Report Card](https://goreportcard.com/badge/github.com/mschoch/smat)](https://goreportcard.com/report/github.com/mschoch/smat) 22 | 23 | ## License 24 | 25 | Apache 2.0 26 | 27 | ## How do I use it? 28 | 29 | ### smat.Context 30 | 31 | Choose a structure to keep track of any state. You pass in an instance of this when you start, and it will be passed to every action when it executes. The actions may mutate this context. 32 | 33 | For example, consider a database library, once you open a database handle, you need to use it inside of the other actions. So you might use a structure like: 34 | 35 | ``` 36 | type context struct { 37 | db *DB 38 | } 39 | ``` 40 | 41 | ### smat.State 42 | 43 | A state represents a state that your application/library can be in, and the probabilities thats certain actions should be taken. 44 | 45 | For example, consider a database library, in a state where the database is open, there many things you can do. Let's consider just two right now, you can set a value, or you can delete a value. 46 | 47 | ``` 48 | func dbOpen(next byte) smat.ActionID { 49 | return smat.PercentExecute(next, 50 | smat.PercentAction{50, setValue}, 51 | smat.PercentAction{50, deleteValue}, 52 | ) 53 | } 54 | ``` 55 | 56 | This says that in the open state, there are two valid actions, 50% of the time you should set a value and 50% of the time you should delete a value. **NOTE**: these percentages are just for characterizing the test workload. 57 | 58 | ### smat.Action 59 | 60 | Actions are functions that do some work, optionally mutate the context, and indicate the next state to transition to. Below we see an example action to set value in a database. 61 | 62 | ``` 63 | func setValueFunc(ctx smat.Context) (next smat.State, err error) { 64 | // type assert to our custom context type 65 | context := ctx.(*context) 66 | // perform the operation 67 | err = context.db.Set("k", "v") 68 | if err != nil { 69 | return nil, err 70 | } 71 | // return the new state 72 | return dbOpen, nil 73 | } 74 | ``` 75 | 76 | ### smat.ActionID and smat.ActionMap 77 | 78 | Actions are just functions, and since we can't compare functions in Go, we need to introduce an external identifier for them. This allows us to build a bi-directional mapping which we'll take advantage of later. 79 | 80 | ``` 81 | const ( 82 | setup smat.ActionID = iota 83 | teardown 84 | setValue 85 | deleteValue 86 | ) 87 | 88 | var actionMap = smat.ActionMap{ 89 | setup: setupFunc, 90 | teardown: teardownFunc, 91 | setValue: setValueFunc, 92 | deleteValue: deleteValueFunc, 93 | } 94 | ``` 95 | 96 | ### smat.ActionSeq 97 | 98 | A common way that many users think about a library is as a sequence of actions to be performed. Using the ActionID's that we've already seen we can build up sequences of operations. 99 | 100 | ``` 101 | actionSeq := smat.ActionSeq{ 102 | open, 103 | setValue, 104 | setValue, 105 | setValue, 106 | } 107 | ``` 108 | 109 | Notice that we build these actions using the constants we defined above, and because of this we can have a bi-directional mapping between a stream of bytes (driving the state machine) and a sequence of actions to be performed. 110 | 111 | ## Fuzzing 112 | 113 | We've built a lot of pieces, lets wire it up to go-fuzz. 114 | 115 | ``` 116 | func Fuzz(data []byte) int { 117 | return smat.Fuzz(&context{}, setup, teardown, actionMap, data) 118 | } 119 | ``` 120 | 121 | * The first argument is an instance of context structure. 122 | * The second argument is the ActionID of our setup function. The setup function does not consume any of the input stream and is used to initialize the context and determine the start state. 123 | * The third argument is the teardown function. This will be called unconditionally to clean up any resources associated with the test. 124 | * The fourth argument is the actionMap which maps all ActionIDs to Actions. 125 | * The fifth argument is the data passed in from the go-fuzz application. 126 | 127 | ### Generating Initial go-fuzz Corpus 128 | 129 | Earlier we mentioned the bi-directional mapping between Actions and the byte stream driving the state machine. We can now leverage this to build the inital go-fuzz corpus. 130 | 131 | Using the `ActinSeq`s we learned about earlier we can build up a list of them as: 132 | 133 | var actionSeqs = []smat.ActionSeq{...} 134 | 135 | Then, we can write them out to disk using: 136 | 137 | ``` 138 | for i, actionSeq := range actionSeqs { 139 | byteSequence, err := actionSeq.ByteEncoding(&context{}, setup, teardown, actionMap) 140 | if err != nil { 141 | // handle error 142 | } 143 | os.MkdirAll("workdir/corpus", 0700) 144 | ioutil.WriteFile(fmt.Sprintf("workdir/corpus/%d", i), byteSequence, 0600) 145 | } 146 | ``` 147 | 148 | You can then either put this into a test case or a main application depending on your needs. 149 | 150 | ## Longevity Testing 151 | 152 | Fuzzing is great, but most of your corpus is likely to be shorter meaningful sequences. And go-fuzz works to find shortest sequences that cause problems, but sometimes you actually want to explore longer sequences that appear to go-fuzz as not triggering additional code coverage. 153 | 154 | For these cases we have another helper you can use: 155 | 156 | ``` 157 | Longevity(ctx, setup, teardown, actionMap, 0, closeChan) 158 | ``` 159 | 160 | The first four arguments are the same, the last two are: 161 | * random seed used to ensure repeatable tests 162 | * closeChan (chan struct{}) - close this channel if you want the function to stop and return ErrClosed, otherwise it will run forever 163 | 164 | ## Examples 165 | 166 | See the examples directory for a working example that tests some BoltDB functionality. 167 | -------------------------------------------------------------------------------- /actionseq.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Marty Schoch 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the 5 | // License. You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an "AS 9 | // IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | // express or implied. See the License for the specific language 11 | // governing permissions and limitations under the License. 12 | 13 | package smat 14 | 15 | // ActionSeq represents a sequence of actions, used for populating a corpus 16 | // of byte sequences for the corresponding fuzz tests 17 | type ActionSeq []ActionID 18 | 19 | // ByteEncoding runs the FSM to produce a byte sequence to trigger the 20 | // desired action 21 | func (a ActionSeq) ByteEncoding(ctx Context, setup, teardown ActionID, actionMap ActionMap) ([]byte, error) { 22 | setupFunc, teardownFunc, err := actionMap.findSetupTeardown(setup, teardown) 23 | if err != nil { 24 | return nil, err 25 | } 26 | state, err := setupFunc(ctx) 27 | if err != nil { 28 | return nil, err 29 | } 30 | defer func() { 31 | _, _ = teardownFunc(ctx) 32 | }() 33 | 34 | var rv []byte 35 | for _, actionID := range a { 36 | b, err := probeStateForAction(state, actionID) 37 | if err != nil { 38 | return nil, err 39 | } 40 | rv = append(rv, b) 41 | action, ok := actionMap[actionID] 42 | if !ok { 43 | continue 44 | } 45 | state, err = action(ctx) 46 | if err != nil { 47 | return nil, err 48 | } 49 | } 50 | return rv, nil 51 | } 52 | 53 | func probeStateForAction(state State, actionID ActionID) (byte, error) { 54 | for i := 0; i < 256; i++ { 55 | nextActionID := state(byte(i)) 56 | if nextActionID == actionID { 57 | return byte(i), nil 58 | } 59 | } 60 | return 0, ErrActionNotPossible 61 | } 62 | -------------------------------------------------------------------------------- /actionseq_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Marty Schoch 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the 5 | // License. You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an "AS 9 | // IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | // express or implied. See the License for the specific language 11 | // governing permissions and limitations under the License. 12 | 13 | package smat 14 | 15 | import ( 16 | "reflect" 17 | "testing" 18 | ) 19 | 20 | func TestByteEncoding(t *testing.T) { 21 | var actionMap = ActionMap{ 22 | setup: setupAction, 23 | teardown: teardownAction, 24 | action1: action1Action, 25 | action2: action2Action, 26 | } 27 | 28 | tests := []struct { 29 | actionSeq ActionSeq 30 | expected []byte 31 | err error 32 | }{ 33 | { 34 | actionSeq: ActionSeq{ 35 | action1, 36 | action2, 37 | action1, 38 | action2, 39 | }, 40 | expected: []byte{0, 129, 0, 129}, 41 | }, 42 | { 43 | actionSeq: ActionSeq{ 44 | action1, 45 | action2, 46 | errExpected, 47 | }, 48 | err: ErrActionNotPossible, 49 | }, 50 | } 51 | 52 | for _, test := range tests { 53 | ctx := &testContext{t: t} 54 | rv, err := test.actionSeq.ByteEncoding(ctx, setup, teardown, actionMap) 55 | if err != test.err { 56 | t.Errorf("expected err: %v got: %v", test.err, err) 57 | } 58 | if !reflect.DeepEqual(rv, test.expected) { 59 | t.Errorf("expected: %v, got %v", test.expected, rv) 60 | } 61 | } 62 | } 63 | 64 | func TestByteEncodingErrors(t *testing.T) { 65 | var actionMap = ActionMap{ 66 | setup: setupAction, 67 | teardown: teardownAction, 68 | errExpected: errExpectedAction, 69 | setupToErr: setupToErrAction, 70 | setupToNop: setupToNopAction, 71 | } 72 | 73 | actionSeq := ActionSeq{ 74 | action1, 75 | action2, 76 | action1, 77 | action2, 78 | } 79 | 80 | // setup missing 81 | _, err := actionSeq.ByteEncoding(nil, -1, teardown, actionMap) 82 | if err != ErrSetupMissing { 83 | t.Errorf("expected ErrSetupMissing, got %v", err) 84 | } 85 | // setup error 86 | _, err = actionSeq.ByteEncoding(nil, errExpected, teardown, actionMap) 87 | if err != errExpectedErr { 88 | t.Errorf("expected errExpectedErr, got %v", err) 89 | } 90 | // err in action 91 | ctx := &testContext{t: t} 92 | actionSeq = ActionSeq{ 93 | errExpected, 94 | } 95 | _, err = actionSeq.ByteEncoding(ctx, setupToErr, teardown, actionMap) 96 | if err != errExpectedErr { 97 | t.Errorf("expected errExpectedErr, got %v", err) 98 | } 99 | } 100 | 101 | func TestByteEncodingNop(t *testing.T) { 102 | 103 | var actionMap = ActionMap{ 104 | teardown: teardownAction, 105 | setupToNop: setupToNopAction, 106 | } 107 | 108 | // nop in actions 109 | ctx := &testContext{t: t} 110 | actionSeq := ActionSeq{ 111 | NopAction, 112 | } 113 | rv, err := actionSeq.ByteEncoding(ctx, setupToNop, teardown, actionMap) 114 | if err != nil { 115 | t.Fatalf("expected no err, got: %v", err) 116 | } 117 | expected := []byte{232} 118 | if !reflect.DeepEqual(rv, expected) { 119 | t.Errorf("expected: %v, got %v", expected, rv) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /examples/bolt/README.md: -------------------------------------------------------------------------------- 1 | # boltsmat 2 | 3 | An example project, showing how you can use [smat](https://github.com/mschoch/smat) to test [Bolt](https://github.com/boltdb/bolt). 4 | 5 | ## Prerequisites 6 | 7 | $ go get github.com/dvyukov/go-fuzz/go-fuzz 8 | $ go get github.com/dvyukov/go-fuzz/go-fuzz-build 9 | 10 | ## Steps 11 | 12 | 1. Generate initial fuzz corpus: 13 | ``` 14 | $ go test -tags=gofuzz -run=TestGenerateFuzzData 15 | ``` 16 | 17 | 2. Build go-fuzz test program with instrumentation: 18 | ``` 19 | $ go-fuzz-build github.com/mschoch/smat/examples/bolt 20 | ``` 21 | 22 | 3. Run go-fuzz: 23 | ``` 24 | $ go-fuzz -bin=./boltsmat-fuzz.zip -workdir=workdir/ -timeout=60 25 | ``` 26 | 27 | ### If you find a crasher... 28 | 29 | You can copy the contents of the .output file provided by go-fuzz, and paste it into the `crasher` variable of the `TestCrasher` function in crash_test.go. Then run: 30 | 31 | $ go test -v -run=TestCrasher 32 | 33 | This will reproduce the crash with additional logging of the state machine turned on. 34 | -------------------------------------------------------------------------------- /examples/bolt/boltsmat.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Marty Schoch 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the 5 | // License. You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an "AS 9 | // IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | // express or implied. See the License for the specific language 11 | // governing permissions and limitations under the License. 12 | 13 | package boltsmat 14 | 15 | import ( 16 | "fmt" 17 | "io/ioutil" 18 | "math/rand" 19 | "os" 20 | 21 | "github.com/boltdb/bolt" 22 | "github.com/mschoch/smat" 23 | ) 24 | 25 | // Fuzz using state machine driven by byte stream 26 | func Fuzz(data []byte) int { 27 | return smat.Fuzz(&context{}, setup, teardown, actionMap, data) 28 | } 29 | 30 | // Context 31 | type context struct { 32 | path string 33 | db *bolt.DB 34 | tx *bolt.Tx 35 | bucket *bolt.Bucket 36 | } 37 | 38 | // *** States *** 39 | 40 | func dbOpen(next byte) smat.ActionID { 41 | return smat.PercentExecute(next, 42 | smat.PercentAction{10, closeReopen}, 43 | smat.PercentAction{90, startWriteTx}, 44 | ) 45 | } 46 | 47 | func writeTxOpen(next byte) smat.ActionID { 48 | return smat.PercentExecute(next, 49 | smat.PercentAction{30, setRandom}, 50 | smat.PercentAction{30, deleteRandom}, 51 | smat.PercentAction{30, commitTx}, 52 | smat.PercentAction{10, rollbackTx}, 53 | ) 54 | } 55 | 56 | // *** Actions *** 57 | const ( 58 | setup smat.ActionID = iota 59 | teardown 60 | closeReopen 61 | startWriteTx 62 | setRandom 63 | deleteRandom 64 | commitTx 65 | rollbackTx 66 | ) 67 | 68 | var actionMap = smat.ActionMap{ 69 | setup: setupFunc, 70 | teardown: teardownFunc, 71 | closeReopen: closeReopenFunc, 72 | startWriteTx: startWriteTxFunc, 73 | setRandom: setRandomFunc, 74 | deleteRandom: deleteRandomFunc, 75 | commitTx: commitTxFunc, 76 | rollbackTx: rollbackTxFunc, 77 | } 78 | 79 | func setupFunc(ctx smat.Context) (next smat.State, err error) { 80 | context := ctx.(*context) 81 | context.path, err = ioutil.TempDir("", "cellar") 82 | if err != nil { 83 | return nil, err 84 | } 85 | context.path += string(os.PathSeparator) + "fuzz.db" 86 | context.db, err = bolt.Open(context.path, 0600, nil) 87 | if err != nil { 88 | return nil, err 89 | } 90 | return dbOpen, nil 91 | } 92 | 93 | func teardownFunc(ctx smat.Context) (next smat.State, err error) { 94 | context := ctx.(*context) 95 | if context.tx != nil { 96 | _ = context.tx.Rollback() 97 | context.tx = nil 98 | } 99 | if context.db != nil { 100 | _ = context.db.Close() 101 | context.db = nil 102 | } 103 | _ = os.RemoveAll(context.path) 104 | return nil, nil 105 | } 106 | 107 | func closeReopenFunc(ctx smat.Context) (next smat.State, err error) { 108 | context := ctx.(*context) 109 | err = context.db.Close() 110 | if err != nil { 111 | return nil, err 112 | } 113 | context.db, err = bolt.Open(context.path, 0600, nil) 114 | if err != nil { 115 | return nil, err 116 | } 117 | return dbOpen, nil 118 | } 119 | 120 | func startWriteTxFunc(ctx smat.Context) (next smat.State, err error) { 121 | context := ctx.(*context) 122 | context.tx, err = context.db.Begin(true) 123 | if err != nil { 124 | return nil, err 125 | } 126 | return writeTxOpen, nil 127 | } 128 | 129 | func setRandomFunc(ctx smat.Context) (next smat.State, err error) { 130 | context := ctx.(*context) 131 | if context.bucket == nil { 132 | context.bucket, err = context.tx.CreateBucketIfNotExists([]byte("default")) 133 | if err != nil { 134 | return nil, err 135 | } 136 | } 137 | err = context.bucket.Put(randomKey(), randomVal()) 138 | if err != nil { 139 | return nil, err 140 | } 141 | return writeTxOpen, nil 142 | } 143 | 144 | func deleteRandomFunc(ctx smat.Context) (next smat.State, err error) { 145 | context := ctx.(*context) 146 | if context.bucket == nil { 147 | context.bucket, err = context.tx.CreateBucketIfNotExists([]byte("default")) 148 | if err != nil { 149 | return nil, err 150 | } 151 | } 152 | err = context.bucket.Delete(randomKey()) 153 | if err != nil { 154 | return nil, err 155 | } 156 | return writeTxOpen, nil 157 | } 158 | 159 | func commitTxFunc(ctx smat.Context) (next smat.State, err error) { 160 | context := ctx.(*context) 161 | err = context.tx.Commit() 162 | if err != nil { 163 | return nil, err 164 | } 165 | context.tx = nil 166 | context.bucket = nil 167 | return dbOpen, nil 168 | } 169 | 170 | func rollbackTxFunc(ctx smat.Context) (next smat.State, err error) { 171 | context := ctx.(*context) 172 | err = context.tx.Rollback() 173 | if err != nil { 174 | return nil, err 175 | } 176 | context.tx = nil 177 | context.bucket = nil 178 | return dbOpen, nil 179 | } 180 | 181 | func randomKey() []byte { 182 | num := rand.Int63() 183 | return []byte(fmt.Sprintf("k%016x", num)) 184 | } 185 | 186 | func randomVal() []byte { 187 | num := rand.Int63() 188 | return []byte(fmt.Sprintf("v%016x", num)) 189 | } 190 | -------------------------------------------------------------------------------- /examples/bolt/crash_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Marty Schoch 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the 5 | // License. You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an "AS 9 | // IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | // express or implied. See the License for the specific language 11 | // governing permissions and limitations under the License. 12 | 13 | package boltsmat 14 | 15 | import ( 16 | "log" 17 | "os" 18 | "testing" 19 | 20 | "github.com/mschoch/smat" 21 | ) 22 | 23 | func TestCrasher(t *testing.T) { 24 | // paste in your crash here: 25 | crasher := []byte("N\x00\xcb���\n\xef\x1a\x00\xbd0N\xb9" + 26 | "a\xaf@\xee\x1e\xd748\xc7\xe9\xed\x02\xfe\xfb\x02\x00\xbd\xbf0N" + 27 | "\xb9a\xaf@\xee\x1e\xd748\xc7\xe9\xed\x02\xfe\xfbM\xfe\xbd\xbf\xef" + 28 | "\xbd\xbfソ\xef\xbd6379788709725" + 29 | "605625\xbfソ\xef\xf6\xfe\xf6N\xafN\xf6N\x9b" + 30 | "J\x88\xac\xd5�\xbf\xbd`�N\xb9NN\xa3\xe2\xd6" + 31 | "\x11\x8d\x15\xd5ǵ\xc7\xef\xbfソ\xef\xbd679709" + 32 | "71251601625\xbfソ\xef\xf6\xfe\xf6N" + 33 | "\xafN\xf6N\x9bJ\x88\xac\xd5뿽\xbf\xbd`\xef\xbf\xcaN\xb9" + 34 | "NN\xa3\xe2\xd6\x11\xd5ǵ") 35 | // turn on logger 36 | smat.Logger = log.New(os.Stderr, "smat ", log.LstdFlags) 37 | // fuzz the crasher input 38 | smat.Fuzz(&context{}, setup, teardown, actionMap, crasher) 39 | } 40 | -------------------------------------------------------------------------------- /examples/bolt/fuzzdata_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Marty Schoch 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the 5 | // License. You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an "AS 9 | // IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | // express or implied. See the License for the specific language 11 | // governing permissions and limitations under the License. 12 | 13 | // +build gofuzz 14 | 15 | package boltsmat 16 | 17 | import ( 18 | "fmt" 19 | "io/ioutil" 20 | "os" 21 | "testing" 22 | 23 | "github.com/mschoch/smat" 24 | ) 25 | 26 | func TestGenerateFuzzData(t *testing.T) { 27 | for i, actionSeq := range actionSeqs { 28 | byteSequence, err := actionSeq.ByteEncoding(&context{}, setup, teardown, actionMap) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | os.MkdirAll("workdir/corpus", 0700) 33 | ioutil.WriteFile(fmt.Sprintf("workdir/corpus/%d", i), byteSequence, 0600) 34 | } 35 | } 36 | 37 | var actionSeqs = []smat.ActionSeq{ 38 | // open tx, write 5 random keys, delete 5 random keys, commit tx 39 | { 40 | startWriteTx, 41 | setRandom, 42 | setRandom, 43 | setRandom, 44 | setRandom, 45 | setRandom, 46 | deleteRandom, 47 | deleteRandom, 48 | deleteRandom, 49 | deleteRandom, 50 | deleteRandom, 51 | commitTx, 52 | }, 53 | // open tx, write 5 random keys, rollback 54 | { 55 | startWriteTx, 56 | setRandom, 57 | setRandom, 58 | setRandom, 59 | setRandom, 60 | setRandom, 61 | rollbackTx, 62 | }, 63 | // crasher due to bug in test bug 64 | { 65 | startWriteTx, 66 | setRandom, 67 | commitTx, 68 | startWriteTx, 69 | setRandom, 70 | }, 71 | } 72 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mschoch/smat 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /smat.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Marty Schoch 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the 5 | // License. You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an "AS 9 | // IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | // express or implied. See the License for the specific language 11 | // governing permissions and limitations under the License. 12 | 13 | package smat 14 | 15 | import ( 16 | "bufio" 17 | "bytes" 18 | "fmt" 19 | "io" 20 | "io/ioutil" 21 | "log" 22 | "math/rand" 23 | ) 24 | 25 | // Logger is a configurable logger used by this package 26 | // by default output is discarded 27 | var Logger = log.New(ioutil.Discard, "smat ", log.LstdFlags) 28 | 29 | // Context is a container for any user state 30 | type Context interface{} 31 | 32 | // State is a function which describes which action to perform in the event 33 | // that a particular byte is seen 34 | type State func(next byte) ActionID 35 | 36 | // PercentAction describes the frequency with which an action should occur 37 | // for example: Action{Percent:10, Action:DonateMoney} means that 10% of 38 | // the time you should donate money. 39 | type PercentAction struct { 40 | Percent int 41 | Action ActionID 42 | } 43 | 44 | // Action is any function which returns the next state to transition to 45 | // it can optionally mutate the provided context object 46 | // if any error occurs, it may return an error which will abort execution 47 | type Action func(Context) (State, error) 48 | 49 | // ActionID is a unique identifier for an action 50 | type ActionID int 51 | 52 | // NopAction does nothing and simply continues to the next input 53 | var NopAction ActionID = -1 54 | 55 | // ActionMap is a mapping form ActionID to Action 56 | type ActionMap map[ActionID]Action 57 | 58 | func (a ActionMap) findSetupTeardown(setup, teardown ActionID) (Action, Action, error) { 59 | setupFunc, ok := a[setup] 60 | if !ok { 61 | return nil, nil, ErrSetupMissing 62 | } 63 | teardownFunc, ok := a[teardown] 64 | if !ok { 65 | return nil, nil, ErrTeardownMissing 66 | } 67 | return setupFunc, teardownFunc, nil 68 | } 69 | 70 | // Fuzz runs the fuzzing state machine with the provided context 71 | // first, the setup action is executed unconditionally 72 | // the start state is determined by this action 73 | // actionMap is a lookup table for all actions 74 | // the data byte slice determines all future state transitions 75 | // finally, the teardown action is executed unconditionally for cleanup 76 | func Fuzz(ctx Context, setup, teardown ActionID, actionMap ActionMap, data []byte) int { 77 | reader := bytes.NewReader(data) 78 | err := runReader(ctx, setup, teardown, actionMap, reader, nil) 79 | if err != nil { 80 | panic(err) 81 | } 82 | return 1 83 | } 84 | 85 | // Longevity runs the state machine with the provided context 86 | // first, the setup action is executed unconditionally 87 | // the start state is determined by this action 88 | // actionMap is a lookup table for all actions 89 | // random bytes are generated to determine all future state transitions 90 | // finally, the teardown action is executed unconditionally for cleanup 91 | func Longevity(ctx Context, setup, teardown ActionID, actionMap ActionMap, seed int64, closeChan chan struct{}) error { 92 | source := rand.NewSource(seed) 93 | return runReader(ctx, setup, teardown, actionMap, rand.New(source), closeChan) 94 | } 95 | 96 | var ( 97 | // ErrSetupMissing is returned when the setup action cannot be found 98 | ErrSetupMissing = fmt.Errorf("setup action missing") 99 | // ErrTeardownMissing is returned when the teardown action cannot be found 100 | ErrTeardownMissing = fmt.Errorf("teardown action missing") 101 | // ErrClosed is returned when the closeChan was closed to cancel the op 102 | ErrClosed = fmt.Errorf("closed") 103 | // ErrActionNotPossible is returned when an action is encountered in a 104 | // FuzzCase that is not possible in the current state 105 | ErrActionNotPossible = fmt.Errorf("action not possible in state") 106 | ) 107 | 108 | func runReader(ctx Context, setup, teardown ActionID, actionMap ActionMap, r io.Reader, closeChan chan struct{}) error { 109 | setupFunc, teardownFunc, err := actionMap.findSetupTeardown(setup, teardown) 110 | if err != nil { 111 | return err 112 | } 113 | Logger.Printf("invoking setup action") 114 | state, err := setupFunc(ctx) 115 | if err != nil { 116 | return err 117 | } 118 | defer func() { 119 | Logger.Printf("invoking teardown action") 120 | _, _ = teardownFunc(ctx) 121 | }() 122 | 123 | reader := bufio.NewReader(r) 124 | for next, err := reader.ReadByte(); err == nil; next, err = reader.ReadByte() { 125 | select { 126 | case <-closeChan: 127 | return ErrClosed 128 | default: 129 | actionID := state(next) 130 | action, ok := actionMap[actionID] 131 | if !ok { 132 | Logger.Printf("no such action defined, continuing") 133 | continue 134 | } 135 | Logger.Printf("invoking action - %d", actionID) 136 | state, err = action(ctx) 137 | if err != nil { 138 | Logger.Printf("it was action %d that returned err %v", actionID, err) 139 | return err 140 | } 141 | } 142 | } 143 | return err 144 | } 145 | 146 | // PercentExecute interprets the next byte as a random value and normalizes it 147 | // to values 0-99, it then looks to see which action should be execued based 148 | // on the action distributions 149 | func PercentExecute(next byte, pas ...PercentAction) ActionID { 150 | percent := int(99 * int(next) / 255) 151 | 152 | sofar := 0 153 | for _, pa := range pas { 154 | sofar = sofar + pa.Percent 155 | if percent < sofar { 156 | return pa.Action 157 | } 158 | 159 | } 160 | return NopAction 161 | } 162 | -------------------------------------------------------------------------------- /smat_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Marty Schoch 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the 5 | // License. You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an "AS 9 | // IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | // express or implied. See the License for the specific language 11 | // governing permissions and limitations under the License. 12 | 13 | package smat 14 | 15 | import ( 16 | "bytes" 17 | "fmt" 18 | "sync" 19 | "testing" 20 | "time" 21 | ) 22 | 23 | type testContext struct { 24 | t *testing.T 25 | setupFired bool 26 | teardownFired bool 27 | action1Fired bool 28 | action2Fired bool 29 | count int 30 | } 31 | 32 | const ( 33 | setup ActionID = iota 34 | teardown 35 | action1 36 | action2 37 | errExpected 38 | setupToNop 39 | setupToErr 40 | ) 41 | 42 | func state1(next byte) ActionID { 43 | return PercentExecute(next, 44 | PercentAction{Percent: 50, Action: action1}, 45 | PercentAction{Percent: 50, Action: action2}, 46 | ) 47 | } 48 | 49 | func stateToErr(next byte) ActionID { 50 | return PercentExecute(next, 51 | PercentAction{Percent: 100, Action: errExpected}) 52 | } 53 | 54 | func stateNop(next byte) ActionID { 55 | return PercentExecute(next, 56 | PercentAction{Percent: 90, Action: action1}) 57 | } 58 | 59 | func setupAction(ctx Context) (next State, err error) { 60 | context := ctx.(*testContext) 61 | context.setupFired = true 62 | return state1, nil 63 | } 64 | 65 | func setupToErrAction(ctx Context) (next State, err error) { 66 | return stateToErr, nil 67 | } 68 | 69 | func setupToNopAction(ctx Context) (next State, err error) { 70 | return stateNop, nil 71 | } 72 | 73 | func teardownAction(ctx Context) (next State, err error) { 74 | context := ctx.(*testContext) 75 | context.teardownFired = true 76 | return state1, nil 77 | } 78 | 79 | func action1Action(ctx Context) (next State, err error) { 80 | context := ctx.(*testContext) 81 | context.action1Fired = true 82 | 83 | return state1, nil 84 | } 85 | 86 | func action2Action(ctx Context) (next State, err error) { 87 | context := ctx.(*testContext) 88 | context.action2Fired = true 89 | context.count++ 90 | return state1, nil 91 | } 92 | 93 | var errExpectedErr = fmt.Errorf("expected") 94 | 95 | func errExpectedAction(ctx Context) (next State, err error) { 96 | return nil, errExpectedErr 97 | } 98 | 99 | func TestRunMachine(t *testing.T) { 100 | 101 | var actionMap = ActionMap{ 102 | setup: setupAction, 103 | teardown: teardownAction, 104 | action1: action1Action, 105 | action2: action2Action, 106 | } 107 | 108 | ctx := &testContext{t: t} 109 | err := runReader(ctx, setup, teardown, actionMap, bytes.NewReader([]byte{0, 255}), nil) 110 | if err != nil { 111 | t.Fatalf("err running reader: %v", err) 112 | } 113 | 114 | if !ctx.setupFired { 115 | t.Errorf("expected setup to happen, did not") 116 | } 117 | if !ctx.teardownFired { 118 | t.Errorf("expected teardown to happen, did not") 119 | } 120 | if !ctx.action1Fired { 121 | t.Errorf("expected action1 to happen, did not") 122 | } 123 | if !ctx.action2Fired { 124 | t.Errorf("expected action2 to happen, did not") 125 | } 126 | } 127 | 128 | func TestRunMachineErrors(t *testing.T) { 129 | var actionMap = ActionMap{ 130 | setup: setupAction, 131 | teardown: teardownAction, 132 | errExpected: errExpectedAction, 133 | setupToErr: setupToErrAction, 134 | } 135 | // setup missing 136 | err := runReader(nil, -1, teardown, actionMap, bytes.NewReader([]byte{}), nil) 137 | if err != ErrSetupMissing { 138 | t.Errorf("expected ErrSetupMissing, got %v", err) 139 | } 140 | // teardown missing 141 | err = runReader(nil, setup, -1, actionMap, bytes.NewReader([]byte{}), nil) 142 | if err != ErrTeardownMissing { 143 | t.Errorf("expected ErrTeardownMissing, got %v", err) 144 | } 145 | // setup error 146 | err = runReader(nil, errExpected, teardown, actionMap, bytes.NewReader([]byte{}), nil) 147 | if err != errExpectedErr { 148 | t.Errorf("expected errExpectedErr, got %v", err) 149 | } 150 | // err in action 151 | ctx := &testContext{t: t} 152 | err = runReader(ctx, setupToErr, teardown, actionMap, bytes.NewReader([]byte{0}), nil) 153 | if err != errExpectedErr { 154 | t.Errorf("expected errExpectedErr, got %v", err) 155 | } 156 | 157 | } 158 | 159 | func TestRunMachineWithNop(t *testing.T) { 160 | 161 | var actionMap = ActionMap{ 162 | setupToNop: setupToNopAction, 163 | teardown: teardownAction, 164 | action1: action1Action, 165 | } 166 | 167 | ctx := &testContext{t: t} 168 | // first go to nop, then to action1 169 | err := runReader(ctx, setupToNop, teardown, actionMap, bytes.NewReader([]byte{255, 0}), nil) 170 | if err != nil { 171 | t.Errorf("expected no err, got %v", err) 172 | } 173 | 174 | if !ctx.action1Fired { 175 | t.Errorf("expected action1 to happen, did not") 176 | } 177 | 178 | } 179 | 180 | func TestFuzz(t *testing.T) { 181 | 182 | var actionMap = ActionMap{ 183 | setup: setupAction, 184 | teardown: teardownAction, 185 | action1: action1Action, 186 | action2: action2Action, 187 | } 188 | 189 | ctx := &testContext{t: t} 190 | res := Fuzz(ctx, setup, teardown, actionMap, []byte{0, 255}) 191 | if res != 1 { 192 | t.Errorf("expected return 1, got %d", res) 193 | } 194 | if !ctx.setupFired { 195 | t.Errorf("expected setup to happen, did not") 196 | } 197 | if !ctx.teardownFired { 198 | t.Errorf("expected teardown to happen, did not") 199 | } 200 | if !ctx.action1Fired { 201 | t.Errorf("expected action1 to happen, did not") 202 | } 203 | if !ctx.action2Fired { 204 | t.Errorf("expected action2 to happen, did not") 205 | } 206 | } 207 | 208 | func TestFuzzErr(t *testing.T) { 209 | 210 | sawPanic := false 211 | 212 | defer func() { 213 | if sawPanic == false { 214 | t.Errorf("expected to see panic, did not") 215 | } 216 | }() 217 | 218 | defer func() { 219 | if r := recover(); r != nil { 220 | sawPanic = true 221 | } 222 | }() 223 | 224 | var actionMap = ActionMap{ 225 | setupToErr: setupToErrAction, 226 | teardown: teardownAction, 227 | errExpected: errExpectedAction, 228 | } 229 | 230 | ctx := &testContext{t: t} 231 | Fuzz(ctx, setupToErr, teardown, actionMap, []byte{0}) 232 | } 233 | 234 | func TestLongevity(t *testing.T) { 235 | var actionMap = ActionMap{ 236 | setup: setupAction, 237 | teardown: teardownAction, 238 | action1: action1Action, 239 | action2: action2Action, 240 | } 241 | 242 | var err error 243 | ctx := &testContext{t: t} 244 | wg := sync.WaitGroup{} 245 | closeChan := make(chan struct{}) 246 | wg.Add(1) 247 | go func() { 248 | err = Longevity(ctx, setup, teardown, actionMap, 0, closeChan) 249 | wg.Done() 250 | }() 251 | // sleep briefly 252 | time.Sleep(1 * time.Second) 253 | // then close 254 | close(closeChan) 255 | // wait for the longeivity function to return 256 | wg.Wait() 257 | if err != ErrClosed { 258 | t.Errorf("expected ErrClosed, got: %v", err) 259 | } 260 | if !ctx.setupFired { 261 | t.Errorf("expected setup to happen, did not") 262 | } 263 | if !ctx.teardownFired { 264 | t.Errorf("expected teardown to happen, did not") 265 | } 266 | if !ctx.action1Fired { 267 | t.Errorf("expected action1 to happen, did not") 268 | } 269 | if !ctx.action2Fired { 270 | t.Errorf("expected action2 to happen, did not") 271 | } 272 | if ctx.count < 10000 { 273 | t.Errorf("expected actions to fire a lot, but only %d", ctx.count) 274 | } 275 | } 276 | --------------------------------------------------------------------------------