|
| 1 | +"""Test the terminal service API.""" |
| 2 | + |
| 3 | +import time |
| 4 | + |
| 5 | +from requests import HTTPError |
| 6 | +from traitlets.config import Config |
| 7 | + |
| 8 | +from notebook.utils import url_path_join |
| 9 | +from notebook.tests.launchnotebook import NotebookTestBase, assert_http_error |
| 10 | + |
| 11 | + |
| 12 | +class TerminalAPI(object): |
| 13 | + """Wrapper for terminal REST API requests""" |
| 14 | + def __init__(self, request, base_url, headers): |
| 15 | + self.request = request |
| 16 | + self.base_url = base_url |
| 17 | + self.headers = headers |
| 18 | + |
| 19 | + def _req(self, verb, path, body=None): |
| 20 | + response = self.request(verb, path, data=body) |
| 21 | + |
| 22 | + if 400 <= response.status_code < 600: |
| 23 | + try: |
| 24 | + response.reason = response.json()['message'] |
| 25 | + except: |
| 26 | + pass |
| 27 | + response.raise_for_status() |
| 28 | + |
| 29 | + return response |
| 30 | + |
| 31 | + def list(self): |
| 32 | + return self._req('GET', 'api/terminals') |
| 33 | + |
| 34 | + def get(self, name): |
| 35 | + return self._req('GET', url_path_join('api/terminals', name)) |
| 36 | + |
| 37 | + def start(self): |
| 38 | + return self._req('POST', 'api/terminals') |
| 39 | + |
| 40 | + def shutdown(self, name): |
| 41 | + return self._req('DELETE', url_path_join('api/terminals', name)) |
| 42 | + |
| 43 | + |
| 44 | +class TerminalAPITest(NotebookTestBase): |
| 45 | + """Test the terminals web service API""" |
| 46 | + def setUp(self): |
| 47 | + self.term_api = TerminalAPI(self.request, |
| 48 | + base_url=self.base_url(), |
| 49 | + headers=self.auth_headers(), |
| 50 | + ) |
| 51 | + |
| 52 | + def tearDown(self): |
| 53 | + for k in self.term_api.list().json(): |
| 54 | + self.term_api.shutdown(k['name']) |
| 55 | + |
| 56 | + def test_no_terminals(self): |
| 57 | + # Make sure there are no terminals running at the start |
| 58 | + terminals = self.term_api.list().json() |
| 59 | + self.assertEqual(terminals, []) |
| 60 | + |
| 61 | + def test_create_terminal(self): |
| 62 | + # POST request |
| 63 | + r = self.term_api._req('POST', 'api/terminals') |
| 64 | + term1 = r.json() |
| 65 | + self.assertEqual(r.status_code, 200) |
| 66 | + self.assertIsInstance(term1, dict) |
| 67 | + |
| 68 | + def test_terminal_root_handler(self): |
| 69 | + # POST request |
| 70 | + r = self.term_api.start() |
| 71 | + term1 = r.json() |
| 72 | + self.assertEqual(r.status_code, 200) |
| 73 | + self.assertIsInstance(term1, dict) |
| 74 | + |
| 75 | + # GET request |
| 76 | + r = self.term_api.list() |
| 77 | + self.assertEqual(r.status_code, 200) |
| 78 | + assert isinstance(r.json(), list) |
| 79 | + self.assertEqual(r.json()[0]['name'], term1['name']) |
| 80 | + |
| 81 | + # create another terminal and check that they both are added to the |
| 82 | + # list of terminals from a GET request |
| 83 | + term2 = self.term_api.start().json() |
| 84 | + assert isinstance(term2, dict) |
| 85 | + r = self.term_api.list() |
| 86 | + terminals = r.json() |
| 87 | + self.assertEqual(r.status_code, 200) |
| 88 | + assert isinstance(terminals, list) |
| 89 | + self.assertEqual(len(terminals), 2) |
| 90 | + |
| 91 | + def test_terminal_handler(self): |
| 92 | + # GET terminal with given name |
| 93 | + term = self.term_api.start().json()['name'] |
| 94 | + r = self.term_api.get(term) |
| 95 | + term1 = r.json() |
| 96 | + self.assertEqual(r.status_code, 200) |
| 97 | + assert isinstance(term1, dict) |
| 98 | + self.assertIn('name', term1) |
| 99 | + self.assertEqual(term1['name'], term) |
| 100 | + |
| 101 | + # Request a bad terminal id and check that a JSON |
| 102 | + # message is returned! |
| 103 | + bad_term = 'nonExistentTerm' |
| 104 | + with assert_http_error(404, 'Terminal not found: ' + bad_term): |
| 105 | + self.term_api.get(bad_term) |
| 106 | + |
| 107 | + # DELETE terminal with name |
| 108 | + r = self.term_api.shutdown(term) |
| 109 | + self.assertEqual(r.status_code, 204) |
| 110 | + terminals = self.term_api.list().json() |
| 111 | + self.assertEqual(terminals, []) |
| 112 | + |
| 113 | + # Request to delete a non-existent terminal name |
| 114 | + bad_term = 'nonExistentTerm' |
| 115 | + with assert_http_error(404, 'Terminal not found: ' + bad_term): |
| 116 | + self.term_api.shutdown(bad_term) |
| 117 | + |
| 118 | + |
| 119 | +class TerminalCullingTest(NotebookTestBase): |
| 120 | + |
| 121 | + # Configure culling |
| 122 | + config = Config({ |
| 123 | + 'NotebookApp': { |
| 124 | + 'TerminalManager': { |
| 125 | + 'cull_interval': 3, |
| 126 | + 'cull_inactive_timeout': 2 |
| 127 | + } |
| 128 | + } |
| 129 | + }) |
| 130 | + |
| 131 | + def setUp(self): |
| 132 | + self.term_api = TerminalAPI(self.request, |
| 133 | + base_url=self.base_url(), |
| 134 | + headers=self.auth_headers(), |
| 135 | + ) |
| 136 | + |
| 137 | + def tearDown(self): |
| 138 | + for k in self.term_api.list().json(): |
| 139 | + self.term_api.shutdown(k['name']) |
| 140 | + |
| 141 | + # Sanity check verifying that the configurable was properly set. |
| 142 | + def test_config(self): |
| 143 | + self.assertEqual(self.config.NotebookApp.TerminalManager.cull_inactive_timeout, 2) |
| 144 | + self.assertEqual(self.config.NotebookApp.TerminalManager.cull_interval, 3) |
| 145 | + terminal_mgr = self.notebook.web_app.settings['terminal_manager'] |
| 146 | + self.assertEqual(terminal_mgr.cull_inactive_timeout, 2) |
| 147 | + self.assertEqual(terminal_mgr.cull_interval, 3) |
| 148 | + |
| 149 | + def test_culling(self): |
| 150 | + # POST request |
| 151 | + r = self.term_api.start() |
| 152 | + self.assertEqual(r.status_code, 200) |
| 153 | + body = r.json() |
| 154 | + term1 = body['name'] |
| 155 | + last_activity = body['last_activity'] |
| 156 | + |
| 157 | + culled = False |
| 158 | + for i in range(10): # Culling should occur in a few seconds |
| 159 | + try: |
| 160 | + r = self.term_api.get(term1) |
| 161 | + except HTTPError as e: |
| 162 | + self.assertEqual(e.response.status_code, 404) |
| 163 | + culled = True |
| 164 | + break |
| 165 | + else: |
| 166 | + self.assertEqual(r.status_code, 200) |
| 167 | + time.sleep(1) |
| 168 | + |
| 169 | + self.assertTrue(culled) |
0 commit comments