Skip to content
This repository was archived by the owner on May 7, 2024. It is now read-only.

Commit 22f4e1c

Browse files
author
Ron Quan
committed
Updated tests with more scenarios, added doc strings to document API of json rpc lib, added skeleton folder structure for future cli tools, added test setup, added dev set up files for installing dependencies and running tests
1 parent 8a4c6f1 commit 22f4e1c

File tree

22 files changed

+252
-48
lines changed

22 files changed

+252
-48
lines changed

doc/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
1. Install the dependencies via pip with the script below.
2+
```Shell
3+
python scripts/dev_setup.py
4+
5+
2. Add `<clone root>\src` to your PYTHONPATH environment variable:
6+
7+
#####Windows
8+
```BatchFile
9+
set PYTHONPATH=<clone root>\src;%PYTHONPATH%
10+
```
11+
#####OSX/Ubuntu (bash)
12+
```Shell
13+
export PYTHONPATH=<clone root>/src:${PYTHONPATH}
14+
15+
##Running Tests:
16+
####Command line
17+
#####Windows:
18+
Provided your PYTHONPATH was set correctly, you can run the tests from your `<root clone>` directory.
19+
20+
To test the common modules of the CLI:
21+
```BatchFile
22+
python -m unittest discover -s src/common/tests
23+
```
24+
25+
To test the scripter module of the CLI:
26+
```BatchFile
27+
python -m unittest discover -s src/mssqlscripter/mssql/scripter/tests
28+
```
29+
30+
Additionally, you can run tests for all CLI tools and common modules using the `Run_All_Tests.bat` script.

requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
enum34==1.1.6
2+
pip==9.0.1
3+
setuptools==30.4.0

src/common/HISTORY.rst

Whitespace-only changes.

src/common/MANIFEST.ini

Whitespace-only changes.

src/common/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Microsoft XPlat Cli common module
2+
=================================

src/common/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------

src/json-rpc/json/rpc/json_rpc.py renamed to src/common/json_rpc.py

Lines changed: 78 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,18 @@
44
# --------------------------------------------------------------------------------------------
55

66
from io import BytesIO
7+
from enum import Enum
78
import json
89

9-
class JSON_RPC_Writer(object):
10+
class Read_State(Enum):
11+
Header = 1
12+
Content = 2
13+
14+
class Json_Rpc_Writer(object):
15+
"""
16+
Writes to the supplied stream through the JSON RPC Protocol where a request is formatted through a method
17+
name and the necessary parameters.
18+
"""
1019
HEADER = "Content-Length: {0}\r\n\r\n"
1120

1221
def __init__(self, stream, encoding = None):
@@ -15,7 +24,10 @@ def __init__(self, stream, encoding = None):
1524
if encoding is None:
1625
self.encoding = 'UTF-8'
1726

