v0.11.1 15800b5

encoding_darwin_test.go RawHistoryFolder

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3
4package objc
5
6import (
7	"fmt"
8	"os"
9	"os/exec"
10	"path/filepath"
11	"reflect"
12	"strings"
13	"structs"
14	"testing"
15	"unsafe"
16)
17
18type encodeTypeTestStruct struct {
19	_ structs.HostLayout
20	A int32
21	B float64
22}
23
24var encodeTypeTests = []struct {
25	typ   reflect.Type
26	cType string
27	want  string
28}{
29	{reflect.TypeFor[bool](), "_Bool", "B"},
30	{reflect.TypeFor[int8](), "signed char", "c"},
31	{reflect.TypeFor[uint8](), "unsigned char", "C"},
32	{reflect.TypeFor[int16](), "short", "s"},
33	{reflect.TypeFor[uint16](), "unsigned short", "S"},
34	{reflect.TypeFor[int32](), "int", "i"},
35	{reflect.TypeFor[uint32](), "unsigned int", "I"},
36	{reflect.TypeFor[int64](), "long long", "q"},
37	{reflect.TypeFor[uint64](), "unsigned long long", "Q"},
38	{reflect.TypeFor[int](), "long", "q"},
39	{reflect.TypeFor[uint](), "unsigned long", "Q"},
40	{reflect.TypeFor[float32](), "float", "f"},
41	{reflect.TypeFor[float64](), "double", "d"},
42	{reflect.TypeFor[string](), "char *", "*"},
43	{reflect.TypeFor[unsafe.Pointer](), "void *", "^v"},
44	{reflect.TypeFor[*int32](), "int *", "^i"},
45	{reflect.TypeFor[**int32](), "int **", "^^i"},
46	{reflect.TypeFor[ID](), "id", "@"},
47	{reflect.TypeFor[Class](), "Class", "#"},
48	{reflect.TypeFor[SEL](), "SEL", ":"},
49	{reflect.TypeFor[encodeTypeTestStruct](), "struct encodeTypeTestStruct", "{encodeTypeTestStruct=id}"},
50}
51
52func TestEncodeType(t *testing.T) {
53	for _, tt := range encodeTypeTests {
54		got, err := encodeType(tt.typ, false)
55		if err != nil {
56			t.Errorf("encodeType(%v) returned error: %v", tt.typ, err)
57			continue
58		}
59		if got != tt.want {
60			t.Errorf("encodeType(%v) = %q; want %q (@encode(%s))", tt.typ, got, tt.want, tt.cType)
61		}
62	}
63}
64
65// TestEncodeTypeMatchesClang uses the C compiler as an oracle: it compiles a
66// program that prints @encode for each C type in encodeTypeTests and checks
67// that the expected encodings are the ones the compiler actually produces.
68func TestEncodeTypeMatchesClang(t *testing.T) {
69	out, err := exec.Command("go", "env", "CC").Output()
70	if err != nil {
71		t.Fatalf("go env CC: %v", err)
72	}
73	compiler := strings.TrimSpace(string(out))
74	if compiler == "" {
75		t.Skip("no C compiler to use as an @encode oracle")
76	}
77	if _, err := exec.LookPath(compiler); err != nil {
78		t.Skipf("no C compiler to use as an @encode oracle: %v", err)
79	}
80
81	var src strings.Builder
82	src.WriteString("#include <stdio.h>\n#include <objc/objc.h>\n")
83	src.WriteString("struct encodeTypeTestStruct { int a; double b; };\n")
84	src.WriteString("int main(void) {\n")
85	for _, tt := range encodeTypeTests {
86		fmt.Fprintf(&src, "\tprintf(\"%%s\\n\", @encode(%s));\n", tt.cType)
87	}
88	src.WriteString("\treturn 0;\n}\n")
89
90	dir := t.TempDir()
91	srcFile := filepath.Join(dir, "encode.m")
92	if err := os.WriteFile(srcFile, []byte(src.String()), 0o644); err != nil {
93		t.Fatal(err)
94	}
95
96	// Built for the host architecture rather than GOARCH, unlike the shared
97	// libraries in the root package's tests: every darwin target Go supports
98	// is LP64, so these encodings do not vary by architecture, and building
99	// for the host avoids needing Rosetta to run an amd64 binary on arm64.
100	exeFile := filepath.Join(dir, "encode")
101	cmd := exec.Command(compiler, "-Wall", "-Werror", "-o", exeFile, srcFile)
102	if out, err := cmd.CombinedOutput(); err != nil {
103		t.Fatalf("compile oracle: %v\n%q\n%s", err, cmd, out)
104	}
105
106	out, err = exec.Command(exeFile).Output()
107	if err != nil {
108		t.Fatalf("run oracle: %v", err)
109	}
110	lines := strings.Split(strings.TrimSuffix(string(out), "\n"), "\n")
111	if len(lines) != len(encodeTypeTests) {
112		t.Fatalf("oracle printed %d encodings; want %d", len(lines), len(encodeTypeTests))
113	}
114	for i, tt := range encodeTypeTests {
115		if lines[i] != tt.want {
116			t.Errorf("@encode(%s) = %q; want %q", tt.cType, lines[i], tt.want)
117		}
118	}
119}
120
121// TestEncodeFunc checks how encodeFunc assembles a method signature: the
122// return type, the implicit self and _cmd parameters, and the argument order.
123// encodeType is checked per type by TestEncodeType, so the cases here vary the
124// shape of the signature rather than the types in it. The first case is the
125// signature from issue #493, which was recorded as v@:LLLLLLLLS - declaring
126// 64-bit arguments as 32-bit made NSInvocation and other consumers of the type
127// encoding truncate values above 2^32.
128func TestEncodeFunc(t *testing.T) {
129	tests := []struct {
130		name string
131		fn   any
132		want string
133	}{
134		{
135			name: "void return, 64-bit args",
136			fn:   func(_ ID, _ SEL, a1, a2, a3, a4, a5, a6, a7, a8 uint64, a9 uint16) {},
137			want: "v@:QQQQQQQQS",
138		},
139		{
140			name: "value return, mixed integer kinds",
141			fn:   func(_ ID, _ SEL, a int, b int64, c uint, d uint64) int { return 0 },
142			want: "q@:qqQQ",
143		},
144		{
145			name: "no arguments",
146			fn:   func(_ ID, _ SEL) {},
147			want: "v@:",
148		},
149		{
150			name: "argument order is preserved",
151			fn:   func(_ ID, _ SEL, a int8, b float64, c ID) Class { return 0 },
152			want: "#@:cd@",
153		},
154	}
155	for _, tt := range tests {
156		t.Run(tt.name, func(t *testing.T) {
157			got, err := encodeFunc(tt.fn)
158			if err != nil {
159				t.Fatal(err)
160			}
161			if got != tt.want {
162				t.Errorf("encodeFunc = %q; want %q", got, tt.want)
163			}
164		})
165	}
166}
167
168func TestEncodeFuncErrors(t *testing.T) {
169	tests := []struct {
170		name string
171		fn   any
172	}{
173		{"not a func", 0},
174		{"too many return values", func(_ ID, _ SEL) (int, int) { return 0, 0 }},
175		{"missing self and _cmd", func() {}},
176		{"missing _cmd", func(_ ID) {}},
177		{"unencodable argument", func(_ ID, _ SEL, c chan int) {}},
178	}
179	for _, tt := range tests {
180		t.Run(tt.name, func(t *testing.T) {
181			if got, err := encodeFunc(tt.fn); err == nil {
182				t.Errorf("encodeFunc = %q; want an error", got)
183			}
184		})
185	}
186}

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 1

Tags 102

↑↓ moveenter openesc close