refactor rule evaluator to make use of expr envs

This commit is contained in:
Héctor Giménez
2024-05-06 10:15:37 +09:00
parent 8315e55594
commit ffac5edff5
5 changed files with 170 additions and 114 deletions

4
go.mod
View File

@@ -6,7 +6,7 @@ require (
github.com/expr-lang/expr v1.16.5 github.com/expr-lang/expr v1.16.5
github.com/faiface/beep v1.1.0 github.com/faiface/beep v1.1.0
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
golang.org/x/sys v0.18.0 golang.org/x/sys v0.20.0
) )
require ( require (
@@ -15,7 +15,7 @@ require (
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 // indirect golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 // indirect
golang.org/x/image v0.15.0 // indirect golang.org/x/image v0.16.0 // indirect
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 // indirect golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

8
go.sum
View File

@@ -34,16 +34,16 @@ golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSin
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 h1:vyLBGJPIl9ZYbcQFM2USFmJBK6KI+t+z6jL0lbwjrnc= golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 h1:vyLBGJPIl9ZYbcQFM2USFmJBK6KI+t+z6jL0lbwjrnc=
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -55,7 +55,7 @@ func ParseNIPFile(filePath string) (Rules, error) {
for fileScanner.Scan() { for fileScanner.Scan() {
lineNumber++ lineNumber++
rule, err := New(fileScanner.Text(), filePath, lineNumber) rule, err := NewRule(fileScanner.Text(), filePath, lineNumber)
if errors.Is(err, ErrEmptyRule) { if errors.Is(err, ErrEmptyRule) {
continue continue
} }

View File

@@ -8,68 +8,43 @@ import (
"strings" "strings"
"github.com/expr-lang/expr" "github.com/expr-lang/expr"
"github.com/expr-lang/expr/vm"
"github.com/hectorgimenez/d2go/pkg/data" "github.com/hectorgimenez/d2go/pkg/data"
"github.com/hectorgimenez/d2go/pkg/data/item" "github.com/hectorgimenez/d2go/pkg/data/item"
"github.com/hectorgimenez/d2go/pkg/data/stat" "github.com/hectorgimenez/d2go/pkg/data/stat"
) )
const (
RuleResultFullMatch RuleResult = 1
RuleResultPartial RuleResult = 2
RuleResultNoMatch RuleResult = 3
)
var ( var (
fixedPropsRegexp = regexp.MustCompile(`(\[(type|quality|class|name|flag|color)]\s*(<=|<|>|>=|!=|==)\s*([a-zA-Z]+))`) fixedPropsRegexp = regexp.MustCompile(`(\[(type|quality|class|name|flag|color)]\s*(<=|<|>|>=|!=|==)\s*([a-zA-Z0-9]+))`)
statsRegexp = regexp.MustCompile(`\[(.*?)]`) statsRegexp = regexp.MustCompile(`\[(.*?)]`)
maxQtyRegexp = regexp.MustCompile(`(\[maxquantity]\s*(<=|<|>|>=|!=|==)\s*([0-9]+))`) maxQtyRegexp = regexp.MustCompile(`(\[maxquantity]\s*(<=|<|>|>=|!=|==)\s*([0-9]+))`)
) )
type Rule struct { type Rule struct {
RawLine string // Original line, don't use it for evaluation RawLine string // Original line, don't use it for evaluation
Stages [3]string
Filename string Filename string
LineNumber int LineNumber int
Enabled bool Enabled bool
maxQuantity int maxQuantity int
stage1 *vm.Program
stage2 *vm.Program
requiredStats []string
} }
type Rules []Rule
type RuleResult int type RuleResult int
type Rules []Rule
var ( var fixedPropsList = map[string]int{"type": 0, "quality": 0, "class": 0, "name": 0, "flag": 0, "color": 0}
RuleResultFullMatch RuleResult = 1
RuleResultPartial RuleResult = 2
RuleResultNoMatch RuleResult = 3
)
func (r Rules) EvaluateAll(it data.Item) (Rule, RuleResult) { func NewRule(rawRule string, filename string, lineNumber int) (Rule, error) {
bestMatch := RuleResultNoMatch
bestMatchingRule := Rule{}
for _, rule := range r {
if rule.Enabled {
result, err := rule.Evaluate(it)
if err != nil {
continue
}
if result == RuleResultFullMatch {
return rule, result
}
if result == RuleResultPartial {
bestMatch = result
bestMatchingRule = rule
}
}
}
return bestMatchingRule, bestMatch
}
func New(rawRule string, filename string, lineNumber int) (Rule, error) {
rule := sanitizeLine(rawRule) rule := sanitizeLine(rawRule)
// Check for not supported stats
for _, prop := range statsRegexp.FindAllStringSubmatch(rule, -1) {
if slices.Contains(notSupportedStats, prop[1]) {
return Rule{}, fmt.Errorf("property %s is not supported, please remove it", prop[1])
}
}
// Try to get the maxquantity value and purge it from the rule, we can not evaluate it // Try to get the maxquantity value and purge it from the rule, we can not evaluate it
maxQuantity := 0 maxQuantity := 0
for _, prop := range maxQtyRegexp.FindAllStringSubmatch(rule, -1) { for _, prop := range maxQtyRegexp.FindAllStringSubmatch(rule, -1) {
@@ -87,53 +62,84 @@ func New(rawRule string, filename string, lineNumber int) (Rule, error) {
return Rule{}, ErrEmptyRule return Rule{}, ErrEmptyRule
} }
stages := [3]string{} r := Rule{
for i, stg := range strings.Split(rule, "#") {
stages[i] = strings.TrimSpace(stg)
}
return Rule{
RawLine: rawRule, RawLine: rawRule,
Stages: stages,
Filename: filename, Filename: filename,
LineNumber: lineNumber, LineNumber: lineNumber,
Enabled: true, Enabled: true,
maxQuantity: maxQuantity, maxQuantity: maxQuantity,
}, nil }
for i, stg := range strings.Split(rule, "#") {
line := strings.TrimSpace(stg)
if line == "" {
break
}
if i == 0 {
line, err := replaceStringPropertiesInStage1(line)
if err != nil {
return Rule{}, err
}
line = strings.ReplaceAll(line, "[", "")
line = strings.ReplaceAll(line, "]", "")
program, err := expr.Compile(line, expr.Env(fixedPropsList))
if err != nil {
return Rule{}, fmt.Errorf("error compiling rule: %w", err)
}
r.stage1 = program
}
if i == 1 {
r.requiredStats = getRequiredStatsForRule(line)
statsMap := make(map[string]int)
for _, prop := range r.requiredStats {
statsMap[prop] = 0
// Check for not supported stats
if slices.Contains(notSupportedStats, prop) {
return Rule{}, fmt.Errorf("property %s is not supported, please remove it", prop)
}
}
line = strings.ReplaceAll(line, "[", "")
line = strings.ReplaceAll(line, "]", "")
program, err := expr.Compile(line, expr.Env(statsMap))
if err != nil {
return Rule{}, fmt.Errorf("error compiling rule: %w", err)
}
r.stage2 = program
// We only care about first and second part, third one is maxquantity and was already parsed
break
}
}
return r, nil
} }
func (r Rule) Evaluate(it data.Item) (RuleResult, error) { func (r Rule) Evaluate(it data.Item) (RuleResult, error) {
stage1 := r.Stages[0] stage1Props := make(map[string]int)
baseProperties := fixedPropsRegexp.FindAllStringSubmatch(stage1, -1) for prop := range fixedPropsList {
for _, prop := range baseProperties { switch prop {
switch prop[2] {
case "type": case "type":
partialReplace := strings.ReplaceAll(prop[0], fmt.Sprintf("[%s]", prop[2]), fmt.Sprintf("%d", typeAliases[it.TypeAsString()])) stage1Props["type"] = typeAliases[it.TypeAsString()]
partialReplace = strings.ReplaceAll(partialReplace, prop[4], fmt.Sprintf("%d", typeAliases[prop[4]]))
stage1 = strings.ReplaceAll(stage1, prop[0], partialReplace)
case "quality": case "quality":
partialReplace := strings.ReplaceAll(prop[0], fmt.Sprintf("[%s]", prop[2]), fmt.Sprintf("%d", it.Quality)) stage1Props["quality"] = int(it.Quality)
partialReplace = strings.ReplaceAll(partialReplace, prop[4], fmt.Sprintf("%d", qualityAliases[prop[4]]))
stage1 = strings.ReplaceAll(stage1, prop[0], partialReplace)
case "class": case "class":
partialReplace := strings.ReplaceAll(prop[0], fmt.Sprintf("[%s]", prop[2]), fmt.Sprintf("%d", it.Desc().Tier())) stage1Props["class"] = int(it.Desc().Tier())
partialReplace = strings.ReplaceAll(partialReplace, prop[4], fmt.Sprintf("%d", classAliases[prop[4]]))
stage1 = strings.ReplaceAll(stage1, prop[0], partialReplace)
case "name": case "name":
partialReplace := strings.ReplaceAll(prop[0], fmt.Sprintf("[%s]", prop[2]), fmt.Sprintf("%d", item.GetIDByName(string(it.Name)))) stage1Props["name"] = it.ID
partialReplace = strings.ReplaceAll(partialReplace, prop[4], fmt.Sprintf("%d", item.GetIDByName(prop[4])))
stage1 = strings.ReplaceAll(stage1, prop[0], partialReplace)
case "flag": case "flag":
partialReplace := strings.ReplaceAll(prop[0], fmt.Sprintf("[%s]", prop[2]), fmt.Sprintf("%d", map[bool]int{true: 1, false: 0}[it.Ethereal])) stage1Props["flag"] = map[bool]int{true: 1, false: 0}[it.Ethereal]
partialReplace = strings.ReplaceAll(partialReplace, prop[4], fmt.Sprintf("%d", 1))
stage1 = strings.ReplaceAll(stage1, prop[0], partialReplace)
case "color": case "color":
// TODO: Not supported yet // TODO: Not supported yet
} }
} }
// Let's evaluate first stage // Let's evaluate first stage
stage1Result, err := expr.Eval(stage1, nil) stage1Result, err := expr.Run(r.stage1, stage1Props)
if err != nil { if err != nil {
return RuleResultNoMatch, fmt.Errorf("error evaluating rule: %w", err) return RuleResultNoMatch, fmt.Errorf("error evaluating rule: %w", err)
} }
@@ -143,27 +149,25 @@ func (r Rule) Evaluate(it data.Item) (RuleResult, error) {
return RuleResultNoMatch, nil return RuleResultNoMatch, nil
} }
// Let's go with other stats now
// TODO: properties are missing (enhanceddefense, enhanceddamage, etc)
stage2 := r.Stages[1]
stage2Result := true stage2Result := true
if stage2 != "" { if r.stage2 != nil {
for _, statName := range statsRegexp.FindAllStringSubmatch(stage2, -1) { stage2Props := make(map[string]int)
statData, found := statAliases[statName[1]] for _, statName := range r.requiredStats {
statData, found := statAliases[statName]
if !found { if !found {
return RuleResultNoMatch, fmt.Errorf("property %s is not valid or not supported", statName[1]) return RuleResultNoMatch, fmt.Errorf("property %s is not valid or not supported", statName)
} }
// Exception for enhanceddefense, is not accurate // Exception for enhanceddefense, is not accurate
if statName[0] == "[enhanceddefense]" { if strings.EqualFold(statName, "enhanceddefense") {
enhancedDefense := r.calculateEnhancedDefense(it) enhancedDefense := r.calculateEnhancedDefense(it)
stage2 = strings.ReplaceAll(stage2, statName[0], fmt.Sprintf("%d", enhancedDefense)) stage2Props[statName] = enhancedDefense
continue continue
} }
if itemTypes, found := blockedStatsForItemType[statName[1]]; found { if itemTypes, found := blockedStatsForItemType[statName]; found {
if slices.Contains(itemTypes, it.TypeAsString()) { if slices.Contains(itemTypes, it.TypeAsString()) {
return RuleResultNoMatch, fmt.Errorf("property %s is not supported for item type %s", statName[0], it.TypeAsString()) return RuleResultNoMatch, fmt.Errorf("property %s is not supported for item type %s", statName, it.TypeAsString())
} }
} }
@@ -172,11 +176,10 @@ func (r Rule) Evaluate(it data.Item) (RuleResult, error) {
layer = statData[1] layer = statData[1]
} }
itemStat, _ := it.FindStat(stat.ID(statData[0]), layer) itemStat, _ := it.FindStat(stat.ID(statData[0]), layer)
// By default, value will be 0 is stat is not found, it's okay for evaluation purposes. stage2Props[statName] = itemStat.Value
stage2 = strings.ReplaceAll(stage2, statName[0], fmt.Sprintf("%d", itemStat.Value))
} }
res, err := expr.Eval(stage2, nil) res, err := expr.Run(r.stage2, stage2Props)
if err != nil { if err != nil {
return RuleResultNoMatch, fmt.Errorf("error evaluating rule: %w", err) return RuleResultNoMatch, fmt.Errorf("error evaluating rule: %w", err)
} }
@@ -192,10 +195,45 @@ func (r Rule) Evaluate(it data.Item) (RuleResult, error) {
if stage1Result.(bool) && !it.Identified { if stage1Result.(bool) && !it.Identified {
return RuleResultPartial, nil return RuleResultPartial, nil
} }
return RuleResultNoMatch, nil return RuleResultNoMatch, nil
} }
func replaceStringPropertiesInStage1(stage1 string) (string, error) {
baseProperties := fixedPropsRegexp.FindAllStringSubmatch(stage1, -1)
for _, prop := range baseProperties {
replaceWith := ""
switch prop[2] {
case "type":
replaceWith = strings.ReplaceAll(prop[0], prop[4], fmt.Sprintf("%d", typeAliases[prop[4]]))
case "quality":
replaceWith = strings.ReplaceAll(prop[0], prop[4], fmt.Sprintf("%d", qualityAliases[prop[4]]))
case "class":
replaceWith = strings.ReplaceAll(prop[0], prop[4], fmt.Sprintf("%d", classAliases[prop[4]]))
case "name":
replaceWith = strings.ReplaceAll(prop[0], prop[4], fmt.Sprintf("%d", item.GetIDByName(prop[4])))
case "flag":
replaceWith = strings.ReplaceAll(prop[0], prop[4], fmt.Sprintf("%d", 1))
case "color":
// TODO: Not supported yet
return "", fmt.Errorf("property %s is not supported yet", prop[2])
}
if replaceWith != "" {
stage1 = strings.ReplaceAll(stage1, prop[0], replaceWith)
}
}
return stage1, nil
}
func getRequiredStatsForRule(line string) []string {
statsList := make([]string, 0)
for _, statName := range statsRegexp.FindAllStringSubmatch(line, -1) {
statsList = append(statsList, statName[1])
}
return statsList
}
// MaxQuantity returns the maximum quantity of items that character can have, 0 means no limit // MaxQuantity returns the maximum quantity of items that character can have, 0 means no limit
func (r Rule) MaxQuantity() int { func (r Rule) MaxQuantity() int {
return r.maxQuantity return r.maxQuantity

View File

@@ -36,6 +36,7 @@ func TestRule_Evaluate(t *testing.T) {
}, },
args: args{ args: args{
item: data.Item{ item: data.Item{
ID: 603,
Name: "SmAlLCharM", Name: "SmAlLCharM",
Quality: item.QualityMagic, Quality: item.QualityMagic,
Ethereal: true, Ethereal: true,
@@ -57,6 +58,7 @@ func TestRule_Evaluate(t *testing.T) {
}, },
args: args{ args: args{
item: data.Item{ item: data.Item{
ID: 373,
Name: "mageplate", Name: "mageplate",
Quality: item.QualitySuperior, Quality: item.QualitySuperior,
Ethereal: false, Ethereal: false,
@@ -76,6 +78,7 @@ func TestRule_Evaluate(t *testing.T) {
}, },
args: args{ args: args{
item: data.Item{ item: data.Item{
ID: 373,
Name: "mageplate", Name: "mageplate",
Stats: []stat.Data{ Stats: []stat.Data{
{ID: stat.AddClassSkills, Value: 3, Layer: 1}, {ID: stat.AddClassSkills, Value: 3, Layer: 1},
@@ -92,6 +95,7 @@ func TestRule_Evaluate(t *testing.T) {
}, },
args: args{ args: args{
item: data.Item{ item: data.Item{
ID: 373,
Name: "mageplate", Name: "mageplate",
Stats: []stat.Data{ Stats: []stat.Data{
{ID: stat.SingleSkill, Value: 3, Layer: 55}, {ID: stat.SingleSkill, Value: 3, Layer: 55},
@@ -100,20 +104,6 @@ func TestRule_Evaluate(t *testing.T) {
}, },
want: RuleResultFullMatch, want: RuleResultFullMatch,
}, },
{
name: "Ensure [color] returns error, not supported yet",
fields: fields{
RawLine: "[type] == armor && [color] == 1000 && [quality] == magic",
Enabled: true,
},
args: args{
item: data.Item{
Name: "mageplate",
Quality: item.QualityMagic,
},
},
wantErr: true,
},
{ {
name: "Unid item matching base stats should return partial match", name: "Unid item matching base stats should return partial match",
fields: fields{ fields: fields{
@@ -122,6 +112,7 @@ func TestRule_Evaluate(t *testing.T) {
}, },
args: args{ args: args{
item: data.Item{ item: data.Item{
ID: 373,
Identified: false, Identified: false,
Name: "mageplate", Name: "mageplate",
Quality: item.QualityMagic, Quality: item.QualityMagic,
@@ -137,6 +128,7 @@ func TestRule_Evaluate(t *testing.T) {
}, },
args: args{ args: args{
item: data.Item{ item: data.Item{
ID: 373,
Identified: false, Identified: false,
Name: "mageplate", Name: "mageplate",
Quality: item.QualityMagic, Quality: item.QualityMagic,
@@ -174,7 +166,7 @@ func TestRule_Evaluate(t *testing.T) {
Identified: true, Identified: true,
ID: 0, ID: 0,
Name: "handaxe", Name: "handaxe",
Quality: item.QualityUnique, Quality: item.QualitySuperior,
Stats: []stat.Data{}, Stats: []stat.Data{},
}, },
}, },
@@ -202,7 +194,7 @@ func TestRule_Evaluate(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
r, err := New(tt.fields.RawLine, tt.fields.Filename, tt.fields.LineNumber) r, err := NewRule(tt.fields.RawLine, tt.fields.Filename, tt.fields.LineNumber)
require.NoError(t, err) require.NoError(t, err)
got, err := r.Evaluate(tt.args.item) got, err := r.Evaluate(tt.args.item)
if !tt.wantErr { if !tt.wantErr {
@@ -234,10 +226,17 @@ func TestNew(t *testing.T) {
}, },
wantErr: true, wantErr: true,
}, },
{
name: "Ensure [color] returns error, not supported yet",
args: args{
rawRule: "[type] == armor && [color] == 1000 && [quality] == magic",
},
wantErr: true,
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := New(tt.args.rawRule, tt.args.filename, tt.args.lineNumber) got, err := NewRule(tt.args.rawRule, tt.args.filename, tt.args.lineNumber)
if tt.wantErr { if tt.wantErr {
require.Error(t, err) require.Error(t, err)
} else { } else {
@@ -247,3 +246,22 @@ func TestNew(t *testing.T) {
}) })
} }
} }
func BenchmarkEvaluate(b *testing.B) {
it := data.Item{
ID: 0,
Name: "Axe",
Quality: item.QualitySuperior,
}
rule, err := NewRule(
"[type] == amulet && [quality] == crafted # ([shapeshiftingskilltab] >= 2 || [elementalskilltab] >= 2 || [druidsummoningskilltab] >= 2) && [fcr] >= 10 && ([strength]+[maxhp]+[maxmana] >= 60 || [dexterity]+[maxhp]+[maxmana] >= 60 || [strength]+[dexterity]+[maxhp] >= 50 || [strength]+[dexterity]+[maxmana] >= 55)",
"test",
1,
)
require.NoError(b, err)
for n := 0; n < b.N; n++ {
rule.Evaluate(it)
}
}