github.com/vlifesystems/rulehunter@v0.0.0-20180501090014-673078aa4a83/report/report_test.go (about)

     1  package report
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"math"
     8  	"os"
     9  	"path/filepath"
    10  	"reflect"
    11  	"syscall"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/lawrencewoodman/dlit"
    16  	"github.com/vlifesystems/rhkit/aggregator"
    17  	rhkassessment "github.com/vlifesystems/rhkit/assessment"
    18  	"github.com/vlifesystems/rhkit/description"
    19  	"github.com/vlifesystems/rhkit/goal"
    20  	"github.com/vlifesystems/rhkit/rule"
    21  	"github.com/vlifesystems/rulehunter/config"
    22  	"github.com/vlifesystems/rulehunter/internal"
    23  	"github.com/vlifesystems/rulehunter/internal/testhelpers"
    24  )
    25  
    26  var testDescription = &description.Description{
    27  	map[string]*description.Field{
    28  		"month": {description.String, nil, nil, 0,
    29  			map[string]description.Value{
    30  				"feb":  {dlit.MustNew("feb"), 3},
    31  				"may":  {dlit.MustNew("may"), 2},
    32  				"june": {dlit.MustNew("june"), 9},
    33  			},
    34  			3,
    35  		},
    36  		"rate": {
    37  			description.Number,
    38  			dlit.MustNew(0.3),
    39  			dlit.MustNew(15.1),
    40  			3,
    41  			map[string]description.Value{
    42  				"0.3":   {dlit.MustNew(0.3), 7},
    43  				"7":     {dlit.MustNew(7), 2},
    44  				"7.3":   {dlit.MustNew(7.3), 9},
    45  				"9.278": {dlit.MustNew(9.278), 4},
    46  			},
    47  			4,
    48  		},
    49  		"method": {description.Ignore, nil, nil, 0,
    50  			map[string]description.Value{}, -1},
    51  	},
    52  }
    53  
    54  func TestNew(t *testing.T) {
    55  	aggregatorSpecs := []aggregator.Spec{
    56  		aggregator.MustNew("numMatches", "count", "true()"),
    57  		aggregator.MustNew(
    58  			"percentMatches",
    59  			"calc",
    60  			"roundto(100.0 * numMatches / numRecords, 2)",
    61  		),
    62  		aggregator.MustNew("numIncomeGt2", "count", "income > 2"),
    63  		aggregator.MustNew("goalsScore", "goalsscore"),
    64  	}
    65  	goals := []*goal.Goal{
    66  		goal.MustNew("numIncomeGt2 == 1"),
    67  		goal.MustNew("numIncomeGt2 == 2"),
    68  	}
    69  
    70  	assessment := rhkassessment.New(aggregatorSpecs, goals)
    71  	assessment.RuleAssessments = []*rhkassessment.RuleAssessment{
    72  		&rhkassessment.RuleAssessment{
    73  			Rule: rule.NewEQFV("month", dlit.NewString("may")),
    74  			Aggregators: map[string]*dlit.Literal{
    75  				"numMatches":     dlit.MustNew("2142"),
    76  				"percentMatches": dlit.MustNew("242"),
    77  				"numIncomeGt2":   dlit.MustNew("22"),
    78  				"goalsScore":     dlit.MustNew(20.1),
    79  			},
    80  			Goals: []*rhkassessment.GoalAssessment{
    81  				&rhkassessment.GoalAssessment{"numIncomeGt2 == 1", false},
    82  				&rhkassessment.GoalAssessment{"numIncomeGt2 == 2", false},
    83  			},
    84  		},
    85  		&rhkassessment.RuleAssessment{
    86  			Rule: rule.NewGEFV("rate", dlit.MustNew(789.2)),
    87  			Aggregators: map[string]*dlit.Literal{
    88  				"numMatches":     dlit.MustNew("3142"),
    89  				"percentMatches": dlit.MustNew("342"),
    90  				"numIncomeGt2":   dlit.MustNew("32"),
    91  				"goalsScore":     dlit.MustNew(30.1),
    92  			},
    93  			Goals: []*rhkassessment.GoalAssessment{
    94  				&rhkassessment.GoalAssessment{"numIncomeGt2 == 1", false},
    95  				&rhkassessment.GoalAssessment{"numIncomeGt2 == 2", false},
    96  			},
    97  		},
    98  		&rhkassessment.RuleAssessment{
    99  			Rule: rule.NewTrue(),
   100  			Aggregators: map[string]*dlit.Literal{
   101  				"numMatches":     dlit.MustNew("142"),
   102  				"percentMatches": dlit.MustNew("42"),
   103  				"numIncomeGt2":   dlit.MustNew("2"),
   104  				"goalsScore":     dlit.MustNew(0.1),
   105  			},
   106  			Goals: []*rhkassessment.GoalAssessment{
   107  				&rhkassessment.GoalAssessment{"numIncomeGt2 == 1", false},
   108  				&rhkassessment.GoalAssessment{"numIncomeGt2 == 2", true},
   109  			},
   110  		},
   111  	}
   112  
   113  	wantReport := &Report{
   114  		Mode:               Train,
   115  		Title:              "some title",
   116  		Tags:               []string{"bank", "test / fred"},
   117  		Category:           "testing",
   118  		Stamp:              time.Now(),
   119  		ExperimentFilename: "somename.yaml",
   120  		NumRecords:         assessment.NumRecords,
   121  		SortOrder: []rhkassessment.SortOrder{
   122  			rhkassessment.SortOrder{
   123  				Aggregator: "goalsScore",
   124  				Direction:  rhkassessment.DESCENDING,
   125  			},
   126  			rhkassessment.SortOrder{
   127  				Aggregator: "percentMatches",
   128  				Direction:  rhkassessment.ASCENDING,
   129  			},
   130  		},
   131  		Aggregators: []AggregatorDesc{
   132  			AggregatorDesc{Name: "numMatches", Kind: "count", Arg: "true()"},
   133  			AggregatorDesc{
   134  				Name: "percentMatches",
   135  				Kind: "calc",
   136  				Arg:  "roundto(100.0 * numMatches / numRecords, 2)",
   137  			},
   138  			AggregatorDesc{Name: "numIncomeGt2", Kind: "count", Arg: "income > 2"},
   139  			AggregatorDesc{Name: "goalsScore", Kind: "goalsscore", Arg: ""},
   140  		},
   141  		Description: testDescription,
   142  		Assessments: []*Assessment{
   143  			&Assessment{
   144  				Rule: "rate >= 789.2",
   145  				Aggregators: []*Aggregator{
   146  					&Aggregator{
   147  						Name:          "goalsScore",
   148  						OriginalValue: "0.1",
   149  						RuleValue:     "30.1",
   150  						Difference:    "30",
   151  					},
   152  					&Aggregator{
   153  						Name:          "numIncomeGt2",
   154  						OriginalValue: "2",
   155  						RuleValue:     "32",
   156  						Difference:    "30",
   157  					},
   158  					&Aggregator{
   159  						Name:          "numMatches",
   160  						OriginalValue: "142",
   161  						RuleValue:     "3142",
   162  						Difference:    "3000",
   163  					},
   164  					&Aggregator{
   165  						Name:          "percentMatches",
   166  						OriginalValue: "42",
   167  						RuleValue:     "342",
   168  						Difference:    "300",
   169  					},
   170  				},
   171  				Goals: []*Goal{
   172  					&Goal{
   173  						Expr:           "numIncomeGt2 == 1",
   174  						OriginalPassed: false,
   175  						RulePassed:     false,
   176  					},
   177  					&Goal{
   178  						Expr:           "numIncomeGt2 == 2",
   179  						OriginalPassed: true,
   180  						RulePassed:     false,
   181  					},
   182  				},
   183  			},
   184  			&Assessment{
   185  				Rule: "month == \"may\"",
   186  				Aggregators: []*Aggregator{
   187  					&Aggregator{
   188  						Name:          "goalsScore",
   189  						OriginalValue: "0.1",
   190  						RuleValue:     "20.1",
   191  						Difference:    "20",
   192  					},
   193  					&Aggregator{
   194  						Name:          "numIncomeGt2",
   195  						OriginalValue: "2",
   196  						RuleValue:     "22",
   197  						Difference:    "20",
   198  					},
   199  					&Aggregator{
   200  						Name:          "numMatches",
   201  						OriginalValue: "142",
   202  						RuleValue:     "2142",
   203  						Difference:    "2000",
   204  					},
   205  					&Aggregator{
   206  						Name:          "percentMatches",
   207  						OriginalValue: "42",
   208  						RuleValue:     "242",
   209  						Difference:    "200",
   210  					},
   211  				},
   212  				Goals: []*Goal{
   213  					&Goal{
   214  						Expr:           "numIncomeGt2 == 1",
   215  						OriginalPassed: false,
   216  						RulePassed:     false,
   217  					},
   218  					&Goal{
   219  						Expr:           "numIncomeGt2 == 2",
   220  						OriginalPassed: true,
   221  						RulePassed:     false,
   222  					},
   223  				},
   224  			},
   225  			&Assessment{
   226  				Rule: "true()",
   227  				Aggregators: []*Aggregator{
   228  					&Aggregator{
   229  						Name:          "goalsScore",
   230  						OriginalValue: "0.1",
   231  						RuleValue:     "0.1",
   232  						Difference:    "0",
   233  					},
   234  					&Aggregator{
   235  						Name:          "numIncomeGt2",
   236  						OriginalValue: "2",
   237  						RuleValue:     "2",
   238  						Difference:    "0",
   239  					},
   240  					&Aggregator{
   241  						Name:          "numMatches",
   242  						OriginalValue: "142",
   243  						RuleValue:     "142",
   244  						Difference:    "0",
   245  					},
   246  					&Aggregator{
   247  						Name:          "percentMatches",
   248  						OriginalValue: "42",
   249  						RuleValue:     "42",
   250  						Difference:    "0",
   251  					},
   252  				},
   253  				Goals: []*Goal{
   254  					&Goal{
   255  						Expr:           "numIncomeGt2 == 1",
   256  						OriginalPassed: false,
   257  						RulePassed:     false,
   258  					},
   259  					&Goal{
   260  						Expr:           "numIncomeGt2 == 2",
   261  						OriginalPassed: true,
   262  						RulePassed:     true,
   263  					},
   264  				},
   265  			},
   266  		},
   267  	}
   268  	got := New(
   269  		wantReport.Mode,
   270  		wantReport.Title,
   271  		wantReport.Description,
   272  		assessment,
   273  		aggregatorSpecs,
   274  		wantReport.SortOrder,
   275  		wantReport.ExperimentFilename,
   276  		wantReport.Tags,
   277  		wantReport.Category,
   278  	)
   279  	if err := checkReportsMatch(got, wantReport); err != nil {
   280  		t.Errorf("New: %s", err)
   281  	}
   282  }
   283  
   284  func TestNew_single_true_rule(t *testing.T) {
   285  	aggregatorSpecs := []aggregator.Spec{
   286  		aggregator.MustNew("numMatches", "count", "true()"),
   287  		aggregator.MustNew(
   288  			"percentMatches",
   289  			"calc",
   290  			"roundto(100.0 * numMatches / numRecords, 2)",
   291  		),
   292  		aggregator.MustNew("numIncomeGt2", "count", "income > 2"),
   293  		aggregator.MustNew("goalsScore", "goalsscore"),
   294  	}
   295  	goals := []*goal.Goal{
   296  		goal.MustNew("numIncomeGt2 == 1"),
   297  		goal.MustNew("numIncomeGt2 == 2"),
   298  	}
   299  
   300  	assessment := rhkassessment.New(aggregatorSpecs, goals)
   301  	assessment.RuleAssessments = []*rhkassessment.RuleAssessment{
   302  		&rhkassessment.RuleAssessment{
   303  			Rule: rule.NewTrue(),
   304  			Aggregators: map[string]*dlit.Literal{
   305  				"numMatches":     dlit.MustNew("142"),
   306  				"percentMatches": dlit.MustNew("42"),
   307  				"numIncomeGt2":   dlit.MustNew("2"),
   308  				"goalsScore":     dlit.MustNew(0.1),
   309  			},
   310  			Goals: []*rhkassessment.GoalAssessment{
   311  				&rhkassessment.GoalAssessment{"numIncomeGt2 == 1", false},
   312  				&rhkassessment.GoalAssessment{"numIncomeGt2 == 2", true},
   313  			},
   314  		},
   315  	}
   316  
   317  	wantReport := &Report{
   318  		Mode:               Train,
   319  		Title:              "some title",
   320  		Tags:               []string{"bank", "test / fred"},
   321  		Category:           "testing",
   322  		Stamp:              time.Now(),
   323  		ExperimentFilename: "somename.yaml",
   324  		NumRecords:         assessment.NumRecords,
   325  		SortOrder: []rhkassessment.SortOrder{
   326  			rhkassessment.SortOrder{
   327  				Aggregator: "goalsScore",
   328  				Direction:  rhkassessment.DESCENDING,
   329  			},
   330  			rhkassessment.SortOrder{
   331  				Aggregator: "percentMatches",
   332  				Direction:  rhkassessment.ASCENDING,
   333  			},
   334  		},
   335  		Aggregators: []AggregatorDesc{
   336  			AggregatorDesc{Name: "numMatches", Kind: "count", Arg: "true()"},
   337  			AggregatorDesc{
   338  				Name: "percentMatches",
   339  				Kind: "calc",
   340  				Arg:  "roundto(100.0 * numMatches / numRecords, 2)",
   341  			},
   342  			AggregatorDesc{Name: "numIncomeGt2", Kind: "count", Arg: "income > 2"},
   343  			AggregatorDesc{Name: "goalsScore", Kind: "goalsscore", Arg: ""},
   344  		},
   345  		Description: testDescription,
   346  		Assessments: []*Assessment{
   347  			&Assessment{
   348  				Rule: "true()",
   349  				Aggregators: []*Aggregator{
   350  					&Aggregator{
   351  						Name:          "goalsScore",
   352  						OriginalValue: "0.1",
   353  						RuleValue:     "0.1",
   354  						Difference:    "0",
   355  					},
   356  					&Aggregator{
   357  						Name:          "numIncomeGt2",
   358  						OriginalValue: "2",
   359  						RuleValue:     "2",
   360  						Difference:    "0",
   361  					},
   362  					&Aggregator{
   363  						Name:          "numMatches",
   364  						OriginalValue: "142",
   365  						RuleValue:     "142",
   366  						Difference:    "0",
   367  					},
   368  					&Aggregator{
   369  						Name:          "percentMatches",
   370  						OriginalValue: "42",
   371  						RuleValue:     "42",
   372  						Difference:    "0",
   373  					},
   374  				},
   375  				Goals: []*Goal{
   376  					&Goal{
   377  						Expr:           "numIncomeGt2 == 1",
   378  						OriginalPassed: false,
   379  						RulePassed:     false,
   380  					},
   381  					&Goal{
   382  						Expr:           "numIncomeGt2 == 2",
   383  						OriginalPassed: true,
   384  						RulePassed:     true,
   385  					},
   386  				},
   387  			},
   388  		},
   389  	}
   390  	got := New(
   391  		wantReport.Mode,
   392  		wantReport.Title,
   393  		wantReport.Description,
   394  		assessment,
   395  		aggregatorSpecs,
   396  		wantReport.SortOrder,
   397  		wantReport.ExperimentFilename,
   398  		wantReport.Tags,
   399  		wantReport.Category,
   400  	)
   401  	if err := checkReportsMatch(got, wantReport); err != nil {
   402  		t.Errorf("New: %s", err)
   403  	}
   404  }
   405  func TestLoadJSON_errors(t *testing.T) {
   406  	// File mode permission used as standard:
   407  	// No special permission bits
   408  	// User: Read, Write Execute
   409  	// Group: Read
   410  	// Other: None
   411  	const modePerm = 0740
   412  	tmpDir := testhelpers.TempDir(t)
   413  	defer os.RemoveAll(tmpDir)
   414  	cfg := &config.Config{BuildDir: tmpDir}
   415  	reportsDir := filepath.Join(tmpDir, "reports")
   416  	if err := os.MkdirAll(reportsDir, modePerm); err != nil {
   417  		t.Fatalf("MkdirAll: %s", err)
   418  	}
   419  	testhelpers.CopyFile(t, filepath.Join("fixtures", "empty.json"), reportsDir)
   420  
   421  	cases := []struct {
   422  		filename string
   423  		wantErr  error
   424  	}{
   425  		{filename: "nonexistent.json",
   426  			wantErr: &os.PathError{
   427  				Op:   "open",
   428  				Path: filepath.Join(reportsDir, "nonexistent.json"),
   429  				Err:  syscall.ENOENT,
   430  			},
   431  		},
   432  		{filename: "empty.json",
   433  			wantErr: fmt.Errorf(
   434  				"can't decode JSON file: %s, %s",
   435  				filepath.Join(reportsDir, "empty.json"),
   436  				io.EOF),
   437  		},
   438  	}
   439  	for _, c := range cases {
   440  		got, err := LoadJSON(cfg, c.filename)
   441  		if got != nil {
   442  			t.Errorf("LoadJSON: got: %v, want: nil", got)
   443  		}
   444  		if err == nil || err.Error() != c.wantErr.Error() {
   445  			t.Errorf("LoadJSON: gotErr: %s, wantErr: %s", err, c.wantErr)
   446  		}
   447  	}
   448  }
   449  
   450  func TestWriteLoadJSON(t *testing.T) {
   451  	// File mode permission used as standard:
   452  	// No special permission bits
   453  	// User: Read, Write Execute
   454  	// Group: Read
   455  	// Other: None
   456  	const modePerm = 0740
   457  
   458  	tmpDir := testhelpers.TempDir(t)
   459  	defer os.RemoveAll(tmpDir)
   460  	reportsDir := filepath.Join(tmpDir, "reports")
   461  	if err := os.MkdirAll(reportsDir, modePerm); err != nil {
   462  		t.Fatalf("MkdirAll: %s", err)
   463  	}
   464  	goals := []*goal.Goal{
   465  		goal.MustNew("numIncomeGt2 == 1"),
   466  		goal.MustNew("numIncomeGt2 == 2"),
   467  	}
   468  	aggregators := []aggregator.Spec{
   469  		aggregator.MustNew("numMatches", "count", "true()"),
   470  		aggregator.MustNew(
   471  			"percentMatches",
   472  			"calc",
   473  			"roundto(100.0 * numMatches / numRecords, 2)",
   474  		),
   475  		aggregator.MustNew("numIncomeGt2", "count", "income > 2"),
   476  		aggregator.MustNew("goalsScore", "goalsscore"),
   477  	}
   478  	assessment := rhkassessment.New(aggregators, goals)
   479  	assessment.RuleAssessments = []*rhkassessment.RuleAssessment{
   480  		&rhkassessment.RuleAssessment{
   481  			Rule: rule.NewEQFV("month", dlit.NewString("may")),
   482  			Aggregators: map[string]*dlit.Literal{
   483  				"numMatches":     dlit.MustNew("2142"),
   484  				"percentMatches": dlit.MustNew("242"),
   485  				"numIncomeGt2":   dlit.MustNew("22"),
   486  				"goalsScore":     dlit.MustNew(20.1),
   487  			},
   488  			Goals: []*rhkassessment.GoalAssessment{
   489  				&rhkassessment.GoalAssessment{"numIncomeGt2 == 1", false},
   490  				&rhkassessment.GoalAssessment{"numIncomeGt2 == 2", true},
   491  			},
   492  		},
   493  		&rhkassessment.RuleAssessment{
   494  			Rule: rule.NewGEFV("rate", dlit.MustNew(789.2)),
   495  			Aggregators: map[string]*dlit.Literal{
   496  				"numMatches":     dlit.MustNew("3142"),
   497  				"percentMatches": dlit.MustNew("342"),
   498  				"numIncomeGt2":   dlit.MustNew("32"),
   499  				"goalsScore":     dlit.MustNew(30.1),
   500  			},
   501  			Goals: []*rhkassessment.GoalAssessment{
   502  				&rhkassessment.GoalAssessment{"numIncomeGt2 == 1", false},
   503  				&rhkassessment.GoalAssessment{"numIncomeGt2 == 2", true},
   504  			},
   505  		},
   506  		&rhkassessment.RuleAssessment{
   507  			Rule: rule.NewTrue(),
   508  			Aggregators: map[string]*dlit.Literal{
   509  				"numMatches":     dlit.MustNew("142"),
   510  				"percentMatches": dlit.MustNew("42"),
   511  				"numIncomeGt2":   dlit.MustNew("2"),
   512  				"goalsScore":     dlit.MustNew(0.1),
   513  			},
   514  			Goals: []*rhkassessment.GoalAssessment{
   515  				&rhkassessment.GoalAssessment{"numIncomeGt2 == 1", false},
   516  				&rhkassessment.GoalAssessment{"numIncomeGt2 == 2", true},
   517  			},
   518  		},
   519  	}
   520  
   521  	title := "some title"
   522  	sortOrder := []rhkassessment.SortOrder{
   523  		rhkassessment.SortOrder{
   524  			Aggregator: "goalsScore",
   525  			Direction:  rhkassessment.DESCENDING,
   526  		},
   527  		rhkassessment.SortOrder{
   528  			Aggregator: "percentMatches",
   529  			Direction:  rhkassessment.ASCENDING,
   530  		},
   531  	}
   532  	experimentFilename := "somename.yaml"
   533  	tags := []string{"bank", "test / fred"}
   534  	category := "testing"
   535  	config := &config.Config{BuildDir: tmpDir}
   536  	report := New(
   537  		Train,
   538  		title,
   539  		testDescription,
   540  		assessment,
   541  		aggregators,
   542  		sortOrder,
   543  		experimentFilename,
   544  		tags,
   545  		category,
   546  	)
   547  
   548  	if err := report.WriteJSON(config); err != nil {
   549  		t.Fatalf("WriteJSON: %s", err)
   550  	}
   551  	buildFilename := internal.MakeBuildFilename(Train.String(), category, title)
   552  	loadedReport, err := LoadJSON(config, buildFilename)
   553  	if err != nil {
   554  		t.Fatalf("LoadJSON: %s", err)
   555  	}
   556  	if err := checkReportsMatch(report, loadedReport); err != nil {
   557  		t.Errorf("Reports don't match: %s", err)
   558  	}
   559  }
   560  
   561  func TestCalcTrueAggregatorDiff(t *testing.T) {
   562  	trueAggregators := map[string]*dlit.Literal{
   563  		"numMatches": dlit.MustNew(176),
   564  		"profit":     dlit.MustNew(23),
   565  		"bigNum":     dlit.MustNew(int64(math.MaxInt64)),
   566  		"flow":       dlit.NewString("11.22"),
   567  	}
   568  	cases := []struct {
   569  		name  string
   570  		value *dlit.Literal
   571  		want  string
   572  	}{
   573  		{name: "numMatches", value: dlit.MustNew(192), want: "16"},
   574  		{name: "numMatches", value: dlit.MustNew(165), want: "-11"},
   575  		{name: "flow", value: dlit.NewString("16.45"), want: "5.23"},
   576  		{name: "bigNum",
   577  			value: dlit.MustNew(int64(math.MinInt64)),
   578  			want: dlit.MustNew(
   579  				float64(math.MinInt64) - float64(math.MaxInt64),
   580  			).String(),
   581  		},
   582  		{name: "bigNum",
   583  			value: dlit.MustNew(errors.New("some error")),
   584  			want:  "N/A",
   585  		},
   586  	}
   587  
   588  	for _, c := range cases {
   589  		got := calcTrueAggregatorDiff(trueAggregators, c.name, c.value)
   590  		if got != c.want {
   591  			t.Errorf("calcTrueAggregatorDifference(trueAggregators, %v, %v) got: %s, want: %s",
   592  				c.name, c.value, got, c.want)
   593  		}
   594  	}
   595  }
   596  
   597  func TestLoadJSON_multiple_attempts(t *testing.T) {
   598  	// File mode permission used as standard:
   599  	// No special permission bits
   600  	// User: Read, Write Execute
   601  	// Group: Read
   602  	// Other: None
   603  	const modePerm = 0740
   604  
   605  	tmpDir := testhelpers.TempDir(t)
   606  	defer os.RemoveAll(tmpDir)
   607  	reportsDir := filepath.Join(tmpDir, "reports")
   608  	if err := os.MkdirAll(reportsDir, modePerm); err != nil {
   609  		t.Fatalf("MkdirAll: %s", err)
   610  	}
   611  	aggregators := []aggregator.Spec{
   612  		aggregator.MustNew("numMatches", "count", "true()"),
   613  		aggregator.MustNew(
   614  			"percentMatches",
   615  			"calc",
   616  			"roundto(100.0 * numMatches / numRecords, 2)",
   617  		),
   618  		aggregator.MustNew("numIncomeGt2", "count", "income > 2"),
   619  		aggregator.MustNew("goalsScore", "goalsscore"),
   620  	}
   621  	goals := []*goal.Goal{
   622  		goal.MustNew("numIncomeGt2 == 1"),
   623  		goal.MustNew("numIncomeGt2 == 2"),
   624  	}
   625  	assessment := rhkassessment.New(aggregators, goals)
   626  	assessment.RuleAssessments = []*rhkassessment.RuleAssessment{
   627  		&rhkassessment.RuleAssessment{
   628  			Rule: rule.NewEQFV("month", dlit.NewString("may")),
   629  			Aggregators: map[string]*dlit.Literal{
   630  				"numMatches":     dlit.MustNew("2142"),
   631  				"percentMatches": dlit.MustNew("242"),
   632  				"numIncomeGt2":   dlit.MustNew("22"),
   633  				"goalsScore":     dlit.MustNew(20.1),
   634  			},
   635  			Goals: []*rhkassessment.GoalAssessment{
   636  				&rhkassessment.GoalAssessment{"numIncomeGt2 == 1", false},
   637  				&rhkassessment.GoalAssessment{"numIncomeGt2 == 2", true},
   638  			},
   639  		},
   640  		&rhkassessment.RuleAssessment{
   641  			Rule: rule.NewGEFV("rate", dlit.MustNew(789.2)),
   642  			Aggregators: map[string]*dlit.Literal{
   643  				"numMatches":     dlit.MustNew("3142"),
   644  				"percentMatches": dlit.MustNew("342"),
   645  				"numIncomeGt2":   dlit.MustNew("32"),
   646  				"goalsScore":     dlit.MustNew(30.1),
   647  			},
   648  			Goals: []*rhkassessment.GoalAssessment{
   649  				&rhkassessment.GoalAssessment{"numIncomeGt2 == 1", false},
   650  				&rhkassessment.GoalAssessment{"numIncomeGt2 == 2", true},
   651  			},
   652  		},
   653  		&rhkassessment.RuleAssessment{
   654  			Rule: rule.NewTrue(),
   655  			Aggregators: map[string]*dlit.Literal{
   656  				"numMatches":     dlit.MustNew("142"),
   657  				"percentMatches": dlit.MustNew("42"),
   658  				"numIncomeGt2":   dlit.MustNew("2"),
   659  				"goalsScore":     dlit.MustNew(0.1),
   660  			},
   661  			Goals: []*rhkassessment.GoalAssessment{
   662  				&rhkassessment.GoalAssessment{"numIncomeGt2 == 1", false},
   663  				&rhkassessment.GoalAssessment{"numIncomeGt2 == 2", true},
   664  			},
   665  		},
   666  	}
   667  
   668  	title := "some title"
   669  	sortOrder := []rhkassessment.SortOrder{
   670  		rhkassessment.SortOrder{
   671  			Aggregator: "goalsScore",
   672  			Direction:  rhkassessment.DESCENDING,
   673  		},
   674  		rhkassessment.SortOrder{
   675  			Aggregator: "percentMatches",
   676  			Direction:  rhkassessment.ASCENDING,
   677  		},
   678  	}
   679  	experimentFilename := "somename.yaml"
   680  	tags := []string{"bank", "test / fred"}
   681  	category := "testing"
   682  	config := &config.Config{BuildDir: tmpDir}
   683  	report := New(
   684  		Train,
   685  		title,
   686  		testDescription,
   687  		assessment,
   688  		aggregators,
   689  		sortOrder,
   690  		experimentFilename,
   691  		tags,
   692  		category,
   693  	)
   694  
   695  	buildFilename := internal.MakeBuildFilename(Train.String(), category, title)
   696  	testhelpers.CopyFile(
   697  		t,
   698  		filepath.Join("fixtures", "empty.json"),
   699  		reportsDir,
   700  		buildFilename,
   701  	)
   702  	go func() {
   703  		time.Sleep(500 * time.Millisecond)
   704  		if err := report.WriteJSON(config); err != nil {
   705  			t.Fatalf("WriteJSON: %s", err)
   706  		}
   707  	}()
   708  	maxLoadAttempts := 5
   709  	loadedReport, err := LoadJSON(config, buildFilename, maxLoadAttempts)
   710  	if err != nil {
   711  		t.Fatalf("LoadJSON: %s", err)
   712  	}
   713  	if err := checkReportsMatch(report, loadedReport); err != nil {
   714  		t.Errorf("Reports don't match: %s", err)
   715  	}
   716  }
   717  
   718  func TestGetSortedAggregatorNames(t *testing.T) {
   719  	aggregators := map[string]*dlit.Literal{
   720  		"numMatches": dlit.MustNew(176),
   721  		"profit":     dlit.MustNew(23),
   722  		"bigNum":     dlit.MustNew(int64(math.MaxInt64)),
   723  	}
   724  	want := []string{"bigNum", "numMatches", "profit"}
   725  	got := getSortedAggregatorNames(aggregators)
   726  	if !reflect.DeepEqual(got, want) {
   727  		t.Errorf("getSortedAggregatorNames - got: %v, want: %v", got, want)
   728  	}
   729  }
   730  
   731  func TestGetTrueRuleAssessment(t *testing.T) {
   732  	assessment := &rhkassessment.Assessment{
   733  		NumRecords: 20,
   734  		RuleAssessments: []*rhkassessment.RuleAssessment{
   735  			&rhkassessment.RuleAssessment{
   736  				Rule: rule.NewEQFV("month", dlit.NewString("may")),
   737  				Aggregators: map[string]*dlit.Literal{
   738  					"numMatches":     dlit.MustNew("2142"),
   739  					"percentMatches": dlit.MustNew("242"),
   740  					"numIncomeGt2":   dlit.MustNew("22"),
   741  					"goalsScore":     dlit.MustNew(20.1),
   742  				},
   743  				Goals: []*rhkassessment.GoalAssessment{
   744  					&rhkassessment.GoalAssessment{"numIncomeGt2 == 1", false},
   745  					&rhkassessment.GoalAssessment{"numIncomeGt2 == 2", true},
   746  				},
   747  			},
   748  			&rhkassessment.RuleAssessment{
   749  				Rule: rule.NewGEFV("rate", dlit.MustNew(789.2)),
   750  				Aggregators: map[string]*dlit.Literal{
   751  					"numMatches":     dlit.MustNew("3142"),
   752  					"percentMatches": dlit.MustNew("342"),
   753  					"numIncomeGt2":   dlit.MustNew("32"),
   754  					"goalsScore":     dlit.MustNew(30.1),
   755  				},
   756  				Goals: []*rhkassessment.GoalAssessment{
   757  					&rhkassessment.GoalAssessment{"numIncomeGt2 == 1", false},
   758  					&rhkassessment.GoalAssessment{"numIncomeGt2 == 2", true},
   759  				},
   760  			},
   761  			&rhkassessment.RuleAssessment{
   762  				Rule: rule.NewTrue(),
   763  				Aggregators: map[string]*dlit.Literal{
   764  					"numMatches":     dlit.MustNew("142"),
   765  					"percentMatches": dlit.MustNew("42"),
   766  					"numIncomeGt2":   dlit.MustNew("2"),
   767  					"goalsScore":     dlit.MustNew(0.1),
   768  				},
   769  				Goals: []*rhkassessment.GoalAssessment{
   770  					&rhkassessment.GoalAssessment{"numIncomeGt2 == 1", false},
   771  					&rhkassessment.GoalAssessment{"numIncomeGt2 == 2", true},
   772  				},
   773  			},
   774  		},
   775  	}
   776  	want := &rhkassessment.RuleAssessment{
   777  		Rule: rule.NewTrue(),
   778  		Aggregators: map[string]*dlit.Literal{
   779  			"numMatches":     dlit.MustNew("142"),
   780  			"percentMatches": dlit.MustNew("42"),
   781  			"numIncomeGt2":   dlit.MustNew("2"),
   782  			"goalsScore":     dlit.MustNew(0.1),
   783  		},
   784  		Goals: []*rhkassessment.GoalAssessment{
   785  			&rhkassessment.GoalAssessment{"numIncomeGt2 == 1", false},
   786  			&rhkassessment.GoalAssessment{"numIncomeGt2 == 2", true},
   787  		},
   788  	}
   789  
   790  	got, err := getTrueRuleAssessment(assessment)
   791  	if err != nil {
   792  		t.Fatalf("getTrueRuleAssessment: %s", err)
   793  	}
   794  	if !reflect.DeepEqual(got, want) {
   795  		t.Errorf("getTrueAggregators - got: %v, want: %v", got, want)
   796  	}
   797  }
   798  
   799  func TestGetTrueRuleAssessment_error(t *testing.T) {
   800  	assessment := &rhkassessment.Assessment{
   801  		NumRecords: 20,
   802  		RuleAssessments: []*rhkassessment.RuleAssessment{
   803  			&rhkassessment.RuleAssessment{
   804  				Rule: rule.NewEQFV("month", dlit.NewString("may")),
   805  				Aggregators: map[string]*dlit.Literal{
   806  					"numMatches":     dlit.MustNew("2142"),
   807  					"percentMatches": dlit.MustNew("242"),
   808  					"numIncomeGt2":   dlit.MustNew("22"),
   809  					"goalsScore":     dlit.MustNew(20.1),
   810  				},
   811  				Goals: []*rhkassessment.GoalAssessment{
   812  					&rhkassessment.GoalAssessment{"numIncomeGt2 == 1", false},
   813  					&rhkassessment.GoalAssessment{"numIncomeGt2 == 2", true},
   814  				},
   815  			},
   816  			&rhkassessment.RuleAssessment{
   817  				Rule: rule.NewGEFV("rate", dlit.MustNew(789.2)),
   818  				Aggregators: map[string]*dlit.Literal{
   819  					"numMatches":     dlit.MustNew("3142"),
   820  					"percentMatches": dlit.MustNew("342"),
   821  					"numIncomeGt2":   dlit.MustNew("32"),
   822  					"goalsScore":     dlit.MustNew(30.1),
   823  				},
   824  				Goals: []*rhkassessment.GoalAssessment{
   825  					&rhkassessment.GoalAssessment{"numIncomeGt2 == 1", false},
   826  					&rhkassessment.GoalAssessment{"numIncomeGt2 == 2", true},
   827  				},
   828  			},
   829  		},
   830  	}
   831  	wantErr := errors.New("can't find true() rule")
   832  
   833  	_, err := getTrueRuleAssessment(assessment)
   834  	if err == nil || err.Error() != wantErr.Error() {
   835  		t.Errorf("getTrueAggregators: err: %s, wantErr: %s", err, wantErr)
   836  	}
   837  }
   838  
   839  /******************************
   840   *  Helper Functions
   841   ******************************/
   842  
   843  func checkReportsMatch(r1, r2 *Report) error {
   844  	if r1.Mode != r2.Mode {
   845  		return fmt.Errorf("Modes don't match - %s != %s", r1.Mode, r2.Mode)
   846  	}
   847  	if r1.Title != r2.Title {
   848  		return fmt.Errorf("Titles don't match - %s != %s", r1.Title, r2.Title)
   849  	}
   850  	if !reflect.DeepEqual(r1.Tags, r2.Tags) {
   851  		return fmt.Errorf("Tags don't match - %v != %v", r1.Tags, r2.Tags)
   852  	}
   853  	if r1.Category != r2.Category {
   854  		return fmt.Errorf("Categories don't match - %s != %s",
   855  			r1.Category, r2.Category)
   856  	}
   857  	if math.Abs(r1.Stamp.Sub(r2.Stamp).Seconds()) > 1 {
   858  		return fmt.Errorf("Stamps don't match - %s != %s", r1.Stamp, r2.Stamp)
   859  	}
   860  	if r1.ExperimentFilename != r2.ExperimentFilename {
   861  		return fmt.Errorf("ExperimentFilenames don't match - %s != %s",
   862  			r1.ExperimentFilename, r2.ExperimentFilename)
   863  	}
   864  	if r1.NumRecords != r2.NumRecords {
   865  		return fmt.Errorf("NumRecords don't match - %d != %d",
   866  			r1.NumRecords, r2.NumRecords)
   867  	}
   868  	if !reflect.DeepEqual(r1.SortOrder, r2.SortOrder) {
   869  		return fmt.Errorf("SortOrder don't match - %v != %v",
   870  			r1.SortOrder, r2.SortOrder)
   871  	}
   872  	if !reflect.DeepEqual(r1.Aggregators, r2.Aggregators) {
   873  		return fmt.Errorf("Aggregators don't match - %v != %v",
   874  			r1.Aggregators, r2.Aggregators)
   875  	}
   876  	if err := checkAssessmentsMatch(r1.Assessments, r2.Assessments); err != nil {
   877  		return fmt.Errorf("Assessments don't match: %s - %v != %v",
   878  			err, r1.Assessments, r2.Assessments)
   879  	}
   880  	return nil
   881  }
   882  
   883  func checkAssessmentsMatch(as1, as2 []*Assessment) error {
   884  	if len(as1) != len(as2) {
   885  		return fmt.Errorf("number of Assessments don't match %d != %d",
   886  			len(as1), len(as2))
   887  	}
   888  	for i, assessment1 := range as1 {
   889  		if assessment1.Rule != as2[i].Rule {
   890  			return fmt.Errorf("assessment[%d] Rules don't match: %v != %v",
   891  				i, assessment1.Rule, as2[i].Rule)
   892  		}
   893  		if !reflect.DeepEqual(assessment1.Aggregators, as2[i].Aggregators) {
   894  			return fmt.Errorf("assessment[%d] Aggregators don't match: %v != %v",
   895  				i, assessment1.Aggregators, as2[i].Aggregators)
   896  		}
   897  		if !reflect.DeepEqual(assessment1.Goals, as2[i].Goals) {
   898  			return fmt.Errorf("assessment[%d] Goals don't match: %v != %v",
   899  				i, assessment1.Goals, as2[i].Goals)
   900  		}
   901  	}
   902  	return nil
   903  }