Przeglądaj źródła

Implement missing Window methods and tests (#3305)

Signed-off-by: sneax <paladesh600@gmail.com>
segfault_bits 7 miesięcy temu
rodzic
commit
a6f28e05c0
2 zmienionych plików z 427 dodań i 97 usunięć
  1. 104 78
      core/pkg/opencost/window.go
  2. 323 19
      core/pkg/opencost/window_test.go

+ 104 - 78
core/pkg/opencost/window.go

@@ -383,7 +383,7 @@ func (w Window) Contains(t time.Time) bool {
 		return false
 	}
 
-	if w.end != nil && t.After(*w.end) {
+	if w.end != nil && !t.Before(*w.end) {
 		return false
 	}
 
@@ -584,83 +584,109 @@ func (w Window) Minutes() float64 {
 
 // Overlaps returns true iff the two given Windows share an amount of temporal
 // coverage.
-// TODO complete (with unit tests!) and then implement in AllocationSet.accumulate
-// TODO:CLEANUP
-// func (w Window) Overlaps(x Window) bool {
-// 	if (w.start == nil && w.end == nil) || (x.start == nil && x.end == nil) {
-// 		// one window is completely open, so overlap is guaranteed
-// 		// <---------->
-// 		//   ?------?
-// 		return true
-// 	}
-
-// 	// Neither window is completely open (nil, nil), but one or the other might
-// 	// still be future- or past-open.
-
-// 	if w.start == nil {
-// 		// w is past-open, future-closed
-// 		// <------]
-
-// 		if x.start != nil && !x.start.Before(*w.end) {
-// 			// x starts after w ends (or eq)
-// 			// <------]
-// 			//          [------?
-// 			return false
-// 		}
-
-// 		// <-----]
-// 		//    ?-----?
-// 		return true
-// 	}
-
-// 	if w.end == nil {
-// 		// w is future-open, past-closed
-// 		// [------>
-
-// 		if x.end != nil && !x.end.After(*w.end) {
-// 			// x ends before w begins (or eq)
-// 			//          [------>
-// 			// ?------]
-// 			return false
-// 		}
-
-// 		//    [------>
-// 		// ?------?
-// 		return true
-// 	}
-
-// 	// Now we know w is closed, but we don't know about x
-// 	//  [------]
-// 	//     ?------?
-// 	if x.start == nil {
-// 		// TODO
-// 	}
-
-// 	if x.end == nil {
-// 		// TODO
-// 	}
-
-// 	// Both are closed.
-
-// 	if !x.start.Before(*w.end) && !x.end.Before(*w.end) {
-// 		// x starts and ends after w ends
-// 		// [------]
-// 		//          [------]
-// 		return false
-// 	}
-
-// 	if !x.start.After(*w.start) && !x.end.After(*w.start) {
-// 		// x starts and ends before w starts
-// 		//          [------]
-// 		// [------]
-// 		return false
-// 	}
-
-// 	// w and x must overlap
-// 	//    [------]
-// 	// [------]
-// 	return true
-// }
+func (w Window) Overlaps(x Window) bool {
+	if (w.start == nil && w.end == nil) || (x.start == nil && x.end == nil) {
+		// one window is completely open, so overlap is guaranteed
+		// <---------->
+		//   ?------?
+		return true
+	}
+
+	// Neither window is completely open (nil, nil), but one or the other might
+	// still be future- or past-open.
+
+	if w.start == nil {
+		// w is past-open, future-closed
+		// <------]
+
+		if x.start != nil && !x.start.Before(*w.end) {
+			// x starts after w ends (or eq)
+			// <------]
+			//          [------?
+			return false
+		}
+
+		// <-----]
+		//    ?-----?
+		return true
+	}
+
+	if w.end == nil {
+		// w is future-open, past-closed
+		// [------>
+
+		if x.end != nil && !x.end.After(*w.start) {
+			// x ends before w begins (or eq)
+			//          [------>
+			// ?------]
+			return false
+		}
+
+		//    [------>
+		// ?------?
+		return true
+	}
+
+	// Now we know w is closed, but we don't know about x
+	//  [------]
+	//     ?------?
+	if x.start == nil {
+		// x is past-open, future-closed
+		// <------]
+		//    [------]
+
+		if !x.end.Before(*w.start) {
+			// x ends after w starts (or eq)
+			// <------]
+			//    [------]
+			return true
+		}
+
+		// x ends before w starts
+		// <------]
+		//          [------]
+		return false
+	}
+
+	if x.end == nil {
+		// x is future-open, past-closed
+		//    [------>
+		// [------]
+
+		if !x.start.After(*w.end) {
+			// x starts before w ends (or eq)
+			//    [------>
+			// [------]
+			return true
+		}
+
+		// x starts after w ends
+		//          [------>
+		// [------]
+		return false
+	}
+
+	// Both are closed.
+
+	if !x.start.Before(*w.end) && !x.end.Before(*w.end) {
+		// x starts and ends after w ends
+		// [------]
+		//          [------]
+		return false
+	}
+
+	if !x.start.After(*w.start) && !x.end.After(*w.start) {
+		// x starts and ends before w starts
+		//          [------]
+		// [------]
+		return false
+	}
+
+	// w and x must overlap
+	//    [------]
+	// [------]
+	return true
+}
 
 func (w *Window) Set(start, end *time.Time) {
 	w.start = start

+ 323 - 19
core/pkg/opencost/window_test.go

@@ -252,7 +252,31 @@ func TestParseWindowUTC(t *testing.T) {
 		t.Fatalf(`expect: window "month" to end before now; actual: %s ends after %s`, month, time.Now().UTC())
 	}
 
-	// TODO lastweek
+	lastweek, err := ParseWindowUTC("lastweek")
+	if err != nil {
+		t.Fatalf(`unexpected error parsing "lastweek": %s`, err)
+	}
+	
+	// Verify lastweek window spans exactly 7 days
+	if lastweek.Duration().Hours() != 7*24 {
+		t.Fatalf(`expect: window "lastweek" to have duration 7 days; actual: %f hours`, lastweek.Duration().Hours())
+	}
+	
+	// Verify lastweek starts on a Sunday (weekday 0)
+	if lastweek.Start().Weekday() != time.Sunday {
+		t.Fatalf(`expect: window "lastweek" to start on Sunday; actual: %s starts on %s`, lastweek, lastweek.Start().Weekday())
+	}
+	
+	// Verify lastweek ends on a Saturday (should be 7 days after start)
+	expectedEnd := lastweek.Start().Add(7 * 24 * time.Hour)
+	if !lastweek.End().Equal(expectedEnd) {
+		t.Fatalf(`expect: window "lastweek" to end 7 days after start; actual: start %s, end %s`, lastweek.Start(), lastweek.End())
+	}
+	
+	// Verify lastweek ends before now
+	if !lastweek.End().Before(time.Now().UTC()) {
+		t.Fatalf(`expect: window "lastweek" to end before now; actual: %s ends after %s`, lastweek, time.Now().UTC())
+	}
 
 	lastmonth, err := ParseWindowUTC("lastmonth")
 	monthMinHours := float64(24 * 28)
@@ -739,26 +763,244 @@ func TestWindow_Duration(t *testing.T) {
 
 }
 
-// TODO
-// func TestWindow_Overlaps(t *testing.T) {}
+func TestWindow_Overlaps(t *testing.T) {
+	t1 := time.Now().Round(time.Hour)
+	t2 := t1.Add(time.Hour)
+	t3 := t1.Add(30 * time.Minute)
+	t4 := t1.Add(90 * time.Minute)
+	t5 := t1.Add(2 * time.Hour)
+
+	cases := []struct {
+		window1  Window
+		window2  Window
+		expected bool
+	}{
+		{
+			window1:  NewClosedWindow(t1, t2),
+			window2:  NewClosedWindow(t3, t4),
+			expected: true, // Overlapping windows
+		},
+		{
+			window1:  NewClosedWindow(t1, t2),
+			window2:  NewClosedWindow(t4, t5),
+			expected: false, // Non-overlapping windows
+		},
+		{
+			window1:  NewClosedWindow(t1, t5),
+			window2:  NewClosedWindow(t2, t4),
+			expected: true, // One window completely inside another
+		},
+		{
+			window1:  NewWindow(nil, &t2),
+			window2:  NewClosedWindow(t3, t4),
+			expected: true, // Open window overlapping with closed window
+		},
+		{
+			window1:  NewWindow(&t1, nil),
+			window2:  NewClosedWindow(t2, t3),
+			expected: true, // Open window overlapping with closed window
+		},
+		{
+			window1:  NewWindow(nil, nil),
+			window2:  NewClosedWindow(t1, t2),
+			expected: true, // Completely open window overlaps any window
+		},
+	}
+
+	for _, c := range cases {
+		result := c.window1.Overlaps(c.window2)
+		if result != c.expected {
+			t.Errorf("Overlaps %s with %s, expected %v but got %v", c.window1, c.window2, c.expected, result)
+		}
+	}
+}
+
+func TestWindow_Contains(t *testing.T) {
+	t1 := time.Now().Round(time.Hour)
+	t2 := t1.Add(time.Hour)
+	t3 := t1.Add(30 * time.Minute)
+	t4 := t1.Add(2 * time.Hour)
+
+	cases := []struct {
+		window   Window
+		time     time.Time
+		expected bool
+	}{
+		{
+			window:   NewClosedWindow(t1, t2),
+			time:     t3,
+			expected: true, // Time inside window
+		},
+		{
+			window:   NewClosedWindow(t1, t2),
+			time:     t4,
+			expected: false, // Time after window
+		},
+		{
+			window:   NewClosedWindow(t1, t2),
+			time:     t1,
+			expected: true, // Time at start of window
+		},
+		{
+			window:   NewClosedWindow(t1, t2),
+			time:     t2,
+			expected: false, // Time at end of window (exclusive)
+		},
+		{
+			window:   NewWindow(nil, &t2),
+			time:     t1,
+			expected: true, // Time in open-start window
+		},
+		{
+			window:   NewWindow(&t1, nil),
+			time:     t4,
+			expected: true, // Time in open-end window
+		},
+	}
+
+	for _, c := range cases {
+		result := c.window.Contains(c.time)
+		if result != c.expected {
+			t.Errorf("Contains %s with %s, expected %v but got %v", c.window, c.time, c.expected, result)
+		}
+	}
+}
+
+func TestWindow_End(t *testing.T) {
+	t1 := time.Now().Round(time.Hour)
+	t2 := t1.Add(time.Hour)
+
+	// Test closed window
+	closedWindow := NewClosedWindow(t1, t2)
+	end := closedWindow.End()
+	if end == nil {
+		t.Fatalf("Expected end time, got nil")
+	}
+	if !end.Equal(t2) {
+		t.Errorf("Expected end time %s, got %s", t2, *end)
+	}
+
+	// Test open-end window
+	openEndWindow := NewWindow(&t1, nil)
+	end = openEndWindow.End()
+	if end != nil {
+		t.Errorf("Expected nil end time, got %s", *end)
+	}
+}
+
+func TestWindow_Equal(t *testing.T) {
+	t1 := time.Now().Round(time.Hour)
+	t2 := t1.Add(time.Hour)
+
+	cases := []struct {
+		window1  Window
+		window2  Window
+		expected bool
+	}{
+		{
+			window1:  NewClosedWindow(t1, t2),
+			window2:  NewClosedWindow(t1, t2),
+			expected: true, // Identical windows
+		},
+		{
+			window1:  NewClosedWindow(t1, t2),
+			window2:  NewClosedWindow(t1, t1.Add(30*time.Minute)),
+			expected: false, // Different end times
+		},
+		{
+			window1:  NewWindow(&t1, nil),
+			window2:  NewWindow(&t1, nil),
+			expected: true, // Identical open-end windows
+		},
+		{
+			window1:  NewWindow(nil, &t2),
+			window2:  NewWindow(&t1, &t2),
+			expected: false, // Different start times (nil vs specific)
+		},
+		{
+			window1:  NewWindow(nil, nil),
+			window2:  NewWindow(nil, nil),
+			expected: true, // Identical fully open windows
+		},
+	}
+
+	for _, c := range cases {
+		result := c.window1.Equal(c.window2)
+		if result != c.expected {
+			t.Errorf("Equal %s with %s, expected %v but got %v", c.window1, c.window2, c.expected, result)
+		}
+	}
+}
 
-// TODO
-// func TestWindow_Contains(t *testing.T) {}
+func TestWindow_ExpandStart(t *testing.T) {
+	t1 := time.Now().Round(time.Hour)
+	t2 := t1.Add(time.Hour)
+	t3 := t1.Add(-30 * time.Minute)
 
-// TODO
-// func TestWindow_Duration(t *testing.T) {}
+	cases := []struct {
+		window   Window
+		newStart time.Time
+		expected Window
+	}{
+		{
+			window:   NewClosedWindow(t1, t2),
+			newStart: t3,
+			expected: NewClosedWindow(t3, t2), // Earlier start time
+		},
+		{
+			window:   NewClosedWindow(t1, t2),
+			newStart: t1.Add(30 * time.Minute),
+			expected: NewClosedWindow(t1, t2), // Later start time, should not change
+		},
+		{
+			window:   NewWindow(nil, &t2),
+			newStart: t3,
+			expected: NewClosedWindow(t3, t2), // Set start time for open window
+		},
+	}
 
-// TODO
-// func TestWindow_End(t *testing.T) {}
+	for _, c := range cases {
+		result := c.window.ExpandStart(c.newStart)
+		if !result.Equal(c.expected) {
+			t.Errorf("ExpandStart %s with %s, expected %s but got %s", c.window, c.newStart, c.expected, result)
+		}
+	}
+}
 
-// TODO
-// func TestWindow_Equal(t *testing.T) {}
+func TestWindow_ExpandEnd(t *testing.T) {
+	t1 := time.Now().Round(time.Hour)
+	t2 := t1.Add(time.Hour)
+	t3 := t1.Add(2 * time.Hour)
 
-// TODO
-// func TestWindow_ExpandStart(t *testing.T) {}
+	cases := []struct {
+		window Window
+		newEnd time.Time
+		expected Window
+	}{
+		{
+			window:   NewClosedWindow(t1, t2),
+			newEnd:   t3,
+			expected: NewClosedWindow(t1, t3), // Later end time
+		},
+		{
+			window:   NewClosedWindow(t1, t2),
+			newEnd:   t1.Add(30 * time.Minute),
+			expected: NewClosedWindow(t1, t2), // Earlier end time, should not change
+		},
+		{
+			window:   NewWindow(&t1, nil),
+			newEnd:   t3,
+			expected: NewClosedWindow(t1, t3), // Set end time for open window
+		},
+	}
 
-// TODO
-// func TestWindow_ExpandEnd(t *testing.T) {}
+	for _, c := range cases {
+		result := c.window.ExpandEnd(c.newEnd)
+		if !result.Equal(c.expected) {
+			t.Errorf("ExpandEnd %s with %s, expected %s but got %s", c.window, c.newEnd, c.expected, result)
+		}
+	}
+}
 
 func TestWindow_Expand(t *testing.T) {
 
@@ -825,11 +1067,73 @@ func TestWindow_Expand(t *testing.T) {
 	}
 }
 
-// TODO
-// func TestWindow_Start(t *testing.T) {}
+func TestWindow_Start(t *testing.T) {
+	t1 := time.Now().Round(time.Hour)
+	t2 := t1.Add(time.Hour)
+
+	// Test closed window
+	closedWindow := NewClosedWindow(t1, t2)
+	if closedWindow.Start() == nil {
+		t.Errorf("Start() should not return nil for closed window")
+	}
+	if !closedWindow.Start().Equal(t1) {
+		t.Errorf("Start() = %s, expected %s", closedWindow.Start(), t1)
+	}
 
-// TODO
-// func TestWindow_String(t *testing.T) {}
+	// Test open-start window
+	openStartWindow := NewWindow(nil, &t2)
+	if openStartWindow.Start() != nil {
+		t.Errorf("Start() should return nil for open-start window, got %s", openStartWindow.Start())
+	}
+
+	// Test open-end window
+	openEndWindow := NewWindow(&t1, nil)
+	if openEndWindow.Start() == nil {
+		t.Errorf("Start() should not return nil for open-end window")
+	}
+	if !openEndWindow.Start().Equal(t1) {
+		t.Errorf("Start() = %s, expected %s", openEndWindow.Start(), t1)
+	}
+
+	// Test fully open window
+	fullyOpenWindow := NewWindow(nil, nil)
+	if fullyOpenWindow.Start() != nil {
+		t.Errorf("Start() should return nil for fully open window, got %s", fullyOpenWindow.Start())
+	}
+}
+
+func TestWindow_String(t *testing.T) {
+	t1 := time.Date(2023, 1, 1, 12, 30, 45, 0, time.UTC)
+	t2 := t1.Add(time.Hour)
+
+	// Test closed window
+	closedWindow := NewClosedWindow(t1, t2)
+	expected := "[2023-01-01T12:30:45+0000, 2023-01-01T13:30:45+0000)"
+	if closedWindow.String() != expected {
+		t.Errorf("String() = %s, expected %s", closedWindow.String(), expected)
+	}
+
+	// Test open-start window
+	openStartWindow := NewWindow(nil, &t2)
+	expected = "[nil, 2023-01-01T13:30:45+0000)"
+	if openStartWindow.String() != expected {
+		t.Errorf("String() = %s, expected %s", openStartWindow.String(), expected)
+	}
+
+	// Test open-end window
+	openEndWindow := NewWindow(&t1, nil)
+	expected = "[2023-01-01T12:30:45+0000, nil)"
+	if openEndWindow.String() != expected {
+		t.Errorf("String() = %s, expected %s", openEndWindow.String(), expected)
+	}
+
+	// Test fully open window
+	fullyOpenWindow := NewWindow(nil, nil)
+	expected = "[nil, nil)"
+	if fullyOpenWindow.String() != expected {
+		t.Errorf("String() = %s, expected %s", fullyOpenWindow.String(), expected)
+	}
+}
 
 func TestWindow_GetPercentInWindow(t *testing.T) {
 	dayStart := time.Date(2022, 12, 6, 0, 0, 0, 0, time.UTC)