diff --git a/tanco/driver.py b/tanco/driver.py index 86f9ac7..56f0b96 100644 --- a/tanco/driver.py +++ b/tanco/driver.py @@ -46,7 +46,12 @@ def do_login(self, _arg): return sid = db.get_server_id(self.client.url) pre = self.client.get_pre_token() - webbrowser.open(self.client.url + 'auth/login?pre=' + pre) + url = self.client.url + 'auth/login?pre=' + pre + # Always print the URL so headless / remote users can open it manually. + print('Open this URL to log in:') + print(url) + if not webbrowser.open(url): + print('(No browser available; open the URL above in any browser.)') jwt = self.client.get_jwt(pre=pre) data = jwtlib.JWT().decode(jwt, do_verify=False) # TODO: verify uid = db.uid_from_tokendata(sid, data['authid'], data['username']) @@ -267,7 +272,16 @@ def do_check(arg): def do_show(self, arg=None): """Show a description of the current test.""" cfg = runner.load_config() - state = db.current_state(cfg.attempt) + if not cfg.attempt: + print('No active attempt.') + print('Use `tanco init` to start a new attempt.') + return + try: + state = db.current_state(cfg.attempt) + except IndexError: + print('No active attempt.') + print('Use `tanco init` to start a new attempt.') + return if state == 'start': print('You have not started the challenge yet.') print('Use `tanco check` to make sure your program runs.') @@ -321,7 +335,16 @@ def do_next(self, _arg): # TODO: double check that all tests pass and repo is clean cfg = runner.load_config() - state = db.current_state(cfg.attempt) + if not cfg.attempt: + print('No active attempt.') + print('Use `tanco init` to start a new attempt.') + return + try: + state = db.current_state(cfg.attempt) + except IndexError: + print('No active attempt.') + print('Use `tanco init` to start a new attempt.') + return if state == 'done': print('You have already completed the challenge!') return @@ -359,6 +382,7 @@ def do_next(self, _arg): self.do_show() def do_spawn(self, _arg): + """Spawn the target program as a persistent process""" self.target = runner.spawn(runner.load_config()) def ensure_target(self): @@ -366,6 +390,7 @@ def ensure_target(self): self.do_spawn('') def do_send(self, msg, suppress=False): + """Send a message to the target program and print its output""" self.ensure_target() if msg.endswith(self.end_cmd): msg = msg[:-1] self.target.stdin.write(msg + f'\n{self.end_cmd}\n') diff --git a/tests/test_basic.py b/tests/test_basic.py index 31aefa7..0a03f52 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -73,3 +73,79 @@ def test_valid_test_names_accepted(self): self.assertEqual(len(c.tests), 2) self.assertEqual(c.tests[0].name, 'foo.bar-1') self.assertEqual(c.tests[1].name, 'baz_2.A') + + + +class DriverNextTest(unittest.TestCase): + """Regression: tanco next/show must not IndexError when no attempt is active.""" + + def setUp(self) -> None: + TANCO_SDB_PATH.unlink(missing_ok=True) + tanco.database.ensure_sdb() + with tanco.database.begin() as conn: + cur = conn.execute( + 'insert into servers (url, name, info) values (?, ?, ?)', + (TANCO_SERVER, 'fakeserver', 'fake')) + sid = cur.lastrowid + cur = conn.execute( + 'insert into users (sid, authid, username) values (?, ?, ?)', + (sid, 'fakeauthid', 'fakeuser')) + uid = cur.lastrowid + conn.execute('insert into tokens (uid, jwt) values (?, ?)', (uid, 'fakejwt')) + self._old_cwd = os.getcwd() + + def tearDown(self) -> None: + os.chdir(self._old_cwd) + TANCO_SDB_PATH.unlink(missing_ok=True) + + def _run_in_empty_project(self, method_name: str) -> str: + import io + from contextlib import redirect_stdout + + import tanco.driver + + buf = io.StringIO() + with tempfile.TemporaryDirectory() as tmp: + os.chdir(tmp) + d = tanco.driver.TancoDriver() + with redirect_stdout(buf): + getattr(d, method_name)('') + os.chdir(self._old_cwd) + return buf.getvalue() + + def test_next_no_active_attempt_prints_friendly_message(self) -> None: + out = self._run_in_empty_project('do_next') + self.assertIn('No active attempt', out) + self.assertIn('tanco init', out) + + def test_show_no_active_attempt_prints_friendly_message(self) -> None: + out = self._run_in_empty_project('do_show') + self.assertIn('No active attempt', out) + self.assertIn('tanco init', out) + + def test_login_prints_oauth_url(self) -> None: + import io + from contextlib import redirect_stdout + from unittest.mock import MagicMock, patch + + import tanco.driver + + d = tanco.driver.TancoDriver() + d.client = MagicMock() + d.client.whoami.return_value = None + d.client.url = 'https://example.test/' + d.client.get_pre_token.return_value = 'PRETOKEN' + d.client.get_jwt.side_effect = RuntimeError('stop-after-print') + + buf = io.StringIO() + with patch('tanco.driver.webbrowser.open', return_value=False) as open_mock, \ + patch('tanco.driver.db.get_server_id', return_value=1): + with redirect_stdout(buf): + try: + d.do_login('') + except RuntimeError as e: + self.assertEqual(str(e), 'stop-after-print') + + out = buf.getvalue() + self.assertIn('https://example.test/auth/login?pre=PRETOKEN', out) + open_mock.assert_called_once_with('https://example.test/auth/login?pre=PRETOKEN')