github.com/vlifesystems/rulehunter@v0.0.0-20180501090014-673078aa4a83/report/report.go (about) 1 // Copyright (C) 2016-2018 vLife Systems Ltd <http://vlifesystems.com> 2 // Licensed under an MIT licence. Please see LICENSE.md for details. 3 4 package report 5 6 import ( 7 "encoding/json" 8 "errors" 9 "fmt" 10 "io/ioutil" 11 "math" 12 "os" 13 "path/filepath" 14 "sort" 15 "strings" 16 "time" 17 18 "github.com/lawrencewoodman/dexpr" 19 "github.com/lawrencewoodman/dlit" 20 rhkaggregator "github.com/vlifesystems/rhkit/aggregator" 21 rhkassessment "github.com/vlifesystems/rhkit/assessment" 22 "github.com/vlifesystems/rhkit/description" 23 "github.com/vlifesystems/rhkit/rule" 24 "github.com/vlifesystems/rulehunter/config" 25 "github.com/vlifesystems/rulehunter/internal" 26 ) 27 28 type Aggregator struct { 29 Name string `json:"name"` 30 OriginalValue string `json:"originalValue"` 31 RuleValue string `json:"ruleValue"` 32 Difference string `json:"difference"` 33 } 34 35 type Goal struct { 36 Expr string `json:"expr"` 37 OriginalPassed bool `json:"originalPassed"` 38 RulePassed bool `json:"rulePassed"` 39 } 40 41 type Assessment struct { 42 Rule string `json:"rule"` 43 Aggregators []*Aggregator `json:"aggregators"` 44 Goals []*Goal `json:"goals"` 45 } 46 47 func (a *Assessment) String() string { 48 return fmt.Sprintf("{Rule: %s, Aggregators: %v, Goals: %v}", 49 a.Rule, a.Aggregators, a.Goals) 50 } 51 52 func (g *Goal) String() string { 53 return fmt.Sprintf("{Expr: %s, OriginalPassed: %t, RulePassed: %t}", 54 g.Expr, g.OriginalPassed, g.RulePassed) 55 } 56 57 func (a *Aggregator) String() string { 58 return fmt.Sprintf( 59 "{Name: %s, OriginalValue: %s, RuleValue: %s, Difference: %s}", 60 a.Name, a.OriginalValue, a.RuleValue, a.Difference, 61 ) 62 } 63 64 type Report struct { 65 Mode ModeKind `json:"mode"` 66 Title string `json:"title"` 67 Tags []string `json:"tags"` 68 Category string `json:"category"` 69 Stamp time.Time `json:"stamp"` 70 ExperimentFilename string `json:"experimentFilename"` 71 NumRecords int64 `json:"numRecords"` 72 SortOrder []rhkassessment.SortOrder `json:"sortOrder"` 73 Aggregators []AggregatorDesc `json:"aggregators"` 74 Description *description.Description `json:"description"` 75 Assessments []*Assessment `json:"assessments"` 76 } 77 78 type AggregatorDesc struct { 79 Name string `json:"name"` 80 Kind string `json:"kind"` 81 Arg string `json:"arg"` 82 } 83 84 // Which mode was the report run in 85 type ModeKind int 86 87 const ( 88 Train ModeKind = iota 89 Test 90 ) 91 92 func (m ModeKind) String() string { 93 if m == Train { 94 return "train" 95 } 96 return "test" 97 } 98 99 func New( 100 mode ModeKind, 101 title string, 102 desc *description.Description, 103 assessment *rhkassessment.Assessment, 104 aggregators []rhkaggregator.Spec, 105 sortOrder []rhkassessment.SortOrder, 106 experimentFilename string, 107 tags []string, 108 category string, 109 ) *Report { 110 assessment.Sort(sortOrder) 111 assessment.Refine() 112 113 aggregatorDescs := make([]AggregatorDesc, len(aggregators)) 114 for i, as := range aggregators { 115 aggregatorDescs[i] = AggregatorDesc{ 116 Name: as.Name(), 117 Kind: as.Kind(), 118 Arg: as.Arg(), 119 } 120 } 121 122 return &Report{ 123 Mode: mode, 124 Title: title, 125 Tags: tags, 126 Category: category, 127 Stamp: time.Now(), 128 ExperimentFilename: experimentFilename, 129 NumRecords: assessment.NumRecords, 130 SortOrder: sortOrder, 131 Aggregators: aggregatorDescs, 132 Assessments: makeAssessments(assessment), 133 Description: desc, 134 } 135 } 136 137 func (r *Report) WriteJSON(config *config.Config) error { 138 // File mode permission: 139 // No special permission bits 140 // User: Read, Write 141 // Group: Read 142 // Other: None 143 const modePerm = 0640 144 json, err := json.Marshal(r) 145 if err != nil { 146 return err 147 } 148 buildFilename := 149 internal.MakeBuildFilename(r.Mode.String(), r.Category, r.Title) 150 reportFilename := filepath.Join(config.BuildDir, "reports", buildFilename) 151 return ioutil.WriteFile(reportFilename, json, modePerm) 152 } 153 154 // LoadJSON loads a report from the specified reportFilename in 155 // reports directory of cfg.BuildDir. Following the reportFilename you 156 // can specify an optional number of times to try to decode the JSON file. 157 // This can be useful in situations where you might try to read the 158 // JSON file before it has been completely written. 159 func LoadJSON( 160 cfg *config.Config, 161 reportFilename string, 162 args ...int, 163 ) (*Report, error) { 164 const sleep = 200 * time.Millisecond 165 var report Report 166 filename := filepath.Join(cfg.BuildDir, "reports", reportFilename) 167 168 maxTries := 1 169 if len(args) == 1 { 170 maxTries = args[0] 171 } else if len(args) > 1 { 172 panic("too many arguments for function") 173 } 174 for tries := 1; ; tries++ { 175 f, err := os.Open(filename) 176 if err != nil { 177 return nil, err 178 } 179 defer f.Close() 180 181 dec := json.NewDecoder(f) 182 if err = dec.Decode(&report); err != nil { 183 if tries > maxTries { 184 return nil, fmt.Errorf("can't decode JSON file: %s, %s", filename, err) 185 } else { 186 time.Sleep(sleep) 187 } 188 } else { 189 break 190 } 191 } 192 return &report, nil 193 } 194 195 func makeAssessments(assessment *rhkassessment.Assessment) []*Assessment { 196 trueRuleAssessment, err := getTrueRuleAssessment(assessment) 197 if err != nil { 198 panic(err) 199 } 200 201 trueAggregators := trueRuleAssessment.Aggregators 202 trueGoals := trueRuleAssessment.Goals 203 assessments := make([]*Assessment, len(assessment.RuleAssessments)) 204 for i, ruleAssessment := range assessment.RuleAssessments { 205 assessments[i] = &Assessment{ 206 Rule: ruleAssessment.Rule.String(), 207 Aggregators: makeAggregators( 208 trueAggregators, 209 ruleAssessment.Aggregators, 210 ), 211 Goals: makeGoals(trueGoals, ruleAssessment.Goals), 212 } 213 } 214 return assessments 215 } 216 217 func makeAggregators( 218 trueAggregators map[string]*dlit.Literal, 219 ruleAggregators map[string]*dlit.Literal, 220 ) []*Aggregator { 221 aggregatorNames := getSortedAggregatorNames(ruleAggregators) 222 aggregators := make([]*Aggregator, len(ruleAggregators)) 223 for j, aggregatorName := range aggregatorNames { 224 aggregator := ruleAggregators[aggregatorName] 225 difference := 226 calcTrueAggregatorDiff(trueAggregators, aggregatorName, aggregator) 227 aggregators[j] = &Aggregator{ 228 Name: aggregatorName, 229 OriginalValue: trueAggregators[aggregatorName].String(), 230 RuleValue: aggregator.String(), 231 Difference: difference, 232 } 233 } 234 return aggregators 235 } 236 237 func makeGoals( 238 trueGoals []*rhkassessment.GoalAssessment, 239 ruleGoals []*rhkassessment.GoalAssessment, 240 ) []*Goal { 241 goals := make([]*Goal, len(ruleGoals)) 242 for i, g := range ruleGoals { 243 goals[i] = &Goal{ 244 Expr: g.Expr, 245 OriginalPassed: trueGoals[i].Passed, 246 RulePassed: g.Passed, 247 } 248 } 249 return goals 250 } 251 252 func getSortedAggregatorNames(aggregators map[string]*dlit.Literal) []string { 253 aggregatorNames := make([]string, len(aggregators)) 254 j := 0 255 for aggregatorName, _ := range aggregators { 256 aggregatorNames[j] = aggregatorName 257 j++ 258 } 259 sort.Strings(aggregatorNames) 260 return aggregatorNames 261 } 262 263 func getTrueRuleAssessment( 264 assessment *rhkassessment.Assessment, 265 ) (*rhkassessment.RuleAssessment, error) { 266 for _, ra := range assessment.RuleAssessments { 267 if _, isTrueRule := ra.Rule.(rule.True); isTrueRule { 268 return ra, nil 269 } 270 } 271 272 return nil, errors.New("can't find true() rule") 273 } 274 275 func numDecPlaces(s string) int { 276 i := strings.IndexByte(s, '.') 277 if i > -1 { 278 s = strings.TrimRight(s, "0") 279 return len(s) - i - 1 280 } 281 return 0 282 } 283 284 type CantConvertToTypeError struct { 285 Kind string 286 Value *dlit.Literal 287 } 288 289 func (e CantConvertToTypeError) Error() string { 290 return fmt.Sprintf("can't convert to %s: %s", e.Kind, e.Value) 291 } 292 293 // roundTo returns a number n, rounded to a number of decimal places dp. 294 // This uses round half-up to tie-break 295 func roundTo(n *dlit.Literal, dp int) (*dlit.Literal, error) { 296 297 if _, isInt := n.Int(); isInt { 298 return n, nil 299 } 300 301 x, isFloat := n.Float() 302 if !isFloat { 303 if err := n.Err(); err != nil { 304 return n, err 305 } 306 err := CantConvertToTypeError{Kind: "float", Value: n} 307 r := dlit.MustNew(err) 308 return r, err 309 } 310 311 // Prevent rounding errors where too high dp is used 312 xNumDP := numDecPlaces(n.String()) 313 if dp > xNumDP { 314 dp = xNumDP 315 } 316 shift := math.Pow(10, float64(dp)) 317 return dlit.New(math.Floor(.5+x*shift) / shift) 318 } 319 320 func calcTrueAggregatorDiff( 321 trueAggregators map[string]*dlit.Literal, 322 aggregatorName string, 323 aggregatorValue *dlit.Literal, 324 ) string { 325 funcs := map[string]dexpr.CallFun{} 326 maxDP := numDecPlaces(aggregatorValue.String()) 327 trueAggregatorValueDP := numDecPlaces(trueAggregators[aggregatorName].String()) 328 if trueAggregatorValueDP > maxDP { 329 maxDP = trueAggregatorValueDP 330 } 331 diffExpr := dexpr.MustNew("r - t", funcs) 332 vars := map[string]*dlit.Literal{ 333 "r": aggregatorValue, 334 "t": trueAggregators[aggregatorName], 335 } 336 differenceL := diffExpr.Eval(vars) 337 if err := differenceL.Err(); err != nil { 338 return "N/A" 339 } 340 roundedDifferenceL, err := roundTo(differenceL, maxDP) 341 if err != nil { 342 return "N/A" 343 } 344 return roundedDifferenceL.String() 345 }