github.com/onflow/flow-go@v0.33.17/consensus/hotstuff/cruisectl/transition_time_test.go (about)

     1  package cruisectl
     2  
     3  import (
     4  	"strings"
     5  	"testing"
     6  	"time"
     7  
     8  	"github.com/stretchr/testify/assert"
     9  	"pgregory.net/rapid"
    10  )
    11  
    12  // TestParseTransition_Valid tests that valid transition configurations have
    13  // consistent parsing and formatting behaviour.
    14  func TestParseTransition_Valid(t *testing.T) {
    15  	cases := []struct {
    16  		transition EpochTransitionTime
    17  		str        string
    18  	}{{
    19  		transition: EpochTransitionTime{time.Sunday, 0, 0},
    20  		str:        "sunday@00:00",
    21  	}, {
    22  		transition: EpochTransitionTime{time.Wednesday, 8, 1},
    23  		str:        "wednesday@08:01",
    24  	}, {
    25  		transition: EpochTransitionTime{time.Monday, 23, 59},
    26  		str:        "monday@23:59",
    27  	}, {
    28  		transition: EpochTransitionTime{time.Friday, 12, 21},
    29  		str:        "FrIdAy@12:21",
    30  	}}
    31  
    32  	for _, c := range cases {
    33  		t.Run(c.str, func(t *testing.T) {
    34  			// 1 - the computed string representation should match the string fixture
    35  			assert.Equal(t, strings.ToLower(c.str), c.transition.String())
    36  			// 2 - the parsed transition should match the transition fixture
    37  			parsed, err := ParseTransition(c.str)
    38  			assert.NoError(t, err)
    39  			assert.Equal(t, c.transition, *parsed)
    40  		})
    41  	}
    42  }
    43  
    44  // TestParseTransition_Invalid tests that a selection of invalid transition strings
    45  // fail validation and return an error.
    46  func TestParseTransition_Invalid(t *testing.T) {
    47  	cases := []string{
    48  		// invalid WD part
    49  		"sundy@12:00",
    50  		"tue@12:00",
    51  		"@12:00",
    52  		// invalid HH part
    53  		"wednesday@24:00",
    54  		"wednesday@1:00",
    55  		"wednesday@:00",
    56  		"wednesday@012:00",
    57  		// invalid MM part
    58  		"wednesday@12:60",
    59  		"wednesday@12:1",
    60  		"wednesday@12:",
    61  		"wednesday@12:030",
    62  		// otherwise invalid
    63  		"",
    64  		"@:",
    65  		"monday@@12:00",
    66  		"monday@09:00am",
    67  		"monday@09:00PM",
    68  		"monday12:00",
    69  		"monday12::00",
    70  		"wednesday@1200",
    71  	}
    72  
    73  	for _, transitionStr := range cases {
    74  		t.Run(transitionStr, func(t *testing.T) {
    75  			_, err := ParseTransition(transitionStr)
    76  			assert.Error(t, err)
    77  		})
    78  	}
    79  }
    80  
    81  // drawTransitionTime draws a random EpochTransitionTime.
    82  func drawTransitionTime(t *rapid.T) EpochTransitionTime {
    83  	day := time.Weekday(rapid.IntRange(0, 6).Draw(t, "wd"))
    84  	hour := rapid.Uint8Range(0, 23).Draw(t, "h")
    85  	minute := rapid.Uint8Range(0, 59).Draw(t, "m")
    86  	return EpochTransitionTime{day, hour, minute}
    87  }
    88  
    89  // TestInferTargetEndTime_Fixture is a single human-readable fixture test,
    90  // in addition to the property-based rapid tests.
    91  func TestInferTargetEndTime_Fixture(t *testing.T) {
    92  	// The target time is around midday Wednesday
    93  	// |S|M|T|W|T|F|S|
    94  	// |      *      |
    95  	ett := EpochTransitionTime{day: time.Wednesday, hour: 13, minute: 24}
    96  	// The current time is mid-morning on Friday. We are about 28% through the epoch in time terms
    97  	// |S|M|T|W|T|F|S|
    98  	// |          *  |
    99  	// Friday, November 20, 2020 11:44
   100  	curTime := time.Date(2020, 11, 20, 11, 44, 0, 0, time.UTC)
   101  	// We are 18% through the epoch in view terms - we are quite behind schedule
   102  	epochFractionComplete := .18
   103  	// We should still be able to infer the target switchover time:
   104  	// Wednesday, November 25, 2020 13:24
   105  	expectedTarget := time.Date(2020, 11, 25, 13, 24, 0, 0, time.UTC)
   106  	target := ett.inferTargetEndTime(curTime, epochFractionComplete)
   107  	assert.Equal(t, expectedTarget, target)
   108  }
   109  
   110  // TestInferTargetEndTime tests that we can infer "the most reasonable" target time.
   111  func TestInferTargetEndTime_Rapid(t *testing.T) {
   112  	rapid.Check(t, func(t *rapid.T) {
   113  		ett := drawTransitionTime(t)
   114  		curTime := time.Unix(rapid.Int64().Draw(t, "ref_unix"), 0).UTC()
   115  		epochFractionComplete := rapid.Float64Range(0, 1).Draw(t, "pct_complete")
   116  		epochFractionRemaining := 1.0 - epochFractionComplete
   117  
   118  		target := ett.inferTargetEndTime(curTime, epochFractionComplete)
   119  		computedEndTime := curTime.Add(time.Duration(float64(epochLength) * epochFractionRemaining))
   120  		// selected target must be the nearest to the computed end time
   121  		delta := computedEndTime.Sub(target).Abs()
   122  		assert.LessOrEqual(t, delta.Hours(), float64(24*7)/2)
   123  		// nearest date must be a target time
   124  		assert.Equal(t, ett.day, target.Weekday())
   125  		assert.Equal(t, int(ett.hour), target.Hour())
   126  		assert.Equal(t, int(ett.minute), target.Minute())
   127  	})
   128  }
   129  
   130  // TestFindNearestTargetTime tests finding the nearest target time to a reference time.
   131  func TestFindNearestTargetTime(t *testing.T) {
   132  	rapid.Check(t, func(t *rapid.T) {
   133  		ett := drawTransitionTime(t)
   134  		ref := time.Unix(rapid.Int64().Draw(t, "ref_unix"), 0).UTC()
   135  
   136  		nearest := ett.findNearestTargetTime(ref)
   137  		distance := nearest.Sub(ref).Abs()
   138  		// nearest date must be at most 1/2 a week away
   139  		assert.LessOrEqual(t, distance.Hours(), float64(24*7)/2)
   140  		// nearest date must be a target time
   141  		assert.Equal(t, ett.day, nearest.Weekday())
   142  		assert.Equal(t, int(ett.hour), nearest.Hour())
   143  		assert.Equal(t, int(ett.minute), nearest.Minute())
   144  	})
   145  }