7 |
8 | Availing himself of the mild, summer-cool weather that now reigned in these latitudes, and in preparation for the peculiarly active pursuits shortly to be anticipated, Perth, the begrimed, blistered old blacksmith, had not removed his portable forge to the hold again, after concluding his contributory work for Ahab's leg, but still retained it on deck, fast lashed to ringbolts by the foremast; being now almost incessantly invoked by the headsmen, and harpooneers, and bowsmen to do some little job for them; altering, or repairing, or new shaping their various weapons and boat furniture. Often he would be surrounded by an eager circle, all waiting to be served; holding boat-spades, pike-heads, harpoons, and lances, and jealously watching his every sooty movement, as he toiled. Nevertheless, this old man's was a patient hammer wielded by a patient arm. No murmur, no impatience, no petulance did come from him. Silent, slow, and solemn; bowing over still further his chronically broken back, he toiled away, as if toil were life itself, and the heavy beating of his hammer the heavy beating of his heart. And so it was.—Most miserable! A peculiar walk in this old man, a certain slight but painful appearing yawing in his gait, had at an early period of the voyage excited the curiosity of the mariners. And to the importunity of their persisted questionings he had finally given in; and so it came to pass that every one now knew the shameful story of his wretched fate. Belated, and not innocently, one bitter winter's midnight, on the road running between two country towns, the blacksmith half-stupidly felt the deadly numbness stealing over him, and sought refuge in a leaning, dilapidated barn. The issue was, the loss of the extremities of both feet. Out of this revelation, part by part, at last came out the four acts of the gladness, and the one long, and as yet uncatastrophied fifth act of the grief of his life's drama. He was an old man, who, at the age of nearly sixty, had postponedly encountered that thing in sorrow's technicals called ruin. He had been an artisan of famed excellence, and with plenty to do; owned a house and garden; embraced a youthful, daughter-like, loving wife, and three blithe, ruddy children; every Sunday went to a cheerful-looking church, planted in a grove. But one night, under cover of darkness, and further concealed in a most cunning disguisement, a desperate burglar slid into his happy home, and robbed them all of everything. And darker yet to tell, the blacksmith himself did ignorantly conduct this burglar into his family's heart. It was the Bottle Conjuror! Upon the opening of that fatal cork, forth flew the fiend, and shrivelled up his home. Now, for prudent, most wise, and economic reasons, the blacksmith's shop was in the basement of his dwelling, but with a separate entrance to it; so that always had the young and loving healthy wife listened with no unhappy nervousness, but with vigorous pleasure, to the stout ringing of her young-armed old husband's hammer; whose reverberations, muffled by passing through the floors and walls, came up to her, not unsweetly, in her nursery; and so, to stout Labor's iron lullaby, the blacksmith's infants were rocked to slumber. Oh, woe on woe! Oh, Death, why canst thou not sometimes be timely? Hadst thou taken this old blacksmith to thyself ere his full ruin came upon him, then had the young widow had a delicious grief, and her orphans a truly venerable, legendary sire to dream of in their after years; and all of them a care-killing competency.
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/tests/test_chrome.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import copy
3 | import json
4 | import logging
5 | import queue
6 |
7 | import pytest
8 | import websockets
9 | from websockets.exceptions import ConnectionClosed
10 |
11 |
12 | from chromewhip import chrome, helpers
13 | from chromewhip.protocol import page, network
14 |
15 | TEST_HOST = 'localhost'
16 | TEST_PORT = 32322
17 |
18 | logging.basicConfig(level=logging.DEBUG)
19 | log = logging.getLogger(__name__)
20 |
21 |
22 | class ChromeMock:
23 |
24 | def __init__(self, host, port):
25 | self._tabs = []
26 |
27 | async def connect(self):
28 | tab = chrome.ChromeTab('test', 'about:blank', f'ws://{TEST_HOST}:{TEST_PORT}', '123')
29 | self._tabs = [tab]
30 |
31 | @property
32 | def tabs(self):
33 | return self._tabs
34 |
35 |
36 | @pytest.fixture
37 | async def chrome_tab():
38 | """Ensure Chrome is running
39 | """
40 | browser = ChromeMock(host=TEST_HOST, port=TEST_PORT)
41 | await browser.connect()
42 | chrome_tab = browser.tabs[0]
43 | yield chrome_tab
44 | print("gracefully disconnecting chrome tab...")
45 | try:
46 | await chrome_tab.disconnect()
47 | except ConnectionClosed:
48 | pass
49 |
50 | delay_s = float
51 |
52 |
53 | def init_test_server(triggers: dict, initial_msgs: [dict] = None, expected: queue.Queue = None):
54 | """
55 | :param initial_msgs:
56 | :param triggers:
57 | :param expected: ordered sequence of messages expected to be sent by chromewhip
58 | :return:
59 | """
60 | async def test_server(websocket, path):
61 | """
62 | :param websocket:
63 | :param path:
64 | :return:
65 | """
66 | log.info('Client connected! Starting handler!')
67 | if initial_msgs:
68 | for m in initial_msgs:
69 | await websocket.send(json.dumps(m, cls=helpers.ChromewhipJSONEncoder))
70 |
71 | c = 0
72 |
73 | try:
74 | while True:
75 | msg = await websocket.recv()
76 | log.info('Test server received message!')
77 | c += 1
78 | obj = json.loads(msg)
79 |
80 | if expected:
81 | try:
82 | exp = expected.get(block=False)
83 | except queue.Empty:
84 | pytest.fail('more messages received that expected')
85 |
86 | assert exp == obj, 'message number %s does not match, exp %s != recv %s' % (c, exp, obj)
87 |
88 | # either id or method
89 | is_method = False
90 | id_ = obj.get('id')
91 |
92 | if not id_:
93 | id_ = obj.get('method')
94 | if not id_:
95 | pytest.fail('received invalid message, no id or method - %s ' % msg)
96 | is_method = True
97 |
98 | response_stream = triggers.get(id_)
99 |
100 | if not response_stream:
101 | pytest.fail('received unexpected message of %s = "%s"'
102 | % ('method' if is_method else 'id', id_))
103 |
104 | if not len(response_stream):
105 | log.debug('expected message but no expected response, continue')
106 |
107 | log.debug('replying with payload "%s"' % response_stream)
108 | for r in response_stream:
109 | if isinstance(r, int):
110 | await asyncio.sleep(r)
111 | else:
112 | await websocket.send(json.dumps(r, cls=helpers.ChromewhipJSONEncoder))
113 | except asyncio.CancelledError as e:
114 | # TODO: look at failure logic here, why cancelled error? why empty? empty could mean it is working properly
115 | # if expected.empty():
116 | # pytest.fail('less messages received that expected')
117 | raise e
118 | return test_server
119 |
120 |
121 | @pytest.mark.asyncio
122 | async def test_send_command_can_trigger_on_event_prior_to_commmand_containing_event_id(event_loop, chrome_tab):
123 |
124 | msg_id = 4
125 | frame_id = '3228.1'
126 | url = 'http://example.com'
127 |
128 | chrome_tab._message_id = msg_id - 1
129 | f = page.Frame(frame_id, 'test', url, 'test', 'text/html')
130 | p = page.Page.navigate(url)
131 | fe = page.FrameNavigatedEvent(f)
132 |
133 | ack = {'id': msg_id, 'result': {'frameId': frame_id}}
134 | triggers = {
135 | msg_id: [ack]
136 | }
137 |
138 | end_msg = copy.copy(p[0])
139 | end_msg['id'] = msg_id
140 | q = queue.Queue()
141 | q.put(end_msg)
142 |
143 | initial_msgs = [fe]
144 |
145 | test_server = init_test_server(triggers, initial_msgs=initial_msgs, expected=q)
146 | start_server = websockets.serve(test_server, TEST_HOST, TEST_PORT)
147 | server = await start_server
148 | await chrome_tab.connect()
149 |
150 | log.info('Sending command and awaiting...')
151 | result = await chrome_tab.send_command(p, await_on_event_type=page.FrameNavigatedEvent)
152 | assert result.get('ack') is not None
153 | assert result.get('event') is not None
154 | event = result.get('event')
155 | assert isinstance(event, page.FrameNavigatedEvent)
156 | assert event.frame.id == f.id
157 | assert event.frame.url == f.url
158 |
159 | server.close()
160 | await server.wait_closed()
161 |
162 | @pytest.mark.asyncio
163 | async def test_send_command_can_trigger_on_event_after_commmand_containing_event_id(event_loop, chrome_tab):
164 | msg_id = 4
165 | frame_id = '3228.1'
166 | url = 'http://example.com'
167 |
168 | chrome_tab._message_id = msg_id - 1
169 | f = page.Frame(frame_id, 'test', url, 'test', 'text/html')
170 | p = page.Page.navigate(url)
171 | fe = page.FrameNavigatedEvent(f)
172 |
173 | ack = {'id': msg_id, 'result': {'frameId': frame_id}}
174 | triggers = {
175 | msg_id: [ack, delay_s(1), fe]
176 | }
177 |
178 | end_msg = copy.copy(p[0])
179 | end_msg['id'] = msg_id
180 | q = queue.Queue()
181 | q.put(end_msg)
182 | q.put(copy.copy(end_msg))
183 |
184 | test_server = init_test_server(triggers, expected=q)
185 | start_server = websockets.serve(test_server, TEST_HOST, TEST_PORT)
186 | server = await start_server
187 | await chrome_tab.connect()
188 |
189 | log.info('Sending command and awaiting...')
190 | result = await chrome_tab.send_command(p, await_on_event_type=page.FrameNavigatedEvent)
191 | assert result.get('ack') is not None
192 | assert result.get('event') is not None
193 | event = result.get('event')
194 | assert isinstance(event, page.FrameNavigatedEvent)
195 | assert event.frame.id == f.id
196 | assert event.frame.url == f.url
197 |
198 | server.close()
199 | await server.wait_closed()
200 |
201 | @pytest.mark.asyncio
202 | async def test_send_command_can_trigger_on_event_with_input_event(event_loop, chrome_tab):
203 | """test_send_command_can_trigger_on_event_with_input_event
204 | Below is test case that will workaround this issue
205 | https://github.com/chuckus/chromewhip/issues/2
206 | """
207 | msg_id = 4
208 | old_frame_id = '2000.1'
209 | frame_id = '3228.1'
210 | url = 'http://example.com'
211 |
212 | chrome_tab._message_id = msg_id - 1
213 | f = page.Frame(frame_id, 'test', url, 'test', 'text/html')
214 | p = page.Page.navigate(url)
215 | fe = page.FrameNavigatedEvent(f)
216 | fsle = page.FrameStoppedLoadingEvent(frame_id)
217 |
218 | # command ack is not related to proceeding events
219 | ack = {'id': msg_id, 'result': {'frameId': old_frame_id}}
220 | triggers = {
221 | msg_id: [ack, delay_s(1), fe, fsle]
222 | }
223 |
224 | end_msg = copy.copy(p[0])
225 | end_msg['id'] = msg_id
226 | q = queue.Queue()
227 | q.put(end_msg)
228 |
229 | test_server = init_test_server(triggers, expected=q)
230 | start_server = websockets.serve(test_server, TEST_HOST, TEST_PORT)
231 | server = await start_server
232 | await chrome_tab.connect()
233 |
234 | log.info('Sending command and awaiting...')
235 | result = await chrome_tab.send_command(p,
236 | input_event_type=page.FrameNavigatedEvent,
237 | await_on_event_type=page.FrameStoppedLoadingEvent)
238 | assert result.get('ack') is not None
239 | assert result.get('event') is not None
240 | event = result.get('event')
241 | assert isinstance(event, page.FrameStoppedLoadingEvent)
242 | assert event.frameId == f.id
243 |
244 | server.close()
245 | await server.wait_closed()
246 |
247 | @pytest.mark.asyncio
248 | async def xtest_can_register_callback_on_devtools_event(event_loop, chrome_tab):
249 | # TODO: double check this part of the api is implemented
250 | interception_id = '3424.1'
251 | msg_id = 7
252 | chrome_tab._message_id = msg_id - 1
253 | fake_request = network.Request(url='http://httplib.org',
254 | method='POST',
255 | headers={},
256 | initialPriority='superlow',
257 | referrerPolicy='origin')
258 | msgs = [
259 | network.RequestInterceptedEvent(interceptionId=interception_id,
260 | request=fake_request,
261 | resourceType="Document",
262 | isNavigationRequest=False)
263 |
264 | ]
265 |
266 | enable = network.Network.setRequestInterceptionEnabled(enabled=True)
267 |
268 | # once emable command comes, send flurry in intercept events
269 | triggers = {
270 | msg_id: msgs
271 | }
272 |
273 | expected = queue.Queue()
274 | e0 = copy.copy(enable[0])
275 | e0['id'] = msg_id
276 | expected.put(e0)
277 | e1 = network.Network.continueInterceptedRequest(interceptionId=interception_id)
278 | expected.put(e1)
279 |
280 | test_server = init_test_server(triggers, expected=expected)
281 | start_server = websockets.serve(test_server, TEST_HOST, TEST_PORT)
282 | server = await start_server
283 | await chrome_tab.connect()
284 |
285 | log.info('Sending command and awaiting...')
286 | # TODO: registration api
287 |
288 | # no point returning data as nothing to do with it.
289 | # but how would i go about storing all the events being collected?
290 | # - this is not the api for it, just add an api for storing events in a queue
291 | # TODO: how do declare return type of method?
292 | async def cb_coro(event: network.RequestInterceptedEvent):
293 | return network.Network.continueInterceptedRequest(interceptionId=event.interceptionId)
294 |
295 | with chrome_tab.schedule_coro_on_event(coro=cb_coro,
296 | event=network.RequestInterceptedEvent):
297 | await chrome_tab.send_command(enable)
298 |
299 | server.close()
300 | await server.wait_closed()
301 |
--------------------------------------------------------------------------------
/tests/test_chromewhip.py:
--------------------------------------------------------------------------------
1 | from urllib.parse import quote
2 | import os
3 | import sys
4 |
5 | import pytest
6 |
7 | PROJECT_ROOT = os.path.abspath(os.path.join(__file__, '../..'))
8 | sys.path.insert(0, PROJECT_ROOT)
9 |
10 | from chromewhip import setup_app
11 | from chromewhip.views import BS
12 | from aiohttp.test_utils import TestClient as tc
13 | HTTPBIN_HOST = 'http://httpbin.org'
14 |
15 | RESPONSES_DIR = os.path.join(os.path.dirname(__file__), 'resources/responses')
16 |
17 | # TODO: start chrome process in here
18 | # https://docs.pytest.org/en/3.1.3/xunit_setup.html#module-level-setup-teardown
19 |
20 | @pytest.mark.asyncio
21 | async def xtest_render_html_basic(event_loop):
22 | expected = BS(open(os.path.join(RESPONSES_DIR, 'httpbin.org.html.txt')).read()).prettify()
23 | client = tc(setup_app(loop=event_loop), loop=event_loop)
24 | await client.start_server()
25 | resp = await client.get('/render.html?url={}'.format(quote('{}/html'.format(HTTPBIN_HOST))))
26 | assert resp.status == 200
27 | text = await resp.text()
28 | assert expected == text
29 |
30 | @pytest.mark.asyncio
31 | async def xtest_render_html_with_js_profile(event_loop):
32 | expected = BS(open(os.path.join(RESPONSES_DIR, 'httpbin.org.html.after_profile.txt')).read()).prettify()
33 | profile_name = 'httpbin-org-html'
34 | profile_path = os.path.join(PROJECT_ROOT, 'tests/resources/js/profiles/{}'.format(profile_name))
35 | client = tc(setup_app(loop=event_loop, js_profiles_path=profile_path), loop=event_loop)
36 | await client.start_server()
37 | resp = await client.get('/render.html?url={}&js={}'.format(
38 | quote('{}/html'.format(HTTPBIN_HOST)),
39 | profile_name))
40 | assert resp.status == 200
41 | text = await resp.text()
42 | assert expected == text
43 |
--------------------------------------------------------------------------------
/tests/test_helpers.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | import pytest
4 |
5 | from chromewhip import helpers
6 | from chromewhip.protocol import page
7 |
8 |
9 | def test_valid_json_to_event():
10 | valid_payload = {"method": "Page.frameNavigated", "params": {
11 | "frame": {"id": "3635.1", "loaderId": "3635.1", "url": "http://httpbin.org/html",
12 | "securityOrigin": "http://httpbin.org", "mimeType": "text/html"}}}
13 |
14 | event = helpers.json_to_event(valid_payload)
15 | assert event.__class__ == page.FrameNavigatedEvent
16 | assert event.frame.loaderId == "3635.1"
17 |
18 |
19 | def test_invalid_method_json_to_event():
20 | valid_payload = {"method": "Page.invalidEvent", "params": {
21 | "frame": {"id": "3635.1", "loaderId": "3635.1", "url": "http://httpbin.org/html",
22 | "securityOrigin": "http://httpbin.org", "mimeType": "text/html"}}}
23 | with pytest.raises(AttributeError) as exc_info:
24 | helpers.json_to_event(valid_payload)
25 |
26 |
27 | def test_json_encoder_event():
28 | f = page.Frame(1, 'test', 'http://example.com', 'test', 'text/html')
29 | fe = page.FrameNavigatedEvent(f)
30 | payload = json.dumps(fe, cls=helpers.ChromewhipJSONEncoder)
31 | assert payload.count('"method":') == 1
32 | assert payload.count('"params":') == 1
33 |
34 |
35 | def test_json_encoder_type():
36 | f = page.Frame(1, 'test', 'http://example.com', 'test', 'text/html')
37 | payload = json.dumps(f, cls=helpers.ChromewhipJSONEncoder)
38 | assert payload.count('"id": 1') == 1
39 | assert payload.count('"url": "http://example.com"') == 1
40 |
41 |
42 | def test_hash_from_concrete_event():
43 | f = page.Frame(3, 'test', 'http://example.com', 'test', 'text/html')
44 | fe = page.FrameNavigatedEvent(f)
45 | assert fe.hash_() == "Page.frameNavigated:frameId=3"
46 |
47 |
48 | def test_build_hash_from_event_cls():
49 | hash = page.FrameNavigatedEvent.build_hash(frameId=3)
50 | assert hash == "Page.frameNavigated:frameId=3"
51 |
52 |
53 |
--------------------------------------------------------------------------------