Skip to content

Commit

Permalink
Add utility functions to allow predicates to take lists of conclusions
Browse files Browse the repository at this point in the history
If a predicate has an `allowedConclusions` member, it will unmarshal
from a list `["a", "b", "c"]` into a set, so that the handler can easily
check if the incoming conclusion was allowed.
  • Loading branch information
iainlane committed Jul 7, 2024
1 parent beb44ef commit caba90e
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 0 deletions.
61 changes: 61 additions & 0 deletions policy/predicate/predicate.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ package predicate

import (
"context"
"fmt"
"slices"
"strings"

"github.com/palantir/policy-bot/policy/common"
"github.com/palantir/policy-bot/pull"
Expand All @@ -27,3 +30,61 @@ type Predicate interface {
// Evaluate determines if the predicate is satisfied.
Evaluate(ctx context.Context, prctx pull.Context) (*common.PredicateResult, error)
}

type unit struct{}
type set map[string]unit

// allowedConclusions can be one of:
// action_required, cancelled, failure, neutral, success, skipped, stale,
// timed_out
type allowedConclusions set

// UnmarshalYAML implements the yaml.Unmarshaler interface for allowedConclusions.
// This allows the predicate to be specified in the input as a list of strings,
// which we then convert to a set of strings, for easy membership testing.
func (c *allowedConclusions) UnmarshalYAML(unmarshal func(interface{}) error) error {
var conclusions []string
if err := unmarshal(&conclusions); err != nil {
return fmt.Errorf("failed to unmarshal conclusions: %v", err)
}

*c = make(allowedConclusions, len(conclusions))
for _, conclusion := range conclusions {
(*c)[conclusion] = unit{}
}

return nil
}

// joinWithOr returns a string that represents the allowed conclusions in a
// format that can be used in a sentence. For example, if the allowed
// conclusions are "success" and "failure", this will return "success or
// failure". If there are more than two conclusions, the first n-1 will be
// separated by commas.
func (c allowedConclusions) joinWithOr() string {
length := len(c)

keys := make([]string, 0, length)
for key := range c {
keys = append(keys, key)
}
slices.Sort(keys)

switch length {
case 0:
return ""
case 1:
return keys[0]
case 2:
return keys[0] + " or " + keys[1]
}

head, tail := keys[:length-1], keys[length-1]

return strings.Join(head, ", ") + ", or " + tail
}

// If unspecified, require the status to be successful.
var defaultConclusions allowedConclusions = allowedConclusions{
"success": unit{},
}
87 changes: 87 additions & 0 deletions policy/predicate/predicate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (

"github.com/palantir/policy-bot/policy/common"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v2"
)

func assertPredicateResult(t *testing.T, expected, actual *common.PredicateResult) {
Expand All @@ -27,3 +28,89 @@ func assertPredicateResult(t *testing.T, expected, actual *common.PredicateResul
assert.Equal(t, expected.ConditionsMap, actual.ConditionsMap, "conditions were not correct")
assert.Equal(t, expected.ConditionValues, actual.ConditionValues, "conditions were not correct")
}

func TestUnmarshalAllowedConclusions(t *testing.T) {
testCases := []struct {
name string
input string
expected allowedConclusions
expectedErr bool
}{
{
name: "empty",
input: "",
expected: nil,
},
{
name: "single",
input: `["success"]`,
expected: allowedConclusions{"success": unit{}},
},
{
name: "multiple",
input: `["success", "failure"]`,
expected: allowedConclusions{"success": unit{}, "failure": unit{}},
},
{
name: "repeat",
input: `["success", "success"]`,
expected: allowedConclusions{"success": unit{}},
},
{
name: "invalid",
input: `notyaml`,
expectedErr: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var actual allowedConclusions
err := yaml.UnmarshalStrict([]byte(tc.input), &actual)

if tc.expectedErr {
assert.Error(t, err, "UnmarshalStrict should have failed")
return
}

assert.NoError(t, err, "UnmarshalStrict failed")
assert.Equal(t, tc.expected, actual, "values were not correct")
})
}
}

func TestJoinWithOr(t *testing.T) {
testCases := []struct {
name string
input allowedConclusions
expected string
}{
{
name: "empty",
input: nil,
expected: "",
},
{
name: "one conclusion",
input: allowedConclusions{"success": unit{}},
expected: "success",
},
{
name: "two conclusions",
input: allowedConclusions{"success": unit{}, "failure": unit{}},
expected: "failure or success",
},
{
name: "three conclusions",
input: allowedConclusions{"success": unit{}, "failure": unit{}, "cancelled": unit{}},
expected: "cancelled, failure, or success",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual := tc.input.joinWithOr()
assert.Equal(t, tc.expected, actual, "values were not correct")
})
}
}

0 comments on commit caba90e

Please sign in to comment.