Skip to content

Commit

Permalink
Merge pull request #1318 from loadimpact/execution-segment-sequences
Browse files Browse the repository at this point in the history
Execution segment sequences
  • Loading branch information
na-- authored Feb 5, 2020
2 parents 4709528 + 686f32b commit a1f3802
Show file tree
Hide file tree
Showing 3 changed files with 205 additions and 33 deletions.
3 changes: 2 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ linters:
- gochecknoinits
- godox
- wsl
- gomnd
fast: false

service:
golangci-lint-version: 1.20.x
golangci-lint-version: 1.23.x
141 changes: 126 additions & 15 deletions lib/execution_segment.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,16 @@ type ExecutionSegment struct {
}

// Ensure we implement those interfaces
var _ encoding.TextUnmarshaler = &ExecutionSegment{}
var _ fmt.Stringer = &ExecutionSegment{}
var (
_ encoding.TextUnmarshaler = &ExecutionSegment{}
_ fmt.Stringer = &ExecutionSegment{}
)

// Helpful "constants" so we don't initialize them in every function call
var zeroRat, oneRat = big.NewRat(0, 1), big.NewRat(1, 1) //nolint:gochecknoglobals
var oneBigInt, twoBigInt = big.NewInt(1), big.NewInt(2) //nolint:gochecknoglobals
var (
zeroRat, oneRat = big.NewRat(0, 1), big.NewRat(1, 1) //nolint:gochecknoglobals
oneBigInt, twoBigInt = big.NewInt(1), big.NewInt(2) //nolint:gochecknoglobals
)

// NewExecutionSegment validates the supplied arguments (basically, that 0 <=
// from < to <= 1) and either returns an error, or it returns a
Expand Down Expand Up @@ -95,12 +99,11 @@ func stringToRat(s string) (*big.Rat, error) {
return rat, nil
}

