v0.4.1 03674e0

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
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}

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