1package graphics
2
3import (
4 "errors"
5 "math"
6 "testing"
7)
8
9func testAsset(t *testing.T, data []byte, width, height int64) AssetBlob {
10 t.Helper()
11 return AssetBlob{Encoded: data, Width: width, Height: height}
12}
13
14func TestRectOperationsRejectOverflow(t *testing.T) {
15 if _, err := NewPixelRect(math.MaxInt64, 0, 1, 1); !errors.Is(err, ErrInvalidRect) {
16 t.Fatalf("overflowing pixel rectangle error = %v", err)
17 }
18 if _, err := NewCellRect(0, math.MaxInt64, 1, 1); !errors.Is(err, ErrInvalidRect) {
19 t.Fatalf("overflowing cell rectangle error = %v", err)
20 }
21 valid := PixelRect{X: math.MinInt64, Y: math.MinInt64, Width: math.MaxInt64, Height: math.MaxInt64}
22 if !valid.Valid() {
23 t.Fatal("maximum representable negative-origin rectangle should be valid")
24 }
25 clipped, ok := valid.Intersect(PixelRect{X: -2, Y: -2, Width: 1, Height: 1})
26 if !ok || clipped != (PixelRect{X: -2, Y: -2, Width: 1, Height: 1}) {
27 t.Fatalf("intersection = %#v, %v", clipped, ok)
28 }
29}
30
31func TestAssetBlobOwnershipAndSnapshotLifecycle(t *testing.T) {
32 scene := NewScene(Limits{MaxEncodedBytes: 32, MaxDecodedPixels: 64})
33 encoded := []byte{1, 2, 3}
34 assetID, err := scene.AddAsset(testAsset(t, encoded, 4, 4))
35 if err != nil {
36 t.Fatal(err)
37 }
38 encoded[0] = 99
39
40 before := scene.Snapshot()
41 asset, ok := before.Asset(assetID)
42 if !ok {
43 t.Fatal("asset missing from snapshot")
44 }
45 got := asset.Encoded()
46 got[1] = 88
47 if got := asset.Encoded(); got[0] != 1 || got[1] != 2 || asset.EncodedSize() != 3 {
48 t.Fatalf("asset bytes were not immutable copies: %v", got)
49 }
50
51 placementID, err := scene.Place(PlacementSpec{Asset: assetID, Destination: PixelRect{Width: 4, Height: 4}})
52 if err != nil {
53 t.Fatal(err)
54 }
55 if err := scene.RemoveAsset(assetID); !errors.Is(err, ErrAssetInUse) {
56 t.Fatalf("remove in-use asset error = %v", err)
57 }
58 if err := scene.RemoveAssetCascade(assetID); err != nil {
59 t.Fatal(err)
60 }
61 if _, ok := scene.Snapshot().Asset(assetID); ok {
62 t.Fatal("removed asset remained in current snapshot")
63 }
64 if _, ok := scene.Snapshot().Placement(placementID); ok {
65 t.Fatal("cascade did not remove placement")
66 }
67 if _, ok := before.Asset(assetID); !ok {
68 t.Fatal("old snapshot lost its asset after scene mutation")
69 }
70}
71
72func TestDecodedPixelAccountingRejectsForgedDeclarations(t *testing.T) {
73 scene := NewScene(Limits{MaxEncodedBytes: 64, MaxDecodedPixels: 4})
74 _, err := scene.AddAsset(AssetBlob{
75 Encoded: []byte{1},
76 Width: 2,
77 Height: 2,
78 DecodedPixels: 1,
79 })
80 if !errors.Is(err, ErrInvalidAsset) {
81 t.Fatalf("forged decoded-pixel declaration error = %v", err)
82 }
83 if got := scene.Usage(); got != (Usage{}) {
84 t.Fatalf("rejected asset changed usage: %#v", got)
85 }
86}
87
88func TestAssetAndPlacementQuotasAreTransactional(t *testing.T) {
89 scene := NewScene(Limits{
90 MaxAssets: 1,
91 MaxPlacements: 1,
92 MaxEncodedBytes: 4,
93 MaxDecodedPixels: 4,
94 })
95 id, err := scene.AddAsset(testAsset(t, []byte{1, 2, 3, 4}, 2, 2))
96 if err != nil {
97 t.Fatal(err)
98 }
99 generation := scene.Snapshot().Generation()
100 if _, err := scene.AddAsset(testAsset(t, []byte{5}, 1, 1)); !errors.Is(err, ErrTooManyAssets) {
101 t.Fatalf("asset quota error = %v", err)
102 }
103 if scene.Snapshot().Generation() != generation {
104 t.Fatal("failed asset addition changed generation")
105 }
106 firstPlacement, err := scene.Place(PlacementSpec{Asset: id, Destination: PixelRect{Width: 2, Height: 2}})
107 if err != nil {
108 t.Fatal(err)
109 }
110 if _, err := scene.Place(PlacementSpec{Asset: id, Destination: PixelRect{X: 2, Width: 2, Height: 2}}); !errors.Is(err, ErrTooManyPlacements) {
111 t.Fatalf("placement quota error = %v", err)
112 }
113 if err := scene.Apply(
114 Operation{Kind: OperationRemovePlacement, PlacementID: firstPlacement},
115 Operation{Kind: OperationPlace, Placement: PlacementSpec{Asset: id, Destination: PixelRect{Width: 2, Height: 2}}},
116 ); err != nil {
117 t.Fatal(err)
118 }
119 if got := scene.Usage(); got.Assets != 1 || got.Placements != 1 || got.EncodedBytes != 4 || got.DecodedPixels != 4 {
120 t.Fatalf("usage = %#v", got)
121 }
122}
123
124func TestSnapshotGenerationAndTransactionalApply(t *testing.T) {
125 scene := NewScene(Limits{MaxEncodedBytes: 64, MaxDecodedPixels: 64})
126 initial := scene.Snapshot()
127 id, err := scene.AddAsset(testAsset(t, []byte{1}, 2, 2))
128 if err != nil {
129 t.Fatal(err)
130 }
131 if got := scene.Snapshot().Generation(); got <= initial.Generation() {
132 t.Fatalf("generation did not advance: initial=%d current=%d", initial.Generation(), got)
133 }
134 before := scene.Snapshot()
135 err = scene.Apply(
136 Operation{Kind: OperationPlace, Placement: PlacementSpec{Asset: id, Destination: PixelRect{Width: 2, Height: 2}}},
137 Operation{Kind: OperationRemovePlacement, PlacementID: PlacementID{value: 999}},
138 )
139 if !errors.Is(err, ErrPlacementNotFound) {
140 t.Fatalf("transaction error = %v", err)
141 }
142 if scene.Snapshot().Generation() != before.Generation() || len(scene.Snapshot().Placements()) != 0 {
143 t.Fatal("failed transaction partially committed")
144 }
145}
146
147func TestVisibleFragmentsPreserveScaledSourceMapping(t *testing.T) {
148 scene := NewScene(Limits{MaxEncodedBytes: 64, MaxDecodedPixels: 10000})
149 assetID, err := scene.AddAsset(testAsset(t, []byte{1}, 100, 50))
150 if err != nil {
151 t.Fatal(err)
152 }
153 placementID, err := scene.Place(PlacementSpec{
154 Asset: assetID,
155 Destination: PixelRect{X: 10, Y: 20, Width: 200, Height: 100},
156 Cells: CellRect{X: 2, Y: 3, Width: 20, Height: 10},
157 })
158 if err != nil {
159 t.Fatal(err)
160 }
161 fragments := scene.Snapshot().VisibleFragments(PixelRect{X: 110, Y: 20, Width: 100, Height: 100})
162 if len(fragments) != 1 {
163 t.Fatalf("fragments = %#v", fragments)
164 }
165 fragment := fragments[0]
166 if fragment.PlacementID != placementID || fragment.AssetID != assetID {
167 t.Fatalf("fragment IDs = %#v", fragment)
168 }
169 if fragment.Destination != (PixelRect{X: 110, Y: 20, Width: 100, Height: 100}) {
170 t.Fatalf("destination = %#v", fragment.Destination)
171 }
172 if fragment.Source != (PixelRect{X: 50, Y: 0, Width: 50, Height: 50}) {
173 t.Fatalf("source mapping = %#v", fragment.Source)
174 }
175 if fragment.Cells != (CellRect{X: 12, Y: 3, Width: 10, Height: 10}) {
176 t.Fatalf("cell mapping = %#v", fragment.Cells)
177 }
178
179 cellFragments := scene.Snapshot().VisibleFragments(CellRect{X: 7, Y: 5, Width: 5, Height: 5})
180 if len(cellFragments) != 1 || cellFragments[0].Destination != (PixelRect{X: 60, Y: 40, Width: 50, Height: 50}) {
181 t.Fatalf("cell clipping = %#v", cellFragments)
182 }
183}
184
185func TestVisibleFragmentsRejectInvalidMixedViewport(t *testing.T) {
186 scene := NewScene(Limits{MaxEncodedBytes: 64, MaxDecodedPixels: 64})
187 assetID, err := scene.AddAsset(testAsset(t, []byte{1}, 2, 2))
188 if err != nil {
189 t.Fatal(err)
190 }
191 if _, err := scene.Place(PlacementSpec{
192 Asset: assetID,
193 Destination: PixelRect{Width: 2, Height: 2},
194 Cells: CellRect{Width: 2, Height: 2},
195 }); err != nil {
196 t.Fatal(err)
197 }
198 for _, viewport := range []Viewport{
199 {Pixels: PixelRect{Width: 2, Height: 2}, Cells: CellRect{Width: -1, Height: 1}},
200 {Pixels: PixelRect{Width: 2, Height: 2}, Cells: CellRect{X: 1}},
201 {Pixels: PixelRect{Width: -1, Height: 1}, Cells: CellRect{Width: 2, Height: 2}},
202 } {
203 if fragments := scene.Snapshot().VisibleFragments(viewport); len(fragments) != 0 {
204 t.Fatalf("invalid mixed viewport %#v produced fragments %#v", viewport, fragments)
205 }
206 }
207}
208
209func TestUpdatePlacementCanResetLayerAndClearCells(t *testing.T) {
210 scene := NewScene(Limits{MaxEncodedBytes: 64, MaxDecodedPixels: 64})
211 assetID, err := scene.AddAsset(testAsset(t, []byte{1}, 4, 4))
212 if err != nil {
213 t.Fatal(err)
214 }
215 placementID, err := scene.Place(PlacementSpec{
216 Asset: assetID,
217 Destination: PixelRect{Width: 4, Height: 4},
218 Cells: CellRect{X: 1, Y: 2, Width: 3, Height: 2},
219 Layer: 7,
220 HasLayer: true,
221 })
222 if err != nil {
223 t.Fatal(err)
224 }
225 if err := scene.UpdatePlacement(placementID, PlacementSpec{HasCells: true, HasLayer: true}); err != nil {
226 t.Fatal(err)
227 }
228 placement, ok := scene.Snapshot().Placement(placementID)
229 if !ok {
230 t.Fatal("updated placement missing")
231 }
232 if placement.Layer() != 0 {
233 t.Fatalf("layer = %d, want 0", placement.Layer())
234 }
235 if placement.Cells() != (CellRect{}) {
236 t.Fatalf("cells = %#v, want cleared", placement.Cells())
237 }
238}