v0.6.0 b250c5e

scene_test.go RawHistoryFolder

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
31// Owned assets keep only accounted memory: a slice with large spare capacity
32// is trimmed, a tight one is kept as is, and a rejected blob is not retained.
33func TestOwnedAssetsRetainOnlyAccountedBytes(t *testing.T) {
34	scene := NewScene(Limits{MaxEncodedBytes: 1 << 20, MaxDecodedPixels: 1 << 20})
35	loose := make([]byte, 4, 1<<16)
36	id, err := scene.AddAssetOwned(testAsset(t, loose, 1, 1))
37	if err != nil {
38		t.Fatal(err)
39	}
40	if got := scene.state.assets[id].encoded; cap(got) != len(got) || &got[0] == &loose[0] {
41		t.Fatalf("loose owned slice retained with cap %d for len %d", cap(got), len(got))
42	}
43	tight := []byte{1, 2, 3, 4}
44	id, err = scene.ReplaceAssetOwned(id, testAsset(t, tight, 1, 1))
45	if err != nil {
46		t.Fatal(err)
47	}
48	if got := scene.state.assets[id].encoded; &got[0] != &tight[0] {
49		t.Fatal("tight owned slice was copied")
50	}
51	before := scene.Snapshot()
52	rejected := make([]byte, 2<<20)
53	if _, err := scene.AddAssetOwned(testAsset(t, rejected, 1, 1)); !errors.Is(err, ErrEncodedBudget) {
54		t.Fatalf("over-budget owned asset error = %v", err)
55	}
56	for _, asset := range scene.state.assets {
57		if len(asset.encoded) > 0 && &asset.encoded[0] == &rejected[0] {
58			t.Fatal("rejected owned blob retained")
59		}
60	}
61	if scene.Snapshot().Usage() != before.Usage() {
62		t.Fatal("rejected owned blob changed usage")
63	}
64}
65
66func TestAssetBlobOwnershipAndSnapshotLifecycle(t *testing.T) {
67	scene := NewScene(Limits{MaxEncodedBytes: 32, MaxDecodedPixels: 64})
68	encoded := []byte{1, 2, 3}
69	assetID, err := scene.AddAsset(testAsset(t, encoded, 4, 4))
70	if err != nil {
71		t.Fatal(err)
72	}
73	encoded[0] = 99
74
75	before := scene.Snapshot()
76	asset, ok := before.Asset(assetID)
77	if !ok {
78		t.Fatal("asset missing from snapshot")
79	}
80	got := asset.Encoded()
81	got[1] = 88
82	if got := asset.Encoded(); got[0] != 1 || got[1] != 2 || asset.EncodedSize() != 3 {
83		t.Fatalf("asset bytes were not immutable copies: %v", got)
84	}
85
86	placementID, err := scene.Place(PlacementSpec{Asset: assetID, Destination: PixelRect{Width: 4, Height: 4}})
87	if err != nil {
88		t.Fatal(err)
89	}
90	if err := scene.RemoveAsset(assetID); !errors.Is(err, ErrAssetInUse) {
91		t.Fatalf("remove in-use asset error = %v", err)
92	}
93	if err := scene.RemoveAssetCascade(assetID); err != nil {
94		t.Fatal(err)
95	}
96	if _, ok := scene.Snapshot().Asset(assetID); ok {
97		t.Fatal("removed asset remained in current snapshot")
98	}
99	if _, ok := scene.Snapshot().Placement(placementID); ok {
100		t.Fatal("cascade did not remove placement")
101	}
102	if _, ok := before.Asset(assetID); !ok {
103		t.Fatal("old snapshot lost its asset after scene mutation")
104	}
105}
106
107func TestDecodedPixelAccountingRejectsForgedDeclarations(t *testing.T) {
108	scene := NewScene(Limits{MaxEncodedBytes: 64, MaxDecodedPixels: 4})
109	_, err := scene.AddAsset(AssetBlob{
110		Encoded:       []byte{1},
111		Width:         2,
112		Height:        2,
113		DecodedPixels: 1,
114	})
115	if !errors.Is(err, ErrInvalidAsset) {
116		t.Fatalf("forged decoded-pixel declaration error = %v", err)
117	}
118	if got := scene.Usage(); got != (Usage{}) {
119		t.Fatalf("rejected asset changed usage: %#v", got)
120	}
121}
122
123func TestAssetAndPlacementQuotasAreTransactional(t *testing.T) {
124	scene := NewScene(Limits{
125		MaxAssets:        1,
126		MaxPlacements:    1,
127		MaxEncodedBytes:  4,
128		MaxDecodedPixels: 4,
129	})
130	id, err := scene.AddAsset(testAsset(t, []byte{1, 2, 3, 4}, 2, 2))
131	if err != nil {
132		t.Fatal(err)
133	}
134	generation := scene.Snapshot().Generation()
135	if _, err := scene.AddAsset(testAsset(t, []byte{5}, 1, 1)); !errors.Is(err, ErrTooManyAssets) {
136		t.Fatalf("asset quota error = %v", err)
137	}
138	if scene.Snapshot().Generation() != generation {
139		t.Fatal("failed asset addition changed generation")
140	}
141	firstPlacement, err := scene.Place(PlacementSpec{Asset: id, Destination: PixelRect{Width: 2, Height: 2}})
142	if err != nil {
143		t.Fatal(err)
144	}
145	if _, err := scene.Place(PlacementSpec{Asset: id, Destination: PixelRect{X: 2, Width: 2, Height: 2}}); !errors.Is(err, ErrTooManyPlacements) {
146		t.Fatalf("placement quota error = %v", err)
147	}
148	if err := scene.Apply(
149		Operation{Kind: OperationRemovePlacement, PlacementID: firstPlacement},
150		Operation{Kind: OperationPlace, Placement: PlacementSpec{Asset: id, Destination: PixelRect{Width: 2, Height: 2}}},
151	); err != nil {
152		t.Fatal(err)
153	}
154	if got := scene.Usage(); got.Assets != 1 || got.Placements != 1 || got.EncodedBytes != 4 || got.DecodedPixels != 4 {
155		t.Fatalf("usage = %#v", got)
156	}
157}
158
159func TestSnapshotGenerationAndTransactionalApply(t *testing.T) {
160	scene := NewScene(Limits{MaxEncodedBytes: 64, MaxDecodedPixels: 64})
161	initial := scene.Snapshot()
162	id, err := scene.AddAsset(testAsset(t, []byte{1}, 2, 2))
163	if err != nil {
164		t.Fatal(err)
165	}
166	if got := scene.Snapshot().Generation(); got <= initial.Generation() {
167		t.Fatalf("generation did not advance: initial=%d current=%d", initial.Generation(), got)
168	}
169	before := scene.Snapshot()
170	err = scene.Apply(
171		Operation{Kind: OperationPlace, Placement: PlacementSpec{Asset: id, Destination: PixelRect{Width: 2, Height: 2}}},
172		Operation{Kind: OperationRemovePlacement, PlacementID: PlacementID{value: 999}},
173	)
174	if !errors.Is(err, ErrPlacementNotFound) {
175		t.Fatalf("transaction error = %v", err)
176	}
177	if scene.Snapshot().Generation() != before.Generation() || len(scene.Snapshot().Placements()) != 0 {
178		t.Fatal("failed transaction partially committed")
179	}
180}
181
182func TestVisibleFragmentsPreserveScaledSourceMapping(t *testing.T) {
183	scene := NewScene(Limits{MaxEncodedBytes: 64, MaxDecodedPixels: 10000})
184	assetID, err := scene.AddAsset(testAsset(t, []byte{1}, 100, 50))
185	if err != nil {
186		t.Fatal(err)
187	}
188	placementID, err := scene.Place(PlacementSpec{
189		Asset:       assetID,
190		Destination: PixelRect{X: 10, Y: 20, Width: 200, Height: 100},
191		Cells:       CellRect{X: 2, Y: 3, Width: 20, Height: 10},
192	})
193	if err != nil {
194		t.Fatal(err)
195	}
196	fragments := scene.Snapshot().VisibleFragments(PixelRect{X: 110, Y: 20, Width: 100, Height: 100})
197	if len(fragments) != 1 {
198		t.Fatalf("fragments = %#v", fragments)
199	}
200	fragment := fragments[0]
201	if fragment.PlacementID != placementID || fragment.AssetID != assetID {
202		t.Fatalf("fragment IDs = %#v", fragment)
203	}
204	if fragment.Destination != (PixelRect{X: 110, Y: 20, Width: 100, Height: 100}) {
205		t.Fatalf("destination = %#v", fragment.Destination)
206	}
207	if fragment.Source != (PixelRect{X: 50, Y: 0, Width: 50, Height: 50}) {
208		t.Fatalf("source mapping = %#v", fragment.Source)
209	}
210	if fragment.Cells != (CellRect{X: 12, Y: 3, Width: 10, Height: 10}) {
211		t.Fatalf("cell mapping = %#v", fragment.Cells)
212	}
213
214	cellFragments := scene.Snapshot().VisibleFragments(CellRect{X: 7, Y: 5, Width: 5, Height: 5})
215	if len(cellFragments) != 1 || cellFragments[0].Destination != (PixelRect{X: 60, Y: 40, Width: 50, Height: 50}) {
216		t.Fatalf("cell clipping = %#v", cellFragments)
217	}
218}
219
220func TestVisibleFragmentsRejectInvalidMixedViewport(t *testing.T) {
221	scene := NewScene(Limits{MaxEncodedBytes: 64, MaxDecodedPixels: 64})
222	assetID, err := scene.AddAsset(testAsset(t, []byte{1}, 2, 2))
223	if err != nil {
224		t.Fatal(err)
225	}
226	if _, err := scene.Place(PlacementSpec{
227		Asset:       assetID,
228		Destination: PixelRect{Width: 2, Height: 2},
229		Cells:       CellRect{Width: 2, Height: 2},
230	}); err != nil {
231		t.Fatal(err)
232	}
233	for _, viewport := range []Viewport{
234		{Pixels: PixelRect{Width: 2, Height: 2}, Cells: CellRect{Width: -1, Height: 1}},
235		{Pixels: PixelRect{Width: 2, Height: 2}, Cells: CellRect{X: 1}},
236		{Pixels: PixelRect{Width: -1, Height: 1}, Cells: CellRect{Width: 2, Height: 2}},
237	} {
238		if fragments := scene.Snapshot().VisibleFragments(viewport); len(fragments) != 0 {
239			t.Fatalf("invalid mixed viewport %#v produced fragments %#v", viewport, fragments)
240		}
241	}
242}
243
244func TestUpdatePlacementCanResetLayerAndClearCells(t *testing.T) {
245	scene := NewScene(Limits{MaxEncodedBytes: 64, MaxDecodedPixels: 64})
246	assetID, err := scene.AddAsset(testAsset(t, []byte{1}, 4, 4))
247	if err != nil {
248		t.Fatal(err)
249	}
250	placementID, err := scene.Place(PlacementSpec{
251		Asset:       assetID,
252		Destination: PixelRect{Width: 4, Height: 4},
253		Cells:       CellRect{X: 1, Y: 2, Width: 3, Height: 2},
254		Layer:       7,
255		HasLayer:    true,
256	})
257	if err != nil {
258		t.Fatal(err)
259	}
260	if err := scene.UpdatePlacement(placementID, PlacementSpec{HasCells: true, HasLayer: true}); err != nil {
261		t.Fatal(err)
262	}
263	placement, ok := scene.Snapshot().Placement(placementID)
264	if !ok {
265		t.Fatal("updated placement missing")
266	}
267	if placement.Layer() != 0 {
268		t.Fatalf("layer = %d, want 0", placement.Layer())
269	}
270	if placement.Cells() != (CellRect{}) {
271		t.Fatalf("cells = %#v, want cleared", placement.Cells())
272	}
273}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 13

↑↓ moveenter openesc close