v0.1.0 c541a90

conversation_extra_linux_test.go RawHistoryFolder

1//go:build linux && (amd64 || arm64)
2
3package pam
4
5import (
6	"testing"
7	"time"
8	"unsafe"
9
10	"github.com/stretchr/testify/mock"
11	"github.com/stretchr/testify/require"
12)
13
14func TestManagedStartFailureRetiresCookie(t *testing.T) {
15	l, calls, _ := testLibrary(t)
16	handler := NewMockConversation(t)
17	var cookie uintptr
18	calls.EXPECT().Start(mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(_, _, conv, _ unsafe.Pointer) Status {
19		cookie = (*nativeCookieConversation)(conv).Cookie
20		return ServiceErr
21	}).Once()
22	tx, err := l.StartConversation("unit-service", "unit-user", handler)
23	require.Error(t, err)
24	require.Nil(t, tx)
25	require.Nil(t, acquireConversation(cookie))
26	require.Zero(t, l.active)
27	_, err = l.StartConversation("unit-service", "unit-user", nil)
28	require.ErrorIs(t, err, ErrInvalidArgument)
29	_, err = l.StartConversation("", "unit-user", handler)
30	require.ErrorIs(t, err, ErrInvalidArgument)
31}
32
33func TestNativeEndCanInvokeManagedConversation(t *testing.T) {
34	l, calls, handle := testLibrary(t)
35	handler := NewMockConversation(t)
36	messages, output := nativeMessages(t, l.memory, TextInfo)
37	var cookie uintptr
38	calls.EXPECT().Start(mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(_, _, c, out unsafe.Pointer) Status {
39		cookie = (*nativeCookieConversation)(c).Cookie
40		*(*unsafe.Pointer)(out) = handle
41		return Success
42	}).Once()
43	tx, err := l.StartConversation("unit-service", "", handler)
44	require.NoError(t, err)
45	handler.EXPECT().Respond(mock.Anything, mock.Anything).Return(0, nil).Once()
46	calls.EXPECT().End(handle, Success).RunAndReturn(func(_ unsafe.Pointer, _ Status) Status {
47		require.Equal(t, Success, invokeConversation(t, cookie, 1, messages, output))
48		l.memory.free(*(*unsafe.Pointer)(output))
49		return SystemErr
50	}).Once()
51	require.Error(t, tx.End())
52	require.Nil(t, acquireConversation(cookie))
53	require.Empty(t, tx.storage)
54	require.Zero(t, l.active)
55}
56
57func TestConcurrentEntriesAreSerializedAndRetirementWaits(t *testing.T) {
58	l, _, _ := testLibrary(t)
59	handler := NewMockConversation(t)
60	s := callbackState(t, handler, l.memory)
61	messages, output1 := nativeMessages(t, l.memory, TextInfo)
62	_, output2 := nativeMessages(t, l.memory, TextInfo)
63	entered, release := make(chan struct{}), make(chan struct{})
64	second := make(chan struct{}, 1)
65	handler.EXPECT().Respond(mock.Anything, mock.Anything).RunAndReturn(func(_ Message, _ []byte) (int, error) {
66		close(entered)
67		<-release
68		return 0, nil
69	}).Once()
70	handler.EXPECT().Respond(mock.Anything, mock.Anything).RunAndReturn(func(_ Message, _ []byte) (int, error) {
71		second <- struct{}{}
72		return 0, nil
73	}).Once()
74	done1, done2 := make(chan Status, 1), make(chan Status, 1)
75	go func() { done1 <- invokeConversation(t, s.token, 1, messages, output1) }()
76	select {
77	case <-entered:
78	case <-time.After(3 * time.Second):
79		t.Fatal("first callback not entered")
80	}
81	// Admit a second entry explicitly; it holds a registry reference until its
82	// callback finishes, so retirement must wait even if it closes admission.
83	admitted := acquireConversation(s.token)
84	require.NotNil(t, admitted)
85	go func() {
86		done2 <- invokeConversation(t, s.token, 1, messages, output2)
87		admitted.leave()
88	}()
89	select {
90	case <-second:
91		t.Fatal("handler reentered while first was active")
92	default:
93	}
94	// Do not retire until both actual callbacks finished: native calls above
95	// exercise serialization, while explicit admitted reference tests the wait.
96	close(release)
97	select {
98	case status := <-done1:
99		require.Equal(t, Success, status)
100	case <-time.After(3 * time.Second):
101		t.Fatal("first callback blocked")
102	}
103	select {
104	case status := <-done2:
105		require.Equal(t, Success, status)
106	case <-time.After(3 * time.Second):
107		t.Fatal("second callback blocked")
108	}
109	for _, output := range []unsafe.Pointer{output1, output2} {
110		l.memory.free(*(*unsafe.Pointer)(output))
111	}
112	held := acquireConversation(s.token)
113	require.NotNil(t, held)
114	retired := make(chan struct{})
115	go func() { s.retire(); close(retired) }()
116	select {
117	case <-retired:
118		t.Fatal("retirement ignored admitted reference")
119	default:
120	}
121	held.leave()
122	select {
123	case <-retired:
124	case <-time.After(3 * time.Second):
125		t.Fatal("retirement did not finish")
126	}
127	require.Nil(t, acquireConversation(s.token))
128}
129
130func TestMaximumAnswerAndMessageCount(t *testing.T) {
131	l, _, _ := testLibrary(t)
132	handler := NewMockConversation(t)
133	s := callbackState(t, handler, l.memory)
134	styles := make([]int32, MaxNumMsg)
135	for i := range styles {
136		styles[i] = PromptEchoOff
137	}
138	messages, output := nativeMessages(t, l.memory, styles...)
139	handler.EXPECT().Respond(mock.Anything, mock.Anything).RunAndReturn(func(_ Message, answer []byte) (int, error) {
140		for i := range answer {
141			answer[i] = 'x'
142		}
143		return MaxRespSize, nil
144	}).Times(MaxNumMsg)
145	require.Equal(t, Success, invokeConversation(t, s.token, MaxNumMsg, messages, output))
146	p := *(*unsafe.Pointer)(output)
147	for _, response := range unsafe.Slice((*nativeResponse)(p), MaxNumMsg) {
148		bytes := unsafe.Slice((*byte)(response.Text), MaxRespSize+1)
149		require.Equal(t, byte('x'), bytes[MaxRespSize-1])
150		require.Zero(t, bytes[MaxRespSize])
151		l.memory.free(response.Text)
152	}
153	l.memory.free(p)
154}

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 1

↑↓ moveenter openesc close