Skip to content

Commit

Permalink
Merge branch 'ZanderBrown-microbit/use-nudatus'
Browse files Browse the repository at this point in the history
  • Loading branch information
ntoll committed Apr 27, 2018
2 parents 236aa22 + 4cc23b8 commit fc519a9
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 31 deletions.
69 changes: 50 additions & 19 deletions mu/modes/microbit.py
Expand Up @@ -21,13 +21,20 @@
import sys
import os.path
import logging
from tokenize import TokenError
from mu.logic import HOME_DIRECTORY
from mu.contrib import uflash, microfs
from mu.modes.api import MICROBIT_APIS, SHARED_APIS
from mu.modes.base import MicroPythonMode
from mu.interface.panes import CHARTS
from PyQt5.QtCore import QObject, QThread, pyqtSignal, QTimer

# We can run without nudatus
can_minify = True
try:
import nudatus
except ImportError: # pragma: no cover
can_minify = False

logger = logging.getLogger(__name__)

Expand All @@ -39,21 +46,18 @@ class DeviceFlasher(QThread):
# Emitted when flashing the micro:bit fails for any reason.
on_flash_fail = pyqtSignal(str)

def __init__(self, paths_to_microbits, python_script, minify,
path_to_runtime):
def __init__(self, paths_to_microbits, python_script, path_to_runtime):
"""
The paths_to_microbits should be a list containing filesystem paths to
attached micro:bits to flash. The python_script should be the text of
the script to flash onto the device. Minify should be a boolean to
indicate if the Python code is to be minified before flashing. The
path_to_runtime should be the path of the hex file for the MicroPython
runtime to use. If the path_to_runtime is None, the default MicroPython
runtime is used by default.
the script to flash onto the device. The path_to_runtime should be the
path of the hex file for the MicroPython runtime to use. If the
path_to_runtime is None, the default MicroPython runtime is used by
default.
"""
QThread.__init__(self)
self.paths_to_microbits = paths_to_microbits
self.python_script = python_script
self.minify = minify
self.path_to_runtime = path_to_runtime

def run(self):
Expand All @@ -63,7 +67,6 @@ def run(self):
try:
uflash.flash(paths_to_microbits=self.paths_to_microbits,
python_script=self.python_script,
minify=self.minify,
path_to_runtime=self.path_to_runtime)
except Exception as ex:
# Catch everything so Mu can recover from all of the wide variety
Expand Down Expand Up @@ -235,11 +238,45 @@ def flash(self):
python_script = tab.text().encode('utf-8')
logger.debug('Python script:')
logger.debug(python_script)
# Check minification status.
minify = False
if uflash.get_minifier():
minify = self.editor.minify
if len(python_script) >= 8192:
message = _('Unable to flash "{}"').format(tab.label)
information = _("Your script is too long!")
self.view.show_message(message, information, 'Warning')
return
if minify and can_minify:
orginal = len(python_script)
script = python_script.decode('utf-8')
try:
mangled = nudatus.mangle(script).encode('utf-8')
except TokenError as e:
msg, (line, col) = e.args
logger.debug('Minify failed')
logger.exception(e)
message = _("Problem with script")
information = _("{} [{}:{}]").format(msg, line, col)
self.view.show_message(message, information, 'Warning')
return
saved = orginal - len(mangled)
percent = saved / orginal * 100
logger.debug('Script minified, {} bytes ({:.2f}%) saved:'
.format(saved, percent))
logger.debug(mangled)
python_script = mangled
if len(python_script) >= 8192:
information = _("Our minifier tried but your "
"script is too long!")
self.view.show_message(message, information, 'Warning')
return
elif minify and not can_minify:
information = _("Your script is too long and the minifier"
" isn't available")
self.view.show_message(message, information, 'Warning')
return
else:
information = _("Your script is too long!")
self.view.show_message(message, information, 'Warning')
return
# Determine the location of the BBC micro:bit. If it can't be found
# fall back to asking the user to locate it.
path_to_microbit = uflash.find_microbit()
Expand All @@ -259,11 +296,6 @@ def flash(self):
logger.debug('Path to micro:bit: {}'.format(path_to_microbit))
if path_to_microbit and os.path.exists(path_to_microbit):
logger.debug('Flashing to device.')
# Flash the microbit
# Check minification status.
minify = False
if uflash.get_minifier():
minify = self.editor.minify
# Check use of custom runtime.
rt_hex_path = self.editor.microbit_runtime.strip()
message = _('Flashing "{}" onto the micro:bit.').format(tab.label)
Expand All @@ -275,8 +307,7 @@ def flash(self):
self.editor.show_status_message(message, 10)
self.set_buttons(flash=False)
self.flash_thread = DeviceFlasher([path_to_microbit],
python_script, minify,
rt_hex_path)
python_script, rt_hex_path)
if sys.platform == 'win32':
# Windows blocks on write.
self.flash_thread.finished.connect(self.flash_finished)
Expand Down
84 changes: 72 additions & 12 deletions tests/modes/test_microbit.py
Expand Up @@ -9,6 +9,7 @@
from mu.modes.microbit import MicrobitMode, FileManager, DeviceFlasher
from mu.modes.api import MICROBIT_APIS, SHARED_APIS
from unittest import mock
from tokenize import TokenError


TEST_ROOT = os.path.split(os.path.dirname(__file__))[0]
Expand All @@ -18,32 +19,30 @@ def test_DeviceFlasher_init():
"""
Ensure the DeviceFlasher thread is set up correctly.
"""
df = DeviceFlasher(['path', ], 'script', False, None)
df = DeviceFlasher(['path', ], 'script', None)
assert df.paths_to_microbits == ['path', ]
assert df.python_script == 'script'
assert df.minify is False
assert df.path_to_runtime is None


