github.com/rothskeller/wppsvr@v1.8.9/analyze/analyze_test.go (about) 1 package analyze 2 3 // This file defines a framework for testing the analysis package. Every *.yaml 4 // file in the testdata tree, except config.yaml, describes a test case, 5 // including configuration and session setup, the message to be analyzed, the 6 // expected results of the analysis, and the expected response messages that 7 // should be generated. The code in this file runs the analysis and tests the 8 // result. 9 10 import ( 11 "fmt" 12 "io" 13 "io/fs" 14 "log" 15 "os" 16 "path/filepath" 17 "regexp" 18 "strings" 19 "testing" 20 "time" 21 22 "github.com/davecgh/go-spew/spew" 23 "github.com/go-test/deep" 24 "gopkg.in/yaml.v3" 25 26 "github.com/rothskeller/packet/envelope" 27 "github.com/rothskeller/packet/message" 28 "github.com/rothskeller/packet/xscmsg" 29 "github.com/rothskeller/wppsvr/config" 30 "github.com/rothskeller/wppsvr/store" 31 ) 32 33 type testdata struct { 34 Now time.Time `yaml:"now"` 35 Config *config.Config `yaml:"config"` 36 SeenHash string `yaml:"seenHash"` 37 Session *store.Session `yaml:"session"` 38 ToBBS string `yaml:"toBBS"` 39 Message string `yaml:"message"` 40 Stored *store.Message `yaml:"stored"` 41 AnalysisREs []string `yaml:"analysisREs"` 42 Responses []*responseCheck `yaml:"responses"` 43 } 44 type responseCheck struct { 45 store.Response `yaml:",inline"` 46 BodyREs []string `yaml:"bodyREs"` 47 } 48 49 func TestAnalyze(t *testing.T) { 50 var testfiles []string 51 log.SetOutput(io.Discard) 52 TestForceJurisdiction = "SNY" 53 xscmsg.Register() 54 filepath.WalkDir("testdata", func(path string, info fs.DirEntry, err error) error { 55 if strings.HasSuffix(path, ".yaml") && path != "testdata/config.yaml" { 56 testfiles = append(testfiles, path) 57 } 58 return nil 59 }) 60 for _, testfile := range testfiles { 61 t.Run(testfile[:len(testfile)-5], func(t *testing.T) { 62 testAnalyze(t, testfile) 63 }) 64 } 65 } 66 67 func testAnalyze(t *testing.T, testfile string) { 68 var testdata testdata 69 70 // First, we need a partial configuration. We'll read that from 71 // testdata/config.yaml, and then allow it to be modified by the test's 72 // yaml file. 73 os.Chdir("testdata") 74 if err := config.Read(); err != nil { 75 t.Fatal(err) 76 } 77 os.Chdir("..") 78 testdata.Config = config.Get() 79 // We also need a session definition, which again, the test's yaml file 80 // can modify. 81 testdata.Session = &store.Session{ 82 ID: 42, 83 CallSign: "PKTTUE", 84 Name: "SVECS Net", 85 Prefix: "TUE", 86 Start: time.Date(2022, 1, 5, 0, 0, 0, 0, time.Local), 87 End: time.Date(2022, 1, 11, 20, 0, 0, 0, time.Local), 88 ToBBSes: []string{"W4XSC"}, 89 DownBBSes: []string{"W2XSC"}, 90 MessageTypes: []string{"plain"}, 91 } 92 // By default, we'll assume that "now" is a moment after the end of the 93 // session. 94 testdata.Now = time.Date(2022, 1, 11, 20, 0, 1, 0, time.Local) 95 now = func() time.Time { return testdata.Now } 96 // By default, we'll assume the test message is sent to the correct BBS. 97 testdata.ToBBS = "W4XSC" 98 // Now, read the test data file, allowing it to override any of the 99 // above as well as defining the test message and expected output. 100 fh, _ := os.Open(testfile) 101 defer fh.Close() 102 dec := yaml.NewDecoder(fh) 103 dec.KnownFields(true) 104 if err := dec.Decode(&testdata); err != nil { 105 t.Fatal(err) 106 } 107 testdata.Config.Validate() 108 // If the test data file included a model message, we need to parse it. 109 if testdata.Session.ModelMessage != "" { 110 env, body, _ := envelope.ParseSaved(testdata.Session.ModelMessage) 111 testdata.Session.ModelMsg = message.Decode(env.SubjectLine, body) 112 } 113 // We'll need a fake store for the analyzer to use. 114 store := &fakeStore{seenHash: testdata.SeenHash, nextID: 100} 115 // Run the analysis. 116 a := Analyze(store, testdata.Session, testdata.ToBBS, testdata.Message) 117 responses := a.Responses(store) 118 a.Commit(store) 119 // First of all, did the analysis store the expected number of analyzed 120 // messages (zero or one)? 121 if testdata.Stored != nil && len(store.saved) == 0 { 122 t.Error("no analysis saved to store") 123 } 124 if testdata.Stored == nil && len(store.saved) != 0 { 125 t.Errorf("unexpected analysis saved to store: %s", spew.Sdump(store.saved[0])) 126 } 127 if len(store.saved) > 1 { 128 t.Error("multiple analyses saved to store") 129 } 130 // If we both expected and got an analysis, is it correct? 131 if testdata.Stored != nil && len(store.saved) != 0 { 132 // The raw message in the analysis should be the same as the 133 // input message. We'll assign that here so it doesn't have to 134 // be redundantly provided in every test file. 135 testdata.Stored.Message = testdata.Message 136 // We'll simply assume that the hash is correct, rather than 137 // need to compute hashes for each test. 138 testdata.Stored.Hash = store.saved[0].Hash 139 // Fill in some defaults for the expected analysis. 140 if testdata.Stored.LocalID == "" { 141 testdata.Stored.LocalID = "TUE-100P" 142 } 143 if testdata.Stored.Session == 0 { 144 testdata.Stored.Session = 42 145 } 146 if testdata.Stored.ToBBS == "" { 147 testdata.Stored.ToBBS = "W4XSC" 148 } 149 // For the purpose of testing, all scores are either 0, 50, or 150 // 100. 151 if store.saved[0].Score != 0 && store.saved[0].Score != 100 { 152 store.saved[0].Score = 50 153 } 154 // We don't want to do byte-for-byte compare of the analysis 155 // HTML. Save it, and then copy it so it compares OK. 156 testdata.Stored.Analysis = store.saved[0].Analysis 157 // With those changes made, the expected and actual analysis 158 // should compare identically. 159 for _, diff := range deep.Equal(testdata.Stored, store.saved[0]) { 160 t.Errorf("analysis mismatch: %s", diff) 161 } 162 // The test file can specify regular expressions that should be 163 // matched by the analysis HTML (an easier way to write the test). Check 164 // those. 165 for _, restr := range testdata.AnalysisREs { 166 re, err := regexp.Compile(restr) 167 if err != nil { 168 t.Errorf("invalid RE in test: %s", err) 169 return 170 } 171 if !re.MatchString(testdata.Stored.Analysis) { 172 t.Errorf("analysis HTML does not match RE %q: %s", restr, spew.Sdump(testdata.Stored.Analysis)) 173 } 174 } 175 } 176 for i := 0; i < len(testdata.Responses) || i < len(responses); i++ { 177 if i >= len(testdata.Responses) { 178 t.Errorf("unexpected response: %s", spew.Sdump(responses[i])) 179 } else if i >= len(responses) { 180 t.Errorf("missing expected response: %s", spew.Sdump(testdata.Responses[i])) 181 } else { 182 checkResponse(t, testdata.Responses[i], responses[i]) 183 } 184 } 185 } 186 187 func checkResponse(t *testing.T, want *responseCheck, have *store.Response) { 188 // Fill in a few defaults so the test file doesn't have to specify 189 // common stuff. 190 if want.ResponseTo == "" { 191 want.ResponseTo = "TUE-100P" 192 } 193 if want.SenderBBS == "" { 194 want.SenderBBS = "W4XSC" 195 } 196 if want.SenderCall == "" { 197 want.SenderCall = "PKTTUE" 198 } 199 // If the test file didn't specify a body for the response, copy over 200 // the one we got so that they'll compare identically. 201 if want.Body == "" { 202 want.Body = have.Body 203 } 204 // Now compare the responses. 205 for _, diff := range deep.Equal(&want.Response, have) { 206 t.Errorf("response mismatch: %s", diff) 207 } 208 // The test file can also specify regular expressions that should be 209 // matched by the body (often an easier way to write the test). Check 210 // those. 211 for _, restr := range want.BodyREs { 212 re, err := regexp.Compile(restr) 213 if err != nil { 214 t.Errorf("invalid RE in test: %s", err) 215 return 216 } 217 if !re.MatchString(have.Body) { 218 t.Errorf("response body does not match RE %q: %s", restr, spew.Sdump(have)) 219 } 220 } 221 } 222 223 type fakeStore struct { 224 seenHash string 225 nextID int 226 saved []*store.Message 227 } 228 229 func (f *fakeStore) HasMessageHash(hash string) string { 230 if f.seenHash == hash { 231 return "XXX-000P" 232 } 233 return "" 234 } 235 236 func (f *fakeStore) NextMessageID(prefix string) string { 237 f.nextID++ 238 return fmt.Sprintf("%s-%03dP", prefix, f.nextID-1) 239 } 240 241 func (f *fakeStore) SaveMessage(m *store.Message) { 242 f.saved = append(f.saved, m) 243 }