Skip to content

Commit d607f8a

Browse files
committed
Fix SplitAsset ending time, added tests.
1 parent 2e23a21 commit d607f8a

3 files changed

Lines changed: 193 additions & 1 deletion

File tree

marketplace/storage/BUILD.bazel

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
load("@rules_go//go:def.bzl", "go_library")
2+
load("//tools:go.bzl", "go_test")
23

34
go_library(
45
name = "go_default_library",
@@ -18,3 +19,14 @@ go_library(
1819
"//private/storage/db:go_default_library",
1920
],
2021
)
22+
23+
go_test(
24+
name = "go_default_test",
25+
srcs = ["split_test.go"],
26+
embed = [":go_default_library"],
27+
deps = [
28+
"//marketplace/db:go_default_library",
29+
"@com_github_stretchr_testify//assert:go_default_library",
30+
"@com_github_stretchr_testify//require:go_default_library",
31+
],
32+
)

marketplace/storage/split.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ func SplitAsset(asset *db.DBAsset, split RequestedSplit) (*SplitResult, error) {
110110
splitResult.Remainders = append(splitResult.Remainders, AssetSegment{
111111
Bandwidth: remainingAsset.Bandwidth,
112112
StartsAt: remainingAsset.StartsAt,
113-
StopsAt: split.ExactTo,
113+
StopsAt: split.ExactFrom,
114114
})
115115
} else {
116116
// a left remainder and a right remainder exists

marketplace/storage/split_test.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// Copyright 2026 ETH Zurich
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package storage
16+
17+
import (
18+
"testing"
19+
"time"
20+
21+
"github.com/stretchr/testify/assert"
22+
"github.com/stretchr/testify/require"
23+
24+
"github.com/scionproto/scion/marketplace/db"
25+
)
26+
27+
// TestSplitAsset covers carving a purchase out of an asset: what the buyer gets,
28+
// and what goes back on the market. The remainders must never overlap the
29+
// purchase, otherwise the same capacity could be sold twice.
30+
func TestSplitAsset(t *testing.T) {
31+
start := time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC)
32+
stop := start.Add(time.Hour)
33+
asset := func() *db.DBAsset {
34+
return &db.DBAsset{
35+
Bandwidth: 1000,
36+
StartAt: start,
37+
StopsAt: stop,
38+
}
39+
}
40+
41+
testCases := map[string]struct {
42+
split RequestedSplit
43+
expected AssetSegment
44+
remainders []AssetSegment
45+
}{
46+
"whole asset": {
47+
split: RequestedSplit{ExactFrom: start, ExactTo: stop, ExactBandwidth: 1000},
48+
expected: AssetSegment{StartsAt: start, StopsAt: stop, Bandwidth: 1000},
49+
},
50+
"tail, only a left remainder": {
51+
split: RequestedSplit{
52+
ExactFrom: start.Add(10 * time.Minute),
53+
ExactTo: stop,
54+
ExactBandwidth: 1000,
55+
},
56+
expected: AssetSegment{
57+
StartsAt: start.Add(10 * time.Minute),
58+
StopsAt: stop,
59+
Bandwidth: 1000,
60+
},
61+
remainders: []AssetSegment{
62+
{StartsAt: start, StopsAt: start.Add(10 * time.Minute), Bandwidth: 1000},
63+
},
64+
},
65+
"head, only a right remainder": {
66+
split: RequestedSplit{
67+
ExactFrom: start,
68+
ExactTo: start.Add(10 * time.Minute),
69+
ExactBandwidth: 1000,
70+
},
71+
expected: AssetSegment{
72+
StartsAt: start,
73+
StopsAt: start.Add(10 * time.Minute),
74+
Bandwidth: 1000,
75+
},
76+
remainders: []AssetSegment{
77+
{StartsAt: start.Add(10 * time.Minute), StopsAt: stop, Bandwidth: 1000},
78+
},
79+
},
80+
"middle, a left and a right remainder": {
81+
split: RequestedSplit{
82+
ExactFrom: start.Add(10 * time.Minute),
83+
ExactTo: start.Add(20 * time.Minute),
84+
ExactBandwidth: 1000,
85+
},
86+
expected: AssetSegment{
87+
StartsAt: start.Add(10 * time.Minute),
88+
StopsAt: start.Add(20 * time.Minute),
89+
Bandwidth: 1000,
90+
},
91+
remainders: []AssetSegment{
92+
{StartsAt: start, StopsAt: start.Add(10 * time.Minute), Bandwidth: 1000},
93+
{StartsAt: start.Add(20 * time.Minute), StopsAt: stop, Bandwidth: 1000},
94+
},
95+
},
96+
"part of the bandwidth of the whole asset": {
97+
split: RequestedSplit{ExactFrom: start, ExactTo: stop, ExactBandwidth: 400},
98+
expected: AssetSegment{StartsAt: start, StopsAt: stop, Bandwidth: 400},
99+
remainders: []AssetSegment{
100+
{StartsAt: start, StopsAt: stop, Bandwidth: 600},
101+
},
102+
},
103+
"part of the bandwidth of a middle slice": {
104+
split: RequestedSplit{
105+
ExactFrom: start.Add(10 * time.Minute),
106+
ExactTo: start.Add(20 * time.Minute),
107+
ExactBandwidth: 400,
108+
},
109+
expected: AssetSegment{
110+
StartsAt: start.Add(10 * time.Minute),
111+
StopsAt: start.Add(20 * time.Minute),
112+
Bandwidth: 400,
113+
},
114+
remainders: []AssetSegment{
115+
// The bandwidth that was not bought, for the whole window.
116+
{StartsAt: start, StopsAt: stop, Bandwidth: 600},
117+
{StartsAt: start, StopsAt: start.Add(10 * time.Minute), Bandwidth: 400},
118+
{StartsAt: start.Add(20 * time.Minute), StopsAt: stop, Bandwidth: 400},
119+
},
120+
},
121+
}
122+
123+
for name, tc := range testCases {
124+
t.Run(name, func(t *testing.T) {
125+
result, err := SplitAsset(asset(), tc.split)
126+
require.NoError(t, err)
127+
assert.Equal(t, tc.expected, result.Split)
128+
assert.ElementsMatch(t, tc.remainders, result.Remainders)
129+
130+
// What is sold and what is left must add up to the asset, so no
131+
// remainder may overlap the purchase in both time and bandwidth.
132+
for _, remainder := range result.Remainders {
133+
overlapsInTime := remainder.StartsAt.Before(result.Split.StopsAt) &&
134+
remainder.StopsAt.After(result.Split.StartsAt)
135+
if !overlapsInTime {
136+
continue
137+
}
138+
assert.Equal(t, asset().Bandwidth, remainder.Bandwidth+result.Split.Bandwidth,
139+
"remainder %v overlaps the purchase %v in time, so together they must "+
140+
"not exceed the bandwidth of the asset", remainder, result.Split)
141+
}
142+
})
143+
}
144+
}
145+
146+
// TestSplitAssetRejects checks that SplitAsset returns error when the asset cannot be split into
147+
// the required parts.
148+
func TestSplitAssetRejects(t *testing.T) {
149+
start := time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC)
150+
stop := start.Add(time.Hour)
151+
asset := &db.DBAsset{Bandwidth: 1000, StartAt: start, StopsAt: stop}
152+
153+
testCases := map[string]RequestedSplit{
154+
"starts before the asset": {
155+
ExactFrom: start.Add(-time.Second), ExactTo: stop, ExactBandwidth: 1000,
156+
},
157+
"stops after the asset": {
158+
ExactFrom: start, ExactTo: stop.Add(time.Second), ExactBandwidth: 1000,
159+
},
160+
"empty range": {
161+
ExactFrom: start, ExactTo: start, ExactBandwidth: 1000,
162+
},
163+
"reversed range": {
164+
ExactFrom: stop, ExactTo: start, ExactBandwidth: 1000,
165+
},
166+
"sub second precision": {
167+
ExactFrom: start.Add(time.Millisecond), ExactTo: stop, ExactBandwidth: 1000,
168+
},
169+
"more bandwidth than the asset has": {
170+
ExactFrom: start, ExactTo: stop, ExactBandwidth: 1001,
171+
},
172+
}
173+
174+
for name, split := range testCases {
175+
t.Run(name, func(t *testing.T) {
176+
_, err := SplitAsset(asset, split)
177+
assert.Error(t, err)
178+
})
179+
}
180+
}

0 commit comments

Comments
 (0)