// UnmarshalText implements the encoding.TextUnmarshaler interface, so that
// execution segments can be specified as CLI flags, environment variables, and
// JSON strings.
// NewExecutionSegmentFromString validates the supplied string value and returns
// the newly created ExecutionSegment or and error from it.
//
// We are able to parse both single percentage/float/fraction values, and actual
// (from; to] segments. For the single values, we just treat them as the
// (from: to] segments. For the single values, we just treat them as the
// beginning segment - thus the execution segment can be used as a shortcut for
// quickly running an arbitrarily scaled-down version of a test.
//
Expand All @@ -109,26 +112,32 @@ func stringToRat(s string) (*big.Rat, error) {
// And values without a colon are the end of a first segment:
// `20%`, `0.2`, and `1/5` should be converted to (0, 1/5]
// empty values should probably be treated as "1", i.e. the whole execution
func (es *ExecutionSegment) UnmarshalText(text []byte) (err error) {
func NewExecutionSegmentFromString(toStr string) (result *ExecutionSegment, err error) {
from := zeroRat
toStr := string(text)
if toStr == "" {
toStr = "1" // an empty string means a full 0:1 execution segment
}
if strings.ContainsRune(toStr, ':') {
fromToStr := strings.SplitN(toStr, ":", 2)
toStr = fromToStr[1]
if from, err = stringToRat(fromToStr[0]); err != nil {
return err
return nil, err
}
}

to, err := stringToRat(toStr)
if err != nil {
return err
return nil, err
}

segment, err := NewExecutionSegment(from, to)
return NewExecutionSegment(from, to)
}

// UnmarshalText implements the encoding.TextUnmarshaler interface, so that
// execution segments can be specified as CLI flags, environment variables, and
// JSON strings. It is a wrapper for the NewExecutionFromString() constructor.
func (es *ExecutionSegment) UnmarshalText(text []byte) (err error) {
segment, err := NewExecutionSegmentFromString(string(text))
if err != nil {
return err
}
Expand Down Expand Up @@ -195,8 +204,6 @@ func (es *ExecutionSegment) Split(numParts int64) ([]*ExecutionSegment, error) {
return results, nil
}

//TODO: add a NewFromString() method

// Equal returns true only if the two execution segments have the same from and
// to values.
func (es *ExecutionSegment) Equal(other *ExecutionSegment) bool {
Expand Down Expand Up @@ -293,3 +300,107 @@ func (es *ExecutionSegment) CopyScaleRat(value *big.Rat) *big.Rat {
}
return new(big.Rat).Mul(value, es.length)
}

// ExecutionSegmentSequence represents an ordered chain of execution segments,
// where the end of one segment is the beginning of the next. It can serialized
// as a comma-separated string of rational numbers "r1,r2,r3,...,rn", which
// represents the sequence (r1, r2], (r2, r3], (r3, r4], ..., (r{n-1}, rn].
// The empty value should be treated as if there is a single (0, 1] segment.
type ExecutionSegmentSequence []*ExecutionSegment

// NewExecutionSegmentSequence validates the that the supplied execution
// segments are non-overlapping and without gaps. It will return a new execution
// segment sequence if that is true, and an error if it's not.
func NewExecutionSegmentSequence(segments ...*ExecutionSegment) (ExecutionSegmentSequence, error) {
if len(segments) > 1 {
to := segments[0].to
for i, segment := range segments[1:] {
if segment.from.Cmp(to) != 0 {
return nil, fmt.Errorf(
"the start value %s of segment #%d should be equal to the end value of the previous one, but it is %s",
segment.from, i+1, to,
)
}
to = segment.to
}
}
return ExecutionSegmentSequence(segments), nil
}

// NewExecutionSegmentSequenceFromString parses strings of the format
// "r1,r2,r3,...,rn", which represents the sequences like (r1, r2], (r2, r3],
// (r3, r4], ..., (r{n-1}, rn].
func NewExecutionSegmentSequenceFromString(strSeq string) (ExecutionSegmentSequence, error) {
if len(strSeq) == 0 {
return nil, nil
}

points := strings.Split(strSeq, ",")
if len(points) < 2 {
return nil, fmt.Errorf("at least 2 points are needed for an execution segment sequence, %d given", len(points))
}
var start *big.Rat

segments := make([]*ExecutionSegment, 0, len(points)-1)
for i, point := range points {
rat, err := stringToRat(point)
if err != nil {
return nil, err
}
if i == 0 {
start = rat
continue
}

segment, err := NewExecutionSegment(start, rat)
if err != nil {
return nil, err
}
segments = append(segments, segment)
start = rat
}

return NewExecutionSegmentSequence(segments...)
}

// UnmarshalText implements the encoding.TextUnmarshaler interface, so that
// execution segment sequences can be specified as CLI flags, environment
// variables, and JSON strings.
func (ess *ExecutionSegmentSequence) UnmarshalText(text []byte) (err error) {
seq, err := NewExecutionSegmentSequenceFromString(string(text))
if err != nil {
return err
}
*ess = seq
return nil
}

// MarshalText implements the encoding.TextMarshaler interface, so is used for
// text and JSON encoding of the execution segment sequences.
func (ess ExecutionSegmentSequence) MarshalText() ([]byte, error) {
return []byte(ess.String()), nil
}

// String just implements the fmt.Stringer interface, encoding the sequence of
// segments as "start1,end1,end2,end3,...,endn".
func (ess ExecutionSegmentSequence) String() string {
result := make([]string, 0, len(ess)+1)
for i, s := range ess {
if i == 0 {
result = append(result, s.from.RatString())
}
result = append(result, s.to.RatString())
}
return strings.Join(result, ",")
}

// GetStripedOffsets returns everything that you need in order to execute only
// the iterations that belong to the supplied segment...
//
// TODO: add a more detailed algorithm description
func (ess ExecutionSegmentSequence) GetStripedOffsets(segment *ExecutionSegment) (int, []int, error) {
start := 0
offsets := []int{}
// TODO: basically https://docs.google.com/spreadsheets/d/1V_ivN2xuaMJIgOf1HkpOw1ex8QOhxp960itGGiRrNzo/edit
return start, offsets, fmt.Errorf("not implemented")
}
94 changes: 77 additions & 17 deletions lib/execution_segment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ import (
)

func stringToES(t *testing.T, str string) *ExecutionSegment {
var es = new(ExecutionSegment)
es := new(ExecutionSegment)
require.NoError(t, es.UnmarshalText([]byte(str)))
return es
}

func TestExecutionSegmentEquals(t *testing.T) {
t.Parallel()

Expand All @@ -44,7 +45,7 @@ func TestExecutionSegmentEquals(t *testing.T) {
})

t.Run("To it's self", func(t *testing.T) {
var es = stringToES(t, "1/2:2/3")
es := stringToES(t, "1/2:2/3")
require.True(t, es.Equal(es))
})
}
Expand Down Expand Up @@ -76,7 +77,7 @@ func TestExecutionSegmentNew(t *testing.T) {

func TestExecutionSegmentUnmarshalText(t *testing.T) {
t.Parallel()
var testCases = []struct {
testCases := []struct {
input string
output *ExecutionSegment
isErr bool
Expand All @@ -98,7 +99,7 @@ func TestExecutionSegmentUnmarshalText(t *testing.T) {
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.input, func(t *testing.T) {
var es = new(ExecutionSegment)
es := new(ExecutionSegment)
err := es.UnmarshalText([]byte(testCase.input))
if testCase.isErr {
require.Error(t, err)
Expand All @@ -116,10 +117,10 @@ func TestExecutionSegmentUnmarshalText(t *testing.T) {

t.Run("Unmarshal nilSegment.String", func(t *testing.T) {
var nilEs *ExecutionSegment
var nilEsStr = nilEs.String()
nilEsStr := nilEs.String()
require.Equal(t, "0:1", nilEsStr)

var es = new(ExecutionSegment)
es := new(ExecutionSegment)
err := es.UnmarshalText([]byte(nilEsStr))
require.NoError(t, err)
require.True(t, es.Equal(nilEs))
Expand Down Expand Up @@ -186,7 +187,7 @@ func TestExecutionSegmentSplit(t *testing.T) {

func TestExecutionSegmentScale(t *testing.T) {
t.Parallel()
var es = new(ExecutionSegment)
es := new(ExecutionSegment)
require.NoError(t, es.UnmarshalText([]byte("0.5")))
require.Equal(t, int64(1), es.Scale(2))
require.Equal(t, int64(2), es.Scale(3))
Expand All @@ -198,9 +199,9 @@ func TestExecutionSegmentScale(t *testing.T) {

func TestExecutionSegmentCopyScaleRat(t *testing.T) {
t.Parallel()
var es = new(ExecutionSegment)
var twoRat = big.NewRat(2, 1)
var threeRat = big.NewRat(3, 1)
es := new(ExecutionSegment)
twoRat := big.NewRat(2, 1)
threeRat := big.NewRat(3, 1)
require.NoError(t, es.UnmarshalText([]byte("0.5")))
require.Equal(t, oneRat, es.CopyScaleRat(twoRat))
require.Equal(t, big.NewRat(3, 2), es.CopyScaleRat(threeRat))
Expand All @@ -216,10 +217,10 @@ func TestExecutionSegmentCopyScaleRat(t *testing.T) {

func TestExecutionSegmentInPlaceScaleRat(t *testing.T) {
t.Parallel()
var es = new(ExecutionSegment)
var twoRat = big.NewRat(2, 1)
var threeRat = big.NewRat(3, 1)
var threeSecondsRat = big.NewRat(3, 2)
es := new(ExecutionSegment)
twoRat := big.NewRat(2, 1)
threeRat := big.NewRat(3, 1)
threeSecondsRat := big.NewRat(3, 2)
require.NoError(t, es.UnmarshalText([]byte("0.5")))
require.Equal(t, oneRat, es.InPlaceScaleRat(twoRat))
require.Equal(t, oneRat, twoRat)
Expand All @@ -245,7 +246,7 @@ func TestExecutionSegmentInPlaceScaleRat(t *testing.T) {

func TestExecutionSegmentSubSegment(t *testing.T) {
t.Parallel()
var testCases = []struct {
testCases := []struct {
name string
base, sub, result *ExecutionSegment
}{
Expand Down Expand Up @@ -281,7 +282,7 @@ func TestExecutionSegmentSubSegment(t *testing.T) {

func TestSplitBadSegment(t *testing.T) {
t.Parallel()
var es = &ExecutionSegment{from: oneRat, to: zeroRat}
es := &ExecutionSegment{from: oneRat, to: zeroRat}
_, err := es.Split(5)
require.Error(t, err)
}
Expand All @@ -293,7 +294,7 @@ func TestSegmentExecutionFloatLength(t *testing.T) {
require.Equal(t, 1.0, nilEs.FloatLength())
})

var testCases = []struct {
testCases := []struct {
es *ExecutionSegment
expected float64
}{
Expand All @@ -320,3 +321,62 @@ func TestSegmentExecutionFloatLength(t *testing.T) {
})
}
}

func TestExecutionSegmentSequences(t *testing.T) {
t.Parallel()

_, err := NewExecutionSegmentSequence(stringToES(t, "0:1/3"), stringToES(t, "1/2:1"))
assert.Error(t, err)
}

func TestExecutionSegmentStringSequences(t *testing.T) {
t.Parallel()
testCases := []struct {
seq string
expSegments []string
expError bool
canReverse bool
// TODO: checks for least common denominator and maybe striped partitioning
}{
{seq: "", expSegments: nil},
{seq: "0.5", expError: true},
{seq: "1,1", expError: true},
{seq: "-0.5,1", expError: true},
{seq: "1/2,1/2", expError: true},
{seq: "1/2,1/3", expError: true},
{seq: "0,1,1/2", expError: true},
{seq: "0.5,1", expSegments: []string{"1/2:1"}},
{seq: "1/2,1", expSegments: []string{"1/2:1"}, canReverse: true},
{seq: "1/3,2/3", expSegments: []string{"1/3:2/3"}, canReverse: true},
{seq: "0,1/3,2/3", expSegments: []string{"0:1/3", "1/3:2/3"}, canReverse: true},
{seq: "0,1/3,2/3,1", expSegments: []string{"0:1/3", "1/3:2/3", "2/3:1"}, canReverse: true},
{seq: "0.5,0.7", expSegments: []string{"1/2:7/10"}},
{seq: "0.5,0.7,1", expSegments: []string{"1/2:7/10", "7/10:1"}},
{seq: "0,1/13,2/13,1/3,1/2,3/4,1", expSegments: []string{
"0:1/13", "1/13:2/13", "2/13:1/3", "1/3:1/2", "1/2:3/4", "3/4:1",
}, canReverse: true},
}

for _, tc := range testCases {
tc := tc
t.Run(tc.seq, func(t *testing.T) {
result, err := NewExecutionSegmentSequenceFromString(tc.seq)
if tc.expError {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, len(tc.expSegments), len(result))
for i, expStrSeg := range tc.expSegments {
expSeg, errl := NewExecutionSegmentFromString(expStrSeg)
require.NoError(t, errl)
assert.Truef(t, expSeg.Equal(result[i]), "Segment %d (%s) should be equal to %s", i, result[i], expSeg)
}
if tc.canReverse {
assert.Equal(t, result.String(), tc.seq)
}
})
}
}

// TODO: test with randomized things

0 comments on commit a1f3802

Please sign in to comment.