def test_DeviceFlasher_run():
"""
Ensure the uflash.flash function is called as expected.
"""
df = DeviceFlasher(['path', ], 'script', False, None)
df = DeviceFlasher(['path', ], 'script', None)
mock_flash = mock.MagicMock()
with mock.patch('mu.modes.microbit.uflash', mock_flash):
df.run()
mock_flash.flash.assert_called_once_with(paths_to_microbits=['path', ],
python_script='script',
minify=False,
path_to_runtime=None)


def test_DeviceFlasher_run_fail():
"""
Ensure the on_flash_fail signal is emitted if an exception is thrown.
"""
df = DeviceFlasher(['path', ], 'script', True, None)
df = DeviceFlasher(['path', ], 'script', None)
df.on_flash_fail = mock.MagicMock()
mock_flash = mock.MagicMock()
mock_flash.flash.side_effect = Exception('Boom')
Expand Down Expand Up @@ -253,7 +252,7 @@ def test_flash_with_attached_device_as_windows():
assert mm.flash_thread == mock_flasher
assert editor.show_status_message.call_count == 1
mm.set_buttons.assert_called_once_with(flash=False)
mock_flasher_class.assert_called_once_with(['bar', ], b'foo', False,
mock_flasher_class.assert_called_once_with(['bar', ], b'foo',
'/foo/bar')
mock_flasher.finished.connect.\
assert_called_once_with(mm.flash_finished)
Expand Down Expand Up @@ -290,8 +289,7 @@ def test_flash_with_attached_device_as_not_windows():
assert mm.flash_timer == mock_timer
assert editor.show_status_message.call_count == 1
mm.set_buttons.assert_called_once_with(flash=False)
mock_flasher_class.assert_called_once_with(['bar', ], b'foo', False,
None)
mock_flasher_class.assert_called_once_with(['bar', ], b'foo', None)
assert mock_flasher.finished.connect.call_count == 0
mock_timer.timeout.connect.assert_called_once_with(mm.flash_finished)
mock_timer.setSingleShot.assert_called_once_with(True)
Expand Down Expand Up @@ -352,8 +350,7 @@ def test_flash_user_specified_device_path():
view.get_microbit_path.assert_called_once_with(home)
assert editor.show_status_message.call_count == 1
assert mm.user_defined_microbit_path == 'bar'
mock_flasher_class.assert_called_once_with(['bar', ], b'foo', False,
None)
mock_flasher_class.assert_called_once_with(['bar', ], b'foo', None)


def test_flash_existing_user_specified_device_path():
Expand Down Expand Up @@ -381,7 +378,7 @@ def test_flash_existing_user_specified_device_path():
mm.flash()
assert view.get_microbit_path.call_count == 0
assert editor.show_status_message.call_count == 1
mock_flasher_class.assert_called_once_with(['baz', ], b'foo', False,
mock_flasher_class.assert_called_once_with(['baz', ], b'foo',
'/foo/bar')


Expand Down Expand Up @@ -454,8 +451,29 @@ def test_flash_script_too_big():
view.current_tab.label = 'foo'
view.show_message = mock.MagicMock()
editor = mock.MagicMock()
editor.minify = True
mm = MicrobitMode(editor, view)
mm.flash()
with mock.patch('mu.modes.microbit.can_minify', True):
mm.flash()
view.show_message.assert_called_once_with('Unable to flash "foo"',
'Our minifier tried but your '
'script is too long!',
'Warning')


def test_flash_script_too_big_no_minify():
"""
If the script in the current tab is too big, abort in the expected way.
"""
view = mock.MagicMock()
view.current_tab.text = mock.MagicMock(return_value='x' * 8193)
view.current_tab.label = 'foo'
view.show_message = mock.MagicMock()
editor = mock.MagicMock()
editor.minify = False
mm = MicrobitMode(editor, view)
with mock.patch('mu.modes.microbit.can_minify', False):
mm.flash()
view.show_message.assert_called_once_with('Unable to flash "foo"',
'Your script is too long!',
'Warning')
Expand Down Expand Up @@ -497,6 +515,48 @@ def test_flash_failed():
mock_timer.stop.assert_called_once_with()


def test_flash_minify():
view = mock.MagicMock()
script = '#' + ('x' * 8193) + '\n'
view.current_tab.text = mock.MagicMock(return_value=script)
view.show_message = mock.MagicMock()
editor = mock.MagicMock()
editor.minify = True
mm = MicrobitMode(editor, view)
mm.set_buttons = mock.MagicMock()
with mock.patch('mu.modes.microbit.DeviceFlasher'):
with mock.patch('nudatus.mangle', return_value='') as m:
mm.flash()
m.assert_called_once_with(script)

ex = TokenError('Bad', (1, 0))
with mock.patch('nudatus.mangle', side_effect=ex) as m:
mm.flash()
view.show_message.assert_called_once_with('Problem with script',
'Bad [1:0]', 'Warning')


def test_flash_minify_no_minify():
view = mock.MagicMock()
view.current_tab.label = 'foo'
view.show_message = mock.MagicMock()
script = '#' + ('x' * 8193) + '\n'
view.current_tab.text = mock.MagicMock(return_value=script)
editor = mock.MagicMock()
editor.minify = True
mm = MicrobitMode(editor, view)
mm.set_buttons = mock.MagicMock()
with mock.patch('mu.modes.microbit.can_minify', False):
with mock.patch('nudatus.mangle', return_value='') as m:
mm.flash()
assert m.call_count == 0
view.show_message.assert_called_once_with('Unable to flash "foo"',
'Your script is too long'
' and the minifier '
'isn\'t available',
'Warning')


def test_add_fs():
"""
It's possible to add the file system pane if the REPL is inactive.
Expand Down

0 comments on commit fc519a9

Please sign in to comment.