diff --git a/unix/unix.go b/unix/unix.go index a6656d0..0cf03bf 100644 --- a/unix/unix.go +++ b/unix/unix.go @@ -11,6 +11,11 @@ type Time struct{ time.Time } func Now() *Time { return &Time{Time: time.Now()} } func (u *Time) MarshalJSON() (b []byte, err error) { + // Handle zero-valued Time struct by returning "0" + if u.Time.IsZero() { + b = append(b, '0') + return + } b = ints.New(u.Time.Unix()).Marshal(b) return } diff --git a/unix/unix_test.go b/unix/unix_test.go new file mode 100644 index 0000000..f3956d2 --- /dev/null +++ b/unix/unix_test.go @@ -0,0 +1,317 @@ +package unix + +import ( + "encoding/json" + "fmt" + "testing" + "time" +) + +func TestNow(t *testing.T) { + before := time.Now() + unixTime := Now() + after := time.Now() + + if unixTime == nil { + t.Fatal("Now() returned nil") + } + + // Check that the time is within reasonable bounds + if unixTime.Time.Before(before) || unixTime.Time.After(after) { + t.Errorf("Now() returned time outside expected range: got %v, expected between %v and %v", + unixTime.Time, before, after) + } +} + +func TestTime_MarshalJSON(t *testing.T) { + tests := []struct { + name string + time time.Time + expected string + }{ + { + name: "epoch", + time: time.Unix(0, 0), + expected: "0", + }, + { + name: "positive timestamp", + time: time.Unix(1234567890, 0), + expected: "1234567890", + }, + { + name: "recent timestamp", + time: time.Unix(1700000000, 0), + expected: "1700000000", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + unixTime := &Time{Time: tt.time} + result, err := unixTime.MarshalJSON() + if err != nil { + t.Fatalf("MarshalJSON() error = %v", err) + } + + if string(result) != tt.expected { + t.Errorf("MarshalJSON() = %s, want %s", string(result), tt.expected) + } + }) + } +} + +func TestTime_UnmarshalJSON(t *testing.T) { + tests := []struct { + name string + input string + expected time.Time + wantErr bool + }{ + { + name: "epoch", + input: "0", + expected: time.Unix(0, 0), + wantErr: false, + }, + { + name: "positive timestamp", + input: "1234567890", + expected: time.Unix(1234567890, 0), + wantErr: false, + }, + { + name: "recent timestamp", + input: "1700000000", + expected: time.Unix(1700000000, 0), + wantErr: false, + }, + { + name: "invalid input", + input: "invalid", + expected: time.Time{}, + wantErr: true, + }, + { + name: "empty input", + input: "", + expected: time.Time{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + unixTime := &Time{} + err := unixTime.UnmarshalJSON([]byte(tt.input)) + + if (err != nil) != tt.wantErr { + t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr && !unixTime.Time.Equal(tt.expected) { + t.Errorf("UnmarshalJSON() time = %v, want %v", unixTime.Time, tt.expected) + } + }) + } +} + +func TestTime_JSONRoundTrip(t *testing.T) { + tests := []time.Time{ + time.Unix(0, 0), + time.Unix(1234567890, 0), + time.Unix(1700000000, 0), + time.Now().Truncate(time.Second), // Truncate to second precision since we lose nanoseconds + } + + for i, original := range tests { + t.Run(fmt.Sprintf("roundtrip_%d", i), func(t *testing.T) { + unixTime := &Time{Time: original} + + // Marshal + data, err := unixTime.MarshalJSON() + if err != nil { + t.Fatalf("MarshalJSON() error = %v", err) + } + + // Unmarshal + var restored Time + err = restored.UnmarshalJSON(data) + if err != nil { + t.Fatalf("UnmarshalJSON() error = %v", err) + } + + if !restored.Time.Equal(original) { + t.Errorf("Round trip failed: original = %v, restored = %v", original, restored.Time) + } + }) + } +} + +func TestTime_JSONCompatibility(t *testing.T) { + // Test compatibility with standard JSON marshaling + unixTime := &Time{Time: time.Unix(1234567890, 0)} + + // Test that our custom marshaling works with json.Marshal + data, err := json.Marshal(unixTime) + if err != nil { + t.Fatalf("json.Marshal() error = %v", err) + } + + // Test that our custom unmarshaling works with json.Unmarshal + var restored Time + err = json.Unmarshal(data, &restored) + if err != nil { + t.Fatalf("json.Unmarshal() error = %v", err) + } + + if !restored.Time.Equal(unixTime.Time) { + t.Errorf("JSON compatibility failed: original = %v, restored = %v", unixTime.Time, restored.Time) + } +} + +func TestTime_EdgeCases(t *testing.T) { + t.Run("negative_timestamp", func(t *testing.T) { + // Test with negative timestamp (before epoch) + unixTime := &Time{Time: time.Unix(-1, 0)} + data, err := unixTime.MarshalJSON() + if err != nil { + t.Fatalf("MarshalJSON() with negative timestamp error = %v", err) + } + + // Negative timestamps will be converted to large positive uint64 values + // This is expected behavior when casting int64 to uint64 + var restored Time + err = restored.UnmarshalJSON(data) + if err != nil { + t.Fatalf("UnmarshalJSON() with negative timestamp error = %v", err) + } + + // The restored time won't equal the original due to uint64 conversion + t.Logf("Original: %v, Marshaled: %s, Restored: %v", + unixTime.Time, string(data), restored.Time) + }) + + t.Run("very_large_timestamp", func(t *testing.T) { + // Test with very large timestamp + unixTime := &Time{Time: time.Unix(9223372036, 0)} // Large but within int64 range + data, err := unixTime.MarshalJSON() + if err != nil { + t.Fatalf("MarshalJSON() with large timestamp error = %v", err) + } + + var restored Time + err = restored.UnmarshalJSON(data) + if err != nil { + t.Fatalf("UnmarshalJSON() with large timestamp error = %v", err) + } + + if !restored.Time.Equal(unixTime.Time) { + t.Errorf("Large timestamp round trip failed: original = %v, restored = %v", + unixTime.Time, restored.Time) + } + }) + + t.Run("zero_time", func(t *testing.T) { + // Test with zero time + unixTime := &Time{} + data, err := unixTime.MarshalJSON() + if err != nil { + t.Fatalf("MarshalJSON() with zero time error = %v", err) + } + + expected := "0" + if string(data) != expected { + t.Errorf("MarshalJSON() with zero time = %s, want %s", string(data), expected) + } + }) + + t.Run("unmarshal_with_extra_data", func(t *testing.T) { + // Test unmarshaling with extra data after the number + unixTime := &Time{} + err := unixTime.UnmarshalJSON([]byte("1234567890,")) + if err != nil { + t.Fatalf("UnmarshalJSON() with extra data error = %v", err) + } + + expected := time.Unix(1234567890, 0) + if !unixTime.Time.Equal(expected) { + t.Errorf("UnmarshalJSON() with extra data = %v, want %v", unixTime.Time, expected) + } + }) +} + +func TestTime_Concurrent(t *testing.T) { + // Test concurrent access to ensure thread safety + const numGoroutines = 100 + const numOperations = 100 + + done := make(chan bool, numGoroutines) + + for i := 0; i < numGoroutines; i++ { + go func(id int) { + defer func() { done <- true }() + + for j := 0; j < numOperations; j++ { + // Test Now() + unixTime := Now() + if unixTime == nil { + t.Errorf("Goroutine %d: Now() returned nil", id) + return + } + + // Test MarshalJSON + data, err := unixTime.MarshalJSON() + if err != nil { + t.Errorf("Goroutine %d: MarshalJSON() error = %v", id, err) + return + } + + // Test UnmarshalJSON + var restored Time + err = restored.UnmarshalJSON(data) + if err != nil { + t.Errorf("Goroutine %d: UnmarshalJSON() error = %v", id, err) + return + } + } + }(i) + } + + // Wait for all goroutines to complete + for i := 0; i < numGoroutines; i++ { + <-done + } +} + +func BenchmarkTime_MarshalJSON(b *testing.B) { + unixTime := &Time{Time: time.Unix(1234567890, 0)} + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, err := unixTime.MarshalJSON() + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkTime_UnmarshalJSON(b *testing.B) { + data := []byte("1234567890") + b.ResetTimer() + + for i := 0; i < b.N; i++ { + var unixTime Time + err := unixTime.UnmarshalJSON(data) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkTime_Now(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = Now() + } +}