Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Execution segment sequences #1318

Merged
merged 2 commits into from
Feb 5, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ linters:
- gochecknoinits
- godox
- wsl
- gomnd
fast: false

service:
Expand Down
131 changes: 116 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,97 @@ 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) > 2 {
mstoykov marked this conversation as resolved.
Show resolved Hide resolved
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) {
var segments []*ExecutionSegment
if len(strSeq) != 0 {
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))
}
start := points[0]

segments = make([]*ExecutionSegment, 0, len(points)-1)
for _, point := range points[1:] {
segment, errl := NewExecutionSegmentFromString(start + ":" + point)
mstoykov marked this conversation as resolved.
Show resolved Hide resolved
if errl != nil {
return nil, errl
}
segments = append(segments, segment)
start = point
}
}

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")
}
83 changes: 66 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,51 @@ func TestSegmentExecutionFloatLength(t *testing.T) {
})
}
}

func TestExecutionSegmentSequences(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.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"}},
}

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