18-
def send_request(self, method, params, id):
27+
def send_request(self, method, params, id = None):
28+
"""
29+
Forms and writes a JSON RPC protocol compliant request a method and it's parameters to the stream.
30+
"""
1931
# Perhaps move to a different def to add some validation
2032
content_body = {
2133
"jsonrpc": "2.0",
@@ -27,10 +39,16 @@ def send_request(self, method, params, id):
2739
json_content = json.dumps(content_body)
2840
header = self.HEADER.format(str(len(json_content)))
2941

30-
self.stream.write(header.encode("ascii"))
42+
self.stream.write(header.encode('ascii'))
3143
self.stream.write(json_content.encode(self.encoding))
32-
33-
class JSON_RPC_Reader(object):
44+
self.stream.flush()
45+
46+
class Json_Rpc_Reader(object):
47+
"""
48+
Reads from the supplied stream through the JSON RPC Protocol. A Content-length header is required in the format
49+
of "Content-Length: <number of bytes>".
50+
Various exceptions may occur during the read process and are documented in each method.
51+
"""
3452
# \r\n
3553
CR = 13
3654
LF = 10
@@ -50,31 +68,46 @@ def __init__(self, stream, encoding = None):
5068
self.read_offset = 0
5169
self.expected_content_length = 0
5270
self.headers = {}
53-
#TODO: Create enum
54-
self.read_state = "Header"
71+
self.read_state = Read_State.Header
5572

5673
def read_response(self):
74+
"""
75+
Reads the response from the supplied stream by chunks into a buffer until all headers and body content are read.
76+
77+
Returns the response body content in JSON
78+
Exceptions raised:
79+
ValueError
80+
if the body-content can not be serialized to a JSON object
81+
"""
5782
# Using a mutable list to hold the value since a immutable string passed by reference won't change the value
5883
content = [""]
5984
while (self.read_next_chunk()):
6085
# If we can't read a header, read the next chunk
61-
if (self.read_state == "Header" and not self.try_read_headers()):
86+
if (self.read_state is Read_State.Header and not self.try_read_headers()):
6287
continue
6388
# If we read the header, try the content. If that fails, read the next chunk
64-
if (self.read_state == "Content" and not self.try_read_content(content)):
89+
if (self.read_state is Read_State.Content and not self.try_read_content(content)):
6590
continue
6691
# We have the content
6792
break
6893

6994
# Resize buffer and remove bytes we have read
70-
self.shift_buffer_bytes_and_reset(self.read_offset)
95+
self.trim_buffer_and_resize(self.read_offset)
7196
try:
7297
return json.loads(content[0])
73-
except ValueError as error:
74-
# response has invalid json object, throw Exception TODO: log message
98+
except ValueError:
99+
# response has invalid json object, throw Exception TODO: log message to telemetry
75100
raise
76101

77102
def read_next_chunk(self):
103+
"""
104+
Reads a chunk of the stream into the byte array. Buffer size is doubled if less than 25% of buffer space is available.abs
105+
Exceptions raised:
106+
EOFError
107+
Stream was empty or Stream did not contain a valid header or content-body
108+
IOError
109+
Stream was closed externally
110+
"""
78111
# Check if we need to resize
79112
current_buffer_size = len(self.buffer)
80113
if ((current_buffer_size - float(self.buffer_end_offset)) / current_buffer_size) < self.BUFFER_RESIZE_TRIGGER:
@@ -92,17 +125,23 @@ def read_next_chunk(self):
92125

93126
if (length_read == 0):
94127
# Nothing was read, could be due to the server process shutting down while leaving stream open
95-
# close stream and return false and/or throw exception?
96-
# for now throwing exception
97128
raise EOFError("End of stream reached with no valid header or content-body")
98129

99130
return True
100131

101-
except Exception:
102-
#TODO: Add more granular exception message
132+
except IOError as error:
133+
# TODO: add to telemetry
103134
raise
104135

105136
def try_read_headers(self):
137+
"""
138+
Attempts to read the Header information from the internal buffer expending the last header contain "\r\n\r\n.
139+
140+
Returns false if the header was not found.
141+
Exceptions:
142+
LookupError The content-length header was not found
143+
ValueError The content-length contained a invalid literal for int
144+
"""
106145
# Scan the buffer up until right before the CRLFCRLF
107146
scan_offset = self.read_offset
108147
while(scan_offset + 3 < self.buffer_end_offset and
@@ -123,42 +162,51 @@ def try_read_headers(self):
123162
colon_index = header.find(':')
124163

125164
if (colon_index == -1):
126-
raise KeyError("Colon missing from Header")
165+
raise KeyError("Colon missing from Header: {0}.".format(header))
127166

128-
header_key = header[:colon_index]
167+
# Making all headers lowercase to support case insensitivity
168+
header_key = header[:colon_index].lower()
129169
header_value = header[colon_index + 1:]
130170

131171
self.headers[header_key] = header_value
132172

133173
#Find content body in the list of headers and parse the Value
134-
if (self.headers["Content-Length"] is None):
135-
raise LookupError("Content Length was not found in headers received")
174+
if (not ("content-length" in self.headers)):
175+
raise LookupError("Content-Length was not found in headers received.")
136176

137-
self.expected_content_length = int(self.headers["Content-Length"])
138-
139-
except Exception:
140-
# Trash the buffer we read and shift past read content
141-
self.shift_buffer_bytes_and_reset(self.scan_offset + 4)
177+
self.expected_content_length = int(self.headers["content-length"])
178+
179+
except ValueError:
180+
# Content-length contained invalid literal for int
181+
self.trim_buffer_and_resize(scan_offset + 4)
142182
raise
143183

144184
# Pushing read pointer past the newline characters
145185
self.read_offset = scan_offset + 4
146-
# TODO: Create enum for this
147-
self.read_state = "Content"
186+
self.read_state = Read_State.Content
187+
148188
return True
149189

150190
def try_read_content(self, content):
191+
"""
192+
Attempts to read the content from the internal buffer.
193+
194+
Returns false if buffer does not contain the entire content.
195+
"""
151196
# if we buffered less than the expected content length, return false
152197
if (self.buffer_end_offset - self.read_offset < self.expected_content_length):
153198
return False
154199

155200
content[0] = self.buffer[self.read_offset:self.read_offset + self.expected_content_length].decode(self.encoding)
156201
self.read_offset += self.expected_content_length
157-
#TODO: Create a enum for this
158-
self.read_state = "Header"
202+
203+
self.read_state = Read_State.Header
159204
return True
160205

161-
def shift_buffer_bytes_and_reset(self, bytes_to_remove):
206+
def trim_buffer_and_resize(self, bytes_to_remove):
207+
"""
208+
Trims the buffer by the passed in bytes_to_remove by creating a new buffer that is at a minimum the default max size.
209+
"""
162210
current_buffer_size = len(self.buffer)
163211
# Create a new buffer with either minumum size or leftover size
164212
new_buffer = bytearray(max(current_buffer_size - bytes_to_remove, self.DEFAULT_BUFFER_SIZE))

0 commit comments

Comments
 (0)