Skip to content

Commit

Permalink
chore: tests for jamtime
Browse files Browse the repository at this point in the history
  • Loading branch information
aranw committed Jul 29, 2024
1 parent af4032f commit 2d68901
Show file tree
Hide file tree
Showing 6 changed files with 544 additions and 29 deletions.
30 changes: 9 additions & 21 deletions internal/jamtime/epoch.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,11 @@ const (
// Epoch represents a JAM Epoch
type Epoch uint32

// ToEpoch converts a JamTime to its corresponding Epoch
func (jt JamTime) ToEpoch() Epoch {
return Epoch(jt.Seconds / uint64(EpochDuration.Seconds()))
}

// FromEpoch creates a JamTime from an Epoch (start of the epoch)
func FromEpoch(e Epoch) JamTime {
return JamTime{Seconds: uint64(e) * uint64(EpochDuration.Seconds())}
}

// ToEpoch converts a Timeslot to its corresponding Epoch
func (ts Timeslot) ToEpoch() Epoch {
return Epoch(ts / TimeslotsPerEpoch)
}

// CurrentEpoch returns the current epoch
func CurrentEpoch() Epoch {
return Now().ToEpoch()
Expand All @@ -45,25 +35,23 @@ func (e Epoch) EpochEnd() JamTime {

// NextEpoch returns the next epoch
func (e Epoch) NextEpoch() Epoch {
maxEpoch := Epoch((1<<32 - 1) / TimeslotsPerEpoch)
if e == maxEpoch {
return 0
}
return e + 1
}

// PreviousEpoch returns the previous epoch
func (e Epoch) PreviousEpoch() Epoch {
return e - 1
return e - 1 // This will naturally wrap around at 0
}

// ValidateEpoch checks if a given Epoch is within the valid range
func ValidateEpoch(e Epoch) error {
jamTime := FromEpoch(e)
return ValidateJamTime(jamTime.ToTime())
}

// EpochAndTimeslotToJamTime converts an Epoch and a timeslot within that epoch to JamTime
func EpochAndTimeslotToJamTime(e Epoch, timeslotInEpoch uint32) (JamTime, error) {
if timeslotInEpoch >= TimeslotsPerEpoch {
return JamTime{}, errors.New("timeslot number exceeds epoch length")
maxEpoch := Epoch((1<<32 - 1) / TimeslotsPerEpoch)
if e > maxEpoch {
return errors.New("epoch is after maximum representable JAM time")
}
epochStart := FromEpoch(e)
return JamTime{Seconds: epochStart.Seconds + uint64(timeslotInEpoch)*uint64(TimeslotDuration.Seconds())}, nil
return nil
}
122 changes: 122 additions & 0 deletions internal/jamtime/epoch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package jamtime

import (
"testing"
"time"

"github.com/go-quicktest/qt"
)

func TestEpoch_FromEpoch(t *testing.T) {
t.Run("first epoch", func(t *testing.T) {
jt := FromEpoch(0)
qt.Assert(t, qt.Equals(jt.Seconds, 0))
})

t.Run("arbitrary epoch", func(t *testing.T) {
jt := FromEpoch(100)
qt.Assert(t, qt.Equals(jt.Seconds, 360000)) // 100 * 3600
})

t.Run("last possible epoch", func(t *testing.T) {
jt := FromEpoch(7158278)
qt.Assert(t, qt.Equals(jt.Seconds, 25769800800)) // 7158278 * 3600
})
}

func TestEpoch_ToEpoch(t *testing.T) {
t.Run("start of first epoch", func(t *testing.T) {
jt := JamTime{Seconds: 0}
epoch := jt.ToEpoch()
qt.Assert(t, qt.Equals(epoch, 0))
})

t.Run("middle of arbitrary epoch", func(t *testing.T) {
jt := JamTime{Seconds: 3601} // 1 second into the second epoch
epoch := jt.ToEpoch()
qt.Assert(t, qt.Equals(epoch, 1))
})

t.Run("end of last possible epoch", func(t *testing.T) {
jt := JamTime{Seconds: 25769803199} // Last second of the last epoch
epoch := jt.ToEpoch()
qt.Assert(t, qt.Equals(epoch, 7158278))
})
}

func TestEpoch_CurrentEpoch(t *testing.T) {
currentEpoch := CurrentEpoch()
now := time.Now().UTC()
expectedEpoch := Epoch((now.Unix() - JamEpoch.Unix()) / 3600)
qt.Assert(t, qt.Equals(currentEpoch, expectedEpoch))
}

func TestEpoch_EpochStart(t *testing.T) {
t.Run("first epoch", func(t *testing.T) {
start := Epoch(0).EpochStart()
qt.Assert(t, qt.Equals(start.Seconds, 0))
})

t.Run("arbitrary epoch", func(t *testing.T) {
start := Epoch(100).EpochStart()
qt.Assert(t, qt.Equals(start.Seconds, 360000)) // 100 * 3600
})
}

func TestEpoch_EpochEnd(t *testing.T) {
t.Run("first epoch", func(t *testing.T) {
end := Epoch(0).EpochEnd()
qt.Assert(t, qt.Equals(end.Seconds, 3600))
})

t.Run("arbitrary epoch", func(t *testing.T) {
end := Epoch(100).EpochEnd()
qt.Assert(t, qt.Equals(end.Seconds, 363600)) // (100 * 3600) + 3599
})
}

func TestEpoch_NextEpoch(t *testing.T) {
t.Run("from first epoch", func(t *testing.T) {
next := Epoch(0).NextEpoch()
qt.Assert(t, qt.Equals(next, 1))
})

t.Run("from last possible epoch", func(t *testing.T) {
lastEpoch := Epoch((1<<32 - 1) / TimeslotsPerEpoch)
next := lastEpoch.NextEpoch()
qt.Assert(t, qt.DeepEquals(next, 0))
})
}

func TestEpoch_PreviousEpoch(t *testing.T) {
t.Run("from second epoch", func(t *testing.T) {
prev := Epoch(1).PreviousEpoch()
qt.Assert(t, qt.Equals(prev, 0))
})

t.Run("from first epoch", func(t *testing.T) {
prev := Epoch(0).PreviousEpoch()
lastEpoch := Epoch((1<<32 - 1) / TimeslotsPerEpoch)
qt.Assert(t, qt.Not(qt.Equals(prev, lastEpoch)))
})
}

func TestEpoch_ValidateEpoch(t *testing.T) {
t.Run("valid epoch", func(t *testing.T) {
err := ValidateEpoch(1000)
qt.Assert(t, qt.IsNil(err))
})

t.Run("max valid epoch", func(t *testing.T) {
maxEpoch := Epoch((1<<32 - 1) / TimeslotsPerEpoch)
err := ValidateEpoch(maxEpoch)
qt.Assert(t, qt.IsNil(err))
})

t.Run("Epoch too large", func(t *testing.T) {
maxEpoch := Epoch((1<<32 - 1) / TimeslotsPerEpoch)
err := ValidateEpoch(maxEpoch + 1)
qt.Assert(t, qt.IsNotNil(err))
qt.Assert(t, qt.ErrorMatches(err, "epoch is after maximum representable JAM time"))
})
}
26 changes: 24 additions & 2 deletions internal/jamtime/jamtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
var now = time.Now

// JamEpoch represents the start of the JAM Common Era
// 2024-01-01 12:00:00
var JamEpoch = time.Date(2024, time.January, 1, 12, 0, 0, 0, time.UTC)

// JamTime represents a time in the JAM Common Era
Expand All @@ -28,6 +29,15 @@ func FromTime(t time.Time) JamTime {
return JamTime{Seconds: seconds}
}

// EpochAndTimeslotToJamTime converts an Epoch and a timeslot within that epoch to JamTime
func EpochAndTimeslotToJamTime(e Epoch, timeslot Timeslot) (JamTime, error) {
if timeslot >= TimeslotsPerEpoch {
return JamTime{}, errors.New("timeslot number exceeds epoch length")
}
epochStart := FromEpoch(e)
return JamTime{Seconds: epochStart.Seconds + uint64(timeslot)*uint64(TimeslotDuration.Seconds())}, nil
}

// ToTime converts a JamTime to a standard time.Time
func (jt JamTime) ToTime() time.Time {
duration := time.Duration(jt.Seconds) * time.Second
Expand Down Expand Up @@ -74,6 +84,12 @@ func (jt JamTime) ToTimeslot() Timeslot {
return Timeslot(jt.Seconds / uint64(TimeslotDuration.Seconds()))
}

// IsZero reports whether jt represents the zero time instant,
// IsZero is true when the date and time equal to 2024-01-01 12:00:00
func (jt JamTime) IsZero() bool {
return jt.Seconds == 0
}

// MarshalJSON implements the json.Marshaler interface
func (jt JamTime) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"%s"`, []byte(jt.ToTime().Format(time.RFC3339)))), nil
Expand All @@ -90,18 +106,24 @@ func (jt *JamTime) UnmarshalJSON(data []byte) error {
}

// JamTimeToEpochAndTimeslot converts a JamTime to its Epoch and timeslot within that epoch
func (jt JamTime) ToEpochAndTimeslot() (Epoch, uint32) {
func (jt JamTime) ToEpochAndTimeslot() (Epoch, Timeslot) {
epoch := jt.ToEpoch()
timeslotInEpoch := uint32((jt.Seconds / uint64(TimeslotDuration.Seconds())) % TimeslotsPerEpoch)
return epoch, timeslotInEpoch
return epoch, Timeslot(timeslotInEpoch)
}

// IsInSameEpoch checks if two JamTimes are in the same epoch
func (jt JamTime) IsInSameEpoch(other JamTime) bool {
return jt.ToEpoch() == other.ToEpoch()
}

// ToEpoch converts a JamTime to its corresponding Epoch
func (jt JamTime) ToEpoch() Epoch {
return Epoch(jt.Seconds / uint64(EpochDuration.Seconds()))
}

// ValidateJamTime checks if a given time.Time is within the valid range for JamTime
// Returns nil if valid and non-nil err if the given time.Time is outside the valid range for JamTime
func ValidateJamTime(t time.Time) error {
if t.Before(JamEpoch) {
return errors.New("time is before JAM Epoch")
Expand Down
Loading

0 comments on commit 2d68901

Please sign in to comment.