-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathconftest.py
More file actions
266 lines (209 loc) · 7.45 KB
/
Copy pathconftest.py
File metadata and controls
266 lines (209 loc) · 7.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
import asyncio
import os
import sys
import types
from contextlib import suppress
from pathlib import Path
import pytest
from asyncssh import PermissionDenied
from herethere.everywhere import ConnectionConfig
from herethere.everywhere.loop import run_sync
from herethere.there.client import Client
from herethere.there.commands import ContextObject, there_group
from kivy.config import Config
from kivy.core.window import Window
from main import PythonHereApp, run_ssh_server
async def run_herethere_sync(awaitable):
"""Run sync magic work while the in-process SSH server keeps its test loop.
Use this only for code paths that exercise herethere's synchronous magic
bridge. Plain async client tests should await the client API directly.
"""
return await asyncio.to_thread(run_sync, awaitable)
@pytest.fixture
def connection_config(app_config):
return ConnectionConfig(
host="localhost",
port=app_config,
username="here",
password="there",
)
@pytest.fixture
def app_config(unused_tcp_port):
Config.read(str(Path(__file__).with_name("config.ini")))
Config.set("pythonhere", "port", str(unused_tcp_port))
return unused_tcp_port
@pytest.fixture
async def app_instance(mocker, capfd, app_config, tmpdir):
original_cwd = Path.cwd()
os.chdir(Path(__file__).parents[1] / "pythonhere")
mocker.patch("main.App.user_data_dir", tmpdir)
Window.size = (800, 600)
app = PythonHereApp()
app.init_asyncio_state()
app._on_ssh_connection_made = app.on_ssh_connection_made
app.on_ssh_connection_made = mocker.Mock()
app_task = asyncio.ensure_future(app.async_run_app())
server_task = asyncio.ensure_future(run_ssh_server(app))
await asyncio.wait_for(app.ssh_server_started.wait(), 5)
yield app
server_task.cancel()
app_task.cancel()
results = await asyncio.gather(app_task, server_task, return_exceptions=True)
for result in results:
if isinstance(result, BaseException) and not isinstance(
result, asyncio.CancelledError
):
raise result
app.root.clear_widgets()
Window.children.clear()
os.chdir(original_cwd)
@pytest.fixture
async def there(app_instance, connection_config):
client = Client()
await asyncio.wait_for(app_instance.ssh_server_started.wait(), 5)
await client.connect(connection_config)
try:
yield client
finally:
connection = client.connection.connection
await client.disconnect()
if connection is not None:
with suppress(Exception):
await connection.wait_closed()
@pytest.fixture
async def sync_there_client(app_instance, connection_config):
"""Client connected on herethere's sync magic loop.
Use with command/magic helpers that call herethere.there.commands, because
those commands call run_sync() internally and expect the client connection
to belong to herethere's background magic loop.
"""
client = Client()
await asyncio.wait_for(app_instance.ssh_server_started.wait(), 5)
await run_herethere_sync(client.connect(connection_config))
try:
yield client
finally:
connection = client.connection.connection
await run_herethere_sync(client.disconnect())
async def wait_closed():
if connection is not None:
await connection.wait_closed()
with suppress(Exception):
await run_herethere_sync(wait_closed())
@pytest.fixture
async def there_with_wrong_password(app_instance, connection_config):
client = Client()
connection_config.password = "nowhere"
await asyncio.wait_for(app_instance.ssh_server_started.wait(), 5)
with pytest.raises(PermissionDenied):
await client.connect(connection_config)
yield client
@pytest.fixture
async def call_there_group(app_instance, sync_there_client):
"""Call the synchronous %there command group from async tests.
The command itself is synchronous, so it is run in a worker thread. This
leaves pytest's event loop free to service the in-process PythonHere SSH
server which receives the command.
"""
async def _callable(args, code):
return await asyncio.to_thread(
there_group,
args,
"test",
standalone_mode=False,
obj=ContextObject(client=sync_there_client, code=code),
)
return _callable
@pytest.fixture
def preserve_cwd():
original_cwd = Path.cwd()
original_path = sys.path[:]
yield original_cwd
sys.path = original_path[:]
os.chdir(original_cwd)
@pytest.fixture
def mocked_android_modules(mocker):
"""Install a small fake Android/Jnius surface for Android-only code paths.
Keep this fake narrow: add methods/constants here only when tests exercise
the corresponding behavior in android_here or launcher_here.
"""
activity = mocker.Mock()
context = mocker.Mock()
app_info = mocker.Mock(icon=1)
manager = mocker.Mock()
manager.isRequestPinShortcutSupported.return_value = True
context.getApplicationInfo.return_value = app_info
activity.getApplicationContext.return_value = context
activity.getSystemService.return_value = manager
class Context:
SHORTCUT_SERVICE = "shortcut"
class Icon:
createWithResource = mocker.Mock(return_value=mocker.Mock())
class Intent:
FLAG_ACTIVITY_NEW_TASK = 1
FLAG_ACTIVITY_CLEAR_TASK = 2
ACTION_MAIN = "android.intent.action.MAIN"
def __init__(self, *args):
self.args = args
self.data = None
self.flags = None
self.action = None
def setAction(self, action):
self.action = action
return self
def setData(self, data):
self.data = data
return self
def setFlags(self, flags):
self.flags = flags
return self
def getData(self):
return self.data
class PythonActivity:
mActivity = activity
class ShortcutInfoBuilder:
def __init__(self, *args):
self.args = args
def setShortLabel(self, label):
self.short_label = label
return self
def setLongLabel(self, label):
self.long_label = label
return self
def setIntent(self, intent):
self.intent = intent
return self
def setIcon(self, icon):
self.icon = icon
return self
def build(self):
return self
class System:
exit = mocker.Mock()
class Uri:
@staticmethod
def parse(value):
uri = mocker.Mock()
uri.toString.return_value = value
return uri
classes = {
"android.content.Context": Context,
"android.graphics.drawable.Icon": Icon,
"android.content.Intent": Intent,
"org.kivy.android.PythonActivity": PythonActivity,
"android.content.pm.ShortcutInfo$Builder": ShortcutInfoBuilder,
"java.lang.System": System,
"android.net.Uri": Uri,
}
def autoclass(name):
return classes[name]
sys.modules["jnius"] = types.SimpleNamespace(
autoclass=autoclass,
cast=mocker.Mock(side_effect=lambda _class_name, obj: obj),
)
sys.modules["android"] = types.SimpleNamespace(activity=mocker.Mock())
@pytest.fixture
def test_py_script(app_instance):
path = Path(app_instance.upload_dir) / "test.py"
path.touch()
return str(path)