├── .github └── workflows │ └── test.yml ├── .gitignore ├── AUTHORS ├── CHANGES.rst ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── docs ├── Makefile ├── conf.py ├── index.rst └── make.bat ├── firmware.hex ├── requirements.txt ├── setup.py ├── tests ├── __init__.py ├── example.py ├── mount_exists.txt ├── mount_missing.txt └── test_uflash.py ├── tox.ini └── uflash.py /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run checks 2 | 3 | on: 4 | push: 5 | branches: '*' 6 | pull_request: 7 | branches: '*' 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, windows-latest] 14 | python-version: [ '2.7', '3.5', '3.6', '3.7', '3.8', '3.9' ] 15 | fail-fast: false 16 | runs-on: ${{ matrix.os }} 17 | name: Python ${{ matrix.python-version }} - ${{ matrix.os }} 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Setup python 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - run: python -m pip install -r requirements.txt 25 | - name: Run all check in Linux 26 | if: runner.os == 'Linux' 27 | run: make check 28 | - name: Run pytest only in Windows (Makefile doesn't work in mingw32) 29 | if: runner.os == 'Windows' 30 | run: py.test --cov-report term-missing --cov=uflash tests/ 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.pyc 3 | uflash.egg-info/* 4 | .cache/* 5 | .coverage 6 | dist/* 7 | build/* 8 | .tox 9 | docs/_build/* 10 | .pytest_cache/* 11 | .idea/* 12 | .vscode/* 13 | htmlcov/* 14 | tests/example.hex 15 | deb_dist/* 16 | uflash-*.tar.gz 17 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Nicholas H.Tollervey (ntoll@ntoll.org) 2 | Matt Wheeler (m@funkyhat.org) 3 | Tom Viner (uflash@viner.tv) 4 | Tom Gurion (nagasaki45@gmail.com) 5 | Scott Webster (ScottDWebsterNJ@gmail.com) 6 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Release History 2 | =============== 3 | 4 | 1.2.4 5 | ----- 6 | 7 | * Updated to the latest version of MicroPython for micro:bit (1.0.1) 8 | * This is the version of uflash to be used in Mu 1.0.2. 9 | 10 | 1.2.3 11 | ----- 12 | 13 | * Update to the latest version of MicroPython for micro:bit (1.0.0). 14 | * This is the version of uflash to be used in Mu 1.0.1. 15 | 16 | 1.2.2 17 | ----- 18 | 19 | * Update to latest version of MicroPython for micro:bit (1.0.0-rc.3). 20 | 21 | 1.2.1 22 | ----- 23 | 24 | * Update to latest version of MicroPython. Thanks to Damien George and Carlos 25 | Pereira Atencio for their hard work. 26 | * This is the version of uflash to be used in Mu 1.0.0 (final). 27 | 28 | 1.2.0 29 | ----- 30 | 31 | * Update to latest version of MicroPython. Thanks to Damien George. 32 | * Add attribute called MICROPYTHON_VERSION to report the version of MicroPython 33 | bundled with uflash. 34 | 35 | 1.1.1 36 | ----- 37 | 38 | * Update to the latest version of MicroPython for the BBC micro:bit -- fixes a 39 | bug relating to flooding and the radio module. As always, many thanks to 40 | Damien George for his work on MicroPython. 41 | 42 | 1.1.0 43 | ----- 44 | 45 | * Update to latest version of MicroPython for the BBC micro:bit (many thanks to Damien George for his amazing efforts!). 46 | * Add a --version flag to uflash that causes it to print the current version number (many thanks to Lenz Grimmer for this work). 47 | * Allow uflash to accept the content of a script as well as the path to a script (many thanks to Zander Brown for this work). 48 | * Ensure uflash works nicely / better with external tools (many thanks to Lex Robinson for this work). 49 | * Added copyright and license information to the start of the script. 50 | 51 | 1.0.8 52 | ----- 53 | 54 | * Refactor hex extraction to not depend on extended address record before script (thanks Carlos). 55 | * Refactor tox tests to fix Windows related Gremlin (thanks again, Carlos). 56 | 57 | 1.0.7 58 | ----- 59 | 60 | * Watch for changes in a script. Automatically flash on save. 61 | 62 | 1.0.5 63 | ----- 64 | 65 | * Update runtime to include latest bug fixes and inclusion of input() builtin. 66 | * Detecting drives on Windows 10 no longer causes pop-ups in certain situations. 67 | * Documentation updates. 68 | 69 | 1.0.4 70 | ----- 71 | 72 | * Add support for flash multiple microbits. 73 | 74 | 1.0.3 75 | ----- 76 | 77 | * Update runtime to include audio and speech modules. 78 | 79 | 1.0.2 80 | ----- 81 | 82 | * Update runtime to include the new radio module. 83 | 84 | 1.0.1 85 | ----- 86 | 87 | * Update runtime to include file system related changes. 88 | 89 | 1.0.0.final.0 90 | ------------- 91 | 92 | * Runtime updated to version 1.0 of MicroPython for the BBC micro:bit. 93 | 94 | 1.0.0.beta.7 95 | ------------ 96 | 97 | * Runtime update to fix display related bug. 98 | 99 | 1.0.0.beta.6 100 | ------------ 101 | 102 | * Runtime update to latest version of the DAL (swaps pins 4 and 5). 103 | 104 | 1.0.0.beta.5 105 | ------------ 106 | 107 | * Runtime update to fix error reporting bug. 108 | 109 | 1.0.0.beta.4 110 | ------------ 111 | 112 | * Documentation update. 113 | * Help text update. 114 | 115 | 1.0.0.beta.3 116 | ------------ 117 | 118 | * Add ability to specify a MicroPython runtime to use. 119 | * Test fixes. 120 | 121 | 1.0.0.beta.2 122 | ------------ 123 | 124 | * Updated to latest version of MicroPython runtime. 125 | 126 | 1.0.0.beta.1 127 | ------------ 128 | 129 | * Works with Python 2.7 (thanks to @Funkyhat). 130 | * Updated to the latest build of MicroPython for the BBC micro:bit. 131 | * Minor refactoring and updates to the test suite due to MicroPython updates. 132 | 133 | 0.9.17 134 | ------ 135 | 136 | * Minor code refactor. 137 | * Documentation update. 138 | 139 | 0.9.14 140 | ------ 141 | 142 | * Feature complete. 143 | * Comprehensive test suite - 100% coverage. 144 | * Tested on Linux and Windows. 145 | * Documentation. 146 | * Access via the "uflash" command. 147 | 148 | 0.0.1 149 | ----- 150 | 151 | * Initial release. Basic functionality. 152 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing to uFlash 2 | ====================== 3 | 4 | Hey! Many thanks for wanting to improve uFlash. 5 | 6 | Contributions are welcome without prejudice from *anyone* irrespective of 7 | age, gender, religion, race or sexuality. If you're thinking, "but they don't 8 | mean me", then we especially mean YOU. Good quality code and engagement with 9 | respect, humour and intelligence wins every time. 10 | 11 | * If you're from a background which isn't well-represented in most geeky groups, get involved - *we want to help you make a difference*. 12 | * If you're from a background which *is* well-represented in most geeky groups, get involved - *we want your help making a difference*. 13 | * If you're worried about not being technical enough, get involved - *your fresh perspective will be invaluable*. 14 | * If you think you're an imposter, get involved. 15 | * If your day job isn't code, get involved. 16 | * This isn't a group of experts, just people. Get involved! 17 | * We are interested in educational, social and technical problems. If you are too, get involved. 18 | * This is a new community. *No-one knows what they are doing*, so, get involved. 19 | 20 | We expect contributors to follow the Python Software Foundation's Code of 21 | Conduct: https://www.python.org/psf/codeofconduct/ 22 | 23 | Feedback may be given for contributions and, where necessary, changes will be 24 | politely requested and discussed with the originating author. Respectful yet 25 | robust argument is most welcome. 26 | 27 | Finally, contributions are subject to the following caveat: the contribution 28 | was created by the contributor who, by submitting the contribution, is 29 | confirming that they have the authority to submit the contribution and place it 30 | under the license as defined in the LICENSE file found within this repository. 31 | 32 | Checklist 33 | --------- 34 | 35 | * Your code should be commented in *plain English* (British spelling). 36 | * If your contribution is for a major block of work and you've not done so 37 | already, add yourself to the AUTHORS file following the convention found 38 | therein. 39 | * You MUST include tests. We have 100% test coverage. 40 | * Have fun! 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2018 Nicholas H.Tollervey and others. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGES.rst 2 | include README.rst 3 | include LICENSE 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | XARGS := xargs -0 $(shell test $$(uname) = Linux && echo -r) 2 | GREP_T_FLAG := $(shell test $$(uname) = Linux && echo -T) 3 | BLACK_INSTALLED := $(shell python -m black --version 2>/dev/null) 4 | 5 | all: 6 | @echo "\nThere is no default Makefile target right now. Try:\n" 7 | @echo "make clean - reset the project and remove auto-generated assets." 8 | @echo "make pyflakes - run the PyFlakes code checker." 9 | @echo "make pep8 - run the PEP8 style checker." 10 | @echo "make test - run the test suite." 11 | @echo "make coverage - view a report on test coverage." 12 | @echo "make check - run all the checkers and tests." 13 | @echo "make package - create a deployable package for the project." 14 | @echo "make rpm - create an rpm package for the project." 15 | @echo "make publish - publish the project to PyPI." 16 | @echo "make docs - run sphinx to create project documentation.\n" 17 | 18 | clean: 19 | rm -rf build 20 | rm -rf dist 21 | rm -rf uflash.egg-info 22 | rm -rf .coverage 23 | rm -rf .tox 24 | rm -rf docs/_build 25 | rm -f tests/example.hex 26 | rm -rf deb_dist 27 | rm -f uflash-*.tar.gz 28 | find . \( -name '*.py[co]' -o -name dropin.cache \) -print0 | $(XARGS) rm 29 | find . \( -name '*.bak' -o -name dropin.cache \) -print0 | $(XARGS) rm 30 | find . \( -name '*.tgz' -o -name dropin.cache \) -print0 | $(XARGS) rm 31 | 32 | pyflakes: 33 | find . \( -name _build -o -name var -o -path ./docs \) -type d -prune -o -name '*.py' -print0 | $(XARGS) pyflakes 34 | 35 | pep8: clean 36 | find . \( -name _build -o -name var \) -type d -prune -o -name '*.py' -print0 | $(XARGS) -n 1 pycodestyle --repeat --exclude=build/*,docs/* --ignore=E731,E402,W504,W503,E203 37 | 38 | test: clean 39 | py.test 40 | 41 | coverage: clean 42 | py.test --cov-report term-missing --cov=uflash tests/ 43 | 44 | tidy: 45 | ifdef BLACK_INSTALLED 46 | python -m black -l79 . 47 | else 48 | @echo Black not present 49 | endif 50 | 51 | black: 52 | ifdef BLACK_INSTALLED 53 | python -m black --check -l79 . 54 | else 55 | @echo Black not present 56 | endif 57 | 58 | check: clean pep8 pyflakes black coverage 59 | 60 | package: check 61 | python setup.py sdist 62 | 63 | rpm: check 64 | python setup.py bdist_rpm 65 | 66 | publish: check 67 | @echo "\nChecks pass, good to publish..." 68 | python setup.py sdist upload 69 | 70 | docs: clean 71 | $(MAKE) -C docs html 72 | @echo "\nDocumentation can be found here:" 73 | @echo file://`pwd`/docs/_build/html/index.html 74 | @echo "\n" 75 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | uFlash 2 | ====== 3 | 4 | **THIS MODULE ONLY WORKS WITH PYTHON 2.7 or 3.3+.** 5 | 6 | A utility for flashing the BBC micro:bit with Python scripts and the 7 | MicroPython runtime. You pronounce the name of this utility "micro-flash". ;-) 8 | 9 | It provides three services: 10 | 11 | 1. A library of functions to programatically create a hex file and flash it onto a BBC micro:bit. 12 | 2. A command line utility called `uflash` that will flash Python scripts onto a BBC micro:bit. 13 | 3. A command line utility called `py2hex` for creating hex files from Python scripts and saving them on the local filesystem 14 | 15 | Several essential operations are implemented: 16 | 17 | * Encode Python into the hex format. 18 | * Embed the resulting hexified Python into the MicroPython runtime hex. 19 | * Extract an encoded Python script from a MicroPython hex file. 20 | * Discover the connected micro:bit. 21 | * Copy the resulting hex onto the micro:bit, thus flashing the device. 22 | * Specify the MicroPython runtime hex in which to embed your Python code. 23 | 24 | Installation 25 | ------------ 26 | 27 | To install simply type:: 28 | 29 | $ pip install uflash 30 | 31 | ...and the package will download from PyPI. If you wish to upgrade to the 32 | latest version, use the following command:: 33 | 34 | $ pip install --no-cache --upgrade uflash 35 | 36 | **NB:** You must use a USB *data* cable to connect the micro:bit to your 37 | computer (some cables are power only). You're in good shape if, when plugged 38 | in, the micro:bit appears as a USB storage device on your file system. 39 | 40 | Linux users: For uflash to work you must ensure the micro:bit is mounted as a 41 | USB storage device. Usually this is done automatically. If not you've probably 42 | configured automounting to be off. If that's the case, we assume you 43 | have the technical knowledge to mount the device yourself or to install the 44 | required kernel modules if they're missing. Default installs of popular Linux 45 | distros "should just work" (tm) out of the box given a default install. 46 | 47 | Command Usage 48 | ------------- 49 | 50 | uflash 51 | ~~~~~~ 52 | 53 | To read help simply type:: 54 | 55 | $ uflash --help 56 | 57 | or:: 58 | 59 | $ uflash -h 60 | 61 | To discover the version information type:: 62 | 63 | $ uflash --version 64 | 65 | If you type the command on its own then uflash will attempt to find a connected 66 | BBC micro:bit and flash an unmodified default version of the MicroPython 67 | runtime onto it:: 68 | 69 | $ uflash 70 | Flashing Python to: /media/ntoll/MICROBIT/micropython.hex 71 | 72 | To flash a version of the MicroPython runtime with a specified script embedded 73 | within it (so that script is run when the BBC micro:bit boots up) then pass 74 | the path to the Python script in as the first argument to the command:: 75 | 76 | $ uflash my_script.py 77 | Flashing my_script.py to: /media/ntoll/MICROBIT/micropython.hex 78 | 79 | You can let uflash watch for changes of your script. It will be flashed 80 | automatically every time you save it:: 81 | 82 | $ uflash -w my_script.py 83 | 84 | or:: 85 | 86 | $ uflash --watch my_script.py 87 | 88 | At this point uflash will try to automatically detect the path to the device. 89 | However, if you have several devices plugged in and/or know what the path on 90 | the filesystem to the BBC micro:bit already is, you can specify this as a 91 | second argument to the command:: 92 | 93 | $ uflash myscript.py /media/ntoll/MICROBIT 94 | Flashing myscript.py to: /media/ntoll/MICROBIT/micropython.hex 95 | 96 | You can even flash multiple devices at once:: 97 | 98 | $ uflash myscript.py /media/ntoll/MICROBIT /media/ntoll/MICROBIT1 99 | Flashing myscript.py to: /media/ntoll/MICROBIT/micropython.hex 100 | Flashing myscript.py to: /media/ntoll/MICROBIT1/micropython.hex 101 | 102 | To extract a Python script from a hex file use the "-e" flag like this:: 103 | 104 | $ uflash -e something.hex myscript.py 105 | 106 | This will save the Python script recovered from "something.hex" into the file 107 | "myscript.py". If you don't supply a target the recovered script will emit to 108 | stdout. 109 | 110 | If you're developing MicroPython and have a custom runtime hex file you can 111 | specify that uflash use it instead of the built-in version of MicroPython in 112 | the following way:: 113 | 114 | $ uflash -r firmware.hex 115 | 116 | or:: 117 | 118 | $ uflash --runtime=firmware.hex 119 | 120 | py2hex 121 | ~~~~~~ 122 | 123 | To create output .hex files in the same directory as the input .py files:: 124 | 125 | $ py2hex tests/example.py 126 | Hexifying example.py as: tests/example.hex 127 | 128 | py2hex includes that same -r/--runtime and -m/--minify options as uflash 129 | and adds an additional option -o/--outdir: 130 | 131 | To create output .hex files in a different directory:: 132 | 133 | $ py2hex example.py -o /tmp 134 | Hexifying example.py as: /tmp/example.hex 135 | 136 | or:: 137 | 138 | $ py2hex example.py --outdir /tmp 139 | Hexifying example.py as: /tmp/example.hex 140 | 141 | py2hex can handle multiple input files:: 142 | 143 | $ py2hex a.py b.py c.py 144 | Hexifying a.py as: a.hex 145 | Hexifying b.py as: b.hex 146 | Hexifying c.py as: c.hex 147 | 148 | or:: 149 | 150 | $ py2hex *.py 151 | Hexifying a.py as: a.hex 152 | Hexifying b.py as: b.hex 153 | Hexifying c.py as: c.hex 154 | 155 | Development 156 | ----------- 157 | 158 | The source code is hosted in GitHub. Please feel free to fork the repository. 159 | Assuming you have Git installed you can download the code from the canonical 160 | repository with the following command:: 161 | 162 | $ git clone https://github.com/ntoll/uflash.git 163 | 164 | Ensure you have the correct dependencies for development installed by creating 165 | a virtualenv and running:: 166 | 167 | $ pip install -r requirements.txt 168 | 169 | To locally install your development version of the module into a virtualenv, 170 | run the following command:: 171 | 172 | $ python setup.py develop 173 | 174 | There is a Makefile that helps with most of the common workflows associated 175 | with development. Typing ``make`` on its own will list the options thus:: 176 | 177 | $ make 178 | 179 | There is no default Makefile target right now. Try: 180 | 181 | make clean - reset the project and remove auto-generated assets. 182 | make pyflakes - run the PyFlakes code checker. 183 | make pep8 - run the PEP8 style checker. 184 | make test - run the test suite. 185 | make coverage - view a report on test coverage. 186 | make check - run all the checkers and tests. 187 | make package - create a deployable package for the project. 188 | make rpm - create an rpm package for the project. 189 | make publish - publish the project to PyPI. 190 | make docs - run sphinx to create project documentation. 191 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/uFlash.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/uFlash.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/uFlash" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/uFlash" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # uFlash documentation build configuration file, created by 5 | # sphinx-quickstart on Thu Dec 17 19:01:47 2015. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | import shlex 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | sys.path.insert(0, os.path.abspath("..")) 24 | import uflash 25 | 26 | # -- General configuration ------------------------------------------------ 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | "sphinx.ext.autodoc", 36 | "sphinx.ext.viewcode", 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ["_templates"] 41 | 42 | # The suffix(es) of source filenames. 43 | # You can specify multiple suffix as a list of string: 44 | # source_suffix = ['.rst', '.md'] 45 | source_suffix = ".rst" 46 | 47 | # The encoding of source files. 48 | # source_encoding = 'utf-8-sig' 49 | 50 | # The master toctree document. 51 | master_doc = "index" 52 | 53 | # General information about the project. 54 | project = "uFlash" 55 | copyright = "2015, Nicholas H.Tollervey" 56 | author = "Nicholas H.Tollervey" 57 | 58 | # The version info for the project you're documenting, acts as replacement for 59 | # |version| and |release|, also used in various other places throughout the 60 | # built documents. 61 | # 62 | # The short X.Y version. 63 | version = uflash.get_version() 64 | # The full version, including alpha/beta/rc tags. 65 | release = uflash.get_version() 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | # 70 | # This is also used if you do content translation via gettext catalogs. 71 | # Usually you set "language" from the command line for these cases. 72 | language = None 73 | 74 | # There are two options for replacing |today|: either, you set today to some 75 | # non-false value, then it is used: 76 | # today = '' 77 | # Else, today_fmt is used as the format for a strftime call. 78 | # today_fmt = '%B %d, %Y' 79 | 80 | # List of patterns, relative to source directory, that match files and 81 | # directories to ignore when looking for source files. 82 | exclude_patterns = ["_build"] 83 | 84 | # The reST default role (used for this markup: `text`) to use for all 85 | # documents. 86 | # default_role = None 87 | 88 | # If true, '()' will be appended to :func: etc. cross-reference text. 89 | # add_function_parentheses = True 90 | 91 | # If true, the current module name will be prepended to all description 92 | # unit titles (such as .. function::). 93 | # add_module_names = True 94 | 95 | # If true, sectionauthor and moduleauthor directives will be shown in the 96 | # output. They are ignored by default. 97 | # show_authors = False 98 | 99 | # The name of the Pygments (syntax highlighting) style to use. 100 | pygments_style = "sphinx" 101 | 102 | # A list of ignored prefixes for module index sorting. 103 | # modindex_common_prefix = [] 104 | 105 | # If true, keep warnings as "system message" paragraphs in the built documents. 106 | # keep_warnings = False 107 | 108 | # If true, `todo` and `todoList` produce output, else they produce nothing. 109 | todo_include_todos = False 110 | 111 | 112 | # -- Options for HTML output ---------------------------------------------- 113 | 114 | # The theme to use for HTML and HTML Help pages. See the documentation for 115 | # a list of builtin themes. 116 | html_theme = "alabaster" 117 | 118 | # Theme options are theme-specific and customize the look and feel of a theme 119 | # further. For a list of options available for each theme, see the 120 | # documentation. 121 | # html_theme_options = {} 122 | 123 | # Add any paths that contain custom themes here, relative to this directory. 124 | # html_theme_path = [] 125 | 126 | # The name for this set of Sphinx documents. If None, it defaults to 127 | # " v documentation". 128 | # html_title = None 129 | 130 | # A shorter title for the navigation bar. Default is the same as html_title. 131 | # html_short_title = None 132 | 133 | # The name of an image file (relative to this directory) to place at the top 134 | # of the sidebar. 135 | # html_logo = None 136 | 137 | # The name of an image file (within the static path) to use as favicon of the 138 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 139 | # pixels large. 140 | # html_favicon = None 141 | 142 | # Add any paths that contain custom static files (such as style sheets) here, 143 | # relative to this directory. They are copied after the builtin static files, 144 | # so a file named "default.css" will overwrite the builtin "default.css". 145 | html_static_path = ["_static"] 146 | 147 | # Add any extra paths that contain custom files (such as robots.txt or 148 | # .htaccess) here, relative to this directory. These files are copied 149 | # directly to the root of the documentation. 150 | # html_extra_path = [] 151 | 152 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 153 | # using the given strftime format. 154 | # html_last_updated_fmt = '%b %d, %Y' 155 | 156 | # If true, SmartyPants will be used to convert quotes and dashes to 157 | # typographically correct entities. 158 | # html_use_smartypants = True 159 | 160 | # Custom sidebar templates, maps document names to template names. 161 | # html_sidebars = {} 162 | 163 | # Additional templates that should be rendered to pages, maps page names to 164 | # template names. 165 | # html_additional_pages = {} 166 | 167 | # If false, no module index is generated. 168 | # html_domain_indices = True 169 | 170 | # If false, no index is generated. 171 | # html_use_index = True 172 | 173 | # If true, the index is split into individual pages for each letter. 174 | # html_split_index = False 175 | 176 | # If true, links to the reST sources are added to the pages. 177 | # html_show_sourcelink = True 178 | 179 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 180 | # html_show_sphinx = True 181 | 182 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 183 | # html_show_copyright = True 184 | 185 | # If true, an OpenSearch description file will be output, and all pages will 186 | # contain a tag referring to it. The value of this option must be the 187 | # base URL from which the finished HTML is served. 188 | # html_use_opensearch = '' 189 | 190 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 191 | # html_file_suffix = None 192 | 193 | # Language to be used for generating the HTML full-text search index. 194 | # Sphinx supports the following languages: 195 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 196 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' 197 | # html_search_language = 'en' 198 | 199 | # A dictionary with options for the search language support, empty by default. 200 | # Now only 'ja' uses this config value 201 | # html_search_options = {'type': 'default'} 202 | 203 | # The name of a javascript file (relative to the configuration directory) that 204 | # implements a search results scorer. If empty, the default will be used. 205 | # html_search_scorer = 'scorer.js' 206 | 207 | # Output file base name for HTML help builder. 208 | htmlhelp_basename = "uFlashdoc" 209 | 210 | # -- Options for LaTeX output --------------------------------------------- 211 | 212 | latex_elements = { 213 | # The paper size ('letterpaper' or 'a4paper'). 214 | #'papersize': 'letterpaper', 215 | # The font size ('10pt', '11pt' or '12pt'). 216 | #'pointsize': '10pt', 217 | # Additional stuff for the LaTeX preamble. 218 | #'preamble': '', 219 | # Latex figure (float) alignment 220 | #'figure_align': 'htbp', 221 | } 222 | 223 | # Grouping the document tree into LaTeX files. List of tuples 224 | # (source start file, target name, title, 225 | # author, documentclass [howto, manual, or own class]). 226 | latex_documents = [ 227 | ( 228 | master_doc, 229 | "uFlash.tex", 230 | "uFlash Documentation", 231 | "Nicholas H.Tollervey", 232 | "manual", 233 | ), 234 | ] 235 | 236 | # The name of an image file (relative to this directory) to place at the top of 237 | # the title page. 238 | # latex_logo = None 239 | 240 | # For "manual" documents, if this is true, then toplevel headings are parts, 241 | # not chapters. 242 | # latex_use_parts = False 243 | 244 | # If true, show page references after internal links. 245 | # latex_show_pagerefs = False 246 | 247 | # If true, show URL addresses after external links. 248 | # latex_show_urls = False 249 | 250 | # Documents to append as an appendix to all manuals. 251 | # latex_appendices = [] 252 | 253 | # If false, no module index is generated. 254 | # latex_domain_indices = True 255 | 256 | 257 | # -- Options for manual page output --------------------------------------- 258 | 259 | # One entry per manual page. List of tuples 260 | # (source start file, name, description, authors, manual section). 261 | man_pages = [(master_doc, "uflash", "uFlash Documentation", [author], 1)] 262 | 263 | # If true, show URL addresses after external links. 264 | # man_show_urls = False 265 | 266 | 267 | # -- Options for Texinfo output ------------------------------------------- 268 | 269 | # Grouping the document tree into Texinfo files. List of tuples 270 | # (source start file, target name, title, author, 271 | # dir menu entry, description, category) 272 | texinfo_documents = [ 273 | ( 274 | master_doc, 275 | "uFlash", 276 | "uFlash Documentation", 277 | author, 278 | "uFlash", 279 | "One line description of project.", 280 | "Miscellaneous", 281 | ), 282 | ] 283 | 284 | # Documents to append as an appendix to all manuals. 285 | # texinfo_appendices = [] 286 | 287 | # If false, no module index is generated. 288 | # texinfo_domain_indices = True 289 | 290 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 291 | # texinfo_show_urls = 'footnote' 292 | 293 | # If true, do not generate a @detailmenu in the "Top" node's menu. 294 | # texinfo_no_detailmenu = False 295 | 296 | 297 | # -- Options for Epub output ---------------------------------------------- 298 | 299 | # Bibliographic Dublin Core info. 300 | epub_title = project 301 | epub_author = author 302 | epub_publisher = author 303 | epub_copyright = copyright 304 | 305 | # The basename for the epub file. It defaults to the project name. 306 | # epub_basename = project 307 | 308 | # The HTML theme for the epub output. Since the default themes are not optimized 309 | # for small screen space, using the same theme for HTML and epub output is 310 | # usually not wise. This defaults to 'epub', a theme designed to save visual 311 | # space. 312 | # epub_theme = 'epub' 313 | 314 | # The language of the text. It defaults to the language option 315 | # or 'en' if the language is not set. 316 | # epub_language = '' 317 | 318 | # The scheme of the identifier. Typical schemes are ISBN or URL. 319 | # epub_scheme = '' 320 | 321 | # The unique identifier of the text. This can be a ISBN number 322 | # or the project homepage. 323 | # epub_identifier = '' 324 | 325 | # A unique identification for the text. 326 | # epub_uid = '' 327 | 328 | # A tuple containing the cover image and cover page html template filenames. 329 | # epub_cover = () 330 | 331 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. 332 | # epub_guide = () 333 | 334 | # HTML files that should be inserted before the pages created by sphinx. 335 | # The format is a list of tuples containing the path and title. 336 | # epub_pre_files = [] 337 | 338 | # HTML files shat should be inserted after the pages created by sphinx. 339 | # The format is a list of tuples containing the path and title. 340 | # epub_post_files = [] 341 | 342 | # A list of files that should not be packed into the epub file. 343 | epub_exclude_files = ["search.html"] 344 | 345 | # The depth of the table of contents in toc.ncx. 346 | # epub_tocdepth = 3 347 | 348 | # Allow duplicate toc entries. 349 | # epub_tocdup = True 350 | 351 | # Choose between 'default' and 'includehidden'. 352 | # epub_tocscope = 'default' 353 | 354 | # Fix unsupported image types using the Pillow. 355 | # epub_fix_images = False 356 | 357 | # Scale large images. 358 | # epub_max_image_width = 0 359 | 360 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 361 | # epub_show_urls = 'inline' 362 | 363 | # If false, no index is generated. 364 | # epub_use_index = True 365 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. uFlash documentation master file, created by 2 | sphinx-quickstart on Thu Dec 17 19:01:47 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. include:: ../README.rst 7 | 8 | .. include:: ../CONTRIBUTING.rst 9 | 10 | API 11 | === 12 | 13 | .. automodule:: uflash 14 | :members: 15 | 16 | .. include:: ../CHANGES.rst 17 | 18 | License 19 | ======= 20 | 21 | .. include:: ../LICENSE 22 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 1>NUL 2>NUL 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\uFlash.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\uFlash.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pycodestyle 3 | pyflakes 4 | coverage 5 | sphinx 6 | pytest-cov 7 | nudatus>=0.0.2 8 | 9 | # Mock is bundled as part of unittest since Python 3.3 10 | # mock_open can't read binary data in <= 3.4.2 11 | mock ; python_version == '2.7' or python_version == '3.4' 12 | 13 | # Black is only available for Python 3.6+ 14 | black>=19.10b0;python_version>'3.5' 15 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from setuptools import setup 3 | from uflash import get_version 4 | 5 | 6 | with open("README.rst") as f: 7 | readme = f.read() 8 | with open("CHANGES.rst") as f: 9 | changes = f.read() 10 | 11 | 12 | setup( 13 | name="uflash", 14 | version=get_version(), 15 | description="A module and utility to flash Python onto the BBC micro:bit.", 16 | long_description=readme + "\n\n" + changes, 17 | author="Nicholas H.Tollervey", 18 | author_email="ntoll@ntoll.org", 19 | url="https://github.com/ntoll/uflash", 20 | py_modules=["uflash", "py2hex"], 21 | license="MIT", 22 | classifiers=[ 23 | "Development Status :: 4 - Beta", 24 | "Environment :: Console", 25 | "Intended Audience :: Developers", 26 | "Intended Audience :: Education", 27 | "License :: OSI Approved :: MIT License", 28 | "Operating System :: POSIX", 29 | "Operating System :: Microsoft :: Windows", 30 | "Programming Language :: Python :: 2.7", 31 | "Programming Language :: Python :: 3.5", 32 | "Programming Language :: Python :: 3.6", 33 | "Programming Language :: Python :: 3.7", 34 | "Programming Language :: Python :: 3.8", 35 | "Programming Language :: Python :: 3.9", 36 | "Topic :: Education", 37 | "Topic :: Software Development :: Embedded Systems", 38 | ], 39 | python_requires="==2.7.*,>=3.5", 40 | entry_points={ 41 | "console_scripts": ["uflash=uflash:main", "py2hex=uflash:py2hex"], 42 | }, 43 | ) 44 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntoll/uflash/147ea945fbe841b0ae17888ab60a60c6080b1225/tests/__init__.py -------------------------------------------------------------------------------- /tests/example.py: -------------------------------------------------------------------------------- 1 | from microbit import display 2 | 3 | display.scroll("Hello, World!") 4 | -------------------------------------------------------------------------------- /tests/mount_exists.txt: -------------------------------------------------------------------------------- 1 | sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime) 2 | proc on /proc type proc (rw,nosuid,nodev,noexec,relatime) 3 | udev on /dev type devtmpfs (rw,relatime,size=10240k,nr_inodes=489849,mode=755) 4 | devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000) 5 | tmpfs on /run type tmpfs (rw,nosuid,relatime,size=787732k,mode=755) 6 | /dev/mapper/heraclitus--vg-root on / type ext4 (rw,relatime,errors=remount-ro,data=ordered) 7 | /dev/sdb on /media/ntoll/MICROBIT type vfat (rw,nosuid,nodev,relatime,uid=1000,gid=1000,fmask=0022,dmask=0077,codepage=437,iocharset=utf8,shortname=mixed,showexec,utf8,flush,errors=remount-ro,uhelper=udisks2) 8 | -------------------------------------------------------------------------------- /tests/mount_missing.txt: -------------------------------------------------------------------------------- 1 | sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime) 2 | proc on /proc type proc (rw,nosuid,nodev,noexec,relatime) 3 | udev on /dev type devtmpfs (rw,relatime,size=10240k,nr_inodes=489849,mode=755) 4 | devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000) 5 | tmpfs on /run type tmpfs (rw,nosuid,relatime,size=787732k,mode=755) 6 | /dev/mapper/heraclitus--vg-root on / type ext4 (rw,relatime,errors=remount-ro,data=ordered) 7 | -------------------------------------------------------------------------------- /tests/test_uflash.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Tests for the uflash module. 4 | """ 5 | import ctypes 6 | import os 7 | import os.path 8 | import sys 9 | import tempfile 10 | import time 11 | import threading 12 | 13 | import pytest 14 | import uflash 15 | 16 | try: 17 | from unittest import mock 18 | except ImportError: 19 | import mock 20 | else: 21 | # mock_open can't read binary data in < 3.4.3 22 | # https://bugs.python.org/issue23004 23 | if (3, 4) <= sys.version_info < (3, 4, 3): 24 | import mock 25 | 26 | 27 | TEST_SCRIPT = b"""from microbit import * 28 | 29 | display.scroll('Hello, World!') 30 | """ 31 | TEST_SCRIPT_HEXLIFIED = ( 32 | ":020000040003F7\n" 33 | ":10E000004D50380066726F6D206D6963726F626982\n" 34 | ":10E010007420696D706F7274202A0A0A64697370C3\n" 35 | ":10E020006C61792E7363726F6C6C282748656C6C19\n" 36 | ":10E030006F2C20576F726C642127290A00000000A2" 37 | ) 38 | 39 | TEST_SCRIPT_FS = ( 40 | b"This is a slightly longer bit of test that " 41 | b"should be more than a single block\nThis is a slightly longer bit of " 42 | b"test that should be more than a single block" 43 | ) 44 | TEST_SCRIPT_FS_V1_HEX_LIST = [ 45 | ":020000040003F7", 46 | ":108C0000FE26076D61696E2E70795468697320695C", 47 | ":108C100073206120736C696768746C79206C6F6E67", 48 | ":108C200067657220626974206F66207465737420B2", 49 | ":108C3000746861742073686F756C64206265206D60", 50 | ":108C40006F7265207468616E20612073696E676C55", 51 | ":108C50006520626C6F636B0A5468697320697320C6", 52 | ":108C60006120736C696768746C79206C6F6E6765DE", 53 | ":108C70007220626974206F662074657374207402B8", 54 | ":108C8000016861742073686F756C64206265206D83", 55 | ":108C90006F7265207468616E20612073696E676C05", 56 | ":108CA0006520626C6F636BFFFFFFFFFFFFFFFFFF3D", 57 | ":108CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4", 58 | ":108CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4", 59 | ":108CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4", 60 | ":108CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94", 61 | ":108CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84", 62 | ":01F80000FD0A", 63 | ] 64 | TEST_SCRIPT_FS_V1_HEX_PADDING_LIST = [ 65 | ":1000000CFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4", 66 | ":1000000CFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4", 67 | ":1000000CFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4", 68 | ":1000000CFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4", 69 | ":1000000CFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4", 70 | ":1000000CFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4", 71 | ":0700000CFFFFFFFFFFFFFFF4", 72 | ] 73 | TEST_SCRIPT_FS_V2_HEX_LIST = [ 74 | ":020000040006F4", 75 | ":10D0000DFE26076D61696E2E70795468697320690B", 76 | ":10D0100D73206120736C696768746C79206C6F6E16", 77 | ":10D0200D67657220626974206F6620746573742061", 78 | ":10D0300D746861742073686F756C64206265206D0F", 79 | ":10D0400D6F7265207468616E20612073696E676C04", 80 | ":10D0500D6520626C6F636B0A546869732069732075", 81 | ":10D0600D6120736C696768746C79206C6F6E67658D", 82 | ":10D0700D7220626974206F66207465737420740267", 83 | ":10D0800D016861742073686F756C64206265206D32", 84 | ":10D0900D6F7265207468616E20612073696E676CB4", 85 | ":10D0A00D6520626C6F636BFFFFFFFFFFFFFFFFFFEC", 86 | ":10D0B00DFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73", 87 | ":10D0C00DFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63", 88 | ":10D0D00DFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53", 89 | ":10D0E00DFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43", 90 | ":10D0F00DFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33", 91 | ":020000040007F3", 92 | ":0120000DFDD5", 93 | ] 94 | TEST_SCRIPT_FS_V2_HEX_PADDING_LIST = [ 95 | ":1000000CFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4", 96 | ":1000000CFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4", 97 | ":1000000CFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4", 98 | ":1000000CFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4", 99 | ":1000000CFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4", 100 | ":0900000CFFFFFFFFFFFFFFFFFFF4", 101 | ":0600000CFFFFFFFFFFFFF4", 102 | ] 103 | TEST_UNIVERSAL_HEX_LIST = [ 104 | # Section for V1 starts 105 | ":020000040000FA", 106 | ":0400000A9900C0DEBB", 107 | ":1000000000400020218E01005D8E01005F8E010006", 108 | ":1000100000000000000000000000000000000000E0", 109 | ":10002000000000000000000000000000618E0100E0", 110 | ":020000040001F9", 111 | ":1000000003D13000F8BD4010F3E7331D0122180082", 112 | ":10001000F8F7B2FD4460EFE7E4B30200F0B5070083", 113 | ":1000200089B000201E000D00019215F0ECFB0E4B74", 114 | ":10003000040007609F4203D1042302791343037134", 115 | ":0888B00095880100C1000000E1", 116 | # V1 UICR 117 | ":020000041000EA", 118 | ":1010C0007CB0EE17FFFFFFFF0A0000000000E30006", 119 | ":0C10D000FFFFFFFF2D6D0300000000007B", 120 | # Section for V2 starts 121 | ":020000040000FA", 122 | ":0400000A9903C0DEB8", 123 | ":1000000D00040020810A000015070000610A0000AD", 124 | ":1000100D1F07000029070000330700000000000043", 125 | ":1000200D000000000000000000000000A50A000014", 126 | ":1000300D3D070000000000004707000051070000C9", 127 | # V2 UICR 128 | ":020000041000EA", 129 | ":0810140D0070070000E0070069", 130 | # V2 Regions table (this in flash again) 131 | ":020000040006F4", 132 | ":102FC00D0100010000B00100000000000000000041", 133 | ":102FD00D02021C00E46504009CA105000000000035", 134 | ":102FE00D03006D0000600000000000000000000004", 135 | ":102FF00DFE307F590100300003000C009DD7B1C198", 136 | ":00000001FF", 137 | "", 138 | ] 139 | TEST_UHEX_V1_INSERTION_INDEX = 11 140 | TEST_UHEX_V2_INSERTION_INDEX = 20 141 | 142 | 143 | def test_get_version(): 144 | """ 145 | Ensure a call to get_version returns the expected string. 146 | """ 147 | result = uflash.get_version() 148 | assert result == ".".join([str(i) for i in uflash._VERSION]) 149 | 150 | 151 | def test_unhexlify(): 152 | """ 153 | Ensure that we can get the script back out using unhexlify and that the 154 | result is a properly decoded string. 155 | """ 156 | unhexlified = uflash.unhexlify(TEST_SCRIPT_HEXLIFIED) 157 | assert unhexlified == TEST_SCRIPT.decode("utf-8") 158 | 159 | 160 | def test_unhexlify_not_python(): 161 | """ 162 | Test that the MicroPython script start format is present. 163 | """ 164 | assert "" == uflash.unhexlify( 165 | ":020000040003F7\n:10E000000000000000000000000000000000000010" 166 | ) 167 | 168 | 169 | def test_unhexlify_bad_unicode(): 170 | """ 171 | Test that invalid Unicode is dealt gracefully returning an empty string. 172 | """ 173 | assert "" == uflash.unhexlify( 174 | ":020000040003F7\n:10E000004D50FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" 175 | ) 176 | 177 | 178 | def test_extract(): 179 | """ 180 | The script should be returned as a string (if there is one). 181 | """ 182 | v1_hex = ( 183 | "020000040000FA\n" 184 | + ":1000000000400020218E01005D8E01005F8E010006\n" 185 | + TEST_SCRIPT_HEXLIFIED 186 | + "\n" 187 | + ":020000041000EA\n" 188 | + ":1010C0007CB0EE17FFFFFFFF0A0000000000E30006\n" 189 | + ":0C10D000FFFFFFFF2D6D0300000000007B\n" 190 | + ":0400000500018E2147\n" 191 | + ":00000001FF\n" 192 | ) 193 | extracted = uflash.extract_script(v1_hex) 194 | assert extracted == TEST_SCRIPT.decode("utf-8") 195 | 196 | 197 | def test_extract_sandwiched(): 198 | """ 199 | The script hex is packed with additional data above and bellow and should 200 | still be returned as a the original string only. 201 | """ 202 | python_hex_lines = TEST_SCRIPT_HEXLIFIED.split("\n") 203 | python_sandwiched = [ 204 | python_hex_lines[0], 205 | ":10DFE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41", 206 | "\n".join(python_hex_lines[1:]), 207 | ":10E50000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B", 208 | "", 209 | ] 210 | v1_hex_sandiwtched_data = ( 211 | "020000040000FA\n" 212 | + ":1000000000400020218E01005D8E01005F8E010006\n" 213 | + "\n".join(python_sandwiched) 214 | + ":020000041000EA\n" 215 | + ":1010C0007CB0EE17FFFFFFFF0A0000000000E30006\n" 216 | + ":0C10D000FFFFFFFF2D6D0300000000007B\n" 217 | + ":0400000500018E2147\n" 218 | + ":00000001FF\n" 219 | ) 220 | extracted = uflash.extract_script(v1_hex_sandiwtched_data) 221 | assert extracted == TEST_SCRIPT.decode("utf-8") 222 | 223 | 224 | def test_extract_not_valid_hex(): 225 | """ 226 | Return a sensible message if the hex file isn't valid 227 | """ 228 | assert uflash.extract_script("invalid input") == "" 229 | 230 | 231 | def test_extract_no_python(): 232 | """ 233 | Ensure that if there's no Python in the input hex then just return an empty 234 | (False) string. 235 | """ 236 | v1_hex_no_py_code = ( 237 | "020000040000FA\n" 238 | + ":1000000000400020218E01005D8E01005F8E010006\n" 239 | + ":020000041000EA\n" 240 | + ":1010C0007CB0EE17FFFFFFFF0A0000000000E30006\n" 241 | + ":0C10D000FFFFFFFF2D6D0300000000007B\n" 242 | + ":0400000500018E2147\n" 243 | + ":00000001FF\n" 244 | ) 245 | assert uflash.extract_script(v1_hex_no_py_code) == "" 246 | 247 | 248 | def test_find_microbit_posix_exists(): 249 | """ 250 | Simulate being on os.name == 'posix' and a call to "mount" returns a 251 | record indicating a connected micro:bit device. 252 | """ 253 | with open("tests/mount_exists.txt", "rb") as fixture_file: 254 | fixture = fixture_file.read() 255 | with mock.patch("os.name", "posix"): 256 | with mock.patch("uflash.check_output", return_value=fixture): 257 | assert uflash.find_microbit() == "/media/ntoll/MICROBIT" 258 | 259 | 260 | def test_find_microbit_posix_missing(): 261 | """ 262 | Simulate being on os.name == 'posix' and a call to "mount" returns a 263 | no records associated with a micro:bit device. 264 | """ 265 | with open("tests/mount_missing.txt", "rb") as fixture_file: 266 | fixture = fixture_file.read() 267 | with mock.patch("os.name", "posix"): 268 | with mock.patch("uflash.check_output", return_value=fixture): 269 | assert uflash.find_microbit() is None 270 | 271 | 272 | def test_find_microbit_nt_exists(): 273 | """ 274 | Simulate being on os.name == 'nt' and a disk with a volume name 'MICROBIT' 275 | exists indicating a connected micro:bit device. 276 | """ 277 | mock_windll = mock.MagicMock() 278 | mock_windll.kernel32 = mock.MagicMock() 279 | mock_windll.kernel32.GetVolumeInformationW = mock.MagicMock() 280 | mock_windll.kernel32.GetVolumeInformationW.return_value = None 281 | # 282 | # Have every drive claim to be removable 283 | # 284 | mock_windll.kernel32.GetDriveTypeW = mock.MagicMock() 285 | mock_windll.kernel32.GetDriveTypeW.return_value = 2 286 | with mock.patch("os.name", "nt"): 287 | with mock.patch("os.path.exists", return_value=True): 288 | return_value = ctypes.create_unicode_buffer("MICROBIT") 289 | with mock.patch( 290 | "ctypes.create_unicode_buffer", return_value=return_value 291 | ): 292 | ctypes.windll = mock_windll 293 | assert uflash.find_microbit() == "A:\\" 294 | 295 | 296 | def test_find_microbit_nt_missing(): 297 | """ 298 | Simulate being on os.name == 'nt' and a disk with a volume name 'MICROBIT' 299 | does not exist for a micro:bit device. 300 | """ 301 | mock_windll = mock.MagicMock() 302 | mock_windll.kernel32 = mock.MagicMock() 303 | mock_windll.kernel32.GetVolumeInformationW = mock.MagicMock() 304 | mock_windll.kernel32.GetVolumeInformationW.return_value = None 305 | with mock.patch("os.name", "nt"): 306 | with mock.patch("os.path.exists", return_value=True): 307 | return_value = ctypes.create_unicode_buffer(1024) 308 | with mock.patch( 309 | "ctypes.create_unicode_buffer", return_value=return_value 310 | ): 311 | ctypes.windll = mock_windll 312 | assert uflash.find_microbit() is None 313 | 314 | 315 | def test_find_microbit_nt_removable_only(): 316 | """ 317 | We should only be considering removable drives as candidates for 318 | micro:bit devices. (Especially so as to avoid interrogating disconnected 319 | network drives). 320 | 321 | Have every drive claim to be a micro:bit, but only drive B: claim 322 | to be removable 323 | """ 324 | 325 | def _drive_type(letter): 326 | if letter == "B:\\": 327 | return 2 # removable 328 | else: 329 | return 4 # network 330 | 331 | mock_windll = mock.MagicMock() 332 | mock_windll.kernel32 = mock.MagicMock() 333 | mock_windll.kernel32.GetVolumeInformationW = mock.MagicMock() 334 | mock_windll.kernel32.GetVolumeInformationW.return_value = None 335 | mock_windll.kernel32.GetDriveTypeW = mock.MagicMock() 336 | mock_windll.kernel32.GetDriveTypeW.side_effect = _drive_type 337 | with mock.patch("os.name", "nt"): 338 | with mock.patch("os.path.exists", return_value=True): 339 | return_value = ctypes.create_unicode_buffer("MICROBIT") 340 | with mock.patch( 341 | "ctypes.create_unicode_buffer", return_value=return_value 342 | ): 343 | ctypes.windll = mock_windll 344 | assert uflash.find_microbit() == "B:\\" 345 | 346 | 347 | def test_find_microbit_unknown_os(): 348 | """ 349 | Raises a NotImplementedError if the host OS is not supported. 350 | """ 351 | with mock.patch("os.name", "foo"): 352 | with pytest.raises(NotImplementedError) as ex: 353 | uflash.find_microbit() 354 | assert ex.value.args[0] == 'OS "foo" not supported.' 355 | 356 | 357 | def test_save_hex(): 358 | """ 359 | Ensure the good case works. 360 | """ 361 | # Ensure we have a temporary file to write to that doesn't already exist. 362 | path_to_hex = os.path.join(tempfile.gettempdir(), "microbit.hex") 363 | if os.path.exists(path_to_hex): 364 | os.remove(path_to_hex) 365 | assert not os.path.exists(path_to_hex) 366 | # Create the hex file we want to "flash" 367 | hex_file = uflash.embed_fs_uhex(uflash._RUNTIME, TEST_SCRIPT) 368 | # Save the hex. 369 | uflash.save_hex(hex_file, path_to_hex) 370 | # Ensure the hex has been written as expected. 371 | assert os.path.exists(path_to_hex) 372 | with open(path_to_hex) as written_file: 373 | assert written_file.read() == hex_file 374 | 375 | 376 | def test_save_hex_no_hex(): 377 | """ 378 | The function raises a ValueError if no hex content is provided. 379 | """ 380 | with pytest.raises(ValueError) as ex: 381 | uflash.save_hex("", "foo") 382 | assert ex.value.args[0] == "Cannot flash an empty .hex file." 383 | 384 | 385 | def test_save_hex_path_not_to_hex_file(): 386 | """ 387 | The function raises a ValueError if the path is NOT to a .hex file. 388 | """ 389 | with pytest.raises(ValueError) as ex: 390 | uflash.save_hex("foo", "") 391 | assert ex.value.args[0] == "The path to flash must be for a .hex file." 392 | 393 | 394 | def test_flash_no_args(): 395 | """ 396 | The good case with no arguments to the flash() function. When it's 397 | possible to find a path to the micro:bit. 398 | 399 | If no path to a Python script is supplied then just flash the unmodified 400 | MicroPython firmware onto the device. 401 | """ 402 | with mock.patch("uflash.find_microbit", return_value="foo"): 403 | with mock.patch("uflash.save_hex") as mock_save: 404 | uflash.flash() 405 | assert mock_save.call_count == 1 406 | assert mock_save.call_args[0][0] == uflash._RUNTIME 407 | expected_path = os.path.join("foo", "micropython.hex") 408 | assert mock_save.call_args[0][1] == expected_path 409 | 410 | 411 | def test_flash_has_python_no_path_to_microbit(): 412 | """ 413 | The good case with a path to a Python file. When it's possible to find a 414 | path to the micro:bit. 415 | 416 | The resulting payload should be a correctly created micropython.hex file. 417 | """ 418 | with mock.patch("uflash.find_microbit", return_value="foo"): 419 | with mock.patch("uflash.save_hex") as mock_save: 420 | uflash.flash("tests/example.py") 421 | assert mock_save.call_count == 1 422 | # Create the hex we're expecting to flash onto the device. 423 | with open("tests/example.py", "rb") as py_file: 424 | py_code = py_file.read() 425 | assert py_code 426 | expected_hex = uflash.embed_fs_uhex(uflash._RUNTIME, py_code) 427 | assert mock_save.call_args[0][0] == expected_hex 428 | expected_path = os.path.join("foo", "micropython.hex") 429 | assert mock_save.call_args[0][1] == expected_path 430 | 431 | 432 | def test_flash_with_path_to_multiple_microbits(): 433 | """ 434 | Flash the referenced paths to the micro:bit with a hex file generated from 435 | the MicroPython firmware and the referenced Python script. 436 | """ 437 | with mock.patch("uflash.save_hex") as mock_save: 438 | uflash.flash("tests/example.py", ["test_path1", "test_path2"]) 439 | assert mock_save.call_count == 2 440 | # Create the hex we're expecting to flash onto the device. 441 | with open("tests/example.py", "rb") as py_file: 442 | py_code = py_file.read() 443 | assert py_code 444 | expected_hex = uflash.embed_fs_uhex(uflash._RUNTIME, py_code) 445 | 446 | assert mock_save.call_args_list[0][0][0] == expected_hex 447 | expected_path = os.path.join("test_path1", "micropython.hex") 448 | assert mock_save.call_args_list[0][0][1] == expected_path 449 | 450 | assert mock_save.call_args_list[1][0][0] == expected_hex 451 | expected_path = os.path.join("test_path2", "micropython.hex") 452 | assert mock_save.call_args_list[1][0][1] == expected_path 453 | 454 | 455 | def test_flash_with_path_to_microbit(): 456 | """ 457 | Flash the referenced path to the micro:bit with a hex file generated from 458 | the MicroPython firmware and the referenced Python script. 459 | """ 460 | with mock.patch("uflash.save_hex") as mock_save: 461 | uflash.flash("tests/example.py", ["test_path"]) 462 | assert mock_save.call_count == 1 463 | # Create the hex we're expecting to flash onto the device. 464 | with open("tests/example.py", "rb") as py_file: 465 | py_code = py_file.read() 466 | assert py_code 467 | expected_hex = uflash.embed_fs_uhex(uflash._RUNTIME, py_code) 468 | assert mock_save.call_args[0][0] == expected_hex 469 | expected_path = os.path.join("test_path", "micropython.hex") 470 | assert mock_save.call_args[0][1] == expected_path 471 | 472 | 473 | def test_flash_with_keepname(): 474 | """ 475 | Flash the referenced path to the micro:bit with a hex file generated from 476 | the MicroPython firmware and the referenced Python script and keep the 477 | original filename root. 478 | """ 479 | with mock.patch("uflash.save_hex") as mock_save: 480 | uflash.flash("tests/example.py", ["test_path"], keepname=True) 481 | assert mock_save.call_count == 1 482 | # Create the hex we're expecting to flash onto the device. 483 | with open("tests/example.py", "rb") as py_file: 484 | py_code = py_file.read() 485 | assert py_code 486 | expected_hex = uflash.embed_fs_uhex(uflash._RUNTIME, py_code) 487 | assert mock_save.call_args[0][0] == expected_hex 488 | expected_path = os.path.join("test_path", "example.hex") 489 | assert mock_save.call_args[0][1] == expected_path 490 | 491 | 492 | def test_main_keepname_message(capsys): 493 | """ 494 | Ensure that the correct message appears when called as from py2hex. 495 | """ 496 | uflash.flash( 497 | "tests/example.py", paths_to_microbits=["tests"], keepname=True 498 | ) 499 | stdout, stderr = capsys.readouterr() 500 | expected = "Hexifying example.py as: {}".format( 501 | os.path.join("tests", "example.hex") 502 | ) 503 | assert (expected in stdout) or (expected in stderr) 504 | 505 | 506 | def test_flash_with_python_script(): 507 | """ 508 | If a byte representation of a Python script is passed into the function it 509 | should hexlify that. 510 | """ 511 | python_script = b"import this" 512 | with mock.patch("uflash.save_hex"): 513 | with mock.patch("uflash.find_microbit", return_value="bar"): 514 | with mock.patch("uflash.embed_fs_uhex") as mock_embed_fs_uhex: 515 | uflash.flash(python_script=python_script) 516 | mock_embed_fs_uhex.assert_called_once_with( 517 | uflash._RUNTIME, python_script 518 | ) 519 | 520 | 521 | def test_flash_cannot_find_microbit(): 522 | """ 523 | Ensure an IOError is raised if it is not possible to find the micro:bit. 524 | """ 525 | with mock.patch("uflash.find_microbit", return_value=None): 526 | with pytest.raises(IOError) as ex: 527 | uflash.flash() 528 | expected = "Unable to find micro:bit. Is it plugged in?" 529 | assert ex.value.args[0] == expected 530 | 531 | 532 | def test_flash_wrong_python(): 533 | """ 534 | Ensures a call to flash will fail if it's not reported that we're using 535 | Python 3. 536 | """ 537 | for version in [(2, 6, 3), (3, 2, 0)]: 538 | with pytest.raises(RuntimeError) as ex: 539 | with mock.patch("sys.version_info", version): 540 | uflash.flash() 541 | assert "Will only run on Python " in ex.value.args[0] 542 | 543 | 544 | def test_main_no_args(): 545 | """ 546 | If there are no args into the main function, it simply calls flash with 547 | no arguments. 548 | """ 549 | with mock.patch( 550 | "sys.argv", 551 | [ 552 | "uflash", 553 | ], 554 | ): 555 | with mock.patch("uflash.flash") as mock_flash: 556 | uflash.main() 557 | mock_flash.assert_called_once_with( 558 | path_to_python=None, paths_to_microbits=[], keepname=False 559 | ) 560 | 561 | 562 | def test_main_first_arg_python(): 563 | """ 564 | If there is a single argument that ends with ".py", it calls flash with 565 | it as the path to the source Python file. 566 | """ 567 | with mock.patch("uflash.flash") as mock_flash: 568 | uflash.main(argv=["foo.py"]) 569 | mock_flash.assert_called_once_with( 570 | path_to_python="foo.py", paths_to_microbits=[], keepname=False 571 | ) 572 | 573 | 574 | def test_main_first_arg_help(capsys): 575 | """ 576 | If there is a single argument of "--help", it prints some help and exits. 577 | """ 578 | with pytest.raises(SystemExit): 579 | uflash.main(argv=["--help"]) 580 | 581 | stdout, _ = capsys.readouterr() 582 | # argparse manipulates the help text (e.g. changes line wrap) 583 | # so it isn't trivial to compare the output to uflash._HELP_TEXT. 584 | expected = "Flash Python onto the BBC micro:bit" 585 | assert expected in stdout 586 | 587 | 588 | def test_main_first_arg_version(capsys): 589 | """ 590 | If there is a single argument of "--version", it prints the version 591 | and exits. 592 | """ 593 | with pytest.raises(SystemExit): 594 | uflash.main(argv=["--version"]) 595 | 596 | stdout, stderr = capsys.readouterr() 597 | expected = uflash.get_version() 598 | # On python 2 --version prints to stderr. On python 3 to stdout. 599 | # https://bugs.python.org/issue18920 600 | assert (expected in stdout) or (expected in stderr) 601 | 602 | 603 | def test_main_first_arg_not_python(capsys): 604 | """ 605 | If the first argument does not end in ".py" then it should display a useful 606 | error message. 607 | """ 608 | with pytest.raises(SystemExit): 609 | uflash.main(argv=["foo.bar"]) 610 | 611 | _, stderr = capsys.readouterr() 612 | expected = 'Python files must end in ".py".' 613 | assert expected in stderr 614 | 615 | 616 | def test_flash_raises(capsys): 617 | """ 618 | If the flash system goes wrong, it should say that's what happened 619 | """ 620 | with mock.patch("uflash.flash", side_effect=RuntimeError("boom")): 621 | with pytest.raises(SystemExit): 622 | uflash.main(argv=["test.py"]) 623 | 624 | _, stderr = capsys.readouterr() 625 | expected = "Error flashing test.py" 626 | assert expected in stderr 627 | 628 | 629 | def test_flash_raises_with_info(capsys): 630 | """ 631 | When flash goes wrong it should mention everything you tell it 632 | """ 633 | with mock.patch("uflash.flash", side_effect=RuntimeError("boom")): 634 | with pytest.raises(SystemExit): 635 | uflash.main(argv=["test.py"]) 636 | 637 | _, stderr = capsys.readouterr() 638 | expected = "Error flashing test.py to microbit: boom\n" 639 | assert stderr == expected 640 | 641 | with mock.patch("uflash.flash", side_effect=RuntimeError("boom")): 642 | with pytest.raises(SystemExit): 643 | uflash.main(argv=["test.py", "D:\\"]) 644 | 645 | _, stderr = capsys.readouterr() 646 | expected = "Error flashing test.py to " + repr(["D:\\"]) + ": boom\n" 647 | assert stderr == expected 648 | 649 | with mock.patch("uflash.flash", side_effect=RuntimeError("boom")): 650 | with pytest.raises(SystemExit): 651 | uflash.main(argv=["test.py", "D:\\"]) 652 | 653 | _, stderr = capsys.readouterr() 654 | expected = "Error flashing test.py to " + repr(["D:\\"]) + ": boom\n" 655 | assert stderr == expected 656 | 657 | 658 | def test_watch_raises(capsys): 659 | """ 660 | If the watch system goes wrong, it should say that's what happened 661 | """ 662 | with mock.patch("uflash.watch_file", side_effect=RuntimeError("boom")): 663 | with pytest.raises(SystemExit): 664 | uflash.main(argv=["--watch", "test.py"]) 665 | 666 | _, stderr = capsys.readouterr() 667 | expected = "Error watching test.py" 668 | assert expected in stderr 669 | 670 | 671 | def test_runtime_not_implemented(capsys): 672 | """ 673 | Raises a NotImplementedError when trying to use the runtime flag. 674 | """ 675 | with pytest.raises(NotImplementedError): 676 | uflash.main(argv=["--runtime", "test.hex"]) 677 | _, stderr = capsys.readouterr() 678 | assert "The 'runtime' flag is no longer supported." in stderr 679 | 680 | 681 | def test_extract_not_implemented(capsys): 682 | """ 683 | Raises a NotImplementedError when trying to use the extract flag. 684 | """ 685 | with pytest.raises(NotImplementedError): 686 | uflash.main(argv=["--extract", "test.py"]) 687 | _, stderr = capsys.readouterr() 688 | assert "The 'extract' flag is no longer supported." in stderr 689 | 690 | 691 | def test_minify_arg(capsys): 692 | """ 693 | Test a the minify flag print an error but doesn't raise an exception. 694 | """ 695 | with mock.patch("uflash.flash") as mock_flash: 696 | uflash.main(argv=["tests/example.py", "-m"]) 697 | _, stderr = capsys.readouterr() 698 | assert "The 'minify' flag is no longer supported, ignoring" in stderr 699 | mock_flash.assert_called_once_with( 700 | path_to_python="tests/example.py", 701 | paths_to_microbits=[], 702 | keepname=False, 703 | ) 704 | 705 | 706 | def test_main_two_args(): 707 | """ 708 | If there are two arguments passed into main, then it should pass them onto 709 | the flash() function. 710 | """ 711 | with mock.patch("uflash.flash", return_value=None) as mock_flash: 712 | uflash.main(argv=["foo.py", "/media/foo/bar"]) 713 | mock_flash.assert_called_once_with( 714 | path_to_python="foo.py", 715 | paths_to_microbits=["/media/foo/bar"], 716 | keepname=False, 717 | ) 718 | 719 | 720 | def test_main_multiple_microbits(): 721 | """ 722 | If there are more than two arguments passed into main, then it should pass 723 | them onto the flash() function. 724 | """ 725 | with mock.patch("uflash.flash", return_value=None) as mock_flash: 726 | uflash.main( 727 | argv=[ 728 | "foo.py", 729 | "/media/foo/bar", 730 | "/media/foo/baz", 731 | "/media/foo/bob", 732 | ] 733 | ) 734 | mock_flash.assert_called_once_with( 735 | path_to_python="foo.py", 736 | paths_to_microbits=[ 737 | "/media/foo/bar", 738 | "/media/foo/baz", 739 | "/media/foo/bob", 740 | ], 741 | keepname=False, 742 | ) 743 | 744 | 745 | def test_main_watch_flag(): 746 | """ 747 | The watch flag cause a call the correct function. 748 | """ 749 | with mock.patch("uflash.watch_file") as mock_watch_file: 750 | uflash.main(argv=["-w"]) 751 | mock_watch_file.assert_called_once_with( 752 | None, uflash.flash, path_to_python=None, paths_to_microbits=[] 753 | ) 754 | 755 | 756 | def test_watch_no_source(): 757 | """ 758 | If there is no source file the watch command should complain. 759 | """ 760 | with pytest.raises(ValueError): 761 | uflash.watch_file(None, lambda: "should never be called!") 762 | 763 | 764 | @mock.patch("uflash.time") 765 | @mock.patch("uflash.os") 766 | def test_watch_file(mock_os, mock_time): 767 | """ 768 | Make sure that the callback is called each time the file changes. 769 | """ 770 | # Our function will throw KeyboardInterrupt when called for the 2nd time, 771 | # ending the watching gracefully.This will help in testing the 772 | # watch_file function. 773 | call_count = [0] 774 | 775 | def func(): 776 | call_count[0] = call_count[0] + 1 777 | if call_count[0] == 2: 778 | raise KeyboardInterrupt() 779 | 780 | # Instead of modifying any file, let's change the return value of 781 | # os.path.getmtime. Start with initial value of 0. 782 | mock_os.path.getmtime.return_value = 0 783 | 784 | t = threading.Thread(target=uflash.watch_file, args=("path/to/file", func)) 785 | t.start() 786 | time.sleep(0.01) 787 | mock_os.path.getmtime.return_value = 1 # Simulate file change 788 | time.sleep(0.01) 789 | assert t.is_alive() 790 | assert call_count[0] == 1 791 | mock_os.path.getmtime.return_value = 2 # Simulate file change 792 | t.join() 793 | assert call_count[0] == 2 794 | 795 | 796 | def test_py2hex_one_arg(): 797 | """ 798 | Test a simple call to main(). 799 | """ 800 | with mock.patch("uflash.flash") as mock_flash: 801 | uflash.py2hex(argv=["tests/example.py"]) 802 | mock_flash.assert_called_once_with( 803 | path_to_python="tests/example.py", 804 | paths_to_microbits=["tests"], 805 | keepname=True, 806 | ) 807 | 808 | 809 | def test_py2hex_minify_arg(capsys): 810 | """ 811 | Test a simple call to main(). 812 | """ 813 | with mock.patch("uflash.flash") as mock_flash: 814 | uflash.py2hex(argv=["tests/example.py", "-m"]) 815 | _, stderr = capsys.readouterr() 816 | assert "The 'minify' flag is no longer supported, ignoring" in stderr 817 | mock_flash.assert_called_once_with( 818 | path_to_python="tests/example.py", 819 | paths_to_microbits=["tests"], 820 | keepname=True, 821 | ) 822 | 823 | 824 | def test_py2hex_outdir_arg(): 825 | """ 826 | Test a simple call to main(). 827 | """ 828 | with mock.patch("uflash.flash") as mock_flash: 829 | uflash.py2hex(argv=["tests/example.py", "-o", "/tmp"]) 830 | mock_flash.assert_called_once_with( 831 | path_to_python="tests/example.py", 832 | paths_to_microbits=["/tmp"], 833 | keepname=True, 834 | ) 835 | 836 | 837 | def test_py2hex_runtime_not_implemented(capsys): 838 | """ 839 | Raises a NotImplementedError when trying to use the runtime flag with the 840 | py2hex command. 841 | """ 842 | with pytest.raises(NotImplementedError): 843 | uflash.py2hex(argv=["--runtime", "test.hex"]) 844 | _, stderr = capsys.readouterr() 845 | assert "The 'runtime' flag is no longer supported." in stderr 846 | 847 | 848 | def test_bytes_to_ihex(): 849 | """ 850 | Test bytes_to_ihex golden path for V1. 851 | """ 852 | data = b"A" * 32 853 | expected_result = "\n".join( 854 | [ 855 | ":020000040003F7", 856 | ":108C10004141414141414141414141414141414144", 857 | ":108C20004141414141414141414141414141414134", 858 | ] 859 | ) 860 | 861 | result = uflash.bytes_to_ihex(0x38C10, data, universal_data_record=False) 862 | 863 | assert result == expected_result 864 | 865 | 866 | def test_bytes_to_ihex_universal(): 867 | """ 868 | Test bytes_to_ihex golden path for V2. 869 | """ 870 | data = b"A" * 32 871 | expected_result = "\n".join( 872 | [ 873 | ":020000040003F7", 874 | ":108C100D4141414141414141414141414141414137", 875 | ":108C200D4141414141414141414141414141414127", 876 | ] 877 | ) 878 | 879 | result = uflash.bytes_to_ihex(0x38C10, data, universal_data_record=True) 880 | 881 | assert result == expected_result 882 | 883 | 884 | def test_bytes_to_ihex_inner_extended_linear_address_record(): 885 | """ 886 | Test bytes_to_ihex golden path for V2. 887 | """ 888 | data = b"A" * 32 889 | expected_result = "\n".join( 890 | [ 891 | ":020000040003F7", 892 | ":10FFF00D41414141414141414141414141414141E4", 893 | ":020000040004F6", 894 | ":1000000D41414141414141414141414141414141D3", 895 | ] 896 | ) 897 | 898 | result = uflash.bytes_to_ihex(0x3FFF0, data, universal_data_record=True) 899 | 900 | assert result == expected_result 901 | 902 | 903 | def test_script_to_fs(): 904 | """ 905 | Test script_to_fs with a random example without anything special about it. 906 | """ 907 | script = b"A" * 364 908 | expected_result = "\n".join( 909 | [ 910 | ":020000040003F7", 911 | ":108C0000FE79076D61696E2E7079414141414141A4", 912 | ":108C10004141414141414141414141414141414144", 913 | ":108C20004141414141414141414141414141414134", 914 | ":108C30004141414141414141414141414141414124", 915 | ":108C40004141414141414141414141414141414114", 916 | ":108C50004141414141414141414141414141414104", 917 | ":108C600041414141414141414141414141414141F4", 918 | ":108C70004141414141414141414141414141410223", 919 | ":108C80000141414141414141414141414141414114", 920 | ":108C900041414141414141414141414141414141C4", 921 | ":108CA00041414141414141414141414141414141B4", 922 | ":108CB00041414141414141414141414141414141A4", 923 | ":108CC0004141414141414141414141414141414194", 924 | ":108CD0004141414141414141414141414141414184", 925 | ":108CE0004141414141414141414141414141414174", 926 | ":108CF00041414141414141414141414141414103A2", 927 | ":108D00000241414141414141414141414141414192", 928 | ":108D10004141414141414141414141414141414143", 929 | ":108D20004141414141414141414141414141414133", 930 | ":108D30004141414141414141414141414141414123", 931 | ":108D40004141414141414141414141414141414113", 932 | ":108D50004141414141414141414141414141414103", 933 | ":108D600041414141414141414141414141414141F3", 934 | ":108D700041414141414141414141FFFFFFFFFFFF6F", 935 | ":01F80000FD0A", 936 | "", 937 | ] 938 | ) 939 | 940 | with mock.patch("uflash._FS_START_ADDR_V1", 0x38C00), mock.patch( 941 | "uflash._FS_END_ADDR_V1", 0x3F800 942 | ): 943 | result = uflash.script_to_fs(script, uflash._MICROBIT_ID_V1) 944 | 945 | assert result == expected_result, script 946 | 947 | 948 | def test_script_to_fs_short(): 949 | """ 950 | Test script_to_fs with a script smaller than a fs chunk. 951 | """ 952 | script = b"Very short example" 953 | expected_result = "\n".join( 954 | [ 955 | ":020000040003F7", 956 | ":108C0000FE1B076D61696E2E70795665727920734F", 957 | ":108C1000686F7274206578616D706C65FFFFFFFF8F", 958 | ":108C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54", 959 | ":108C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44", 960 | ":108C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34", 961 | ":108C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24", 962 | ":108C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14", 963 | ":108C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04", 964 | ":01F80000FD0A", 965 | "", 966 | ] 967 | ) 968 | 969 | with mock.patch("uflash._FS_START_ADDR_V1", 0x38C00), mock.patch( 970 | "uflash._FS_END_ADDR_V1", 0x3F800 971 | ): 972 | result = uflash.script_to_fs(script, uflash._MICROBIT_ID_V1) 973 | 974 | assert result == expected_result, script 975 | 976 | 977 | def test_script_to_fs_two_chunks(): 978 | """ 979 | Test script_to_fs with a script that takes two chunks for V1 and V2. 980 | """ 981 | expected_result_v1 = "\n".join(TEST_SCRIPT_FS_V1_HEX_LIST + [""]) 982 | expected_result_v2 = "\n".join(TEST_SCRIPT_FS_V2_HEX_LIST + [""]) 983 | 984 | with mock.patch("uflash._FS_START_ADDR_V1", 0x38C00), mock.patch( 985 | "uflash._FS_END_ADDR_V1", 0x3F800 986 | ): 987 | result_v1 = uflash.script_to_fs(TEST_SCRIPT_FS, uflash._MICROBIT_ID_V1) 988 | result_v2 = uflash.script_to_fs(TEST_SCRIPT_FS, uflash._MICROBIT_ID_V2) 989 | 990 | assert result_v1 == expected_result_v1 991 | assert result_v2 == expected_result_v2 992 | 993 | 994 | def test_script_to_fs_chunk_boundary(): 995 | """ 996 | Test script_to_fs edge case with the taking exactly one chunk. 997 | """ 998 | script_short = ( 999 | b"This is an edge case test to fill the last byte of " 1000 | b"the first chunk.\n" + (b"A" * 48) 1001 | ) 1002 | expected_result_short = "\n".join( 1003 | [ 1004 | ":020000040003F7", 1005 | ":108C0000FE7D076D61696E2E707954686973206905", 1006 | ":108C10007320616E206564676520636173652074ED", 1007 | ":108C200065737420746F2066696C6C2074686520AD", 1008 | ":108C30006C6173742062797465206F662074686556", 1009 | ":108C4000206669727374206368756E6B2E0A4141E9", 1010 | ":108C50004141414141414141414141414141414104", 1011 | ":108C600041414141414141414141414141414141F4", 1012 | ":108C70004141414141414141414141414141FFFF68", 1013 | ":01F80000FD0A", 1014 | "", 1015 | ] 1016 | ) 1017 | script_exact = ( 1018 | b"This is an edge case test to fill the last byte of " 1019 | b"the first chunk.\n" + (b"A" * 49) 1020 | ) 1021 | expected_result_exact = "\n".join( 1022 | [ 1023 | ":020000040003F7", 1024 | ":108C0000FE00076D61696E2E707954686973206982", 1025 | ":108C10007320616E206564676520636173652074ED", 1026 | ":108C200065737420746F2066696C6C2074686520AD", 1027 | ":108C30006C6173742062797465206F662074686556", 1028 | ":108C4000206669727374206368756E6B2E0A4141E9", 1029 | ":108C50004141414141414141414141414141414104", 1030 | ":108C600041414141414141414141414141414141F4", 1031 | ":108C70004141414141414141414141414141410223", 1032 | ":108C800001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2", 1033 | ":108C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4", 1034 | ":108CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4", 1035 | ":108CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4", 1036 | ":108CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4", 1037 | ":108CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4", 1038 | ":108CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94", 1039 | ":108CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84", 1040 | ":01F80000FD0A", 1041 | "", 1042 | ] 1043 | ) 1044 | script_large = ( 1045 | b"This is an edge case test to fill the last byte of " 1046 | b"the first chunk.\n" + (b"A" * 50) 1047 | ) 1048 | expected_result_large = "\n".join( 1049 | [ 1050 | ":020000040003F7", 1051 | ":108C0000FE01076D61696E2E707954686973206981", 1052 | ":108C10007320616E206564676520636173652074ED", 1053 | ":108C200065737420746F2066696C6C2074686520AD", 1054 | ":108C30006C6173742062797465206F662074686556", 1055 | ":108C4000206669727374206368756E6B2E0A4141E9", 1056 | ":108C50004141414141414141414141414141414104", 1057 | ":108C600041414141414141414141414141414141F4", 1058 | ":108C70004141414141414141414141414141410223", 1059 | ":108C80000141FFFFFFFFFFFFFFFFFFFFFFFFFFFFB0", 1060 | ":108C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4", 1061 | ":108CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4", 1062 | ":108CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4", 1063 | ":108CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4", 1064 | ":108CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4", 1065 | ":108CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94", 1066 | ":108CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84", 1067 | ":01F80000FD0A", 1068 | "", 1069 | ] 1070 | ) 1071 | 1072 | with mock.patch("uflash._FS_START_ADDR_V1", 0x38C00), mock.patch( 1073 | "uflash._FS_END_ADDR_V1", 0x3F800 1074 | ): 1075 | result_short = uflash.script_to_fs( 1076 | script_short, uflash._MICROBIT_ID_V1 1077 | ) 1078 | result_exact = uflash.script_to_fs( 1079 | script_exact, uflash._MICROBIT_ID_V1 1080 | ) 1081 | result_large = uflash.script_to_fs( 1082 | script_large, uflash._MICROBIT_ID_V1 1083 | ) 1084 | 1085 | assert result_short == expected_result_short, script_short 1086 | assert result_exact == expected_result_exact, script_exact 1087 | assert result_large == expected_result_large, script_large 1088 | 1089 | 1090 | def test_script_to_fs_script_too_long(): 1091 | """ 1092 | Test script_to_fs when the script is too long and won't fit. 1093 | """ 1094 | script = (b"shouldfit" * 3023)[:-1] 1095 | _ = uflash.script_to_fs(script, uflash._MICROBIT_ID_V1) 1096 | 1097 | script += b"1" 1098 | with pytest.raises(ValueError) as ex: 1099 | _ = uflash.script_to_fs(script, uflash._MICROBIT_ID_V1) 1100 | assert "Python script must be less than" in ex.value.args[0] 1101 | 1102 | 1103 | def test_script_to_fs_empty_code(): 1104 | """ 1105 | Test script_to_fs results an empty string if the input code is empty. 1106 | """ 1107 | result = uflash.script_to_fs("", uflash._MICROBIT_ID_V1) 1108 | assert result == "" 1109 | 1110 | 1111 | def test_script_to_fs_line_endings(): 1112 | """ 1113 | Test script_to_fs converts line endings before embedding script. 1114 | """ 1115 | script_win_lines = TEST_SCRIPT_FS.replace(b"\n", b"\r\n") 1116 | script_cr_lines = TEST_SCRIPT_FS.replace(b"\n", b"\r") 1117 | expected_result = "\n".join(TEST_SCRIPT_FS_V1_HEX_LIST + [""]) 1118 | 1119 | with mock.patch("uflash._FS_START_ADDR_V1", 0x38C00), mock.patch( 1120 | "uflash._FS_END_ADDR_V1", 0x3F800 1121 | ): 1122 | result_win = uflash.script_to_fs( 1123 | script_win_lines, uflash._MICROBIT_ID_V1 1124 | ) 1125 | result_cr = uflash.script_to_fs( 1126 | script_cr_lines, uflash._MICROBIT_ID_V1 1127 | ) 1128 | 1129 | assert result_win == expected_result 1130 | assert result_cr == expected_result 1131 | 1132 | 1133 | def test_script_to_fs_unknown_microbit_id(): 1134 | """ 1135 | Test script_to_fs when the micro:bit ID is not recognised. 1136 | """ 1137 | with pytest.raises(ValueError) as ex: 1138 | _ = uflash.script_to_fs(TEST_SCRIPT_FS, "1234") 1139 | 1140 | assert "Incompatible micro:bit ID found: 1234" in ex.value.args[0] 1141 | 1142 | 1143 | def test_embed_fs_uhex(): 1144 | """ 1145 | Test embed_fs_uhex to add the filesystem into a Universal Hex with standard 1146 | two sections, one for V1 and one for V2. 1147 | """ 1148 | uhex = "\n".join(TEST_UNIVERSAL_HEX_LIST) 1149 | uhex_alignment = len(uhex) % 512 1150 | v1_fs_i = 11 1151 | v2_fs_i = 20 1152 | expected_uhex = "\n".join( 1153 | TEST_UNIVERSAL_HEX_LIST[:v1_fs_i] 1154 | + TEST_SCRIPT_FS_V1_HEX_LIST 1155 | + TEST_SCRIPT_FS_V1_HEX_PADDING_LIST 1156 | + TEST_UNIVERSAL_HEX_LIST[v1_fs_i:v2_fs_i] 1157 | + TEST_SCRIPT_FS_V2_HEX_LIST 1158 | + TEST_SCRIPT_FS_V2_HEX_PADDING_LIST 1159 | + TEST_UNIVERSAL_HEX_LIST[v2_fs_i:] 1160 | ) 1161 | 1162 | with mock.patch("uflash._FS_START_ADDR_V1", 0x38C00), mock.patch( 1163 | "uflash._FS_END_ADDR_V1", 0x3F800 1164 | ), mock.patch("uflash._FS_START_ADDR_V2", 0x6D000), mock.patch( 1165 | "uflash._FS_END_ADDR_V2", 0x72000 1166 | ): 1167 | uhex_with_fs = uflash.embed_fs_uhex(uhex, TEST_SCRIPT_FS) 1168 | 1169 | assert expected_uhex == uhex_with_fs 1170 | assert uhex_alignment == (len(uhex_with_fs) % 512) 1171 | 1172 | 1173 | def test_pad_hex_records(): 1174 | """ 1175 | Test the function pads a generic fs hex block to 512 byte alignment. 1176 | """ 1177 | fs_v1_hex_str = "\n".join(TEST_SCRIPT_FS_V1_HEX_LIST) 1178 | fs_v2_hex_str = "\n".join(TEST_SCRIPT_FS_V2_HEX_LIST) 1179 | 1180 | padded_v1 = uflash.pad_hex_string(fs_v1_hex_str) 1181 | padded_v2 = uflash.pad_hex_string(fs_v2_hex_str) 1182 | 1183 | assert len(fs_v1_hex_str) % 512 != 0 1184 | assert len(padded_v1) % 512 == 0 1185 | assert len(fs_v2_hex_str) % 512 != 0 1186 | assert len(padded_v2) % 512 == 0 1187 | 1188 | 1189 | def test_pad_hex_records_no_padding_needed(): 1190 | """ 1191 | Test the function doesn't pad is the string is already memory aligned. 1192 | """ 1193 | fs_v1_hex_str = "\n".join( 1194 | TEST_SCRIPT_FS_V1_HEX_LIST + TEST_SCRIPT_FS_V1_HEX_PADDING_LIST + [""] 1195 | ) 1196 | fs_v2_hex_str = "\n".join( 1197 | TEST_SCRIPT_FS_V2_HEX_LIST + TEST_SCRIPT_FS_V2_HEX_PADDING_LIST + [""] 1198 | ) 1199 | 1200 | padded_v1 = uflash.pad_hex_string(fs_v1_hex_str) 1201 | padded_v2 = uflash.pad_hex_string(fs_v2_hex_str) 1202 | 1203 | assert len(fs_v1_hex_str) % 512 == 0 1204 | assert padded_v1 == fs_v1_hex_str 1205 | assert len(fs_v2_hex_str) % 512 == 0 1206 | assert padded_v2 == padded_v2 1207 | 1208 | 1209 | def test_embed_fs_uhex_extra_uicr_jump_record(): 1210 | uhex_list = [ 1211 | # Section for V1 starts 1212 | ":020000040000FA", 1213 | ":0400000A9900C0DEBB", 1214 | ":1000000000400020218E01005D8E01005F8E010006", 1215 | ":1000100000000000000000000000000000000000E0", 1216 | ":10002000000000000000000000000000618E0100E0", 1217 | ":10003000040007609F4203D1042302791343037134", 1218 | ":0888B00095880100C1000000E1", 1219 | # V1 UICR 1220 | ":020000041000EA", 1221 | ":1010C0007CB0EE17FFFFFFFF0A0000000000E30006", 1222 | ":0C10D000FFFFFFFF2D6D0300000000007B", 1223 | # Section for V2 starts 1224 | ":020000040000FA", 1225 | ":0400000A9903C0DEB8", 1226 | ":1000000D00040020810A000015070000610A0000AD", 1227 | ":020000040001F9", 1228 | ":1000000D03D13000F8BD4010F3E7331D0122180082", 1229 | ":1000100DF8F7B2FD4460EFE7E4B30200F0B5070083", 1230 | ":1000200D89B000201E000D00019215F0ECFB0E4B74", 1231 | # V2 UICR with an extra extended linear address record 1232 | ":020000040000FA", 1233 | ":020000041000EA", 1234 | ":0810140D0070070000E0070069", 1235 | # V2 Regions table (this in flash again) 1236 | ":020000040006F4", 1237 | ":102FC00D0100010000B00100000000000000000041", 1238 | ":102FD00D02021C00E46504009CA105000000000035", 1239 | ":102FE00D03006D0000600000000000000000000004", 1240 | ":102FF00DFE307F590100300003000C009DD7B1C198", 1241 | ":00000001FF", 1242 | "", 1243 | ] 1244 | uhex_ela_record = "\n".join(uhex_list) 1245 | uhex_ela_record_alignment = len(uhex_ela_record) % 512 1246 | v1_fs_i = 7 1247 | v2_fs_i = 17 1248 | expected_uhex_ela_record = "\n".join( 1249 | uhex_list[:v1_fs_i] 1250 | + TEST_SCRIPT_FS_V1_HEX_LIST 1251 | + TEST_SCRIPT_FS_V1_HEX_PADDING_LIST 1252 | + uhex_list[v1_fs_i:v2_fs_i] 1253 | + TEST_SCRIPT_FS_V2_HEX_LIST 1254 | + TEST_SCRIPT_FS_V2_HEX_PADDING_LIST 1255 | + uhex_list[v2_fs_i:] 1256 | ) 1257 | # Replace Extended linear Address with Segmented record 1258 | uhex_list[v2_fs_i] = ":020000020000FC" 1259 | uhex_esa_record = "\n".join(uhex_list) 1260 | uhex_esa_record_alignment = len(uhex_esa_record) % 512 1261 | expected_uhex_esa_record = "\n".join( 1262 | uhex_list[:v1_fs_i] 1263 | + TEST_SCRIPT_FS_V1_HEX_LIST 1264 | + TEST_SCRIPT_FS_V1_HEX_PADDING_LIST 1265 | + uhex_list[v1_fs_i:v2_fs_i] 1266 | + TEST_SCRIPT_FS_V2_HEX_LIST 1267 | + TEST_SCRIPT_FS_V2_HEX_PADDING_LIST 1268 | + uhex_list[v2_fs_i:] 1269 | ) 1270 | 1271 | with mock.patch("uflash._FS_START_ADDR_V1", 0x38C00), mock.patch( 1272 | "uflash._FS_END_ADDR_V1", 0x3F800 1273 | ), mock.patch("uflash._FS_START_ADDR_V2", 0x6D000), mock.patch( 1274 | "uflash._FS_END_ADDR_V2", 0x72000 1275 | ): 1276 | uhex_ela_with_fs = uflash.embed_fs_uhex( 1277 | uhex_ela_record, TEST_SCRIPT_FS 1278 | ) 1279 | uhex_esa_with_fs = uflash.embed_fs_uhex( 1280 | uhex_esa_record, TEST_SCRIPT_FS 1281 | ) 1282 | 1283 | assert expected_uhex_ela_record == uhex_ela_with_fs 1284 | assert uhex_ela_record_alignment == (len(uhex_ela_with_fs) % 512) 1285 | assert expected_uhex_esa_record == uhex_esa_with_fs 1286 | assert uhex_esa_record_alignment == (len(uhex_esa_with_fs) % 512) 1287 | 1288 | 1289 | def test_embed_fs_uhex_empty_code(): 1290 | uhex = "\n".join(TEST_UNIVERSAL_HEX_LIST) 1291 | 1292 | identical_uhex = uflash.embed_fs_uhex(uhex, "") 1293 | 1294 | assert identical_uhex == uhex 1295 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py35, py36, py37, py38, py39, lint 3 | 4 | [testenv] 5 | commands = py.test --cov-report term-missing --cov=uflash {posargs:tests/} 6 | deps = 7 | pytest 8 | pytest-cov 9 | coveralls 10 | nudatus 11 | # Mock is bundled as part of unittest since Python 3.3 12 | # mock_open can't read binary data in < 3.4.3 13 | py{27,34}: mock 14 | 15 | [testenv:lint] 16 | commands = 17 | pyflakes setup.py uflash.py tests/ 18 | pycodestyle setup.py uflash.py tests/ 19 | deps = 20 | pyflakes 21 | pycodestyle 22 | --------------------------------------------------------------------------------