From 8c523fe7b01379aab97f8a2f4c26fd5921b9b433 Mon Sep 17 00:00:00 2001 From: usserr Date: Sat, 21 Sep 2019 13:46:20 +0300 Subject: [PATCH 01/26] fix for CHANGELOG --- CHANGELOG | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index afcf3d2..07afc68 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -96,6 +96,5 @@ stm32pio changelog: - Changed: logging output in standard (non-verbose) mode is simpler - Changed: move tests in new location - Changed: revised and improved tests - - Changed: tests are now run by 'python -m unittest' and cannot be run as standalone - Changed: actualized .ioc file and clean-up the code according to the latest STM32CubeMX version (5.3.0 at the moment) - Changed: revised and improved util module From e7f9043bce16a775c15ac5b9c0415ea5935a4925 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sat, 21 Sep 2019 21:59:58 +0300 Subject: [PATCH 02/26] v0.9-alpha: introducing Stm32pio class --- TODO.md | 8 +- stm32pio/stm32pio.py | 14 +- stm32pio/tests/test.py | 85 ++++++----- stm32pio/util.py | 336 +++++++++++++++++++---------------------- 4 files changed, 213 insertions(+), 230 deletions(-) diff --git a/TODO.md b/TODO.md index 32f3df7..a51e4af 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,9 @@ # TODOs - [ ] Middleware support (FreeRTOS, etc.) - [ ] Add more checks, for example when updating the project (`generate` command), check for boards matching and so on... + - [ ] Function annotations - [ ] Do we need some sort of GUI? For example, drop the folder into small window (with checkboxes corresponding with CLI options) and get the output - - [ ] remade as Class (constructor init(project_path)) - - [ ] test CLI (i.e. run stm32pio as subprocess) - - [ ] upload to PyPI + - [ ] Remade as Class (constructor `__init__(project_path)`) + - [ ] Test CLI (i.e. run stm32pio as subprocess) + - [ ] Upload to PyPI + - [ ] `__main__` diff --git a/stm32pio/stm32pio.py b/stm32pio/stm32pio.py index e63f706..a282a2d 100755 --- a/stm32pio/stm32pio.py +++ b/stm32pio/stm32pio.py @@ -62,19 +62,21 @@ def main(): import stm32pio.util try: + project = stm32pio.util.Stm32pio(args.project_path) + if args.subcommand == 'new' or args.subcommand == 'generate': - stm32pio.util.generate_code(args.project_path) + project.generate_code() if args.subcommand == 'new': - stm32pio.util.pio_init(args.project_path, args.board) - stm32pio.util.patch_platformio_ini(args.project_path) + project.pio_init(args.board) + project.patch_platformio_ini() if args.with_build: - stm32pio.util.pio_build(args.project_path) + project.pio_build() if args.editor: - stm32pio.util.start_editor(args.project_path, args.editor) + project.start_editor(args.editor) elif args.subcommand == 'clean': - stm32pio.util.clean(args.project_path) + project.clean() except Exception as e: print(e.__repr__()) diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index 65ea76f..8384633 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -7,32 +7,26 @@ import stm32pio.util +# Test data project_path = pathlib.Path('stm32pio/tests/stm32pio-test-project').resolve() +if not project_path.is_dir() and not project_path.joinpath('stm32pio-test-project.ioc').is_file(): + raise FileNotFoundError("No test project is present") board = 'nucleo_f031k6' -def clean_run(test): - """ - The decorator that clean the project directory before the test (or any other function). Its functionality can also - be done using setUp method of unittest. Also, we assume that stm32pio.util.clean() does not contain any errors - itself :) - """ - def wrapper(self): - stm32pio.util.clean(project_path) - return test(self) - return wrapper - - - class Test(unittest.TestCase): - @clean_run + def setUp(self) -> None: + project_to_clean = stm32pio.util.Stm32pio(project_path) + project_to_clean.clean() + def test_generate_code(self): """ Check whether files and folders have been created """ - stm32pio.util.generate_code(project_path) + project = stm32pio.util.Stm32pio(project_path) + project.generate_code() # Assuming that the presence of these files indicates a success files_should_be_present = [stm32pio.settings.cubemx_script_filename, 'Src/main.c', 'Inc/main.h'] self.assertEqual([project_path.joinpath(file).is_file() for file in files_should_be_present], @@ -40,24 +34,24 @@ def test_generate_code(self): msg=f"At least one of {files_should_be_present} files haven't been created") - @clean_run def test_pio_init(self): """ Consider that existence of 'platformio.ini' file is displaying successful PlatformIO project initialization """ - stm32pio.util.pio_init(project_path, board) + project = stm32pio.util.Stm32pio(project_path) + project.pio_init(board) self.assertTrue(project_path.joinpath('platformio.ini').is_file(), msg="platformio.ini is not there") - @clean_run def test_patch_platformio_ini(self): """ Compare contents of the patched string and the desired patch """ + project = stm32pio.util.Stm32pio(project_path) test_content = "*** TEST PLATFORMIO.INI FILE ***" project_path.joinpath('platformio.ini').write_text(test_content) - stm32pio.util.patch_platformio_ini(project_path) + project.patch_platformio_ini() after_patch_content = project_path.joinpath('platformio.ini').read_text() @@ -69,38 +63,40 @@ def test_patch_platformio_ini(self): msg="patch content is not as expected") - @clean_run def test_build_should_raise(self): """ Build an empty project so PlatformIO should return non-zero code and we should throw the exception """ - stm32pio.util.pio_init(project_path, board) + project = stm32pio.util.Stm32pio(project_path) + project.pio_init(board) with self.assertRaisesRegex(Exception, "PlatformIO build error", msg="Build error exception hadn't been raised"): - stm32pio.util.pio_build(project_path) + project.pio_build() - @clean_run def test_build(self): """ Initialize a new project and try to build it """ - stm32pio.util.generate_code(project_path) - stm32pio.util.pio_init(project_path, board) - stm32pio.util.patch_platformio_ini(project_path) + project = stm32pio.util.Stm32pio(project_path) + project.generate_code() + project.pio_init(board) + project.patch_platformio_ini() - result = stm32pio.util.pio_build(project_path) + result = project.pio_build() self.assertEqual(result, 0, msg="Build failed") + # TODO: use subTest() def test_run_editor(self): """ Call the editors """ - stm32pio.util.start_editor(project_path, 'atom') - stm32pio.util.start_editor(project_path, 'code') - stm32pio.util.start_editor(project_path, 'subl') + project = stm32pio.util.Stm32pio(project_path) + project.start_editor('atom') + project.start_editor('code') + project.start_editor('subl') time.sleep(1) # wait a little bit for apps to start if stm32pio.settings.my_os == 'Windows': @@ -123,17 +119,18 @@ def test_run_editor(self): self.assertIn('sublime', result.stdout) - @clean_run def test_regenerate_code(self): """ Simulate new project creation, its changing and CubeMX code re-generation (for example, after adding new hardware features and some new files) """ + project = stm32pio.util.Stm32pio(project_path) + # Generate a new project ... - stm32pio.util.generate_code(project_path) - stm32pio.util.pio_init(project_path, board) - stm32pio.util.patch_platformio_ini(project_path) + project.generate_code() + project.pio_init(board) + project.patch_platformio_ini() # ... change it: test_file_1 = project_path.joinpath('Src', 'main.c') @@ -149,7 +146,7 @@ def test_regenerate_code(self): test_file_2.write_text(test_content_2) # Re-generate CubeMX project - stm32pio.util.generate_code(project_path) + project.generate_code() # Check if added information is preserved main_c_after_regenerate_content = test_file_1.read_text() @@ -160,20 +157,22 @@ def test_regenerate_code(self): msg=f"{test_file_2} does not preserve user content after regeneration") - def test_file_not_found(self): - """ - Pass non-existing path and expect the error - """ - not_existing_path = project_path.joinpath('does_not_exist') - with self.assertRaises(FileNotFoundError, msg="FileNotFoundError was not raised"): - stm32pio.util._get_project_path(not_existing_path) + # def test_file_not_found(self): + # """ + # Pass non-existing path and expect the error + # """ + # project = stm32pio.util.Stm32pio(project_path) + # not_existing_path = project_path.joinpath('does_not_exist') + # with self.assertRaises(FileNotFoundError, msg="FileNotFoundError was not raised"): + # project._get_project_path(not_existing_path) def tearDownModule(): """ Clean up after yourself """ - stm32pio.util.clean(project_path) + project_to_clean = stm32pio.util.Stm32pio(project_path) + project_to_clean.clean() diff --git a/stm32pio/util.py b/stm32pio/util.py index b89dc21..88b8729 100644 --- a/stm32pio/util.py +++ b/stm32pio/util.py @@ -9,199 +9,179 @@ -def _get_project_path(dirty_path): +class Stm32pio: """ - Handle '/path/to/proj' and '/path/to/proj/', . (current directory) and other cases - - Args: - dirty_path: some filesystem path - """ - correct_path = pathlib.Path(dirty_path).resolve() - if not correct_path.exists(): - logger.error("incorrect project path") - raise FileNotFoundError(correct_path) - else: - return correct_path - - - -def generate_code(dirty_path): + Main class """ - Call STM32CubeMX app as a 'java -jar' file with the automatically prearranged 'cubemx-script' file - Args: - dirty_path: path to the project (folder with a .ioc file) - """ - - project_path = _get_project_path(dirty_path) - - # Assuming the name of the '.ioc' file is the same as the project folder, we extract it from the given string - project_name = project_path.name - logger.debug(f"searching for {project_name}.ioc file...") - cubemx_ioc_full_filename = project_path.joinpath(f'{project_name}.ioc') - if cubemx_ioc_full_filename.exists(): - logger.debug(f"{project_name}.ioc file was found") - else: - logger.error(f"there is no {project_name}.ioc file") - raise FileNotFoundError(cubemx_ioc_full_filename) - - # Find/create 'cubemx-script' file - logger.debug(f"searching for '{stm32pio.settings.cubemx_script_filename}' file...") - cubemx_script_full_filename = project_path.joinpath(stm32pio.settings.cubemx_script_filename) - if not cubemx_script_full_filename.is_file(): - logger.debug(f"'{stm32pio.settings.cubemx_script_filename}' file wasn't found, creating one...") - cubemx_script_content = stm32pio.settings.cubemx_script_content.format( - project_path=project_path, cubemx_ioc_full_filename=cubemx_ioc_full_filename) - cubemx_script_full_filename.write_text(cubemx_script_content) - logger.debug(f"'{stm32pio.settings.cubemx_script_filename}' file has been successfully created") - else: - logger.debug(f"'{stm32pio.settings.cubemx_script_filename}' file is already there") - - logger.info("starting to generate a code from the CubeMX .ioc file...") - command_arr = [stm32pio.settings.java_cmd, '-jar', stm32pio.settings.cubemx_path, '-q', - str(cubemx_script_full_filename)] - if logger.level <= logging.DEBUG: - result = subprocess.run(command_arr) - else: - result = subprocess.run(command_arr, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + def __init__(self, dirty_path): + self.project_path = self._resolve_project_path(dirty_path) + + + @staticmethod + def _resolve_project_path(dirty_path): + """ + Handle 'path/to/proj' and 'path/to/proj/', '.' (current directory) and other cases + + Args: + dirty_path: some directory in the filesystem + """ + correct_path = pathlib.Path(dirty_path).resolve() + if not correct_path.exists(): + logger.error("incorrect project path") + raise FileNotFoundError(correct_path) + else: + return correct_path + + + def generate_code(self): + """ + Call STM32CubeMX app as a 'java -jar' file with the automatically prearranged 'cubemx-script' file + """ + + # Assuming the name of the '.ioc' file is the same as the project folder, we extract it from the given string + project_name = self.project_path.name + logger.debug(f"searching for {project_name}.ioc file...") + cubemx_ioc_full_filename = self.project_path.joinpath(f'{project_name}.ioc') + if cubemx_ioc_full_filename.exists(): + logger.debug(f"{project_name}.ioc file was found") + else: + logger.error(f"there is no {project_name}.ioc file") + raise FileNotFoundError(cubemx_ioc_full_filename) + + # Find/create 'cubemx-script' file + logger.debug(f"searching for '{stm32pio.settings.cubemx_script_filename}' file...") + cubemx_script_full_filename = self.project_path.joinpath(stm32pio.settings.cubemx_script_filename) + if not cubemx_script_full_filename.is_file(): + logger.debug(f"'{stm32pio.settings.cubemx_script_filename}' file wasn't found, creating one...") + cubemx_script_content = stm32pio.settings.cubemx_script_content.format( + project_path=self.project_path, cubemx_ioc_full_filename=cubemx_ioc_full_filename) + cubemx_script_full_filename.write_text(cubemx_script_content) + logger.debug(f"'{stm32pio.settings.cubemx_script_filename}' file has been successfully created") + else: + logger.debug(f"'{stm32pio.settings.cubemx_script_filename}' file is already there") + + logger.info("starting to generate a code from the CubeMX .ioc file...") + command_arr = [stm32pio.settings.java_cmd, '-jar', stm32pio.settings.cubemx_path, '-q', + str(cubemx_script_full_filename)] + if logger.level <= logging.DEBUG: + result = subprocess.run(command_arr) + else: + result = subprocess.run(command_arr, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + # Or, for Python 3.7 and above: + # result = subprocess.run(command_arr, capture_output=True) + if result.returncode == 0: + logger.info("successful code generation") + else: + logger.error(f"code generation error (CubeMX return code is {result.returncode}).\n" + "Try to enable a verbose output or generate a code from the CubeMX itself.") + raise Exception("code generation error") + + + def pio_init(self, board): + """ + Call PlatformIO CLI to initialize a new project + + Args: + board: string displaying PlatformIO name of MCU/board (from 'pio boards' command) + """ + + # Check board name + logger.debug("searching for PlatformIO board...") + result = subprocess.run([stm32pio.settings.platformio_cmd, 'boards'], encoding='utf-8', + stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Or, for Python 3.7 and above: - # result = subprocess.run(command_arr, capture_output=True) - if result.returncode == 0: - logger.info("successful code generation") - else: - logger.error(f"code generation error (CubeMX return code is {result.returncode}).\n" - "Try to enable a verbose output or generate a code from the CubeMX itself.") - raise Exception("code generation error") - - -def pio_init(dirty_path, board): - """ - Call PlatformIO CLI to initialize a new project - - Args: - dirty_path: path to the project (folder with a .ioc file) - board: string displaying PlatformIO name of MCU/board (from 'pio boards' command) - """ - - project_path = _get_project_path(dirty_path) - - # Check board name - logger.debug("searching for PlatformIO board...") - result = subprocess.run([stm32pio.settings.platformio_cmd, 'boards'], encoding='utf-8', - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - # Or, for Python 3.7 and above: - # result = subprocess.run(['platformio', 'boards'], encoding='utf-8', capture_output=True) - if result.returncode == 0: - if board not in result.stdout.split(): - logger.error("wrong STM32 board. Run 'platformio boards' for possible names") - raise Exception("wrong STM32 board") - else: - logger.error("failed to start PlatformIO") - raise Exception("failed to start PlatformIO") - - logger.info("starting PlatformIO project initialization...") - command_arr = [stm32pio.settings.platformio_cmd, 'init', '-d', str(project_path), '-b', board, - '-O', 'framework=stm32cube'] - if logger.level > logging.DEBUG: - command_arr.append('--silent') - result = subprocess.run(command_arr) - if result.returncode == 0: - logger.info("successful PlatformIO project initialization") - else: - logger.error("PlatformIO project initialization error") - raise Exception("PlatformIO error") - - - -def patch_platformio_ini(dirty_path): - """ - Patch platformio.ini file to use created earlier by CubeMX 'Src' and 'Inc' folders as sources - - Args: - dirty_path: path to the project (folder with .ioc and platformio.ini files) - """ - - project_path = _get_project_path(dirty_path) - - logger.debug("patching 'platformio.ini' file...") - - platformio_ini_file = project_path.joinpath('platformio.ini') - if platformio_ini_file.is_file(): - with platformio_ini_file.open(mode='a') as f: - f.write(stm32pio.settings.platformio_ini_patch_content) - logger.info("'platformio.ini' patched") - else: - logger.warning("'platformio.ini' file not found") - - shutil.rmtree(str(project_path.joinpath('include')), ignore_errors=True) - if not project_path.joinpath('SRC').is_dir(): # case sensitive file system - shutil.rmtree(str(project_path.joinpath('src')), ignore_errors=True) - - - -def start_editor(dirty_path, editor_command): - """ - Start 'editor' with project at 'project_path' opened - - Args: - dirty_path: path to the project - editor: editor keyword - """ - - project_path = _get_project_path(dirty_path) - - logger.info("starting an editor...") + # result = subprocess.run(['platformio', 'boards'], encoding='utf-8', capture_output=True) + if result.returncode == 0: + if board not in result.stdout.split(): + logger.error("wrong STM32 board. Run 'platformio boards' for possible names") + raise Exception("wrong STM32 board") + else: + logger.error("failed to start PlatformIO") + raise Exception("failed to start PlatformIO") + + logger.info("starting PlatformIO project initialization...") + command_arr = [stm32pio.settings.platformio_cmd, 'init', '-d', str(self.project_path), '-b', board, + '-O', 'framework=stm32cube'] + if logger.level > logging.DEBUG: + command_arr.append('--silent') + result = subprocess.run(command_arr) + if result.returncode == 0: + logger.info("successful PlatformIO project initialization") + else: + logger.error("PlatformIO project initialization error") + raise Exception("PlatformIO error") - try: - subprocess.run([editor_command, str(project_path)], check=True) - except subprocess.CalledProcessError as e: - logger.error(f"Failed to start the editor {editor_command}: {e.stderr}") + def patch_platformio_ini(self): + """ + Patch platformio.ini file to use created earlier by CubeMX 'Src' and 'Inc' folders as sources + """ + logger.debug("patching 'platformio.ini' file...") -def pio_build(dirty_path): - """ - Initiate a build of the PlatformIO project by the PlatformIO ('run' command) + platformio_ini_file = self.project_path.joinpath('platformio.ini') + if platformio_ini_file.is_file(): + with platformio_ini_file.open(mode='a') as f: + f.write(stm32pio.settings.platformio_ini_patch_content) + logger.info("'platformio.ini' patched") + else: + logger.warning("'platformio.ini' file not found") - Args: - dirty_path: path to the project - Returns: - 0 if success, raise an exception otherwise - """ + shutil.rmtree(str(self.project_path.joinpath('include')), ignore_errors=True) + if not self.project_path.joinpath('SRC').is_dir(): # case sensitive file system + shutil.rmtree(str(self.project_path.joinpath('src')), ignore_errors=True) - project_path = _get_project_path(dirty_path) - logger.info("starting PlatformIO build...") - command_arr = [stm32pio.settings.platformio_cmd, 'run', '-d', str(project_path)] - if logger.level > logging.DEBUG: - command_arr.append('--silent') - result = subprocess.run(command_arr) - if result.returncode == 0: - logger.info("successful PlatformIO build") - return result.returncode - else: - logger.error("PlatformIO build error") - raise Exception("PlatformIO build error") + def start_editor(self, editor_command): + """ + Start the editor specified by 'editor_command' with the project opened + Args: + editor_command: editor command (as we start in the terminal) + """ + logger.info("starting an editor...") -def clean(dirty_path): - """ - Clean-up the project folder and preserve only an '.ioc' file + try: + subprocess.run([editor_command, str(self.project_path)], check=True) + except subprocess.CalledProcessError as e: + logger.error(f"Failed to start the editor {editor_command}: {e.stderr}") - Args: - dirty_path: path to the project - """ - project_path = _get_project_path(dirty_path) + def pio_build(self): + """ + Initiate a build of the PlatformIO project by the PlatformIO ('run' command) - for child in project_path.iterdir(): - if child.name != f"{project_path.name}.ioc": - if child.is_dir(): - shutil.rmtree(str(child), ignore_errors=True) - logger.debug(f"del {child}/") - elif child.is_file(): - child.unlink() - logger.debug(f"del {child}") + Returns: + 0 if success, raise an exception otherwise + """ - logger.info("project has been cleaned") + logger.info("starting PlatformIO build...") + command_arr = [stm32pio.settings.platformio_cmd, 'run', '-d', str(self.project_path)] + if logger.level > logging.DEBUG: + command_arr.append('--silent') + result = subprocess.run(command_arr) + if result.returncode == 0: + logger.info("successful PlatformIO build") + return result.returncode + else: + logger.error("PlatformIO build error") + raise Exception("PlatformIO build error") + + + def clean(self): + """ + Clean-up the project folder and preserve only an '.ioc' file + """ + + for child in self.project_path.iterdir(): + if child.name != f"{self.project_path.name}.ioc": + if child.is_dir(): + shutil.rmtree(str(child), ignore_errors=True) + logger.debug(f"del {child}/") + elif child.is_file(): + child.unlink() + logger.debug(f"del {child}") + + logger.info("project has been cleaned") From 1f6f8a2b14e4dc2c30494715d02dbec672668943 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sun, 22 Sep 2019 23:51:21 +0300 Subject: [PATCH 03/26] v0.9-alpha: working on dedicated argparse function --- CHANGELOG | 1 + TODO.md | 4 +- stm32pio/settings.py | 7 ++- stm32pio/stm32pio.py | 57 +++++++++-------- stm32pio/tests/test.py | 140 ++++++++++++++++++++++++++++------------- stm32pio/util.py | 20 ++++++ 6 files changed, 154 insertions(+), 75 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 07afc68..64281a8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -86,6 +86,7 @@ stm32pio changelog: - New: add PyCharm to .gitignore - New: add clear TODOs for the next release (some sort of a roadmap) - New: single __version__ reference + - New: extended shebang - New: add some new tests (test_build_should_raise, test_file_not_found) - Fixed: options '--start-editor' and '--with-build' can now be used both for 'new' and 'generate' commands - Fixed: import scheme is now as it should be diff --git a/TODO.md b/TODO.md index a51e4af..56d55f1 100644 --- a/TODO.md +++ b/TODO.md @@ -4,6 +4,8 @@ - [ ] Function annotations - [ ] Do we need some sort of GUI? For example, drop the folder into small window (with checkboxes corresponding with CLI options) and get the output - [ ] Remade as Class (constructor `__init__(project_path)`) - - [ ] Test CLI (i.e. run stm32pio as subprocess) + - [ ] Config file for every project instead of the `settings.py` (but we still sill be storing the default parameters there) + - [ ] Test CLI (integration testing) (i.e. run stm32pio as subprocess) + - [ ] Maybe move test fixtures out of the 'tests' so we can use it for multiple projects (for example CLI and GUI versions) - [ ] Upload to PyPI - [ ] `__main__` diff --git a/stm32pio/settings.py b/stm32pio/settings.py index 6384baa..6d7f344 100644 --- a/stm32pio/settings.py +++ b/stm32pio/settings.py @@ -1,13 +1,16 @@ +# TODO: how we will be set these parameters if the app will be run after the 'setup' process? Or even obtained by 'pip'? +# Maybe we should describe the config file to the user instead of this Python source. + import platform import pathlib my_os = platform.system() -# (default is OK) How do you start Java from command line? (edit if Java not in PATH) +# (default is OK) How do you start Java from the command line? (edit if Java not in PATH) java_cmd = 'java' -# (default is OK) How do you start PlatformIO from command line? (edit if not in PATH, check +# (default is OK) How do you start PlatformIO from the command line? (edit if not in PATH, check # https://docs.platformio.org/en/latest/installation.html#install-shell-commands) platformio_cmd = 'platformio' diff --git a/stm32pio/stm32pio.py b/stm32pio/stm32pio.py index a282a2d..ff3715d 100755 --- a/stm32pio/stm32pio.py +++ b/stm32pio/stm32pio.py @@ -9,7 +9,7 @@ import pathlib -def main(): +def parse_args(args): parser = argparse.ArgumentParser(description="Automation of creating and updating STM32CubeMX-PlatformIO projects. " "Requirements: Python 3.6+, STM32CubeMX, Java, PlatformIO CLI. Edit " "settings.py to set path to the STM32CubeMX (if default doesn't work)") @@ -37,9 +37,20 @@ def main(): parser_new.add_argument('-b', '--board', dest='board', help="PlatformIO name of the board", required=True) - args = parser.parse_args() + # Show help and exit if no arguments were given + if len(args) <= 1: + parser.print_help() + return None + + return parser.parse_args() +def main(sys_argv): + args = parse_args(sys_argv) + if args is None: + print('here') + return + # Logger instance goes through the whole program. # Currently only 2 levels of verbosity through the '-v' option are counted (INFO and DEBUG) logger = logging.getLogger() @@ -51,39 +62,31 @@ def main(): logging.basicConfig(format="%(levelname)-8s %(message)s") logger.setLevel(logging.INFO) - - # Show help and exit if no arguments were given - if not len(sys.argv) > 1: - parser.print_help() - sys.exit() - # Main routine - else: - import stm32pio.util - - try: - project = stm32pio.util.Stm32pio(args.project_path) + import stm32pio.util - if args.subcommand == 'new' or args.subcommand == 'generate': - project.generate_code() - if args.subcommand == 'new': - project.pio_init(args.board) - project.patch_platformio_ini() + try: + project = stm32pio.util.Stm32pio(args.project_path) - if args.with_build: - project.pio_build() - if args.editor: - project.start_editor(args.editor) + if args.subcommand == 'new' or args.subcommand == 'generate': + project.generate_code() + if args.subcommand == 'new': + project.pio_init(args.board) + project.patch_platformio_ini() - elif args.subcommand == 'clean': - project.clean() + if args.with_build: + project.pio_build() + if args.editor: + project.start_editor(args.editor) - except Exception as e: - print(e.__repr__()) + elif args.subcommand == 'clean': + project.clean() + except Exception as e: + print(e.__repr__()) logger.info("exiting...") if __name__ == '__main__': - main() + main(sys.argv) diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index 8384633..d82e955 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -1,4 +1,6 @@ import pathlib +import platform +import shutil import subprocess import time import unittest @@ -13,13 +15,26 @@ raise FileNotFoundError("No test project is present") board = 'nucleo_f031k6' +def clean(): + """ + Clean-up the project folder and preserve only an '.ioc' file + """ + for child in project_path.iterdir(): + if child.name != f"{project_path.name}.ioc": + if child.is_dir(): + shutil.rmtree(str(child), ignore_errors=True) + elif child.is_file(): + child.unlink() + -class Test(unittest.TestCase): +class TestUnit(unittest.TestCase): + """ + + """ def setUp(self) -> None: - project_to_clean = stm32pio.util.Stm32pio(project_path) - project_to_clean.clean() + clean() def test_generate_code(self): """ @@ -65,7 +80,7 @@ def test_patch_platformio_ini(self): def test_build_should_raise(self): """ - Build an empty project so PlatformIO should return non-zero code and we should throw the exception + Build an empty project so PlatformIO should return non-zero code and we, in turn, should throw the exception """ project = stm32pio.util.Stm32pio(project_path) project.pio_init(board) @@ -74,6 +89,63 @@ def test_build_should_raise(self): project.pio_build() + def test_run_editor(self): + """ + Call the editors + """ + project = stm32pio.util.Stm32pio(project_path) + editors = { + 'atom': { + 'Windows': 'atom.exe', + 'Darwin': 'Atom', + 'Linux': 'atom' + }, + 'code': { + 'Windows': 'Code.exe', + 'Darwin': 'Visual Studio Code', + 'Linux': 'code' + }, + 'subl': { + 'Windows': 'sublime_text.exe', + 'Darwin': 'Sublime', + 'Linux': 'sublime' + } + } + for command, name in editors.items(): + with self.subTest(command=command, name=name): + project.start_editor(command) + time.sleep(1) # wait a little bit for app to start + if platform.system() == 'Windows': + # "encoding='utf-8'" is for "a bytes-like object is required, not 'str'" in "assertIn" + result = subprocess.run(['wmic', 'process', 'get', 'description'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8') + else: + result = subprocess.run(['ps', '-A'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, + encoding='utf-8') + # Or, for Python 3.7 and above: + # result = subprocess.run(['ps', '-A'], capture_output=True, encoding='utf-8') + self.assertIn(name[platform.system()], result.stdout) + + + def test_file_not_found(self): + """ + Pass non-existing path and expect the error + """ + not_existing_path = project_path.joinpath('does_not_exist') + with self.assertRaises(FileNotFoundError, msg="FileNotFoundError was not raised"): + stm32pio.util.Stm32pio(not_existing_path) + + + +class TestIntegration(unittest.TestCase): + """ + + """ + + def setUp(self) -> None: + clean() + + def test_build(self): """ Initialize a new project and try to build it @@ -88,37 +160,6 @@ def test_build(self): self.assertEqual(result, 0, msg="Build failed") - # TODO: use subTest() - def test_run_editor(self): - """ - Call the editors - """ - project = stm32pio.util.Stm32pio(project_path) - project.start_editor('atom') - project.start_editor('code') - project.start_editor('subl') - time.sleep(1) # wait a little bit for apps to start - - if stm32pio.settings.my_os == 'Windows': - result = subprocess.run(['wmic', 'process', 'get', 'description'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8') - self.assertIn('atom.exe', result.stdout) - self.assertIn('Code.exe', result.stdout) - self.assertIn('sublime_text.exe', result.stdout) - else: - result = subprocess.run(['ps', '-A'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8') - # Or, for Python 3.7 and above: - # result = subprocess.run(['ps', '-A'], capture_output=True, encoding='utf-8') - if stm32pio.settings.my_os == 'Darwin': - self.assertIn('Atom', result.stdout) - self.assertIn('Visual Studio Code', result.stdout) - self.assertIn('Sublime', result.stdout) - if stm32pio.settings.my_os == 'Linux': - self.assertIn('atom', result.stdout) - self.assertIn('code', result.stdout) - self.assertIn('sublime', result.stdout) - - def test_regenerate_code(self): """ Simulate new project creation, its changing and CubeMX code re-generation (for example, after adding new @@ -157,22 +198,31 @@ def test_regenerate_code(self): msg=f"{test_file_2} does not preserve user content after regeneration") - # def test_file_not_found(self): - # """ - # Pass non-existing path and expect the error - # """ - # project = stm32pio.util.Stm32pio(project_path) - # not_existing_path = project_path.joinpath('does_not_exist') - # with self.assertRaises(FileNotFoundError, msg="FileNotFoundError was not raised"): - # project._get_project_path(not_existing_path) + +class TestCLI(unittest.TestCase): + """ + + """ + + def setUp(self) -> None: + clean() + + def test_new(self): + pass + + def test_generate(self): + pass + + def clean(self): + pass + def tearDownModule(): """ Clean up after yourself """ - project_to_clean = stm32pio.util.Stm32pio(project_path) - project_to_clean.clean() + clean() diff --git a/stm32pio/util.py b/stm32pio/util.py index 88b8729..eead899 100644 --- a/stm32pio/util.py +++ b/stm32pio/util.py @@ -2,6 +2,7 @@ import pathlib import shutil import subprocess +import enum import stm32pio.settings @@ -9,6 +10,25 @@ +# TODO: add states and check the current state for every operation (so we can't, for example, go to build stage without +# a pio_init performed before). Also, it naturally helps us to construct the GUI in which we manage the list of +# multiple projects. (use enum for this) +# Also, we would probably need some method to detect a current project state on program start (or store it explicitly +# in the dotted system file) +@enum.unique +class ProjectState(enum.Enum): + """ + """ + + INITIALIZED = enum.auto() + GENERATED = enum.auto() + PIO_INITIALIZED = enum.auto() + PIO_INI_PATCHED = enum.auto() + BUILT = enum.auto() + + ERROR = enum.auto() + + class Stm32pio: """ Main class From 2f36716a2a54f98b6d6983a0ca8f4bb686c2b704 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sat, 28 Sep 2019 00:26:27 +0300 Subject: [PATCH 04/26] v0.9-alpha: correct imports for all cases (basic run, test, setup) --- README.md | 46 +++--- TODO.md | 2 +- stm32pio/stm32pio.py | 31 ++-- stm32pio/tests/test.py | 352 +++++++++++++++++++++-------------------- 4 files changed, 224 insertions(+), 207 deletions(-) diff --git a/README.md b/README.md index 8c87e9d..d0cbe0d 100644 --- a/README.md +++ b/README.md @@ -12,23 +12,27 @@ Small cross-platform Python app that can create and update [PlatformIO](https:// - *[optional]* Make an initial build of the project -## Restrictions - - The tool doesn't check for different parameters compatibility, e.g. CPU frequency, memory sizes and so on. It simply eases your workflow with these 2 programs (PlatformIO and STM32CubeMX) a little bit. - - CubeMX middlewares don't support yet because it's hard to be prepared for every possible configuration. You need to manually adjust them to build appropriately. For example, FreeRTOS can be added via PlatformIO' `lib` feature or be directly compiled in its own directory using `lib_extra_dirs` option: - ```ini - lib_extra_dirs = Middlewares/Third_Party/FreeRTOS - ``` - You also need to move all `.c`/`.h` files to the `src`/`include` folders respectively. See PlatformIO documentation for more information. - - ## Requirements: - For this app: - Python 3.6+ - For usage: - macOS, Linux, Windows - - STM32CubeMX (all recent versions) with downloaded necessary frameworks (F0, F1, etc.). Try to generate code in ordinary way (through the GUI) at least once before running stm32pio + - STM32CubeMX (all recent versions) with downloaded necessary frameworks (F0, F1, etc.) - Java CLI (JRE) (likely is already installed if STM32CubeMX works) - - PlatformIO CLI. + - PlatformIO CLI + +A general recommendation there would be to try to generate and build a code manually (via the CubeMX GUI and PlatformIO CLI or IDE) at least once before using stm32pio to make sure that all tools are working properly. + + +## Installation +Starting from v0.8 it is possible to install the utility to be able to run stm32pio from anywhere. Use +```shell script +stm32pio-repo/ $ pip3 install . +``` +command to launch the setup process. To uninstall run +```shell script +$ pip3 uninstall stm32pio +``` ## Usage @@ -49,17 +53,6 @@ $ python3 stm32pio.py --help to see help. -## Installation -Starting from v0.8 it is possible to install the utility to be able to run stm32pio from anywhere. Use -```shell script -stm32pio-repo/ $ pip3 install . -``` -command to launch the setup process. To uninstall run -```shell script -$ pip3 uninstall stm32pio -``` - - ## Example 1. Run CubeMX, choose MCU/board, do all necessary stuff 2. Select `Project Manager -> Project` tab, specify "Project Name", choose "Other Toolchains (GPDSC)". In `Code Generator` tab check "Copy only the necessary library files" and "Generate periphery initialization as a pair of '.c/.h' files per peripheral" options @@ -97,3 +90,12 @@ or stm32pio-repo/ $ python3 -m stm32pio.tests.test -v ``` to test the app. It uses STM32F0 framework to generate and build a code from the `stm32pio/tests/stm32pio-test-project/stm32pio-test-project.ioc` file. It's fine to fail an editor test as you not necessarily should have all the editors on your machine. CI is hard to implement for all target OSes during the requirement to have all tools (PlatformIO, Java, CubeMX, etc.) installed during the test. For example, ST doesn't even provide a direct link to CubeMX for downloading + + +## Restrictions + - The tool doesn't check for different parameters compatibility, e.g. CPU frequency, memory sizes and so on. It simply eases your workflow with these 2 programs (PlatformIO and STM32CubeMX) a little bit. + - CubeMX middlewares don't support yet because it's hard to be prepared for every possible configuration. You need to manually adjust them to build appropriately. For example, FreeRTOS can be added via PlatformIO' `lib` feature or be directly compiled in its own directory using `lib_extra_dirs` option: + ```ini + lib_extra_dirs = Middlewares/Third_Party/FreeRTOS + ``` + You also need to move all `.c`/`.h` files to the `src`/`include` folders respectively. See PlatformIO documentation for more information. diff --git a/TODO.md b/TODO.md index 56d55f1..40cced1 100644 --- a/TODO.md +++ b/TODO.md @@ -6,6 +6,6 @@ - [ ] Remade as Class (constructor `__init__(project_path)`) - [ ] Config file for every project instead of the `settings.py` (but we still sill be storing the default parameters there) - [ ] Test CLI (integration testing) (i.e. run stm32pio as subprocess) - - [ ] Maybe move test fixtures out of the 'tests' so we can use it for multiple projects (for example CLI and GUI versions) + - [ ] Maybe move test fixtures out of the 'tests' so we can use it for multiple projects (for example while testing CLI and GUI versions) - [ ] Upload to PyPI - [ ] `__main__` diff --git a/stm32pio/stm32pio.py b/stm32pio/stm32pio.py index ff3715d..bae2e8b 100755 --- a/stm32pio/stm32pio.py +++ b/stm32pio/stm32pio.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__version__ = "0.8" +__version__ = "0.9" import argparse import logging @@ -10,6 +10,10 @@ def parse_args(args): + """ + + """ + parser = argparse.ArgumentParser(description="Automation of creating and updating STM32CubeMX-PlatformIO projects. " "Requirements: Python 3.6+, STM32CubeMX, Java, PlatformIO CLI. Edit " "settings.py to set path to the STM32CubeMX (if default doesn't work)") @@ -20,8 +24,7 @@ def parse_args(args): subparsers = parser.add_subparsers(dest='subcommand', title='subcommands', description="valid subcommands", help="modes of operation") - parser_new = subparsers.add_parser('new', - help="generate CubeMX code, create PlatformIO project [and start the editor]") + parser_new = subparsers.add_parser('new', help="generate CubeMX code, create PlatformIO project") parser_generate = subparsers.add_parser('generate', help="generate CubeMX code") parser_clean = subparsers.add_parser('clean', help="clean-up the project (WARNING: it deletes ALL content of " "'path' except the .ioc file)") @@ -42,14 +45,18 @@ def parse_args(args): parser.print_help() return None - return parser.parse_args() + return parser.parse_args(args) + + +def main(sys_argv=sys.argv[1:]): + """ + """ -def main(sys_argv): args = parse_args(sys_argv) if args is None: - print('here') - return + print("No arguments were given, exiting...") + return -1 # Logger instance goes through the whole program. # Currently only 2 levels of verbosity through the '-v' option are counted (INFO and DEBUG) @@ -83,10 +90,16 @@ def main(sys_argv): project.clean() except Exception as e: - print(e.__repr__()) + if logger.level <= logging.DEBUG: # verbose + raise e + else: + print(e.__repr__()) + return -1 logger.info("exiting...") + return 0 if __name__ == '__main__': - main(sys.argv) + sys.path.insert(0, str(pathlib.Path(sys.path[0]).parent)) # hack to be able to run the app as 'python3 stm32pio.py' + sys.exit(main()) diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index d82e955..c4e2814 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -5,6 +5,7 @@ import time import unittest +import stm32pio.stm32pio import stm32pio.settings import stm32pio.util @@ -28,174 +29,174 @@ def clean(): -class TestUnit(unittest.TestCase): - """ - - """ - - def setUp(self) -> None: - clean() - - def test_generate_code(self): - """ - Check whether files and folders have been created - """ - project = stm32pio.util.Stm32pio(project_path) - project.generate_code() - # Assuming that the presence of these files indicates a success - files_should_be_present = [stm32pio.settings.cubemx_script_filename, 'Src/main.c', 'Inc/main.h'] - self.assertEqual([project_path.joinpath(file).is_file() for file in files_should_be_present], - [True] * len(files_should_be_present), - msg=f"At least one of {files_should_be_present} files haven't been created") - - - def test_pio_init(self): - """ - Consider that existence of 'platformio.ini' file is displaying successful PlatformIO project initialization - """ - project = stm32pio.util.Stm32pio(project_path) - project.pio_init(board) - self.assertTrue(project_path.joinpath('platformio.ini').is_file(), msg="platformio.ini is not there") - - - def test_patch_platformio_ini(self): - """ - Compare contents of the patched string and the desired patch - """ - project = stm32pio.util.Stm32pio(project_path) - test_content = "*** TEST PLATFORMIO.INI FILE ***" - project_path.joinpath('platformio.ini').write_text(test_content) - - project.patch_platformio_ini() - - after_patch_content = project_path.joinpath('platformio.ini').read_text() - - # Initial content wasn't corrupted - self.assertEqual(after_patch_content[:len(test_content)], test_content, - msg="Initial content of platformio.ini is corrupted") - # Patch content is as expected - self.assertEqual(after_patch_content[len(test_content):], stm32pio.settings.platformio_ini_patch_content, - msg="patch content is not as expected") - - - def test_build_should_raise(self): - """ - Build an empty project so PlatformIO should return non-zero code and we, in turn, should throw the exception - """ - project = stm32pio.util.Stm32pio(project_path) - project.pio_init(board) - with self.assertRaisesRegex(Exception, "PlatformIO build error", - msg="Build error exception hadn't been raised"): - project.pio_build() - - - def test_run_editor(self): - """ - Call the editors - """ - project = stm32pio.util.Stm32pio(project_path) - editors = { - 'atom': { - 'Windows': 'atom.exe', - 'Darwin': 'Atom', - 'Linux': 'atom' - }, - 'code': { - 'Windows': 'Code.exe', - 'Darwin': 'Visual Studio Code', - 'Linux': 'code' - }, - 'subl': { - 'Windows': 'sublime_text.exe', - 'Darwin': 'Sublime', - 'Linux': 'sublime' - } - } - for command, name in editors.items(): - with self.subTest(command=command, name=name): - project.start_editor(command) - time.sleep(1) # wait a little bit for app to start - if platform.system() == 'Windows': - # "encoding='utf-8'" is for "a bytes-like object is required, not 'str'" in "assertIn" - result = subprocess.run(['wmic', 'process', 'get', 'description'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8') - else: - result = subprocess.run(['ps', '-A'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, - encoding='utf-8') - # Or, for Python 3.7 and above: - # result = subprocess.run(['ps', '-A'], capture_output=True, encoding='utf-8') - self.assertIn(name[platform.system()], result.stdout) - - - def test_file_not_found(self): - """ - Pass non-existing path and expect the error - """ - not_existing_path = project_path.joinpath('does_not_exist') - with self.assertRaises(FileNotFoundError, msg="FileNotFoundError was not raised"): - stm32pio.util.Stm32pio(not_existing_path) - - - -class TestIntegration(unittest.TestCase): - """ - - """ - - def setUp(self) -> None: - clean() - - - def test_build(self): - """ - Initialize a new project and try to build it - """ - project = stm32pio.util.Stm32pio(project_path) - project.generate_code() - project.pio_init(board) - project.patch_platformio_ini() - - result = project.pio_build() - - self.assertEqual(result, 0, msg="Build failed") - - - def test_regenerate_code(self): - """ - Simulate new project creation, its changing and CubeMX code re-generation (for example, after adding new - hardware features and some new files) - """ - - project = stm32pio.util.Stm32pio(project_path) - - # Generate a new project ... - project.generate_code() - project.pio_init(board) - project.patch_platformio_ini() - - # ... change it: - test_file_1 = project_path.joinpath('Src', 'main.c') - test_content_1 = "*** TEST STRING 1 ***\n" - test_file_2 = project_path.joinpath('Inc', 'my_header.h') - test_content_2 = "*** TEST STRING 2 ***\n" - # - add some sample string inside CubeMX' /* BEGIN - END */ block - main_c_content = test_file_1.read_text() - pos = main_c_content.index("while (1)") - main_c_new_content = main_c_content[:pos] + test_content_1 + main_c_content[pos:] - test_file_1.write_text(main_c_new_content) - # - add new file inside the project - test_file_2.write_text(test_content_2) - - # Re-generate CubeMX project - project.generate_code() - - # Check if added information is preserved - main_c_after_regenerate_content = test_file_1.read_text() - my_header_h_after_regenerate_content = test_file_2.read_text() - self.assertIn(test_content_1, main_c_after_regenerate_content, - msg=f"{test_file_1} does not preserve user content after regeneration") - self.assertIn(test_content_2, my_header_h_after_regenerate_content, - msg=f"{test_file_2} does not preserve user content after regeneration") +# class TestUnit(unittest.TestCase): +# """ +# +# """ +# +# def setUp(self) -> None: +# clean() +# +# def test_generate_code(self): +# """ +# Check whether files and folders have been created +# """ +# project = stm32pio.util.Stm32pio(project_path) +# project.generate_code() +# # Assuming that the presence of these files indicates a success +# files_should_be_present = [stm32pio.settings.cubemx_script_filename, 'Src/main.c', 'Inc/main.h'] +# self.assertEqual([project_path.joinpath(file).is_file() for file in files_should_be_present], +# [True] * len(files_should_be_present), +# msg=f"At least one of {files_should_be_present} files haven't been created") +# +# +# def test_pio_init(self): +# """ +# Consider that existence of 'platformio.ini' file is displaying successful PlatformIO project initialization +# """ +# project = stm32pio.util.Stm32pio(project_path) +# project.pio_init(board) +# self.assertTrue(project_path.joinpath('platformio.ini').is_file(), msg="platformio.ini is not there") +# +# +# def test_patch_platformio_ini(self): +# """ +# Compare contents of the patched string and the desired patch +# """ +# project = stm32pio.util.Stm32pio(project_path) +# test_content = "*** TEST PLATFORMIO.INI FILE ***" +# project_path.joinpath('platformio.ini').write_text(test_content) +# +# project.patch_platformio_ini() +# +# after_patch_content = project_path.joinpath('platformio.ini').read_text() +# +# # Initial content wasn't corrupted +# self.assertEqual(after_patch_content[:len(test_content)], test_content, +# msg="Initial content of platformio.ini is corrupted") +# # Patch content is as expected +# self.assertEqual(after_patch_content[len(test_content):], stm32pio.settings.platformio_ini_patch_content, +# msg="patch content is not as expected") +# +# +# def test_build_should_raise(self): +# """ +# Build an empty project so PlatformIO should return non-zero code and we, in turn, should throw the exception +# """ +# project = stm32pio.util.Stm32pio(project_path) +# project.pio_init(board) +# with self.assertRaisesRegex(Exception, "PlatformIO build error", +# msg="Build error exception hadn't been raised"): +# project.pio_build() +# +# +# def test_run_editor(self): +# """ +# Call the editors +# """ +# project = stm32pio.util.Stm32pio(project_path) +# editors = { +# 'atom': { +# 'Windows': 'atom.exe', +# 'Darwin': 'Atom', +# 'Linux': 'atom' +# }, +# 'code': { +# 'Windows': 'Code.exe', +# 'Darwin': 'Visual Studio Code', +# 'Linux': 'code' +# }, +# 'subl': { +# 'Windows': 'sublime_text.exe', +# 'Darwin': 'Sublime', +# 'Linux': 'sublime' +# } +# } +# for command, name in editors.items(): +# with self.subTest(command=command, name=name): +# project.start_editor(command) +# time.sleep(1) # wait a little bit for app to start +# if platform.system() == 'Windows': +# # "encoding='utf-8'" is for "a bytes-like object is required, not 'str'" in "assertIn" +# result = subprocess.run(['wmic', 'process', 'get', 'description'], +# stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8') +# else: +# result = subprocess.run(['ps', '-A'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, +# encoding='utf-8') +# # Or, for Python 3.7 and above: +# # result = subprocess.run(['ps', '-A'], capture_output=True, encoding='utf-8') +# self.assertIn(name[platform.system()], result.stdout) +# +# +# def test_file_not_found(self): +# """ +# Pass non-existing path and expect the error +# """ +# not_existing_path = project_path.joinpath('does_not_exist') +# with self.assertRaises(FileNotFoundError, msg="FileNotFoundError was not raised"): +# stm32pio.util.Stm32pio(not_existing_path) +# +# +# +# class TestIntegration(unittest.TestCase): +# """ +# +# """ +# +# def setUp(self) -> None: +# clean() +# +# +# def test_build(self): +# """ +# Initialize a new project and try to build it +# """ +# project = stm32pio.util.Stm32pio(project_path) +# project.generate_code() +# project.pio_init(board) +# project.patch_platformio_ini() +# +# result = project.pio_build() +# +# self.assertEqual(result, 0, msg="Build failed") +# +# +# def test_regenerate_code(self): +# """ +# Simulate new project creation, its changing and CubeMX code re-generation (for example, after adding new +# hardware features and some new files) +# """ +# +# project = stm32pio.util.Stm32pio(project_path) +# +# # Generate a new project ... +# project.generate_code() +# project.pio_init(board) +# project.patch_platformio_ini() +# +# # ... change it: +# test_file_1 = project_path.joinpath('Src', 'main.c') +# test_content_1 = "*** TEST STRING 1 ***\n" +# test_file_2 = project_path.joinpath('Inc', 'my_header.h') +# test_content_2 = "*** TEST STRING 2 ***\n" +# # - add some sample string inside CubeMX' /* BEGIN - END */ block +# main_c_content = test_file_1.read_text() +# pos = main_c_content.index("while (1)") +# main_c_new_content = main_c_content[:pos] + test_content_1 + main_c_content[pos:] +# test_file_1.write_text(main_c_new_content) +# # - add new file inside the project +# test_file_2.write_text(test_content_2) +# +# # Re-generate CubeMX project +# project.generate_code() +# +# # Check if added information is preserved +# main_c_after_regenerate_content = test_file_1.read_text() +# my_header_h_after_regenerate_content = test_file_2.read_text() +# self.assertIn(test_content_1, main_c_after_regenerate_content, +# msg=f"{test_file_1} does not preserve user content after regeneration") +# self.assertIn(test_content_2, my_header_h_after_regenerate_content, +# msg=f"{test_file_2} does not preserve user content after regeneration") @@ -207,14 +208,15 @@ class TestCLI(unittest.TestCase): def setUp(self) -> None: clean() - def test_new(self): - pass - - def test_generate(self): - pass + # def test_new(self): + # pass + # + # def test_generate(self): + # pass - def clean(self): - pass + def test_clean(self): + stm32pio.stm32pio.main(sys_argv=['-v', 'clean', '-d', 'stm32pio/tests/stm32pio-test-project/']) + self.assertFalse(False, msg='test of test') From 38f0fcb16e1dfbbaa6dce01265f3429bd763552f Mon Sep 17 00:00:00 2001 From: ussserrr Date: Wed, 2 Oct 2019 09:33:24 +0300 Subject: [PATCH 05/26] v0.9-alpha: working on CLI tests --- TODO.md | 1 + stm32pio/stm32pio.py | 2 +- stm32pio/tests/test.py | 94 +++++++++++++++++++++++++++++++----------- 3 files changed, 71 insertions(+), 26 deletions(-) diff --git a/TODO.md b/TODO.md index 40cced1..1b8035c 100644 --- a/TODO.md +++ b/TODO.md @@ -9,3 +9,4 @@ - [ ] Maybe move test fixtures out of the 'tests' so we can use it for multiple projects (for example while testing CLI and GUI versions) - [ ] Upload to PyPI - [ ] `__main__` + - [ ] Abort `--with-build` if no platformio.ini file is present diff --git a/stm32pio/stm32pio.py b/stm32pio/stm32pio.py index bae2e8b..1cfeb40 100755 --- a/stm32pio/stm32pio.py +++ b/stm32pio/stm32pio.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__version__ = "0.9" +__version__ = '0.9' import argparse import logging diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index c4e2814..48ae057 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -11,17 +11,17 @@ # Test data -project_path = pathlib.Path('stm32pio/tests/stm32pio-test-project').resolve() -if not project_path.is_dir() and not project_path.joinpath('stm32pio-test-project.ioc').is_file(): +PROJECT_PATH = pathlib.Path('stm32pio/tests/stm32pio-test-project').resolve() +if not PROJECT_PATH.is_dir() and not PROJECT_PATH.joinpath('stm32pio-test-project.ioc').is_file(): raise FileNotFoundError("No test project is present") -board = 'nucleo_f031k6' +PROJECT_BOARD = 'nucleo_f031k6' def clean(): """ Clean-up the project folder and preserve only an '.ioc' file """ - for child in project_path.iterdir(): - if child.name != f"{project_path.name}.ioc": + for child in PROJECT_PATH.iterdir(): + if child.name != f"{PROJECT_PATH.name}.ioc": if child.is_dir(): shutil.rmtree(str(child), ignore_errors=True) elif child.is_file(): @@ -41,11 +41,11 @@ def clean(): # """ # Check whether files and folders have been created # """ -# project = stm32pio.util.Stm32pio(project_path) +# project = stm32pio.util.Stm32pio(PROJECT_PATH) # project.generate_code() # # Assuming that the presence of these files indicates a success # files_should_be_present = [stm32pio.settings.cubemx_script_filename, 'Src/main.c', 'Inc/main.h'] -# self.assertEqual([project_path.joinpath(file).is_file() for file in files_should_be_present], +# self.assertEqual([PROJECT_PATH.joinpath(file).is_file() for file in files_should_be_present], # [True] * len(files_should_be_present), # msg=f"At least one of {files_should_be_present} files haven't been created") # @@ -54,22 +54,22 @@ def clean(): # """ # Consider that existence of 'platformio.ini' file is displaying successful PlatformIO project initialization # """ -# project = stm32pio.util.Stm32pio(project_path) -# project.pio_init(board) -# self.assertTrue(project_path.joinpath('platformio.ini').is_file(), msg="platformio.ini is not there") +# project = stm32pio.util.Stm32pio(PROJECT_PATH) +# project.pio_init(PROJECT_BOARD) +# self.assertTrue(PROJECT_PATH.joinpath('platformio.ini').is_file(), msg="platformio.ini is not there") # # # def test_patch_platformio_ini(self): # """ # Compare contents of the patched string and the desired patch # """ -# project = stm32pio.util.Stm32pio(project_path) +# project = stm32pio.util.Stm32pio(PROJECT_PATH) # test_content = "*** TEST PLATFORMIO.INI FILE ***" -# project_path.joinpath('platformio.ini').write_text(test_content) +# PROJECT_PATH.joinpath('platformio.ini').write_text(test_content) # # project.patch_platformio_ini() # -# after_patch_content = project_path.joinpath('platformio.ini').read_text() +# after_patch_content = PROJECT_PATH.joinpath('platformio.ini').read_text() # # # Initial content wasn't corrupted # self.assertEqual(after_patch_content[:len(test_content)], test_content, @@ -83,8 +83,8 @@ def clean(): # """ # Build an empty project so PlatformIO should return non-zero code and we, in turn, should throw the exception # """ -# project = stm32pio.util.Stm32pio(project_path) -# project.pio_init(board) +# project = stm32pio.util.Stm32pio(PROJECT_PATH) +# project.pio_init(PROJECT_BOARD) # with self.assertRaisesRegex(Exception, "PlatformIO build error", # msg="Build error exception hadn't been raised"): # project.pio_build() @@ -94,7 +94,7 @@ def clean(): # """ # Call the editors # """ -# project = stm32pio.util.Stm32pio(project_path) +# project = stm32pio.util.Stm32pio(PROJECT_PATH) # editors = { # 'atom': { # 'Windows': 'atom.exe', @@ -132,7 +132,7 @@ def clean(): # """ # Pass non-existing path and expect the error # """ -# not_existing_path = project_path.joinpath('does_not_exist') +# not_existing_path = PROJECT_PATH.joinpath('does_not_exist') # with self.assertRaises(FileNotFoundError, msg="FileNotFoundError was not raised"): # stm32pio.util.Stm32pio(not_existing_path) # @@ -151,9 +151,9 @@ def clean(): # """ # Initialize a new project and try to build it # """ -# project = stm32pio.util.Stm32pio(project_path) +# project = stm32pio.util.Stm32pio(PROJECT_PATH) # project.generate_code() -# project.pio_init(board) +# project.pio_init(PROJECT_BOARD) # project.patch_platformio_ini() # # result = project.pio_build() @@ -167,17 +167,17 @@ def clean(): # hardware features and some new files) # """ # -# project = stm32pio.util.Stm32pio(project_path) +# project = stm32pio.util.Stm32pio(PROJECT_PATH) # # # Generate a new project ... # project.generate_code() -# project.pio_init(board) +# project.pio_init(PROJECT_BOARD) # project.patch_platformio_ini() # # # ... change it: -# test_file_1 = project_path.joinpath('Src', 'main.c') +# test_file_1 = PROJECT_PATH.joinpath('Src', 'main.c') # test_content_1 = "*** TEST STRING 1 ***\n" -# test_file_2 = project_path.joinpath('Inc', 'my_header.h') +# test_file_2 = PROJECT_PATH.joinpath('Inc', 'my_header.h') # test_content_2 = "*** TEST STRING 2 ***\n" # # - add some sample string inside CubeMX' /* BEGIN - END */ block # main_c_content = test_file_1.read_text() @@ -215,8 +215,52 @@ def setUp(self) -> None: # pass def test_clean(self): - stm32pio.stm32pio.main(sys_argv=['-v', 'clean', '-d', 'stm32pio/tests/stm32pio-test-project/']) - self.assertFalse(False, msg='test of test') + """ + """ + + # create files and folders + file_should_be_deleted = PROJECT_PATH.joinpath('file.should.be.deleted') + dir_should_be_deleted = PROJECT_PATH.joinpath('dir.should.be.deleted') + file_should_be_deleted.touch(exist_ok=False) + dir_should_be_deleted.mkdir(exist_ok=False) + + # clean + return_code = stm32pio.stm32pio.main(sys_argv=['clean', '-d', 'stm32pio/tests/stm32pio-test-project/']) + self.assertEqual(return_code, 0, msg="Non-zero return code") + + # look for remaining items + self.assertFalse(file_should_be_deleted.is_file(), msg=f"{file_should_be_deleted} is still there") + self.assertFalse(dir_should_be_deleted.is_dir(), msg=f"{dir_should_be_deleted} is still there") + + # but .ioc file should be preserved + self.assertTrue(PROJECT_PATH.joinpath(f"{PROJECT_PATH.name}.ioc").is_file(), msg="Missing .ioc file") + + def test_new(self): + """ + Successful build is the best indicator that all went right + """ + return_code = stm32pio.stm32pio.main(sys_argv=['new', '-d', 'stm32pio/tests/stm32pio-test-project/', + '-b', 'nucleo_f031k6', '--with-build']) + self.assertEqual(return_code, 0, msg="Non-zero return code") + + # .ioc file should be preserved + self.assertTrue(PROJECT_PATH.joinpath(f"{PROJECT_PATH.name}.ioc").is_file(), msg="Missing .ioc file") + + # TODO: think about some more advanced checks + + def test_generate(self): + """ + """ + return_code = stm32pio.stm32pio.main(sys_argv=['generate', '-d', 'stm32pio/tests/stm32pio-test-project/']) + self.assertEqual(return_code, 0, msg="Non-zero return code") + + self.assertTrue(PROJECT_PATH.joinpath('Inc').is_dir()) + self.assertTrue(PROJECT_PATH.joinpath('Src').is_dir()) + self.assertFalse(len([child for child in PROJECT_PATH.joinpath('Inc').iterdir()]) == 0) + self.assertFalse(len([child for child in PROJECT_PATH.joinpath('Src').iterdir()]) == 0) + + # .ioc file should be preserved + self.assertTrue(PROJECT_PATH.joinpath(f"{PROJECT_PATH.name}.ioc").is_file(), msg="Missing .ioc file") From e353fcdc0a9ac8362d9996819d05a8a352a82ac2 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sun, 6 Oct 2019 14:11:34 +0300 Subject: [PATCH 06/26] v0.9-alpha: working on CLI tests, add __main__.py, abort build if no platformio.ini file is present --- TODO.md | 12 ++++++------ stm32pio/__main__.py | 8 ++++++++ stm32pio/stm32pio.py | 2 +- stm32pio/tests/test.py | 36 +++++++++++++++++++++++------------- stm32pio/util.py | 7 ++++++- 5 files changed, 44 insertions(+), 21 deletions(-) create mode 100644 stm32pio/__main__.py diff --git a/TODO.md b/TODO.md index 1b8035c..5077dd4 100644 --- a/TODO.md +++ b/TODO.md @@ -2,11 +2,11 @@ - [ ] Middleware support (FreeRTOS, etc.) - [ ] Add more checks, for example when updating the project (`generate` command), check for boards matching and so on... - [ ] Function annotations - - [ ] Do we need some sort of GUI? For example, drop the folder into small window (with checkboxes corresponding with CLI options) and get the output - - [ ] Remade as Class (constructor `__init__(project_path)`) + - [ ] GUI. For example, drop the folder into small window (with checkboxes corresponding with CLI options) and get the output. At left is a list of recent projects + - [x] Remade as Class (constructor `__init__(project_path)`) - [ ] Config file for every project instead of the `settings.py` (but we still sill be storing the default parameters there) - - [ ] Test CLI (integration testing) (i.e. run stm32pio as subprocess) - - [ ] Maybe move test fixtures out of the 'tests' so we can use it for multiple projects (for example while testing CLI and GUI versions) + - [x] Test CLI (integration testing) + - [ ] Move test fixtures out of the 'tests' so we can use it for multiple projects (for example while testing CLI and GUI versions). Set up test folder for every single test so we make sure the .ioc file is always present and not deleted after failed test - [ ] Upload to PyPI - - [ ] `__main__` - - [ ] Abort `--with-build` if no platformio.ini file is present + - [x] `__main__` + - [x] Abort `--with-build` if no platformio.ini file is present diff --git a/stm32pio/__main__.py b/stm32pio/__main__.py new file mode 100644 index 0000000..56efdbe --- /dev/null +++ b/stm32pio/__main__.py @@ -0,0 +1,8 @@ +import sys +import pathlib + +import stm32pio.stm32pio + +if __name__ == '__main__': + sys.path.insert(0, str(pathlib.Path(sys.path[0]).parent)) # hack to be able to run the app as 'python3 stm32pio.py' + sys.exit(stm32pio.stm32pio.main()) diff --git a/stm32pio/stm32pio.py b/stm32pio/stm32pio.py index 1cfeb40..8a2f025 100755 --- a/stm32pio/stm32pio.py +++ b/stm32pio/stm32pio.py @@ -55,7 +55,7 @@ def main(sys_argv=sys.argv[1:]): args = parse_args(sys_argv) if args is None: - print("No arguments were given, exiting...") + print("\nNo arguments were given, exiting...") return -1 # Logger instance goes through the whole program. diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index 48ae057..f4cb2df 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -208,14 +208,9 @@ class TestCLI(unittest.TestCase): def setUp(self) -> None: clean() - # def test_new(self): - # pass - # - # def test_generate(self): - # pass - def test_clean(self): """ + Dangerous test actually... """ # create files and folders @@ -239,8 +234,8 @@ def test_new(self): """ Successful build is the best indicator that all went right """ - return_code = stm32pio.stm32pio.main(sys_argv=['new', '-d', 'stm32pio/tests/stm32pio-test-project/', - '-b', 'nucleo_f031k6', '--with-build']) + return_code = stm32pio.stm32pio.main(sys_argv=['new', '-d', str(PROJECT_PATH), '-b', str(PROJECT_BOARD), + '--with-build']) self.assertEqual(return_code, 0, msg="Non-zero return code") # .ioc file should be preserved @@ -251,17 +246,32 @@ def test_new(self): def test_generate(self): """ """ - return_code = stm32pio.stm32pio.main(sys_argv=['generate', '-d', 'stm32pio/tests/stm32pio-test-project/']) + return_code = stm32pio.stm32pio.main(sys_argv=['generate', '-d', str(PROJECT_PATH)]) self.assertEqual(return_code, 0, msg="Non-zero return code") - self.assertTrue(PROJECT_PATH.joinpath('Inc').is_dir()) - self.assertTrue(PROJECT_PATH.joinpath('Src').is_dir()) - self.assertFalse(len([child for child in PROJECT_PATH.joinpath('Inc').iterdir()]) == 0) - self.assertFalse(len([child for child in PROJECT_PATH.joinpath('Src').iterdir()]) == 0) + inc_dir = 'Inc' + src_dir = 'Src' + + self.assertTrue(PROJECT_PATH.joinpath(inc_dir).is_dir(), msg=f"Missing '{inc_dir}'") + self.assertTrue(PROJECT_PATH.joinpath(src_dir).is_dir(), msg=f"Missing '{src_dir}'") + self.assertFalse(len([child for child in PROJECT_PATH.joinpath(inc_dir).iterdir()]) == 0, + msg=f"'{inc_dir}' is empty") + self.assertFalse(len([child for child in PROJECT_PATH.joinpath(src_dir).iterdir()]) == 0, + msg=f"'{src_dir}' is empty") # .ioc file should be preserved self.assertTrue(PROJECT_PATH.joinpath(f"{PROJECT_PATH.name}.ioc").is_file(), msg="Missing .ioc file") + def test_relative_path(self): + pass + + def test_incorrect_path(self): + pass + + def test_no_ioc_file(self): + pass + + def tearDownModule(): diff --git a/stm32pio/util.py b/stm32pio/util.py index eead899..756f218 100644 --- a/stm32pio/util.py +++ b/stm32pio/util.py @@ -177,7 +177,12 @@ def pio_build(self): 0 if success, raise an exception otherwise """ - logger.info("starting PlatformIO build...") + if self.project_path.joinpath('platformio.ini').is_file(): + logger.info("starting PlatformIO build...") + else: + logger.error("no 'platformio.ini' file, build is impossible") + return -1 + command_arr = [stm32pio.settings.platformio_cmd, 'run', '-d', str(self.project_path)] if logger.level > logging.DEBUG: command_arr.append('--silent') From 688a9a8e2dcd4c633c713f4952a3e93da8bab2c2 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sun, 13 Oct 2019 00:05:33 +0300 Subject: [PATCH 07/26] v0.9-alpha: working on tests, improve input path resolving, exceptions handling --- README.md | 13 +- TODO.md | 3 + stm32pio/stm32pio.py | 5 +- stm32pio/tests/test.py | 383 ++++++++++++++++++++++------------------- stm32pio/util.py | 2 +- 5 files changed, 220 insertions(+), 186 deletions(-) diff --git a/README.md b/README.md index d0cbe0d..c958c83 100644 --- a/README.md +++ b/README.md @@ -87,9 +87,18 @@ stm32pio-repo/ $ python3 -m unittest discover -v ``` or ```shell script -stm32pio-repo/ $ python3 -m stm32pio.tests.test -v +stm32pio-repo/ $ python3 -m stm32pio.tests.test -b -v ``` -to test the app. It uses STM32F0 framework to generate and build a code from the `stm32pio/tests/stm32pio-test-project/stm32pio-test-project.ioc` file. It's fine to fail an editor test as you not necessarily should have all the editors on your machine. CI is hard to implement for all target OSes during the requirement to have all tools (PlatformIO, Java, CubeMX, etc.) installed during the test. For example, ST doesn't even provide a direct link to CubeMX for downloading +to test the app. It uses STM32F0 framework to generate and build a code from the `stm32pio/tests/stm32pio-test-project/stm32pio-test-project.ioc` file. + +For specific test you may use +```shell script +stm32pio-repo/ $ python3 -m unittest stm32pio.tests.test.TestCLI -b -v +``` + +It's fine to fail an editor test as you not necessarily should have all the editors on your machine. + +CI is hard to implement for all target OSes during the requirement to have all tools (PlatformIO, Java, CubeMX, etc.) installed during the test. For example, ST doesn't even provide a direct link to CubeMX for downloading ## Restrictions diff --git a/TODO.md b/TODO.md index 5077dd4..fd74607 100644 --- a/TODO.md +++ b/TODO.md @@ -10,3 +10,6 @@ - [ ] Upload to PyPI - [x] `__main__` - [x] Abort `--with-build` if no platformio.ini file is present + - [ ] Rename 'stm32pio.py' -> 'app.py' + - [ ] Rename 'util.py' -> 'lib.py' ('util' probably is for 'clean', for example, that we can use in tests) + - [ ] Return codes for all methods diff --git a/stm32pio/stm32pio.py b/stm32pio/stm32pio.py index 8a2f025..aeb543b 100755 --- a/stm32pio/stm32pio.py +++ b/stm32pio/stm32pio.py @@ -7,6 +7,7 @@ import logging import sys import pathlib +import traceback def parse_args(args): @@ -91,9 +92,9 @@ def main(sys_argv=sys.argv[1:]): except Exception as e: if logger.level <= logging.DEBUG: # verbose - raise e + traceback.print_exception(*sys.exc_info()) else: - print(e.__repr__()) + print(repr(e)) return -1 logger.info("exiting...") diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index f4cb2df..34f8e9e 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -3,6 +3,8 @@ import shutil import subprocess import time +import inspect +import sys import unittest import stm32pio.stm32pio @@ -29,174 +31,174 @@ def clean(): -# class TestUnit(unittest.TestCase): -# """ -# -# """ -# -# def setUp(self) -> None: -# clean() -# -# def test_generate_code(self): -# """ -# Check whether files and folders have been created -# """ -# project = stm32pio.util.Stm32pio(PROJECT_PATH) -# project.generate_code() -# # Assuming that the presence of these files indicates a success -# files_should_be_present = [stm32pio.settings.cubemx_script_filename, 'Src/main.c', 'Inc/main.h'] -# self.assertEqual([PROJECT_PATH.joinpath(file).is_file() for file in files_should_be_present], -# [True] * len(files_should_be_present), -# msg=f"At least one of {files_should_be_present} files haven't been created") -# -# -# def test_pio_init(self): -# """ -# Consider that existence of 'platformio.ini' file is displaying successful PlatformIO project initialization -# """ -# project = stm32pio.util.Stm32pio(PROJECT_PATH) -# project.pio_init(PROJECT_BOARD) -# self.assertTrue(PROJECT_PATH.joinpath('platformio.ini').is_file(), msg="platformio.ini is not there") -# -# -# def test_patch_platformio_ini(self): -# """ -# Compare contents of the patched string and the desired patch -# """ -# project = stm32pio.util.Stm32pio(PROJECT_PATH) -# test_content = "*** TEST PLATFORMIO.INI FILE ***" -# PROJECT_PATH.joinpath('platformio.ini').write_text(test_content) -# -# project.patch_platformio_ini() -# -# after_patch_content = PROJECT_PATH.joinpath('platformio.ini').read_text() -# -# # Initial content wasn't corrupted -# self.assertEqual(after_patch_content[:len(test_content)], test_content, -# msg="Initial content of platformio.ini is corrupted") -# # Patch content is as expected -# self.assertEqual(after_patch_content[len(test_content):], stm32pio.settings.platformio_ini_patch_content, -# msg="patch content is not as expected") -# -# -# def test_build_should_raise(self): -# """ -# Build an empty project so PlatformIO should return non-zero code and we, in turn, should throw the exception -# """ -# project = stm32pio.util.Stm32pio(PROJECT_PATH) -# project.pio_init(PROJECT_BOARD) -# with self.assertRaisesRegex(Exception, "PlatformIO build error", -# msg="Build error exception hadn't been raised"): -# project.pio_build() -# -# -# def test_run_editor(self): -# """ -# Call the editors -# """ -# project = stm32pio.util.Stm32pio(PROJECT_PATH) -# editors = { -# 'atom': { -# 'Windows': 'atom.exe', -# 'Darwin': 'Atom', -# 'Linux': 'atom' -# }, -# 'code': { -# 'Windows': 'Code.exe', -# 'Darwin': 'Visual Studio Code', -# 'Linux': 'code' -# }, -# 'subl': { -# 'Windows': 'sublime_text.exe', -# 'Darwin': 'Sublime', -# 'Linux': 'sublime' -# } -# } -# for command, name in editors.items(): -# with self.subTest(command=command, name=name): -# project.start_editor(command) -# time.sleep(1) # wait a little bit for app to start -# if platform.system() == 'Windows': -# # "encoding='utf-8'" is for "a bytes-like object is required, not 'str'" in "assertIn" -# result = subprocess.run(['wmic', 'process', 'get', 'description'], -# stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8') -# else: -# result = subprocess.run(['ps', '-A'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, -# encoding='utf-8') -# # Or, for Python 3.7 and above: -# # result = subprocess.run(['ps', '-A'], capture_output=True, encoding='utf-8') -# self.assertIn(name[platform.system()], result.stdout) -# -# -# def test_file_not_found(self): -# """ -# Pass non-existing path and expect the error -# """ -# not_existing_path = PROJECT_PATH.joinpath('does_not_exist') -# with self.assertRaises(FileNotFoundError, msg="FileNotFoundError was not raised"): -# stm32pio.util.Stm32pio(not_existing_path) -# -# -# -# class TestIntegration(unittest.TestCase): -# """ -# -# """ -# -# def setUp(self) -> None: -# clean() -# -# -# def test_build(self): -# """ -# Initialize a new project and try to build it -# """ -# project = stm32pio.util.Stm32pio(PROJECT_PATH) -# project.generate_code() -# project.pio_init(PROJECT_BOARD) -# project.patch_platformio_ini() -# -# result = project.pio_build() -# -# self.assertEqual(result, 0, msg="Build failed") -# -# -# def test_regenerate_code(self): -# """ -# Simulate new project creation, its changing and CubeMX code re-generation (for example, after adding new -# hardware features and some new files) -# """ -# -# project = stm32pio.util.Stm32pio(PROJECT_PATH) -# -# # Generate a new project ... -# project.generate_code() -# project.pio_init(PROJECT_BOARD) -# project.patch_platformio_ini() -# -# # ... change it: -# test_file_1 = PROJECT_PATH.joinpath('Src', 'main.c') -# test_content_1 = "*** TEST STRING 1 ***\n" -# test_file_2 = PROJECT_PATH.joinpath('Inc', 'my_header.h') -# test_content_2 = "*** TEST STRING 2 ***\n" -# # - add some sample string inside CubeMX' /* BEGIN - END */ block -# main_c_content = test_file_1.read_text() -# pos = main_c_content.index("while (1)") -# main_c_new_content = main_c_content[:pos] + test_content_1 + main_c_content[pos:] -# test_file_1.write_text(main_c_new_content) -# # - add new file inside the project -# test_file_2.write_text(test_content_2) -# -# # Re-generate CubeMX project -# project.generate_code() -# -# # Check if added information is preserved -# main_c_after_regenerate_content = test_file_1.read_text() -# my_header_h_after_regenerate_content = test_file_2.read_text() -# self.assertIn(test_content_1, main_c_after_regenerate_content, -# msg=f"{test_file_1} does not preserve user content after regeneration") -# self.assertIn(test_content_2, my_header_h_after_regenerate_content, -# msg=f"{test_file_2} does not preserve user content after regeneration") +class TestUnit(unittest.TestCase): + """ + + """ + + def setUp(self) -> None: + clean() + + def test_generate_code(self): + """ + Check whether files and folders have been created + """ + project = stm32pio.util.Stm32pio(PROJECT_PATH) + project.generate_code() + # Assuming that the presence of these files indicates a success + files_should_be_present = [stm32pio.settings.cubemx_script_filename, 'Src/main.c', 'Inc/main.h'] + self.assertEqual([PROJECT_PATH.joinpath(file).is_file() for file in files_should_be_present], + [True] * len(files_should_be_present), + msg=f"At least one of {files_should_be_present} files haven't been created") + + + def test_pio_init(self): + """ + Consider that existence of 'platformio.ini' file is displaying successful PlatformIO project initialization + """ + project = stm32pio.util.Stm32pio(PROJECT_PATH) + project.pio_init(PROJECT_BOARD) + self.assertTrue(PROJECT_PATH.joinpath('platformio.ini').is_file(), msg="platformio.ini is not there") + + + def test_patch_platformio_ini(self): + """ + Compare contents of the patched string and the desired patch + """ + project = stm32pio.util.Stm32pio(PROJECT_PATH) + test_content = "*** TEST PLATFORMIO.INI FILE ***" + PROJECT_PATH.joinpath('platformio.ini').write_text(test_content) + + project.patch_platformio_ini() + + after_patch_content = PROJECT_PATH.joinpath('platformio.ini').read_text() + + # Initial content wasn't corrupted + self.assertEqual(after_patch_content[:len(test_content)], test_content, + msg="Initial content of platformio.ini is corrupted") + # Patch content is as expected + self.assertEqual(after_patch_content[len(test_content):], stm32pio.settings.platformio_ini_patch_content, + msg="patch content is not as expected") + + + def test_build_should_raise(self): + """ + Build an empty project so PlatformIO should return non-zero code and we, in turn, should throw the exception + """ + project = stm32pio.util.Stm32pio(PROJECT_PATH) + project.pio_init(PROJECT_BOARD) + with self.assertRaisesRegex(Exception, "PlatformIO build error", + msg="Build error exception hadn't been raised"): + project.pio_build() + + + def test_run_editor(self): + """ + Call the editors + """ + project = stm32pio.util.Stm32pio(PROJECT_PATH) + editors = { + 'atom': { + 'Windows': 'atom.exe', + 'Darwin': 'Atom', + 'Linux': 'atom' + }, + 'code': { + 'Windows': 'Code.exe', + 'Darwin': 'Visual Studio Code', + 'Linux': 'code' + }, + 'subl': { + 'Windows': 'sublime_text.exe', + 'Darwin': 'Sublime', + 'Linux': 'sublime' + } + } + for command, name in editors.items(): + with self.subTest(command=command, name=name): + project.start_editor(command) + time.sleep(1) # wait a little bit for app to start + if platform.system() == 'Windows': + # "encoding='utf-8'" is for "a bytes-like object is required, not 'str'" in "assertIn" + result = subprocess.run(['wmic', 'process', 'get', 'description'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8') + else: + result = subprocess.run(['ps', '-A'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, + encoding='utf-8') + # Or, for Python 3.7 and above: + # result = subprocess.run(['ps', '-A'], capture_output=True, encoding='utf-8') + self.assertIn(name[platform.system()], result.stdout) + + + def test_file_not_found(self): + """ + Pass non-existing path and expect the error + """ + not_existing_path = PROJECT_PATH.joinpath('does_not_exist') + with self.assertRaises(FileNotFoundError, msg="FileNotFoundError was not raised"): + stm32pio.util.Stm32pio(not_existing_path) + + + +class TestIntegration(unittest.TestCase): + """ + + """ + + def setUp(self) -> None: + clean() + + + def test_build(self): + """ + Initialize a new project and try to build it + """ + project = stm32pio.util.Stm32pio(PROJECT_PATH) + project.generate_code() + project.pio_init(PROJECT_BOARD) + project.patch_platformio_ini() + + result = project.pio_build() + + self.assertEqual(result, 0, msg="Build failed") + + + def test_regenerate_code(self): + """ + Simulate new project creation, its changing and CubeMX code re-generation (for example, after adding new + hardware features and some new files) + """ + + project = stm32pio.util.Stm32pio(PROJECT_PATH) + + # Generate a new project ... + project.generate_code() + project.pio_init(PROJECT_BOARD) + project.patch_platformio_ini() + + # ... change it: + test_file_1 = PROJECT_PATH.joinpath('Src', 'main.c') + test_content_1 = "*** TEST STRING 1 ***\n" + test_file_2 = PROJECT_PATH.joinpath('Inc', 'my_header.h') + test_content_2 = "*** TEST STRING 2 ***\n" + # - add some sample string inside CubeMX' /* BEGIN - END */ block + main_c_content = test_file_1.read_text() + pos = main_c_content.index("while (1)") + main_c_new_content = main_c_content[:pos] + test_content_1 + main_c_content[pos:] + test_file_1.write_text(main_c_new_content) + # - add new file inside the project + test_file_2.write_text(test_content_2) + + # Re-generate CubeMX project + project.generate_code() + + # Check if added information is preserved + main_c_after_regenerate_content = test_file_1.read_text() + my_header_h_after_regenerate_content = test_file_2.read_text() + self.assertIn(test_content_1, main_c_after_regenerate_content, + msg=f"{test_file_1} does not preserve user content after regeneration") + self.assertIn(test_content_2, my_header_h_after_regenerate_content, + msg=f"{test_file_2} does not preserve user content after regeneration") @@ -213,26 +215,26 @@ def test_clean(self): Dangerous test actually... """ - # create files and folders + # Create files and folders file_should_be_deleted = PROJECT_PATH.joinpath('file.should.be.deleted') dir_should_be_deleted = PROJECT_PATH.joinpath('dir.should.be.deleted') file_should_be_deleted.touch(exist_ok=False) dir_should_be_deleted.mkdir(exist_ok=False) - # clean - return_code = stm32pio.stm32pio.main(sys_argv=['clean', '-d', 'stm32pio/tests/stm32pio-test-project/']) + # Clean + return_code = stm32pio.stm32pio.main(sys_argv=['clean', '-d', str(PROJECT_PATH)]) self.assertEqual(return_code, 0, msg="Non-zero return code") - # look for remaining items + # Look for remaining items self.assertFalse(file_should_be_deleted.is_file(), msg=f"{file_should_be_deleted} is still there") self.assertFalse(dir_should_be_deleted.is_dir(), msg=f"{dir_should_be_deleted} is still there") - # but .ioc file should be preserved + # And .ioc file should be preserved self.assertTrue(PROJECT_PATH.joinpath(f"{PROJECT_PATH.name}.ioc").is_file(), msg="Missing .ioc file") def test_new(self): """ - Successful build is the best indicator that all went right + Successful build is the best indicator that all went right so we use '--with-build' option """ return_code = stm32pio.stm32pio.main(sys_argv=['new', '-d', str(PROJECT_PATH), '-b', str(PROJECT_BOARD), '--with-build']) @@ -241,8 +243,6 @@ def test_new(self): # .ioc file should be preserved self.assertTrue(PROJECT_PATH.joinpath(f"{PROJECT_PATH.name}.ioc").is_file(), msg="Missing .ioc file") - # TODO: think about some more advanced checks - def test_generate(self): """ """ @@ -262,15 +262,36 @@ def test_generate(self): # .ioc file should be preserved self.assertTrue(PROJECT_PATH.joinpath(f"{PROJECT_PATH.name}.ioc").is_file(), msg="Missing .ioc file") - def test_relative_path(self): - pass - def test_incorrect_path(self): - pass + """ + """ + return_code = stm32pio.stm32pio.main(sys_argv=['generate', '-d', '~/path/does/not/exist']) + self.assertNotEqual(return_code, 0, msg='Return code should be non-zero') def test_no_ioc_file(self): - pass + """ + """ + + dir_with_no_ioc_file = PROJECT_PATH.joinpath('dir.with.no.ioc.file') + dir_with_no_ioc_file.mkdir(exist_ok=False) + + return_code = stm32pio.stm32pio.main(sys_argv=['generate', '-d', str(dir_with_no_ioc_file)]) + self.assertNotEqual(return_code, 0, msg='Return code should be non-zero') + + def test_verbose(self): + """ + Run as subprocess + """ + stm32pio_exec = inspect.getfile(stm32pio.stm32pio) # get the path to the main stm32pio script + # Get the path of the current python executable (no need to guess python or python3) (can probably use another + # approach to retrieve the executable) + python_exec = sys.executable + result = subprocess.run([python_exec, stm32pio_exec, '-v', 'clean', '-d', str(PROJECT_PATH)], encoding='utf-8', + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + self.assertEqual(result.returncode, 0, msg="Non-zero return code") + # Somehow stderr contains actual output + self.assertIn('DEBUG', result.stderr.split(), msg="Verbose logging output has not been enabled") diff --git a/stm32pio/util.py b/stm32pio/util.py index 756f218..a849336 100644 --- a/stm32pio/util.py +++ b/stm32pio/util.py @@ -46,7 +46,7 @@ def _resolve_project_path(dirty_path): Args: dirty_path: some directory in the filesystem """ - correct_path = pathlib.Path(dirty_path).resolve() + correct_path = pathlib.Path(dirty_path).expanduser().resolve() if not correct_path.exists(): logger.error("incorrect project path") raise FileNotFoundError(correct_path) From 7efaf6a7100dc6e6f3653fc26534e226fa4f4175 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sun, 13 Oct 2019 22:23:26 +0300 Subject: [PATCH 08/26] v0.9-alpha: rename start script stm32pio.py -> app.py, fix error when only argument given is '-v' --- README.md | 4 ++-- TODO.md | 3 +-- setup.py | 4 ++-- stm32pio/__main__.py | 4 ++-- stm32pio/{stm32pio.py => app.py} | 8 ++++---- stm32pio/tests/test.py | 32 ++++++++++---------------------- 6 files changed, 21 insertions(+), 34 deletions(-) rename stm32pio/{stm32pio.py => app.py} (96%) diff --git a/README.md b/README.md index c958c83..d21a31b 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ stm32pio will create an accessory file `cubemx-script`'` in your project directo Check `settings.py` to make sure that all user-specific parameters are valid. Run ```shell script -$ python3 stm32pio.py --help +$ python3 app.py --help ``` to see help. @@ -75,7 +75,7 @@ to see help. ``` 7. If you will be in need to update hardware configuration in the future, make all necessary stuff in CubeMX and run `generate` command in a similar way: ```shell script - $ python3 stm32pio.py generate -d /path/to/cubemx/project + $ python3 app.py generate -d /path/to/cubemx/project ``` 8. To clean-up the folder and keep only the `.ioc` file run `clean` command diff --git a/TODO.md b/TODO.md index fd74607..f0e9c55 100644 --- a/TODO.md +++ b/TODO.md @@ -10,6 +10,5 @@ - [ ] Upload to PyPI - [x] `__main__` - [x] Abort `--with-build` if no platformio.ini file is present - - [ ] Rename 'stm32pio.py' -> 'app.py' + - [x] Rename 'stm32pio.py' -> 'app.py' - [ ] Rename 'util.py' -> 'lib.py' ('util' probably is for 'clean', for example, that we can use in tests) - - [ ] Return codes for all methods diff --git a/setup.py b/setup.py index 40fc197..e20deb7 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ import setuptools -from stm32pio.stm32pio import __version__ +from stm32pio.app import __version__ with open("README.md", 'r') as fh: long_description = fh.read() @@ -24,7 +24,7 @@ include_package_data=True, entry_points={ 'console_scripts': [ - 'stm32pio = stm32pio.stm32pio:main' + 'stm32pio = stm32pio.app:main' ] } ) diff --git a/stm32pio/__main__.py b/stm32pio/__main__.py index 56efdbe..a199082 100644 --- a/stm32pio/__main__.py +++ b/stm32pio/__main__.py @@ -1,8 +1,8 @@ import sys import pathlib -import stm32pio.stm32pio +import stm32pio.app if __name__ == '__main__': sys.path.insert(0, str(pathlib.Path(sys.path[0]).parent)) # hack to be able to run the app as 'python3 stm32pio.py' - sys.exit(stm32pio.stm32pio.main()) + sys.exit(stm32pio.app.main()) diff --git a/stm32pio/stm32pio.py b/stm32pio/app.py similarity index 96% rename from stm32pio/stm32pio.py rename to stm32pio/app.py index aeb543b..19b5571 100755 --- a/stm32pio/stm32pio.py +++ b/stm32pio/app.py @@ -19,7 +19,7 @@ def parse_args(args): "Requirements: Python 3.6+, STM32CubeMX, Java, PlatformIO CLI. Edit " "settings.py to set path to the STM32CubeMX (if default doesn't work)") # Global arguments (there is also an automatically added '-h, --help' option) - parser.add_argument('--version', action='version', version=f"%(prog)s v{__version__}") + parser.add_argument('--version', action='version', version=f"stm32pio v{__version__}") parser.add_argument('-v', '--verbose', help="enable verbose output (default: INFO)", action='count', required=False) subparsers = parser.add_subparsers(dest='subcommand', title='subcommands', @@ -42,7 +42,7 @@ def parse_args(args): parser_new.add_argument('-b', '--board', dest='board', help="PlatformIO name of the board", required=True) # Show help and exit if no arguments were given - if len(args) <= 1: + if len(args) == 0: parser.print_help() return None @@ -55,7 +55,7 @@ def main(sys_argv=sys.argv[1:]): """ args = parse_args(sys_argv) - if args is None: + if args is None or args.subcommand is None: print("\nNo arguments were given, exiting...") return -1 @@ -102,5 +102,5 @@ def main(sys_argv=sys.argv[1:]): if __name__ == '__main__': - sys.path.insert(0, str(pathlib.Path(sys.path[0]).parent)) # hack to be able to run the app as 'python3 stm32pio.py' + sys.path.insert(0, str(pathlib.Path(sys.path[0]).parent)) # hack to be able to run the app as 'python3 app.py' sys.exit(main()) diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index 34f8e9e..cf05e9c 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -7,7 +7,7 @@ import sys import unittest -import stm32pio.stm32pio +import stm32pio.app import stm32pio.settings import stm32pio.util @@ -30,7 +30,6 @@ def clean(): child.unlink() - class TestUnit(unittest.TestCase): """ @@ -51,7 +50,6 @@ def test_generate_code(self): [True] * len(files_should_be_present), msg=f"At least one of {files_should_be_present} files haven't been created") - def test_pio_init(self): """ Consider that existence of 'platformio.ini' file is displaying successful PlatformIO project initialization @@ -60,7 +58,6 @@ def test_pio_init(self): project.pio_init(PROJECT_BOARD) self.assertTrue(PROJECT_PATH.joinpath('platformio.ini').is_file(), msg="platformio.ini is not there") - def test_patch_platformio_ini(self): """ Compare contents of the patched string and the desired patch @@ -78,8 +75,7 @@ def test_patch_platformio_ini(self): msg="Initial content of platformio.ini is corrupted") # Patch content is as expected self.assertEqual(after_patch_content[len(test_content):], stm32pio.settings.platformio_ini_patch_content, - msg="patch content is not as expected") - + msg="Patch content is not as expected") def test_build_should_raise(self): """ @@ -91,7 +87,6 @@ def test_build_should_raise(self): msg="Build error exception hadn't been raised"): project.pio_build() - def test_run_editor(self): """ Call the editors @@ -129,7 +124,6 @@ def test_run_editor(self): # result = subprocess.run(['ps', '-A'], capture_output=True, encoding='utf-8') self.assertIn(name[platform.system()], result.stdout) - def test_file_not_found(self): """ Pass non-existing path and expect the error @@ -139,7 +133,6 @@ def test_file_not_found(self): stm32pio.util.Stm32pio(not_existing_path) - class TestIntegration(unittest.TestCase): """ @@ -148,7 +141,6 @@ class TestIntegration(unittest.TestCase): def setUp(self) -> None: clean() - def test_build(self): """ Initialize a new project and try to build it @@ -162,7 +154,6 @@ def test_build(self): self.assertEqual(result, 0, msg="Build failed") - def test_regenerate_code(self): """ Simulate new project creation, its changing and CubeMX code re-generation (for example, after adding new @@ -196,10 +187,9 @@ def test_regenerate_code(self): main_c_after_regenerate_content = test_file_1.read_text() my_header_h_after_regenerate_content = test_file_2.read_text() self.assertIn(test_content_1, main_c_after_regenerate_content, - msg=f"{test_file_1} does not preserve user content after regeneration") + msg=f"User content hasn't been preserved after regeneration in {test_file_1}") self.assertIn(test_content_2, my_header_h_after_regenerate_content, - msg=f"{test_file_2} does not preserve user content after regeneration") - + msg=f"User content hasn't been preserved after regeneration in {test_file_2}") class TestCLI(unittest.TestCase): @@ -222,7 +212,7 @@ def test_clean(self): dir_should_be_deleted.mkdir(exist_ok=False) # Clean - return_code = stm32pio.stm32pio.main(sys_argv=['clean', '-d', str(PROJECT_PATH)]) + return_code = stm32pio.app.main(sys_argv=['clean', '-d', str(PROJECT_PATH)]) self.assertEqual(return_code, 0, msg="Non-zero return code") # Look for remaining items @@ -236,7 +226,7 @@ def test_new(self): """ Successful build is the best indicator that all went right so we use '--with-build' option """ - return_code = stm32pio.stm32pio.main(sys_argv=['new', '-d', str(PROJECT_PATH), '-b', str(PROJECT_BOARD), + return_code = stm32pio.app.main(sys_argv=['new', '-d', str(PROJECT_PATH), '-b', str(PROJECT_BOARD), '--with-build']) self.assertEqual(return_code, 0, msg="Non-zero return code") @@ -246,7 +236,7 @@ def test_new(self): def test_generate(self): """ """ - return_code = stm32pio.stm32pio.main(sys_argv=['generate', '-d', str(PROJECT_PATH)]) + return_code = stm32pio.app.main(sys_argv=['generate', '-d', str(PROJECT_PATH)]) self.assertEqual(return_code, 0, msg="Non-zero return code") inc_dir = 'Inc' @@ -265,7 +255,7 @@ def test_generate(self): def test_incorrect_path(self): """ """ - return_code = stm32pio.stm32pio.main(sys_argv=['generate', '-d', '~/path/does/not/exist']) + return_code = stm32pio.app.main(sys_argv=['generate', '-d', '~/path/does/not/exist']) self.assertNotEqual(return_code, 0, msg='Return code should be non-zero') def test_no_ioc_file(self): @@ -275,7 +265,7 @@ def test_no_ioc_file(self): dir_with_no_ioc_file = PROJECT_PATH.joinpath('dir.with.no.ioc.file') dir_with_no_ioc_file.mkdir(exist_ok=False) - return_code = stm32pio.stm32pio.main(sys_argv=['generate', '-d', str(dir_with_no_ioc_file)]) + return_code = stm32pio.app.main(sys_argv=['generate', '-d', str(dir_with_no_ioc_file)]) self.assertNotEqual(return_code, 0, msg='Return code should be non-zero') def test_verbose(self): @@ -283,7 +273,7 @@ def test_verbose(self): Run as subprocess """ - stm32pio_exec = inspect.getfile(stm32pio.stm32pio) # get the path to the main stm32pio script + stm32pio_exec = inspect.getfile(stm32pio.app) # get the path to the main stm32pio script # Get the path of the current python executable (no need to guess python or python3) (can probably use another # approach to retrieve the executable) python_exec = sys.executable @@ -294,7 +284,6 @@ def test_verbose(self): self.assertIn('DEBUG', result.stderr.split(), msg="Verbose logging output has not been enabled") - def tearDownModule(): """ Clean up after yourself @@ -302,6 +291,5 @@ def tearDownModule(): clean() - if __name__ == '__main__': unittest.main() From fc230e9d1a706fee2ea559ce2927f9f084aebfb7 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sun, 20 Oct 2019 20:15:34 +0300 Subject: [PATCH 09/26] v0.9-alpha: deploy temp test fixture for every run, function annotations --- MANIFEST.in | 4 +- TODO.md | 8 +- .../stm32pio-test-project.ioc | 0 stm32pio/app.py | 10 +- stm32pio/tests/test.py | 96 ++++++++++--------- stm32pio/util.py | 42 ++++---- 6 files changed, 85 insertions(+), 75 deletions(-) rename {stm32pio/tests/stm32pio-test-project => stm32pio-test-project}/stm32pio-test-project.ioc (100%) diff --git a/MANIFEST.in b/MANIFEST.in index d1bb712..3c52153 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,8 @@ +include MANIFEST.in include README.md include LICENSE -include MANIFEST.in include CHANGELOG include TODO.md include .gitignore -recursive-include stm32pio/tests * +recursive-include stm32pio-test-project * include screenshots/*.png diff --git a/TODO.md b/TODO.md index f0e9c55..3729bfc 100644 --- a/TODO.md +++ b/TODO.md @@ -1,14 +1,16 @@ # TODOs - [ ] Middleware support (FreeRTOS, etc.) - [ ] Add more checks, for example when updating the project (`generate` command), check for boards matching and so on... - - [ ] Function annotations + - [x] Function annotations - [ ] GUI. For example, drop the folder into small window (with checkboxes corresponding with CLI options) and get the output. At left is a list of recent projects - [x] Remade as Class (constructor `__init__(project_path)`) - [ ] Config file for every project instead of the `settings.py` (but we still sill be storing the default parameters there) - [x] Test CLI (integration testing) - - [ ] Move test fixtures out of the 'tests' so we can use it for multiple projects (for example while testing CLI and GUI versions). Set up test folder for every single test so we make sure the .ioc file is always present and not deleted after failed test + - [x] Move test fixtures out of the 'tests' so we can use it for multiple projects (for example while testing CLI and GUI versions). Set up test folder for every single test so we make sure the .ioc file is always present and not deleted after failed test - [ ] Upload to PyPI - [x] `__main__` - [x] Abort `--with-build` if no platformio.ini file is present - [x] Rename 'stm32pio.py' -> 'app.py' - - [ ] Rename 'util.py' -> 'lib.py' ('util' probably is for 'clean', for example, that we can use in tests) + - [ ] Rename 'util.py' -> 'lib.py' + - [ ] Do not require matching of the project folder and .ioc file names + - [ ] Remove casts to string when we can use path-like objects diff --git a/stm32pio/tests/stm32pio-test-project/stm32pio-test-project.ioc b/stm32pio-test-project/stm32pio-test-project.ioc similarity index 100% rename from stm32pio/tests/stm32pio-test-project/stm32pio-test-project.ioc rename to stm32pio-test-project/stm32pio-test-project.ioc diff --git a/stm32pio/app.py b/stm32pio/app.py index 19b5571..ac31f06 100755 --- a/stm32pio/app.py +++ b/stm32pio/app.py @@ -8,9 +8,12 @@ import sys import pathlib import traceback +from typing import Optional +import stm32pio.util -def parse_args(args): + +def parse_args(args: list) -> Optional[argparse.Namespace]: """ """ @@ -49,7 +52,7 @@ def parse_args(args): return parser.parse_args(args) -def main(sys_argv=sys.argv[1:]): +def main(sys_argv: list = sys.argv[1:]) -> int: """ """ @@ -71,8 +74,6 @@ def main(sys_argv=sys.argv[1:]): logger.setLevel(logging.INFO) # Main routine - import stm32pio.util - try: project = stm32pio.util.Stm32pio(args.project_path) @@ -90,6 +91,7 @@ def main(sys_argv=sys.argv[1:]): elif args.subcommand == 'clean': project.clean() + # util library is designed to throw the exception in bad cases so we catch here globally except Exception as e: if logger.level <= logging.DEBUG: # verbose traceback.print_exception(*sys.exc_info()) diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index cf05e9c..1f8b28d 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -2,6 +2,7 @@ import platform import shutil import subprocess +import tempfile import time import inspect import sys @@ -13,21 +14,14 @@ # Test data -PROJECT_PATH = pathlib.Path('stm32pio/tests/stm32pio-test-project').resolve() -if not PROJECT_PATH.is_dir() and not PROJECT_PATH.joinpath('stm32pio-test-project.ioc').is_file(): +TEST_PROJECT_PATH = pathlib.Path('stm32pio-test-project').resolve() +if not TEST_PROJECT_PATH.is_dir() or not TEST_PROJECT_PATH.joinpath('stm32pio-test-project.ioc').is_file(): raise FileNotFoundError("No test project is present") PROJECT_BOARD = 'nucleo_f031k6' -def clean(): - """ - Clean-up the project folder and preserve only an '.ioc' file - """ - for child in PROJECT_PATH.iterdir(): - if child.name != f"{PROJECT_PATH.name}.ioc": - if child.is_dir(): - shutil.rmtree(str(child), ignore_errors=True) - elif child.is_file(): - child.unlink() + +temp_dir = tempfile.TemporaryDirectory() +fixture_path = pathlib.Path(temp_dir.name).joinpath(TEST_PROJECT_PATH.name) class TestUnit(unittest.TestCase): @@ -36,17 +30,21 @@ class TestUnit(unittest.TestCase): """ def setUp(self) -> None: - clean() + self.tearDown() + shutil.copytree(str(TEST_PROJECT_PATH), str(fixture_path)) + + def tearDown(self) -> None: + shutil.rmtree(str(fixture_path), ignore_errors=True) def test_generate_code(self): """ Check whether files and folders have been created """ - project = stm32pio.util.Stm32pio(PROJECT_PATH) + project = stm32pio.util.Stm32pio(fixture_path) project.generate_code() # Assuming that the presence of these files indicates a success files_should_be_present = [stm32pio.settings.cubemx_script_filename, 'Src/main.c', 'Inc/main.h'] - self.assertEqual([PROJECT_PATH.joinpath(file).is_file() for file in files_should_be_present], + self.assertEqual([fixture_path.joinpath(file).is_file() for file in files_should_be_present], [True] * len(files_should_be_present), msg=f"At least one of {files_should_be_present} files haven't been created") @@ -54,21 +52,21 @@ def test_pio_init(self): """ Consider that existence of 'platformio.ini' file is displaying successful PlatformIO project initialization """ - project = stm32pio.util.Stm32pio(PROJECT_PATH) + project = stm32pio.util.Stm32pio(fixture_path) project.pio_init(PROJECT_BOARD) - self.assertTrue(PROJECT_PATH.joinpath('platformio.ini').is_file(), msg="platformio.ini is not there") + self.assertTrue(fixture_path.joinpath('platformio.ini').is_file(), msg="platformio.ini is not there") def test_patch_platformio_ini(self): """ Compare contents of the patched string and the desired patch """ - project = stm32pio.util.Stm32pio(PROJECT_PATH) + project = stm32pio.util.Stm32pio(fixture_path) test_content = "*** TEST PLATFORMIO.INI FILE ***" - PROJECT_PATH.joinpath('platformio.ini').write_text(test_content) + fixture_path.joinpath('platformio.ini').write_text(test_content) project.patch_platformio_ini() - after_patch_content = PROJECT_PATH.joinpath('platformio.ini').read_text() + after_patch_content = fixture_path.joinpath('platformio.ini').read_text() # Initial content wasn't corrupted self.assertEqual(after_patch_content[:len(test_content)], test_content, @@ -81,7 +79,7 @@ def test_build_should_raise(self): """ Build an empty project so PlatformIO should return non-zero code and we, in turn, should throw the exception """ - project = stm32pio.util.Stm32pio(PROJECT_PATH) + project = stm32pio.util.Stm32pio(fixture_path) project.pio_init(PROJECT_BOARD) with self.assertRaisesRegex(Exception, "PlatformIO build error", msg="Build error exception hadn't been raised"): @@ -91,7 +89,7 @@ def test_run_editor(self): """ Call the editors """ - project = stm32pio.util.Stm32pio(PROJECT_PATH) + project = stm32pio.util.Stm32pio(fixture_path) editors = { 'atom': { 'Windows': 'atom.exe', @@ -128,7 +126,7 @@ def test_file_not_found(self): """ Pass non-existing path and expect the error """ - not_existing_path = PROJECT_PATH.joinpath('does_not_exist') + not_existing_path = fixture_path.joinpath('does_not_exist') with self.assertRaises(FileNotFoundError, msg="FileNotFoundError was not raised"): stm32pio.util.Stm32pio(not_existing_path) @@ -139,13 +137,17 @@ class TestIntegration(unittest.TestCase): """ def setUp(self) -> None: - clean() + self.tearDown() + shutil.copytree(str(TEST_PROJECT_PATH), str(fixture_path)) + + def tearDown(self) -> None: + shutil.rmtree(str(fixture_path), ignore_errors=True) def test_build(self): """ Initialize a new project and try to build it """ - project = stm32pio.util.Stm32pio(PROJECT_PATH) + project = stm32pio.util.Stm32pio(fixture_path) project.generate_code() project.pio_init(PROJECT_BOARD) project.patch_platformio_ini() @@ -160,7 +162,7 @@ def test_regenerate_code(self): hardware features and some new files) """ - project = stm32pio.util.Stm32pio(PROJECT_PATH) + project = stm32pio.util.Stm32pio(fixture_path) # Generate a new project ... project.generate_code() @@ -168,9 +170,9 @@ def test_regenerate_code(self): project.patch_platformio_ini() # ... change it: - test_file_1 = PROJECT_PATH.joinpath('Src', 'main.c') + test_file_1 = fixture_path.joinpath('Src', 'main.c') test_content_1 = "*** TEST STRING 1 ***\n" - test_file_2 = PROJECT_PATH.joinpath('Inc', 'my_header.h') + test_file_2 = fixture_path.joinpath('Inc', 'my_header.h') test_content_2 = "*** TEST STRING 2 ***\n" # - add some sample string inside CubeMX' /* BEGIN - END */ block main_c_content = test_file_1.read_text() @@ -198,7 +200,11 @@ class TestCLI(unittest.TestCase): """ def setUp(self) -> None: - clean() + self.tearDown() + shutil.copytree(str(TEST_PROJECT_PATH), str(fixture_path)) + + def tearDown(self) -> None: + shutil.rmtree(str(fixture_path), ignore_errors=True) def test_clean(self): """ @@ -206,13 +212,13 @@ def test_clean(self): """ # Create files and folders - file_should_be_deleted = PROJECT_PATH.joinpath('file.should.be.deleted') - dir_should_be_deleted = PROJECT_PATH.joinpath('dir.should.be.deleted') + file_should_be_deleted = fixture_path.joinpath('file.should.be.deleted') + dir_should_be_deleted = fixture_path.joinpath('dir.should.be.deleted') file_should_be_deleted.touch(exist_ok=False) dir_should_be_deleted.mkdir(exist_ok=False) # Clean - return_code = stm32pio.app.main(sys_argv=['clean', '-d', str(PROJECT_PATH)]) + return_code = stm32pio.app.main(sys_argv=['clean', '-d', str(fixture_path)]) self.assertEqual(return_code, 0, msg="Non-zero return code") # Look for remaining items @@ -220,37 +226,37 @@ def test_clean(self): self.assertFalse(dir_should_be_deleted.is_dir(), msg=f"{dir_should_be_deleted} is still there") # And .ioc file should be preserved - self.assertTrue(PROJECT_PATH.joinpath(f"{PROJECT_PATH.name}.ioc").is_file(), msg="Missing .ioc file") + self.assertTrue(fixture_path.joinpath(f"{fixture_path.name}.ioc").is_file(), msg="Missing .ioc file") def test_new(self): """ Successful build is the best indicator that all went right so we use '--with-build' option """ - return_code = stm32pio.app.main(sys_argv=['new', '-d', str(PROJECT_PATH), '-b', str(PROJECT_BOARD), - '--with-build']) + return_code = stm32pio.app.main(sys_argv=['new', '-d', str(fixture_path), '-b', str(PROJECT_BOARD), + '--with-build']) self.assertEqual(return_code, 0, msg="Non-zero return code") # .ioc file should be preserved - self.assertTrue(PROJECT_PATH.joinpath(f"{PROJECT_PATH.name}.ioc").is_file(), msg="Missing .ioc file") + self.assertTrue(fixture_path.joinpath(f"{fixture_path.name}.ioc").is_file(), msg="Missing .ioc file") def test_generate(self): """ """ - return_code = stm32pio.app.main(sys_argv=['generate', '-d', str(PROJECT_PATH)]) + return_code = stm32pio.app.main(sys_argv=['generate', '-d', str(fixture_path)]) self.assertEqual(return_code, 0, msg="Non-zero return code") inc_dir = 'Inc' src_dir = 'Src' - self.assertTrue(PROJECT_PATH.joinpath(inc_dir).is_dir(), msg=f"Missing '{inc_dir}'") - self.assertTrue(PROJECT_PATH.joinpath(src_dir).is_dir(), msg=f"Missing '{src_dir}'") - self.assertFalse(len([child for child in PROJECT_PATH.joinpath(inc_dir).iterdir()]) == 0, + self.assertTrue(fixture_path.joinpath(inc_dir).is_dir(), msg=f"Missing '{inc_dir}'") + self.assertTrue(fixture_path.joinpath(src_dir).is_dir(), msg=f"Missing '{src_dir}'") + self.assertFalse(len([child for child in fixture_path.joinpath(inc_dir).iterdir()]) == 0, msg=f"'{inc_dir}' is empty") - self.assertFalse(len([child for child in PROJECT_PATH.joinpath(src_dir).iterdir()]) == 0, + self.assertFalse(len([child for child in fixture_path.joinpath(src_dir).iterdir()]) == 0, msg=f"'{src_dir}' is empty") # .ioc file should be preserved - self.assertTrue(PROJECT_PATH.joinpath(f"{PROJECT_PATH.name}.ioc").is_file(), msg="Missing .ioc file") + self.assertTrue(fixture_path.joinpath(f"{fixture_path.name}.ioc").is_file(), msg="Missing .ioc file") def test_incorrect_path(self): """ @@ -262,7 +268,7 @@ def test_no_ioc_file(self): """ """ - dir_with_no_ioc_file = PROJECT_PATH.joinpath('dir.with.no.ioc.file') + dir_with_no_ioc_file = fixture_path.joinpath('dir.with.no.ioc.file') dir_with_no_ioc_file.mkdir(exist_ok=False) return_code = stm32pio.app.main(sys_argv=['generate', '-d', str(dir_with_no_ioc_file)]) @@ -277,7 +283,7 @@ def test_verbose(self): # Get the path of the current python executable (no need to guess python or python3) (can probably use another # approach to retrieve the executable) python_exec = sys.executable - result = subprocess.run([python_exec, stm32pio_exec, '-v', 'clean', '-d', str(PROJECT_PATH)], encoding='utf-8', + result = subprocess.run([python_exec, stm32pio_exec, '-v', 'clean', '-d', str(fixture_path)], encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE) self.assertEqual(result.returncode, 0, msg="Non-zero return code") # Somehow stderr contains actual output @@ -288,7 +294,7 @@ def tearDownModule(): """ Clean up after yourself """ - clean() + temp_dir.cleanup() if __name__ == '__main__': diff --git a/stm32pio/util.py b/stm32pio/util.py index a849336..c3b9f80 100644 --- a/stm32pio/util.py +++ b/stm32pio/util.py @@ -2,7 +2,7 @@ import pathlib import shutil import subprocess -import enum +# import enum import stm32pio.settings @@ -15,18 +15,18 @@ # multiple projects. (use enum for this) # Also, we would probably need some method to detect a current project state on program start (or store it explicitly # in the dotted system file) -@enum.unique -class ProjectState(enum.Enum): - """ - """ - - INITIALIZED = enum.auto() - GENERATED = enum.auto() - PIO_INITIALIZED = enum.auto() - PIO_INI_PATCHED = enum.auto() - BUILT = enum.auto() - - ERROR = enum.auto() +# @enum.unique +# class ProjectState(enum.Enum): +# """ +# """ +# +# INITIALIZED = enum.auto() +# GENERATED = enum.auto() +# PIO_INITIALIZED = enum.auto() +# PIO_INI_PATCHED = enum.auto() +# BUILT = enum.auto() +# +# ERROR = enum.auto() class Stm32pio: @@ -34,12 +34,12 @@ class Stm32pio: Main class """ - def __init__(self, dirty_path): + def __init__(self, dirty_path: str): self.project_path = self._resolve_project_path(dirty_path) @staticmethod - def _resolve_project_path(dirty_path): + def _resolve_project_path(dirty_path: str) -> pathlib.Path: """ Handle 'path/to/proj' and 'path/to/proj/', '.' (current directory) and other cases @@ -54,7 +54,7 @@ def _resolve_project_path(dirty_path): return correct_path - def generate_code(self): + def generate_code(self) -> None: """ Call STM32CubeMX app as a 'java -jar' file with the automatically prearranged 'cubemx-script' file """ @@ -98,7 +98,7 @@ def generate_code(self): raise Exception("code generation error") - def pio_init(self, board): + def pio_init(self, board: str) -> None: """ Call PlatformIO CLI to initialize a new project @@ -133,7 +133,7 @@ def pio_init(self, board): raise Exception("PlatformIO error") - def patch_platformio_ini(self): + def patch_platformio_ini(self) -> None: """ Patch platformio.ini file to use created earlier by CubeMX 'Src' and 'Inc' folders as sources """ @@ -153,7 +153,7 @@ def patch_platformio_ini(self): shutil.rmtree(str(self.project_path.joinpath('src')), ignore_errors=True) - def start_editor(self, editor_command): + def start_editor(self, editor_command: str) -> None: """ Start the editor specified by 'editor_command' with the project opened @@ -169,7 +169,7 @@ def start_editor(self, editor_command): logger.error(f"Failed to start the editor {editor_command}: {e.stderr}") - def pio_build(self): + def pio_build(self) -> int: """ Initiate a build of the PlatformIO project by the PlatformIO ('run' command) @@ -195,7 +195,7 @@ def pio_build(self): raise Exception("PlatformIO build error") - def clean(self): + def clean(self) -> None: """ Clean-up the project folder and preserve only an '.ioc' file """ From 04d4cbbf60c82c974f07fae45434b1b1edcde60e Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sun, 20 Oct 2019 23:44:16 +0300 Subject: [PATCH 10/26] v0.9-alpha: remove redundant conversions to strings, multi-line string templates parameters, add more setup classifiers --- TODO.md | 12 ++++++++---- setup.py | 19 +++++++++++-------- stm32pio/app.py | 4 ++-- stm32pio/settings.py | 17 +++++++++++------ stm32pio/tests/test.py | 34 +++++++++++++++------------------- stm32pio/util.py | 16 ++++++++-------- 6 files changed, 55 insertions(+), 47 deletions(-) diff --git a/TODO.md b/TODO.md index 3729bfc..6b70f20 100644 --- a/TODO.md +++ b/TODO.md @@ -1,4 +1,5 @@ # TODOs + - [ ] Middleware support (FreeRTOS, etc.) - [ ] Add more checks, for example when updating the project (`generate` command), check for boards matching and so on... - [x] Function annotations @@ -10,7 +11,10 @@ - [ ] Upload to PyPI - [x] `__main__` - [x] Abort `--with-build` if no platformio.ini file is present - - [x] Rename 'stm32pio.py' -> 'app.py' - - [ ] Rename 'util.py' -> 'lib.py' - - [ ] Do not require matching of the project folder and .ioc file names - - [ ] Remove casts to string when we can use path-like objects + - [x] Rename `stm32pio.py` -> `app.py` + - [ ] Rename `util.py` -> `lib.py` (maybe) + - [ ] Do not require matching of the project folder and .ioc file names (use first .ioc file found) + - [x] Remove casts to string where we can use path-like objects + - [x] Settings string templates and multi line + - [ ] Smart `start_editor` test (detect in system, maybe use unittest `skipIf` decorator) + - [ ] Maybe split tests to avoid long names (e.g. => `stm32pio.tests.test_unit` (but then we will have `stm32pio.tests.test_unit.Test` or so...) diff --git a/setup.py b/setup.py index e20deb7..551700d 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,15 @@ import setuptools -from stm32pio.app import __version__ +import stm32pio.app -with open("README.md", 'r') as fh: - long_description = fh.read() +with open("README.md", 'r') as readme: + long_description = readme.read() setuptools.setup( - name="stm32pio", - version=__version__, - author="ussserrr", - author_email="andrei4.2008@gmail.com", + name='stm32pio', + version=stm32pio.app.__version__, + author='ussserrr', + author_email='andrei4.2008@gmail.com', description="Small cross-platform Python app that can create and update PlatformIO projects from STM32CubeMX .ioc " "files.", long_description=long_description, @@ -17,9 +17,12 @@ url="https://github.com/ussserrr/stm32pio", packages=setuptools.find_packages(), classifiers=[ - "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Topic :: Software Development :: Embedded Systems" ], include_package_data=True, entry_points={ diff --git a/stm32pio/app.py b/stm32pio/app.py index ac31f06..e6549b1 100755 --- a/stm32pio/app.py +++ b/stm32pio/app.py @@ -10,8 +10,6 @@ import traceback from typing import Optional -import stm32pio.util - def parse_args(args: list) -> Optional[argparse.Namespace]: """ @@ -74,6 +72,8 @@ def main(sys_argv: list = sys.argv[1:]) -> int: logger.setLevel(logging.INFO) # Main routine + import stm32pio.util # as we modify sys.path we should import the module there (i.e. after modification) + try: project = stm32pio.util.Stm32pio(args.project_path) diff --git a/stm32pio/settings.py b/stm32pio/settings.py index 6d7f344..520b53e 100644 --- a/stm32pio/settings.py +++ b/stm32pio/settings.py @@ -3,6 +3,7 @@ import platform import pathlib +import string my_os = platform.system() @@ -30,11 +31,15 @@ cubemx_script_filename = 'cubemx-script' # (default is OK) see CubeMX user manual PDF to see other useful options -cubemx_script_content = "config load {cubemx_ioc_full_filename}\n" \ - "generate code {project_path}\n" \ - "exit\n" +cubemx_script_content = string.Template('''\ +config load $cubemx_ioc_full_filename +generate code $project_path +exit +''') # (default is OK) -platformio_ini_patch_content = "\n[platformio]\n" \ - "include_dir = Inc\n" \ - "src_dir = Src\n" +platformio_ini_patch_content = '''\ +[platformio] +include_dir = Inc +src_dir = Src +''' diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index 1f8b28d..3c425b4 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -31,10 +31,10 @@ class TestUnit(unittest.TestCase): def setUp(self) -> None: self.tearDown() - shutil.copytree(str(TEST_PROJECT_PATH), str(fixture_path)) + shutil.copytree(TEST_PROJECT_PATH, fixture_path) def tearDown(self) -> None: - shutil.rmtree(str(fixture_path), ignore_errors=True) + shutil.rmtree(fixture_path, ignore_errors=True) def test_generate_code(self): """ @@ -42,6 +42,7 @@ def test_generate_code(self): """ project = stm32pio.util.Stm32pio(fixture_path) project.generate_code() + # Assuming that the presence of these files indicates a success files_should_be_present = [stm32pio.settings.cubemx_script_filename, 'Src/main.c', 'Inc/main.h'] self.assertEqual([fixture_path.joinpath(file).is_file() for file in files_should_be_present], @@ -54,6 +55,7 @@ def test_pio_init(self): """ project = stm32pio.util.Stm32pio(fixture_path) project.pio_init(PROJECT_BOARD) + self.assertTrue(fixture_path.joinpath('platformio.ini').is_file(), msg="platformio.ini is not there") def test_patch_platformio_ini(self): @@ -68,10 +70,8 @@ def test_patch_platformio_ini(self): after_patch_content = fixture_path.joinpath('platformio.ini').read_text() - # Initial content wasn't corrupted self.assertEqual(after_patch_content[:len(test_content)], test_content, msg="Initial content of platformio.ini is corrupted") - # Patch content is as expected self.assertEqual(after_patch_content[len(test_content):], stm32pio.settings.platformio_ini_patch_content, msg="Patch content is not as expected") @@ -81,6 +81,7 @@ def test_build_should_raise(self): """ project = stm32pio.util.Stm32pio(fixture_path) project.pio_init(PROJECT_BOARD) + with self.assertRaisesRegex(Exception, "PlatformIO build error", msg="Build error exception hadn't been raised"): project.pio_build() @@ -138,10 +139,10 @@ class TestIntegration(unittest.TestCase): def setUp(self) -> None: self.tearDown() - shutil.copytree(str(TEST_PROJECT_PATH), str(fixture_path)) + shutil.copytree(TEST_PROJECT_PATH, fixture_path) def tearDown(self) -> None: - shutil.rmtree(str(fixture_path), ignore_errors=True) + shutil.rmtree(fixture_path, ignore_errors=True) def test_build(self): """ @@ -201,10 +202,10 @@ class TestCLI(unittest.TestCase): def setUp(self) -> None: self.tearDown() - shutil.copytree(str(TEST_PROJECT_PATH), str(fixture_path)) + shutil.copytree(TEST_PROJECT_PATH, fixture_path) def tearDown(self) -> None: - shutil.rmtree(str(fixture_path), ignore_errors=True) + shutil.rmtree(fixture_path, ignore_errors=True) def test_clean(self): """ @@ -232,8 +233,7 @@ def test_new(self): """ Successful build is the best indicator that all went right so we use '--with-build' option """ - return_code = stm32pio.app.main(sys_argv=['new', '-d', str(fixture_path), '-b', str(PROJECT_BOARD), - '--with-build']) + return_code = stm32pio.app.main(sys_argv=['new', '-d', str(fixture_path), '-b', PROJECT_BOARD, '--with-build']) self.assertEqual(return_code, 0, msg="Non-zero return code") # .ioc file should be preserved @@ -250,10 +250,8 @@ def test_generate(self): self.assertTrue(fixture_path.joinpath(inc_dir).is_dir(), msg=f"Missing '{inc_dir}'") self.assertTrue(fixture_path.joinpath(src_dir).is_dir(), msg=f"Missing '{src_dir}'") - self.assertFalse(len([child for child in fixture_path.joinpath(inc_dir).iterdir()]) == 0, - msg=f"'{inc_dir}' is empty") - self.assertFalse(len([child for child in fixture_path.joinpath(src_dir).iterdir()]) == 0, - msg=f"'{src_dir}' is empty") + self.assertFalse(len(list(fixture_path.joinpath(inc_dir).iterdir())) == 0, msg=f"'{inc_dir}' is empty") + self.assertFalse(len(list(fixture_path.joinpath(src_dir).iterdir())) == 0, msg=f"'{src_dir}' is empty") # .ioc file should be preserved self.assertTrue(fixture_path.joinpath(f"{fixture_path.name}.ioc").is_file(), msg="Missing .ioc file") @@ -280,13 +278,11 @@ def test_verbose(self): """ stm32pio_exec = inspect.getfile(stm32pio.app) # get the path to the main stm32pio script - # Get the path of the current python executable (no need to guess python or python3) (can probably use another - # approach to retrieve the executable) - python_exec = sys.executable - result = subprocess.run([python_exec, stm32pio_exec, '-v', 'clean', '-d', str(fixture_path)], encoding='utf-8', + python_exec = sys.executable # get the current python executable (no need to guess python or python3) + result = subprocess.run([python_exec, stm32pio_exec, '-v', 'clean', '-d', fixture_path], encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE) self.assertEqual(result.returncode, 0, msg="Non-zero return code") - # Somehow stderr contains actual output + # Somehow stderr and not stdout contains the actual output self.assertIn('DEBUG', result.stderr.split(), msg="Verbose logging output has not been enabled") diff --git a/stm32pio/util.py b/stm32pio/util.py index c3b9f80..f9373c3 100644 --- a/stm32pio/util.py +++ b/stm32pio/util.py @@ -74,7 +74,7 @@ def generate_code(self) -> None: cubemx_script_full_filename = self.project_path.joinpath(stm32pio.settings.cubemx_script_filename) if not cubemx_script_full_filename.is_file(): logger.debug(f"'{stm32pio.settings.cubemx_script_filename}' file wasn't found, creating one...") - cubemx_script_content = stm32pio.settings.cubemx_script_content.format( + cubemx_script_content = stm32pio.settings.cubemx_script_content.substitute( project_path=self.project_path, cubemx_ioc_full_filename=cubemx_ioc_full_filename) cubemx_script_full_filename.write_text(cubemx_script_content) logger.debug(f"'{stm32pio.settings.cubemx_script_filename}' file has been successfully created") @@ -83,7 +83,7 @@ def generate_code(self) -> None: logger.info("starting to generate a code from the CubeMX .ioc file...") command_arr = [stm32pio.settings.java_cmd, '-jar', stm32pio.settings.cubemx_path, '-q', - str(cubemx_script_full_filename)] + cubemx_script_full_filename] if logger.level <= logging.DEBUG: result = subprocess.run(command_arr) else: @@ -121,7 +121,7 @@ def pio_init(self, board: str) -> None: raise Exception("failed to start PlatformIO") logger.info("starting PlatformIO project initialization...") - command_arr = [stm32pio.settings.platformio_cmd, 'init', '-d', str(self.project_path), '-b', board, + command_arr = [stm32pio.settings.platformio_cmd, 'init', '-d', self.project_path, '-b', board, '-O', 'framework=stm32cube'] if logger.level > logging.DEBUG: command_arr.append('--silent') @@ -148,9 +148,9 @@ def patch_platformio_ini(self) -> None: else: logger.warning("'platformio.ini' file not found") - shutil.rmtree(str(self.project_path.joinpath('include')), ignore_errors=True) + shutil.rmtree(self.project_path.joinpath('include'), ignore_errors=True) if not self.project_path.joinpath('SRC').is_dir(): # case sensitive file system - shutil.rmtree(str(self.project_path.joinpath('src')), ignore_errors=True) + shutil.rmtree(self.project_path.joinpath('src'), ignore_errors=True) def start_editor(self, editor_command: str) -> None: @@ -164,7 +164,7 @@ def start_editor(self, editor_command: str) -> None: logger.info("starting an editor...") try: - subprocess.run([editor_command, str(self.project_path)], check=True) + subprocess.run([editor_command, self.project_path], check=True) except subprocess.CalledProcessError as e: logger.error(f"Failed to start the editor {editor_command}: {e.stderr}") @@ -183,7 +183,7 @@ def pio_build(self) -> int: logger.error("no 'platformio.ini' file, build is impossible") return -1 - command_arr = [stm32pio.settings.platformio_cmd, 'run', '-d', str(self.project_path)] + command_arr = [stm32pio.settings.platformio_cmd, 'run', '-d', self.project_path] if logger.level > logging.DEBUG: command_arr.append('--silent') result = subprocess.run(command_arr) @@ -203,7 +203,7 @@ def clean(self) -> None: for child in self.project_path.iterdir(): if child.name != f"{self.project_path.name}.ioc": if child.is_dir(): - shutil.rmtree(str(child), ignore_errors=True) + shutil.rmtree(child, ignore_errors=True) logger.debug(f"del {child}/") elif child.is_file(): child.unlink() From f79d982ae4224e3f1086314e6e91d739186571e4 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Mon, 21 Oct 2019 20:08:07 +0300 Subject: [PATCH 11/26] v0.9-alpha: get state of the project --- TODO.md | 1 + stm32pio/tests/test.py | 4 ++-- stm32pio/util.py | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/TODO.md b/TODO.md index 6b70f20..979af87 100644 --- a/TODO.md +++ b/TODO.md @@ -18,3 +18,4 @@ - [x] Settings string templates and multi line - [ ] Smart `start_editor` test (detect in system, maybe use unittest `skipIf` decorator) - [ ] Maybe split tests to avoid long names (e.g. => `stm32pio.tests.test_unit` (but then we will have `stm32pio.tests.test_unit.Test` or so...) + - [ ] Clean only related stuff, not the entire folder content diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index 3c425b4..7262b85 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -109,7 +109,7 @@ def test_run_editor(self): } } for command, name in editors.items(): - with self.subTest(command=command, name=name): + with self.subTest(command=command, name=name[platform.system()]): project.start_editor(command) time.sleep(1) # wait a little bit for app to start if platform.system() == 'Windows': @@ -121,7 +121,7 @@ def test_run_editor(self): encoding='utf-8') # Or, for Python 3.7 and above: # result = subprocess.run(['ps', '-A'], capture_output=True, encoding='utf-8') - self.assertIn(name[platform.system()], result.stdout) + self.assertIn(name, result.stdout) def test_file_not_found(self): """ diff --git a/stm32pio/util.py b/stm32pio/util.py index f9373c3..5df14cb 100644 --- a/stm32pio/util.py +++ b/stm32pio/util.py @@ -38,6 +38,20 @@ def __init__(self, dirty_path: str): self.project_path = self._resolve_project_path(dirty_path) + def get_state(self): + """ + """ + + project_content = [item.name for item in self.project_path.iterdir()] + print('project_content', project_content) + # if 'Src' in project_content and 'Inc' in project_content and self.project_path.joinpath(stm32pio.settings.cubemx_script_filename).is_file(): + # print('CubeMX generated') + # if 'platformio.ini' in project_content: + # print('PlatformIO initialized') + # if '.pioenvs' in project_content: + # print('Built') + + @staticmethod def _resolve_project_path(dirty_path: str) -> pathlib.Path: """ From ae84076777952c9554c63d7f1692fc79f627f551 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Tue, 22 Oct 2019 00:19:09 +0300 Subject: [PATCH 12/26] v0.9-alpha: get project state development --- README.md | 2 +- TODO.md | 1 + stm32pio/util.py | 17 +++++++++++------ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d21a31b..f9f1071 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ to see help. 5. Run `platformio boards` (`pio boards`) or go to [boards](https://docs.platformio.org/en/latest/boards) to list all supported devices. Pick one and use its ID as a `-b` argument (for example, `nucleo_f031k6`) 6. All done! You can now run ```shell script - $ python3 stm32pio.py new -d path/to/cubemx/project/ -b nucleo_f031k6 --start-editor=vscode --with-build + $ python3 stm32pio.py new -d path/to/cubemx/project/ -b nucleo_f031k6 --start-editor=code --with-build ``` to complete generation, start the Visual Studio Code editor with opened folder and compile the project (as an example, not required). Make sure you have all tools in PATH (`java` (or set its path in `settings.py`), `python`, editor). You can use shorter form if you are already located in the project directory (also using shebang alias): ```shell script diff --git a/TODO.md b/TODO.md index 979af87..0927c1d 100644 --- a/TODO.md +++ b/TODO.md @@ -19,3 +19,4 @@ - [ ] Smart `start_editor` test (detect in system, maybe use unittest `skipIf` decorator) - [ ] Maybe split tests to avoid long names (e.g. => `stm32pio.tests.test_unit` (but then we will have `stm32pio.tests.test_unit.Test` or so...) - [ ] Clean only related stuff, not the entire folder content + - [ ] rename `patch_platformio_ini()` diff --git a/stm32pio/util.py b/stm32pio/util.py index 5df14cb..a05cc47 100644 --- a/stm32pio/util.py +++ b/stm32pio/util.py @@ -44,12 +44,17 @@ def get_state(self): project_content = [item.name for item in self.project_path.iterdir()] print('project_content', project_content) - # if 'Src' in project_content and 'Inc' in project_content and self.project_path.joinpath(stm32pio.settings.cubemx_script_filename).is_file(): - # print('CubeMX generated') - # if 'platformio.ini' in project_content: - # print('PlatformIO initialized') - # if '.pioenvs' in project_content: - # print('Built') + + # generate: ['Inc', 'Src', 'cubemx-script', 'stm32pio-test-project.ioc'] + # pio init: ['test', 'include', 'Inc', 'platformio.ini', '.gitignore', 'Src', 'cubemx-script', 'lib', 'stm32pio-test-project.ioc', '.travis.yml', 'src'] + # pio patched: ['test', 'Inc', 'platformio.ini', '.gitignore', 'Src', 'cubemx-script', 'lib', 'stm32pio-test-project.ioc', '.travis.yml'] + # pio built: ['.pio', 'test', 'Inc', 'platformio.ini', '.gitignore', 'Src', 'cubemx-script', 'lib', 'stm32pio-test-project.ioc', '.travis.yml'] + + # .pio/build/nucleo_f031k6/firmware.bin .pio/build/nucleo_f031k6/firmware.elf + + # can use + # print(self.project_path.joinpath('Src').is_dir()) + # or + # 'in' inclusion checks @staticmethod From e3b11a91a6cee7d80f1dcc90976bda75156ae2e4 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Tue, 22 Oct 2019 00:30:08 +0300 Subject: [PATCH 13/26] v0.9-alpha: remove unneeded sys.path modification from __main__.py --- stm32pio/__main__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/stm32pio/__main__.py b/stm32pio/__main__.py index a199082..b424b14 100644 --- a/stm32pio/__main__.py +++ b/stm32pio/__main__.py @@ -1,8 +1,6 @@ import sys -import pathlib import stm32pio.app if __name__ == '__main__': - sys.path.insert(0, str(pathlib.Path(sys.path[0]).parent)) # hack to be able to run the app as 'python3 stm32pio.py' sys.exit(stm32pio.app.main()) From 9b81583719e106a8d9e7ae86570823f75774326a Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sun, 10 Nov 2019 19:47:46 +0300 Subject: [PATCH 14/26] v0.9-alpha: * rename patch_platformio_ini() to patch() * hierarchical loggers * settings are now stored on per-project base using configparser * generate cubemx script temporarily * new Stm32pio initialization * new get_state() function (beta) * search for .ioc file dynamically (no need for particular name) --- CHANGELOG | 3 + TODO.md | 8 +- .../stm32pio-test-project.ioc | 5 +- stm32pio/app.py | 12 +- stm32pio/settings.py | 67 +++--- stm32pio/tests/test.py | 18 +- stm32pio/util.py | 212 ++++++++++++------ 7 files changed, 196 insertions(+), 129 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 64281a8..58f40a7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -99,3 +99,6 @@ stm32pio changelog: - Changed: revised and improved tests - Changed: actualized .ioc file and clean-up the code according to the latest STM32CubeMX version (5.3.0 at the moment) - Changed: revised and improved util module + + ver. 0.9 (11.19): + - New: tested with Python3 version of PlatformIO diff --git a/TODO.md b/TODO.md index 0927c1d..e9c8a0c 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,7 @@ # TODOs - [ ] Middleware support (FreeRTOS, etc.) + - [ ] Arduino framework support - [ ] Add more checks, for example when updating the project (`generate` command), check for boards matching and so on... - [x] Function annotations - [ ] GUI. For example, drop the folder into small window (with checkboxes corresponding with CLI options) and get the output. At left is a list of recent projects @@ -13,10 +14,9 @@ - [x] Abort `--with-build` if no platformio.ini file is present - [x] Rename `stm32pio.py` -> `app.py` - [ ] Rename `util.py` -> `lib.py` (maybe) - - [ ] Do not require matching of the project folder and .ioc file names (use first .ioc file found) + - [x] Do not require matching of the project folder and .ioc file names (use first .ioc file found) - [x] Remove casts to string where we can use path-like objects - [x] Settings string templates and multi line - - [ ] Smart `start_editor` test (detect in system, maybe use unittest `skipIf` decorator) + - [ ] Smart `start_editor` test (detect editors in system, maybe use unittest `skipIf` decorator) - [ ] Maybe split tests to avoid long names (e.g. => `stm32pio.tests.test_unit` (but then we will have `stm32pio.tests.test_unit.Test` or so...) - - [ ] Clean only related stuff, not the entire folder content - - [ ] rename `patch_platformio_ini()` + - [ ] For GUI: indicate progress as states goes forward (see `scratch.py`) diff --git a/stm32pio-test-project/stm32pio-test-project.ioc b/stm32pio-test-project/stm32pio-test-project.ioc index 73411a1..b005e21 100644 --- a/stm32pio-test-project/stm32pio-test-project.ioc +++ b/stm32pio-test-project/stm32pio-test-project.ioc @@ -21,6 +21,7 @@ Mcu.UserConstants= Mcu.UserName=STM32F031K6Tx MxCube.Version=5.3.0 MxDb.Version=DB.5.0.30 +NVIC.ForceEnableDMAVector=true NVIC.HardFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false NVIC.NonMaskableInt_IRQn=true\:0\:0\:false\:false\:true\:false\:false NVIC.PendSV_IRQn=true\:0\:0\:false\:false\:true\:false\:false @@ -58,7 +59,7 @@ PF0-OSC_IN.Locked=true PF0-OSC_IN.Mode=HSE-External-Clock-Source PF0-OSC_IN.Signal=RCC_OSC_IN PinOutPanel.RotationAngle=0 -ProjectManager.AskForMigrate=true +ProjectManager.AskForMigrate=false ProjectManager.BackupPrevious=false ProjectManager.CompilerOptimize=6 ProjectManager.ComputerToolchain=false @@ -83,7 +84,7 @@ ProjectManager.ProjectName=stm32pio-test-project ProjectManager.StackSize=0x400 ProjectManager.TargetToolchain=Other Toolchains (GPDSC) ProjectManager.ToolChainLocation= -ProjectManager.UnderRoot=true +ProjectManager.UnderRoot=false ProjectManager.functionlistsort=1-MX_GPIO_Init-GPIO-false-HAL-true,2-SystemClock_Config-RCC-false-HAL-false,3-MX_USART1_UART_Init-USART1-false-HAL-true RCC.CECFreq_Value=32786.88524590164 RCC.FamilyName=M diff --git a/stm32pio/app.py b/stm32pio/app.py index e6549b1..a05b5e5 100755 --- a/stm32pio/app.py +++ b/stm32pio/app.py @@ -62,14 +62,14 @@ def main(sys_argv: list = sys.argv[1:]) -> int: # Logger instance goes through the whole program. # Currently only 2 levels of verbosity through the '-v' option are counted (INFO and DEBUG) - logger = logging.getLogger() + logger = logging.getLogger('stm32pio') if args.verbose: - logging.basicConfig(format="%(levelname)-8s %(funcName)-16s %(message)s") - logger.setLevel(logging.DEBUG) + logging.basicConfig(format="%(levelname)-8s %(funcName)-16s %(message)s", level=logging.DEBUG) + # logger.setLevel(logging.DEBUG) logger.debug("debug logging enabled") else: - logging.basicConfig(format="%(levelname)-8s %(message)s") - logger.setLevel(logging.INFO) + logging.basicConfig(format="%(levelname)-8s %(message)s", level=logging.INFO) + # logger.setLevel(logging.INFO) # Main routine import stm32pio.util # as we modify sys.path we should import the module there (i.e. after modification) @@ -81,7 +81,7 @@ def main(sys_argv: list = sys.argv[1:]) -> int: project.generate_code() if args.subcommand == 'new': project.pio_init(args.board) - project.patch_platformio_ini() + project.patch() if args.with_build: project.pio_build() diff --git a/stm32pio/settings.py b/stm32pio/settings.py index 520b53e..2866864 100644 --- a/stm32pio/settings.py +++ b/stm32pio/settings.py @@ -1,45 +1,34 @@ -# TODO: how we will be set these parameters if the app will be run after the 'setup' process? Or even obtained by 'pip'? -# Maybe we should describe the config file to the user instead of this Python source. - import platform import pathlib -import string +import collections my_os = platform.system() -# (default is OK) How do you start Java from the command line? (edit if Java not in PATH) -java_cmd = 'java' - -# (default is OK) How do you start PlatformIO from the command line? (edit if not in PATH, check -# https://docs.platformio.org/en/latest/installation.html#install-shell-commands) -platformio_cmd = 'platformio' - -# (default is OK) We trying to guess STM32CubeMX location. You can just avoid this and hard-code it. -# Note that STM32CubeMX will be invoked as 'java -jar CUBEMX' -# macOS default: 'Applications' folder -if my_os == 'Darwin': - cubemx_path = "/Applications/STMicroelectronics/STM32CubeMX.app/Contents/Resources/STM32CubeMX" -# Linux (Ubuntu) default: -elif my_os == 'Linux': - cubemx_path = pathlib.Path.home().joinpath("STM32CubeMX/STM32CubeMX") -# Windows default: -elif my_os == 'Windows': - cubemx_path = "C:/Program Files/STMicroelectronics/STM32Cube/STM32CubeMX/STM32CubeMX.exe" - -# (default is OK) choose a file name in which we store the CubeMX script -cubemx_script_filename = 'cubemx-script' - -# (default is OK) see CubeMX user manual PDF to see other useful options -cubemx_script_content = string.Template('''\ -config load $cubemx_ioc_full_filename -generate code $project_path -exit -''') - -# (default is OK) -platformio_ini_patch_content = '''\ -[platformio] -include_dir = Inc -src_dir = Src -''' +config_default = collections.OrderedDict( + app={ + # (default is OK) How do you start Java from the command line? (edit if Java not in PATH) + 'java_cmd': 'java', + + # (default is OK) How do you start PlatformIO from the command line? (edit if not in PATH, check + # https://docs.platformio.org/en/latest/installation.html#install-shell-commands) + 'platformio_cmd': 'platformio', + + # (default is OK) We trying to guess STM32CubeMX location. You can just avoid this and hard-code it. + # Note that STM32CubeMX will be invoked as 'java -jar CUBEMX' + 'cubemx_cmd': + # macOS default: 'Applications' folder + "/Applications/STMicroelectronics/STM32CubeMX.app/Contents/Resources/STM32CubeMX" if my_os == 'Darwin' else + # Linux (Ubuntu) default: + pathlib.Path.home().joinpath("STM32CubeMX/STM32CubeMX") if my_os == 'Linux' else + # Windows default: + "C:/Program Files/STMicroelectronics/STM32Cube/STM32CubeMX/STM32CubeMX.exe" if my_os == 'Windows' else None + }, + project={ + # (default is OK) see CubeMX user manual PDF to get other useful options + 'cubemx_script_content': "config load $cubemx_ioc_full_filename\ngenerate code $project_path\nexit", + + # override the defaults to comply with CubeMX project structure + 'platformio_ini_patch_content': "[platformio]\ninclude_dir = Inc\nsrc_dir = Src\n" + } +) diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index 7262b85..f18359c 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -44,7 +44,8 @@ def test_generate_code(self): project.generate_code() # Assuming that the presence of these files indicates a success - files_should_be_present = [stm32pio.settings.cubemx_script_filename, 'Src/main.c', 'Inc/main.h'] + # TODO: remake as subTest + files_should_be_present = ['Src/main.c', 'Inc/main.h'] self.assertEqual([fixture_path.joinpath(file).is_file() for file in files_should_be_present], [True] * len(files_should_be_present), msg=f"At least one of {files_should_be_present} files haven't been created") @@ -66,13 +67,15 @@ def test_patch_platformio_ini(self): test_content = "*** TEST PLATFORMIO.INI FILE ***" fixture_path.joinpath('platformio.ini').write_text(test_content) - project.patch_platformio_ini() + project.patch() + # TODO: check 'include' deletion after_patch_content = fixture_path.joinpath('platformio.ini').read_text() self.assertEqual(after_patch_content[:len(test_content)], test_content, msg="Initial content of platformio.ini is corrupted") - self.assertEqual(after_patch_content[len(test_content):], stm32pio.settings.platformio_ini_patch_content, + self.assertEqual(after_patch_content[len(test_content):], + stm32pio.settings.config_default['project']['platformio_ini_patch_content'], msg="Patch content is not as expected") def test_build_should_raise(self): @@ -121,7 +124,7 @@ def test_run_editor(self): encoding='utf-8') # Or, for Python 3.7 and above: # result = subprocess.run(['ps', '-A'], capture_output=True, encoding='utf-8') - self.assertIn(name, result.stdout) + self.assertIn(name[platform.system()], result.stdout) def test_file_not_found(self): """ @@ -151,7 +154,7 @@ def test_build(self): project = stm32pio.util.Stm32pio(fixture_path) project.generate_code() project.pio_init(PROJECT_BOARD) - project.patch_platformio_ini() + project.patch() result = project.pio_build() @@ -168,7 +171,7 @@ def test_regenerate_code(self): # Generate a new project ... project.generate_code() project.pio_init(PROJECT_BOARD) - project.patch_platformio_ini() + project.patch() # ... change it: test_file_1 = fixture_path.joinpath('Src', 'main.c') @@ -278,7 +281,8 @@ def test_verbose(self): """ stm32pio_exec = inspect.getfile(stm32pio.app) # get the path to the main stm32pio script - python_exec = sys.executable # get the current python executable (no need to guess python or python3) + # get the current python executable (no need to guess whether it's python or python3 and so on) + python_exec = sys.executable result = subprocess.run([python_exec, stm32pio_exec, '-v', 'clean', '-d', fixture_path], encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE) self.assertEqual(result.returncode, 0, msg="Non-zero return code") diff --git a/stm32pio/util.py b/stm32pio/util.py index a05cc47..1122c06 100644 --- a/stm32pio/util.py +++ b/stm32pio/util.py @@ -2,12 +2,14 @@ import pathlib import shutil import subprocess -# import enum +import enum +import configparser +import string +import tempfile import stm32pio.settings -logger = logging.getLogger() - +logger = logging.getLogger('stm32pio.util') # TODO: add states and check the current state for every operation (so we can't, for example, go to build stage without @@ -15,18 +17,18 @@ # multiple projects. (use enum for this) # Also, we would probably need some method to detect a current project state on program start (or store it explicitly # in the dotted system file) -# @enum.unique -# class ProjectState(enum.Enum): -# """ -# """ -# -# INITIALIZED = enum.auto() -# GENERATED = enum.auto() -# PIO_INITIALIZED = enum.auto() -# PIO_INI_PATCHED = enum.auto() -# BUILT = enum.auto() -# -# ERROR = enum.auto() +@enum.unique +class ProjectState(enum.IntEnum): + """ + """ + UNDEFINED = enum.auto() + GENERATED = enum.auto() + PIO_INITIALIZED = enum.auto() + PIO_INI_PATCHED = enum.auto() + BUILT = enum.auto() + + +# NUM_OF_STATES = len(list(ProjectState)) class Stm32pio: @@ -36,25 +38,112 @@ class Stm32pio: def __init__(self, dirty_path: str): self.project_path = self._resolve_project_path(dirty_path) + self.config = self._load_settings_file() + + ioc_file = self._find_ioc_file() + self.config.set('project', 'ioc_file', str(ioc_file)) + + # self.config.set('project', 'state', str(self.get_state().value)) + + cubemx_script_template = string.Template(self.config.get('project', 'cubemx_script_content')) + cubemx_script_content = cubemx_script_template.substitute(project_path=self.project_path, + cubemx_ioc_full_filename=str(ioc_file)) + self.config.set('project', 'cubemx_script_content', cubemx_script_content) + + self._save_config() - def get_state(self): + def _save_config(self): + with self.project_path.joinpath('stm32pio.ini').open(mode='w') as config_file: + self.config.write(config_file) + + + def get_state(self) -> ProjectState: """ + Hint: Files/folders to be present on every project state: + generated: ['Inc', 'Src', 'cubemx-script', 'stm32pio-test-project.ioc'] + pio initted: ['test', 'include', 'Inc', 'platformio.ini', '.gitignore', 'Src', 'cubemx-script', 'lib', 'stm32pio-test-project.ioc', '.travis.yml', 'src'] + patched: ['test', 'Inc', 'platformio.ini', '.gitignore', 'Src', 'cubemx-script', 'lib', 'stm32pio-test-project.ioc', '.travis.yml'] + built: ['.pio', 'test', 'Inc', 'platformio.ini', '.gitignore', 'Src', 'cubemx-script', 'lib', 'stm32pio-test-project.ioc', '.travis.yml'] + + .pio/build/nucleo_f031k6/firmware.bin, .pio/build/nucleo_f031k6/firmware.elf """ - project_content = [item.name for item in self.project_path.iterdir()] - print('project_content', project_content) + logger.debug("Calculating project state...") + logger.debug(f"Project content: {[item.name for item in self.project_path.iterdir()]}") + + states_conditions = { + ProjectState.UNDEFINED: [True], + ProjectState.GENERATED: [self.project_path.joinpath('Inc').is_dir(), + self.project_path.joinpath('Src').is_dir()], + ProjectState.PIO_INITIALIZED: [self.project_path.joinpath('platformio.ini').is_file()], + ProjectState.PIO_INI_PATCHED: [not self.project_path.joinpath('include').is_dir(), + self.project_path.joinpath('platformio.ini').is_file() and + self.config.get('project', 'platformio_ini_patch_content') in self.project_path.joinpath('platformio.ini').read_text()], + ProjectState.BUILT: [self.project_path.joinpath('.pio').is_dir(), + any([path.is_file() for path in self.project_path.joinpath('.pio').rglob('*firmware*')])] + } + + # Use (1,0) instead of (True,False) because on debug printing it looks cleaner + conditions_results = [1 if all(conditions is True for conditions in states_conditions[state]) else 0 + for state in ProjectState] + # Put away unnecessary processing as the string still will be formed even if the logging level doesn't allow + # propagation of this message + if logger.level <= logging.DEBUG: + states_info_str = '\n'.join(f"{state.name:20}{conditions_results[state.value-1]}" for state in ProjectState) + logger.debug(f"Determined states: {states_info_str}") + + last_true_index = 0 + for index, value in enumerate(conditions_results): + if value == 1: + last_true_index = index + else: + break - # generate: ['Inc', 'Src', 'cubemx-script', 'stm32pio-test-project.ioc'] - # pio init: ['test', 'include', 'Inc', 'platformio.ini', '.gitignore', 'Src', 'cubemx-script', 'lib', 'stm32pio-test-project.ioc', '.travis.yml', 'src'] - # pio patched: ['test', 'Inc', 'platformio.ini', '.gitignore', 'Src', 'cubemx-script', 'lib', 'stm32pio-test-project.ioc', '.travis.yml'] - # pio built: ['.pio', 'test', 'Inc', 'platformio.ini', '.gitignore', 'Src', 'cubemx-script', 'lib', 'stm32pio-test-project.ioc', '.travis.yml'] + - # .pio/build/nucleo_f031k6/firmware.bin .pio/build/nucleo_f031k6/firmware.elf + project_state = ProjectState(last_true_index + 1) if 1 not in conditions_results[last_true_index + 1:] \ + else ProjectState.UNDEFINED # edit there to get first approach from second - # can use - # print(self.project_path.joinpath('Src').is_dir()) - # or - # 'in' inclusion checks + return project_state + + + def _find_ioc_file(self) -> pathlib.Path: + """ + """ + + ioc_file = self.config.get('project', 'ioc_file', fallback=None) + if ioc_file: + return pathlib.Path(ioc_file).resolve() + else: + logger.debug("Searching for any .ioc file...") + candidates = list(self.project_path.glob('*.ioc')) + if len(candidates) == 0: + raise FileNotFoundError("CubeMX project .ioc file") + elif len(candidates) == 1: + logger.debug(f"{candidates[0].name} is selected") + return candidates[0] + else: + logger.warning(f"There are multiple .ioc files, {candidates[0].name} is selected") + return candidates[0] + + + def _load_settings_file(self) -> configparser.ConfigParser: + """ + """ + # logger.debug("Searching for any .ioc file...") + stm32pio_ini = self.project_path.joinpath('stm32pio.ini') + # if stm32pio_ini.is_file(): + config = configparser.ConfigParser() + + # Fill with default values + config.read_dict(stm32pio.settings.config_default) + + config.read(str(stm32pio_ini)) + + # for section in config.sections(): + # print('=========== ' + section + ' ===========') + # for item in config.items(section): + # print(item) + + return config @staticmethod @@ -78,43 +167,25 @@ def generate_code(self) -> None: Call STM32CubeMX app as a 'java -jar' file with the automatically prearranged 'cubemx-script' file """ - # Assuming the name of the '.ioc' file is the same as the project folder, we extract it from the given string - project_name = self.project_path.name - logger.debug(f"searching for {project_name}.ioc file...") - cubemx_ioc_full_filename = self.project_path.joinpath(f'{project_name}.ioc') - if cubemx_ioc_full_filename.exists(): - logger.debug(f"{project_name}.ioc file was found") - else: - logger.error(f"there is no {project_name}.ioc file") - raise FileNotFoundError(cubemx_ioc_full_filename) - - # Find/create 'cubemx-script' file - logger.debug(f"searching for '{stm32pio.settings.cubemx_script_filename}' file...") - cubemx_script_full_filename = self.project_path.joinpath(stm32pio.settings.cubemx_script_filename) - if not cubemx_script_full_filename.is_file(): - logger.debug(f"'{stm32pio.settings.cubemx_script_filename}' file wasn't found, creating one...") - cubemx_script_content = stm32pio.settings.cubemx_script_content.substitute( - project_path=self.project_path, cubemx_ioc_full_filename=cubemx_ioc_full_filename) - cubemx_script_full_filename.write_text(cubemx_script_content) - logger.debug(f"'{stm32pio.settings.cubemx_script_filename}' file has been successfully created") - else: - logger.debug(f"'{stm32pio.settings.cubemx_script_filename}' file is already there") - - logger.info("starting to generate a code from the CubeMX .ioc file...") - command_arr = [stm32pio.settings.java_cmd, '-jar', stm32pio.settings.cubemx_path, '-q', - cubemx_script_full_filename] - if logger.level <= logging.DEBUG: - result = subprocess.run(command_arr) - else: - result = subprocess.run(command_arr, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - # Or, for Python 3.7 and above: - # result = subprocess.run(command_arr, capture_output=True) - if result.returncode == 0: - logger.info("successful code generation") - else: - logger.error(f"code generation error (CubeMX return code is {result.returncode}).\n" - "Try to enable a verbose output or generate a code from the CubeMX itself.") - raise Exception("code generation error") + # buffering=0 leads to the immediate flushing on writing + with tempfile.NamedTemporaryFile(buffering=0) as cubemx_script: + cubemx_script.write(self.config.get('project', 'cubemx_script_content').encode()) + + logger.info("starting to generate a code from the CubeMX .ioc file...") + command_arr = [self.config.get('app', 'java_cmd'), '-jar', self.config.get('app', 'cubemx_cmd'), '-q', + cubemx_script.name] + if logger.level <= logging.DEBUG: + result = subprocess.run(command_arr) + else: + result = subprocess.run(command_arr, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + # Or, for Python 3.7 and above: + # result = subprocess.run(command_arr, capture_output=True) + if result.returncode == 0: + logger.info("successful code generation") + else: + logger.error(f"code generation error (CubeMX return code is {result.returncode}).\n" + "Try to enable a verbose output or generate a code from the CubeMX itself.") + raise Exception("code generation error") def pio_init(self, board: str) -> None: @@ -127,7 +198,7 @@ def pio_init(self, board: str) -> None: # Check board name logger.debug("searching for PlatformIO board...") - result = subprocess.run([stm32pio.settings.platformio_cmd, 'boards'], encoding='utf-8', + result = subprocess.run([self.config.get('app', 'platformio_cmd'), 'boards'], encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Or, for Python 3.7 and above: # result = subprocess.run(['platformio', 'boards'], encoding='utf-8', capture_output=True) @@ -140,7 +211,7 @@ def pio_init(self, board: str) -> None: raise Exception("failed to start PlatformIO") logger.info("starting PlatformIO project initialization...") - command_arr = [stm32pio.settings.platformio_cmd, 'init', '-d', self.project_path, '-b', board, + command_arr = [self.config.get('app', 'platformio_cmd'), 'init', '-d', self.project_path, '-b', board, '-O', 'framework=stm32cube'] if logger.level > logging.DEBUG: command_arr.append('--silent') @@ -152,7 +223,7 @@ def pio_init(self, board: str) -> None: raise Exception("PlatformIO error") - def patch_platformio_ini(self) -> None: + def patch(self) -> None: """ Patch platformio.ini file to use created earlier by CubeMX 'Src' and 'Inc' folders as sources """ @@ -162,13 +233,13 @@ def patch_platformio_ini(self) -> None: platformio_ini_file = self.project_path.joinpath('platformio.ini') if platformio_ini_file.is_file(): with platformio_ini_file.open(mode='a') as f: - f.write(stm32pio.settings.platformio_ini_patch_content) + f.write(self.config.get('project', 'platformio_ini_patch_content')) logger.info("'platformio.ini' patched") else: logger.warning("'platformio.ini' file not found") shutil.rmtree(self.project_path.joinpath('include'), ignore_errors=True) - if not self.project_path.joinpath('SRC').is_dir(): # case sensitive file system + if not self.project_path.joinpath('SRC').is_dir(): # check for case sensitive file system shutil.rmtree(self.project_path.joinpath('src'), ignore_errors=True) @@ -202,7 +273,7 @@ def pio_build(self) -> int: logger.error("no 'platformio.ini' file, build is impossible") return -1 - command_arr = [stm32pio.settings.platformio_cmd, 'run', '-d', self.project_path] + command_arr = [self.config.get('app', 'platformio_cmd'), 'run', '-d', self.project_path] if logger.level > logging.DEBUG: command_arr.append('--silent') result = subprocess.run(command_arr) @@ -213,7 +284,6 @@ def pio_build(self) -> int: logger.error("PlatformIO build error") raise Exception("PlatformIO build error") - def clean(self) -> None: """ Clean-up the project folder and preserve only an '.ioc' file From b7c2d3a4639093396344d61197806b270dafcf36 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sat, 23 Nov 2019 01:38:00 +0300 Subject: [PATCH 15/26] v0.9-alpha: * rename 'util' to 'lib' * run STM32CubeMX without a splash screen * new 'init' subcommand * catch PlatformIO errors when return code is 0 --- TODO.md | 8 +- stm32pio/app.py | 44 ++++++---- stm32pio/{util.py => lib.py} | 129 +++++++++++++++++------------ stm32pio/tests/test.py | 154 +++++++++++++++++++++++++---------- 4 files changed, 221 insertions(+), 114 deletions(-) rename stm32pio/{util.py => lib.py} (76%) diff --git a/TODO.md b/TODO.md index e9c8a0c..68b910e 100644 --- a/TODO.md +++ b/TODO.md @@ -6,17 +6,21 @@ - [x] Function annotations - [ ] GUI. For example, drop the folder into small window (with checkboxes corresponding with CLI options) and get the output. At left is a list of recent projects - [x] Remade as Class (constructor `__init__(project_path)`) - - [ ] Config file for every project instead of the `settings.py` (but we still sill be storing the default parameters there) + - [x] Config file for every project instead of the `settings.py` (but we still sill be storing the default parameters there) - [x] Test CLI (integration testing) - [x] Move test fixtures out of the 'tests' so we can use it for multiple projects (for example while testing CLI and GUI versions). Set up test folder for every single test so we make sure the .ioc file is always present and not deleted after failed test - [ ] Upload to PyPI - [x] `__main__` - [x] Abort `--with-build` if no platformio.ini file is present - [x] Rename `stm32pio.py` -> `app.py` - - [ ] Rename `util.py` -> `lib.py` (maybe) + - [x] Rename `util.py` -> `lib.py` (maybe) - [x] Do not require matching of the project folder and .ioc file names (use first .ioc file found) - [x] Remove casts to string where we can use path-like objects - [x] Settings string templates and multi line - [ ] Smart `start_editor` test (detect editors in system, maybe use unittest `skipIf` decorator) - [ ] Maybe split tests to avoid long names (e.g. => `stm32pio.tests.test_unit` (but then we will have `stm32pio.tests.test_unit.Test` or so...) - [ ] For GUI: indicate progress as states goes forward (see `scratch.py`) + - [x] `init` command + - [x] New argparse algo cause now we have config file + - [ ] Update `.ioc` file + - [ ] `str(path)` -> `path` were possible diff --git a/stm32pio/app.py b/stm32pio/app.py index a05b5e5..2babdfc 100755 --- a/stm32pio/app.py +++ b/stm32pio/app.py @@ -30,18 +30,20 @@ def parse_args(args: list) -> Optional[argparse.Namespace]: parser_generate = subparsers.add_parser('generate', help="generate CubeMX code") parser_clean = subparsers.add_parser('clean', help="clean-up the project (WARNING: it deletes ALL content of " "'path' except the .ioc file)") + parser_init = subparsers.add_parser('init', help="create config .ini file so you can tweak parameters before " + "proceeding") # Common subparsers options - for p in [parser_new, parser_generate, parser_clean]: - p.add_argument('-d', '--directory', dest='project_path', help="path to the project (current directory, if not " - "given)", default=pathlib.Path.cwd()) + for p in [parser_new, parser_generate, parser_clean, parser_init]: + p.add_argument('-d', '--directory', dest='project_path', default=pathlib.Path.cwd(), + help="path to the project (current directory, if not given)") + for p in [parser_new, parser_init]: + p.add_argument('-b', '--board', dest='board', help="PlatformIO name of the board", required=False) for p in [parser_new, parser_generate]: p.add_argument('--start-editor', dest='editor', help="use specified editor to open PlatformIO project (e.g. " - "subl, code, atom)", required=False) + "subl, code, atom, etc.)", required=False) p.add_argument('--with-build', action='store_true', help="build a project after generation", required=False) - parser_new.add_argument('-b', '--board', dest='board', help="PlatformIO name of the board", required=True) - # Show help and exit if no arguments were given if len(args) == 0: parser.print_help() @@ -58,45 +60,53 @@ def main(sys_argv: list = sys.argv[1:]) -> int: args = parse_args(sys_argv) if args is None or args.subcommand is None: print("\nNo arguments were given, exiting...") - return -1 + return 0 # Logger instance goes through the whole program. # Currently only 2 levels of verbosity through the '-v' option are counted (INFO and DEBUG) logger = logging.getLogger('stm32pio') if args.verbose: - logging.basicConfig(format="%(levelname)-8s %(funcName)-16s %(message)s", level=logging.DEBUG) - # logger.setLevel(logging.DEBUG) + logging.basicConfig(format="%(levelname)-8s %(funcName)-26s %(message)s", level=logging.DEBUG) + logger.setLevel(logging.DEBUG) logger.debug("debug logging enabled") else: logging.basicConfig(format="%(levelname)-8s %(message)s", level=logging.INFO) - # logger.setLevel(logging.INFO) + logger.setLevel(logging.INFO) # Main routine - import stm32pio.util # as we modify sys.path we should import the module there (i.e. after modification) + import stm32pio.lib # import the module after sys.path modification try: - project = stm32pio.util.Stm32pio(args.project_path) + project = stm32pio.lib.Stm32pio(args.project_path) + + if args.subcommand == 'init' or args.subcommand == 'new' or args.subcommand == 'generate': + project.init(board=args.board if 'board' in args else None) + if (args.subcommand == 'init' or args.subcommand == 'new') and project.config.get('project', 'board') == '': + logger.warning("STM32 board is not specified, it will be needed on PlatformIO project creation") + if args.subcommand == 'init': + logger.info('stm32pio project has been initialized. You can now edit parameters in stm32pio.ini file') + project.save_config() if args.subcommand == 'new' or args.subcommand == 'generate': project.generate_code() if args.subcommand == 'new': - project.pio_init(args.board) + project.pio_init() project.patch() + project.save_config() if args.with_build: project.pio_build() if args.editor: project.start_editor(args.editor) - elif args.subcommand == 'clean': + if args.subcommand == 'clean': project.clean() - # util library is designed to throw the exception in bad cases so we catch here globally + # library is designed to throw the exception in bad cases so we catch here globally except Exception as e: + logger.error(repr(e)) if logger.level <= logging.DEBUG: # verbose traceback.print_exception(*sys.exc_info()) - else: - print(repr(e)) return -1 logger.info("exiting...") diff --git a/stm32pio/util.py b/stm32pio/lib.py similarity index 76% rename from stm32pio/util.py rename to stm32pio/lib.py index 1122c06..648ce66 100644 --- a/stm32pio/util.py +++ b/stm32pio/lib.py @@ -6,22 +6,19 @@ import configparser import string import tempfile +# import weakref import stm32pio.settings logger = logging.getLogger('stm32pio.util') -# TODO: add states and check the current state for every operation (so we can't, for example, go to build stage without -# a pio_init performed before). Also, it naturally helps us to construct the GUI in which we manage the list of -# multiple projects. (use enum for this) -# Also, we would probably need some method to detect a current project state on program start (or store it explicitly -# in the dotted system file) @enum.unique class ProjectState(enum.IntEnum): """ """ UNDEFINED = enum.auto() + INITIALIZED = enum.auto() GENERATED = enum.auto() PIO_INITIALIZED = enum.auto() PIO_INI_PATCHED = enum.auto() @@ -43,17 +40,27 @@ def __init__(self, dirty_path: str): ioc_file = self._find_ioc_file() self.config.set('project', 'ioc_file', str(ioc_file)) - # self.config.set('project', 'state', str(self.get_state().value)) + # self._finalizer = weakref.finalize(self, self.save_config) + + def init(self, **kwargs): cubemx_script_template = string.Template(self.config.get('project', 'cubemx_script_content')) cubemx_script_content = cubemx_script_template.substitute(project_path=self.project_path, - cubemx_ioc_full_filename=str(ioc_file)) + cubemx_ioc_full_filename=self.config.get('project', 'ioc_file')) self.config.set('project', 'cubemx_script_content', cubemx_script_content) - self._save_config() + board = '' + if 'board' in kwargs and kwargs['board'] is not None: + try: + board = self._resolve_board(kwargs['board']) + except Exception as e: + logger.warning(e) + self.config.set('project', 'board', board) + elif self.config.get('project', 'board', fallback=None) is None: + self.config.set('project', 'board', board) - def _save_config(self): + def save_config(self): with self.project_path.joinpath('stm32pio.ini').open(mode='w') as config_file: self.config.write(config_file) @@ -73,14 +80,18 @@ def get_state(self) -> ProjectState: states_conditions = { ProjectState.UNDEFINED: [True], - ProjectState.GENERATED: [self.project_path.joinpath('Inc').is_dir(), - self.project_path.joinpath('Src').is_dir()], - ProjectState.PIO_INITIALIZED: [self.project_path.joinpath('platformio.ini').is_file()], - ProjectState.PIO_INI_PATCHED: [not self.project_path.joinpath('include').is_dir(), - self.project_path.joinpath('platformio.ini').is_file() and - self.config.get('project', 'platformio_ini_patch_content') in self.project_path.joinpath('platformio.ini').read_text()], - ProjectState.BUILT: [self.project_path.joinpath('.pio').is_dir(), - any([path.is_file() for path in self.project_path.joinpath('.pio').rglob('*firmware*')])] + ProjectState.INITIALIZED: [self.project_path.joinpath('stm32pio.ini').is_file()], + ProjectState.GENERATED: [self.project_path.joinpath('Inc').is_dir() and + len(list(self.project_path.joinpath('Inc').iterdir())) > 0, + self.project_path.joinpath('Src').is_dir() and + len(list(self.project_path.joinpath('Src').iterdir())) > 0], + ProjectState.PIO_INITIALIZED: [self.project_path.joinpath('platformio.ini').is_file() and + len(self.project_path.joinpath('platformio.ini').read_text()) > 0], + ProjectState.PIO_INI_PATCHED: [self.project_path.joinpath('platformio.ini').is_file() and + self.config.get('project', 'platformio_ini_patch_content') in + self.project_path.joinpath('platformio.ini').read_text()], + ProjectState.BUILT: [self.project_path.joinpath('.pio').is_dir() and + any([item.is_file() for item in self.project_path.joinpath('.pio').rglob('*firmware*')])] } # Use (1,0) instead of (True,False) because on debug printing it looks cleaner @@ -88,19 +99,20 @@ def get_state(self) -> ProjectState: for state in ProjectState] # Put away unnecessary processing as the string still will be formed even if the logging level doesn't allow # propagation of this message - if logger.level <= logging.DEBUG: + if logger.getEffectiveLevel() <= logging.DEBUG: states_info_str = '\n'.join(f"{state.name:20}{conditions_results[state.value-1]}" for state in ProjectState) logger.debug(f"Determined states: {states_info_str}") - last_true_index = 0 + last_true_index = 0 # UNDEFINED is always True for index, value in enumerate(conditions_results): if value == 1: last_true_index = index else: break - project_state = ProjectState(last_true_index + 1) if 1 not in conditions_results[last_true_index + 1:] \ - else ProjectState.UNDEFINED # edit there to get first approach from second + project_state = ProjectState.UNDEFINED + if 1 not in conditions_results[last_true_index + 1:]: + project_state = ProjectState(last_true_index + 1) return project_state @@ -135,7 +147,7 @@ def _load_settings_file(self) -> configparser.ConfigParser: # Fill with default values config.read_dict(stm32pio.settings.config_default) - + # Then override by user values (if exist) config.read(str(stm32pio_ini)) # for section in config.sections(): @@ -154,12 +166,29 @@ def _resolve_project_path(dirty_path: str) -> pathlib.Path: Args: dirty_path: some directory in the filesystem """ - correct_path = pathlib.Path(dirty_path).expanduser().resolve() - if not correct_path.exists(): - logger.error("incorrect project path") - raise FileNotFoundError(correct_path) + resolved_path = pathlib.Path(dirty_path).expanduser().resolve() + if not resolved_path.exists(): + raise FileNotFoundError(resolved_path) + else: + return resolved_path + + + def _resolve_board(self, board: str) -> str: + """ + + """ + logger.debug("searching for PlatformIO board...") + result = subprocess.run([self.config.get('app', 'platformio_cmd'), 'boards'], encoding='utf-8', + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + # Or, for Python 3.7 and above: + # result = subprocess.run(['platformio', 'boards'], encoding='utf-8', capture_output=True) + if result.returncode == 0: + if board not in result.stdout.split(): + raise Exception("wrong STM32 board. Run 'platformio boards' for possible names") + else: + return board else: - return correct_path + raise Exception("failed to search for PlatformIO boards") def generate_code(self) -> None: @@ -173,8 +202,8 @@ def generate_code(self) -> None: logger.info("starting to generate a code from the CubeMX .ioc file...") command_arr = [self.config.get('app', 'java_cmd'), '-jar', self.config.get('app', 'cubemx_cmd'), '-q', - cubemx_script.name] - if logger.level <= logging.DEBUG: + cubemx_script.name, '-s'] + if logger.getEffectiveLevel() <= logging.DEBUG: result = subprocess.run(command_arr) else: result = subprocess.run(command_arr, stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -188,7 +217,7 @@ def generate_code(self) -> None: raise Exception("code generation error") - def pio_init(self, board: str) -> None: + def pio_init(self) -> int: """ Call PlatformIO CLI to initialize a new project @@ -196,31 +225,28 @@ def pio_init(self, board: str) -> None: board: string displaying PlatformIO name of MCU/board (from 'pio boards' command) """ - # Check board name - logger.debug("searching for PlatformIO board...") - result = subprocess.run([self.config.get('app', 'platformio_cmd'), 'boards'], encoding='utf-8', - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - # Or, for Python 3.7 and above: - # result = subprocess.run(['platformio', 'boards'], encoding='utf-8', capture_output=True) - if result.returncode == 0: - if board not in result.stdout.split(): - logger.error("wrong STM32 board. Run 'platformio boards' for possible names") - raise Exception("wrong STM32 board") - else: - logger.error("failed to start PlatformIO") - raise Exception("failed to start PlatformIO") - logger.info("starting PlatformIO project initialization...") - command_arr = [self.config.get('app', 'platformio_cmd'), 'init', '-d', self.project_path, '-b', board, + + command_arr = [self.config.get('app', 'platformio_cmd'), 'init', '-d', self.project_path, '-b', self.config.get('project', 'board'), '-O', 'framework=stm32cube'] - if logger.level > logging.DEBUG: + if logger.getEffectiveLevel() > logging.DEBUG: command_arr.append('--silent') - result = subprocess.run(command_arr) + + error_msg = "PlatformIO project initialization error" + + result = subprocess.run(command_arr, encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE) if result.returncode == 0: + # PlatformIO returns 0 even on some errors ('platformio.ini' wasn't created, e.g. no '--board' argument) + if 'ERROR' in result.stdout.upper(): + print(result.stdout) + raise Exception(error_msg) + if 'ERROR' in result.stderr.upper(): + print(result.stderr) + raise Exception(error_msg) logger.info("successful PlatformIO project initialization") + return result.returncode else: - logger.error("PlatformIO project initialization error") - raise Exception("PlatformIO error") + raise Exception(error_msg) def patch(self) -> None: @@ -233,7 +259,7 @@ def patch(self) -> None: platformio_ini_file = self.project_path.joinpath('platformio.ini') if platformio_ini_file.is_file(): with platformio_ini_file.open(mode='a') as f: - f.write(self.config.get('project', 'platformio_ini_patch_content')) + f.write(self.config.get('project', 'platformio_ini_patch_content') + '\n') logger.info("'platformio.ini' patched") else: logger.warning("'platformio.ini' file not found") @@ -274,7 +300,7 @@ def pio_build(self) -> int: return -1 command_arr = [self.config.get('app', 'platformio_cmd'), 'run', '-d', self.project_path] - if logger.level > logging.DEBUG: + if logger.getEffectiveLevel() > logging.DEBUG: command_arr.append('--silent') result = subprocess.run(command_arr) if result.returncode == 0: @@ -284,6 +310,7 @@ def pio_build(self) -> int: logger.error("PlatformIO build error") raise Exception("PlatformIO build error") + def clean(self) -> None: """ Clean-up the project folder and preserve only an '.ioc' file diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index f18359c..9fc399f 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -1,3 +1,4 @@ +import configparser import pathlib import platform import shutil @@ -10,14 +11,14 @@ import stm32pio.app import stm32pio.settings -import stm32pio.util +import stm32pio.lib # Test data TEST_PROJECT_PATH = pathlib.Path('stm32pio-test-project').resolve() if not TEST_PROJECT_PATH.is_dir() or not TEST_PROJECT_PATH.joinpath('stm32pio-test-project.ioc').is_file(): raise FileNotFoundError("No test project is present") -PROJECT_BOARD = 'nucleo_f031k6' +TEST_PROJECT_BOARD = 'nucleo_f031k6' temp_dir = tempfile.TemporaryDirectory() @@ -40,30 +41,31 @@ def test_generate_code(self): """ Check whether files and folders have been created """ - project = stm32pio.util.Stm32pio(fixture_path) + project = stm32pio.lib.Stm32pio(fixture_path) project.generate_code() # Assuming that the presence of these files indicates a success - # TODO: remake as subTest files_should_be_present = ['Src/main.c', 'Inc/main.h'] - self.assertEqual([fixture_path.joinpath(file).is_file() for file in files_should_be_present], - [True] * len(files_should_be_present), - msg=f"At least one of {files_should_be_present} files haven't been created") + for file in files_should_be_present: + with self.subTest(file_should_be_present=file, msg=f"{file} hasn't been created"): + self.assertEqual(fixture_path.joinpath(file).is_file(), True) def test_pio_init(self): """ Consider that existence of 'platformio.ini' file is displaying successful PlatformIO project initialization """ - project = stm32pio.util.Stm32pio(fixture_path) - project.pio_init(PROJECT_BOARD) + project = stm32pio.lib.Stm32pio(fixture_path) + project.init(board=TEST_PROJECT_BOARD) + result = project.pio_init() + self.assertEqual(result, 0, msg="Non-zero return code") self.assertTrue(fixture_path.joinpath('platformio.ini').is_file(), msg="platformio.ini is not there") - def test_patch_platformio_ini(self): + def test_patch(self): """ Compare contents of the patched string and the desired patch """ - project = stm32pio.util.Stm32pio(fixture_path) + project = stm32pio.lib.Stm32pio(fixture_path) test_content = "*** TEST PLATFORMIO.INI FILE ***" fixture_path.joinpath('platformio.ini').write_text(test_content) @@ -82,8 +84,9 @@ def test_build_should_raise(self): """ Build an empty project so PlatformIO should return non-zero code and we, in turn, should throw the exception """ - project = stm32pio.util.Stm32pio(fixture_path) - project.pio_init(PROJECT_BOARD) + project = stm32pio.lib.Stm32pio(fixture_path) + project.init(board=TEST_PROJECT_BOARD) + project.pio_init() with self.assertRaisesRegex(Exception, "PlatformIO build error", msg="Build error exception hadn't been raised"): @@ -93,7 +96,8 @@ def test_run_editor(self): """ Call the editors """ - project = stm32pio.util.Stm32pio(fixture_path) + project = stm32pio.lib.Stm32pio(fixture_path) + editors = { 'atom': { 'Windows': 'atom.exe', @@ -111,28 +115,50 @@ def test_run_editor(self): 'Linux': 'sublime' } } + for command, name in editors.items(): - with self.subTest(command=command, name=name[platform.system()]): - project.start_editor(command) - time.sleep(1) # wait a little bit for app to start - if platform.system() == 'Windows': - # "encoding='utf-8'" is for "a bytes-like object is required, not 'str'" in "assertIn" - result = subprocess.run(['wmic', 'process', 'get', 'description'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8') - else: - result = subprocess.run(['ps', '-A'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, - encoding='utf-8') - # Or, for Python 3.7 and above: - # result = subprocess.run(['ps', '-A'], capture_output=True, encoding='utf-8') - self.assertIn(name[platform.system()], result.stdout) - - def test_file_not_found(self): + # TODO: add Windows + editor_exists = True if subprocess.run(['command', '-v', command]).returncode == 0 else False + if editor_exists: + with self.subTest(command=command, name=name[platform.system()]): + project.start_editor(command) + time.sleep(1) # wait a little bit for app to start + if platform.system() == 'Windows': + # "encoding='utf-8'" is for "a bytes-like object is required, not 'str'" in "assertIn" + result = subprocess.run(['wmic', 'process', 'get', 'description'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8') + else: + result = subprocess.run(['ps', '-A'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, + encoding='utf-8') + # Or, for Python 3.7 and above: + # result = subprocess.run(['ps', '-A'], capture_output=True, encoding='utf-8') + self.assertIn(name[platform.system()], result.stdout) + + def test_init_path_not_found_should_raise(self): """ Pass non-existing path and expect the error """ - not_existing_path = fixture_path.joinpath('does_not_exist') - with self.assertRaises(FileNotFoundError, msg="FileNotFoundError was not raised"): - stm32pio.util.Stm32pio(not_existing_path) + path_does_not_exist = 'does_not_exist' + + not_existing_path = fixture_path.joinpath(path_does_not_exist) + with self.assertRaises(FileNotFoundError, msg="FileNotFoundError was not raised") as cm: + stm32pio.lib.Stm32pio(not_existing_path) + self.assertIn(path_does_not_exist, str(cm.exception), msg="Exception doesn't contain a description") + + def test_save_config(self): + project = stm32pio.lib.Stm32pio(fixture_path) + project.init(board=TEST_PROJECT_BOARD) + project.save_config() + + self.assertTrue(fixture_path.joinpath('stm32pio.ini').is_file(), msg="'stm32pio.ini' file hasn't been created") + + config = configparser.ConfigParser() + config.read(str(fixture_path.joinpath('stm32pio.ini'))) + for section, parameters in stm32pio.settings.config_default.items(): + for option, value in parameters.items(): + with self.subTest(section=section, option=option, msg="Section/key is not found in saved config file"): + self.assertNotEqual(config.get(section, option, fallback="Not found"), "Not found") + self.assertEqual(config.get('project', 'board', fallback="Not found"), TEST_PROJECT_BOARD) class TestIntegration(unittest.TestCase): @@ -151,9 +177,10 @@ def test_build(self): """ Initialize a new project and try to build it """ - project = stm32pio.util.Stm32pio(fixture_path) + project = stm32pio.lib.Stm32pio(fixture_path) + project.init(board=TEST_PROJECT_BOARD) project.generate_code() - project.pio_init(PROJECT_BOARD) + project.pio_init() project.patch() result = project.pio_build() @@ -166,11 +193,12 @@ def test_regenerate_code(self): hardware features and some new files) """ - project = stm32pio.util.Stm32pio(fixture_path) + project = stm32pio.lib.Stm32pio(fixture_path) # Generate a new project ... + project.init(board=TEST_PROJECT_BOARD) project.generate_code() - project.pio_init(PROJECT_BOARD) + project.pio_init() project.patch() # ... change it: @@ -210,6 +238,20 @@ def setUp(self) -> None: def tearDown(self) -> None: shutil.rmtree(fixture_path, ignore_errors=True) + def test_init(self): + return_code = stm32pio.app.main(sys_argv=['init', '-d', str(fixture_path), '-b', TEST_PROJECT_BOARD]) + self.assertEqual(return_code, 0, msg="Non-zero return code") + + self.assertTrue(fixture_path.joinpath('stm32pio.ini').is_file(), msg="'stm32pio.ini' file hasn't been created") + + config = configparser.ConfigParser() + config.read(str(fixture_path.joinpath('stm32pio.ini'))) + for section, parameters in stm32pio.settings.config_default.items(): + for option, value in parameters.items(): + with self.subTest(section=section, option=option, msg="Section/key is not found in saved config file"): + self.assertNotEqual(config.get(section, option, fallback="Not found"), "Not found") + self.assertEqual(config.get('project', 'board', fallback="Not found"), TEST_PROJECT_BOARD) + def test_clean(self): """ Dangerous test actually... @@ -236,7 +278,7 @@ def test_new(self): """ Successful build is the best indicator that all went right so we use '--with-build' option """ - return_code = stm32pio.app.main(sys_argv=['new', '-d', str(fixture_path), '-b', PROJECT_BOARD, '--with-build']) + return_code = stm32pio.app.main(sys_argv=['new', '-d', str(fixture_path), '-b', TEST_PROJECT_BOARD, '--with-build']) self.assertEqual(return_code, 0, msg="Non-zero return code") # .ioc file should be preserved @@ -262,8 +304,11 @@ def test_generate(self): def test_incorrect_path(self): """ """ - return_code = stm32pio.app.main(sys_argv=['generate', '-d', '~/path/does/not/exist']) - self.assertNotEqual(return_code, 0, msg='Return code should be non-zero') + with self.assertLogs(level='ERROR') as logs: + return_code = stm32pio.app.main(sys_argv=['init', '-d', '~/path/does/not/exist']) + self.assertNotEqual(return_code, 0, msg='Return code should be non-zero') + self.assertTrue(next(True for item in logs.output if 'path/does/not/exist' in item), + msg="ERROR logging message hasn't been printed") def test_no_ioc_file(self): """ @@ -272,22 +317,43 @@ def test_no_ioc_file(self): dir_with_no_ioc_file = fixture_path.joinpath('dir.with.no.ioc.file') dir_with_no_ioc_file.mkdir(exist_ok=False) - return_code = stm32pio.app.main(sys_argv=['generate', '-d', str(dir_with_no_ioc_file)]) - self.assertNotEqual(return_code, 0, msg='Return code should be non-zero') + with self.assertLogs(level='ERROR') as logs: + return_code = stm32pio.app.main(sys_argv=['init', '-d', str(dir_with_no_ioc_file)]) + self.assertNotEqual(return_code, 0, msg='Return code should be non-zero') + self.assertTrue(next(True for item in logs.output if "CubeMX project .ioc file" in item), + msg="ERROR logging message hasn't been printed") def test_verbose(self): """ - Run as subprocess + Run as subprocess (can be done as assertLogs(), see above) """ stm32pio_exec = inspect.getfile(stm32pio.app) # get the path to the main stm32pio script # get the current python executable (no need to guess whether it's python or python3 and so on) python_exec = sys.executable - result = subprocess.run([python_exec, stm32pio_exec, '-v', 'clean', '-d', fixture_path], encoding='utf-8', + + result = subprocess.run([python_exec, stm32pio_exec, '-v', 'generate', '-d', fixture_path], encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE) + self.assertEqual(result.returncode, 0, msg="Non-zero return code") # Somehow stderr and not stdout contains the actual output - self.assertIn('DEBUG', result.stderr.split(), msg="Verbose logging output has not been enabled") + self.assertIn('DEBUG', result.stderr, msg="Verbose logging output hasn't been enabled on stderr") + self.assertIn('Starting STM32CubeMX', result.stdout, msg="STM32CubeMX didn't print its logs") + + def test_non_verbose(self): + """ + """ + + stm32pio_exec = inspect.getfile(stm32pio.app) + python_exec = sys.executable + + result = subprocess.run([python_exec, stm32pio_exec, 'generate', '-d', fixture_path], encoding='utf-8', + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + self.assertEqual(result.returncode, 0, msg="Non-zero return code") + self.assertNotIn('DEBUG', result.stderr, msg="Verbose logging output has been enabled on stderr") + self.assertNotIn('DEBUG', result.stdout, msg="Verbose logging output has been enabled on stdout") + self.assertNotIn('Starting STM32CubeMX', result.stdout, msg="STM32CubeMX printed its logs") def tearDownModule(): From 2fa207622a51fccd0560932f797f23db5629cf82 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sun, 24 Nov 2019 02:13:03 +0300 Subject: [PATCH 16/26] v0.9-alpha: * new tests, refactored old * steps for each subcommand are now more clean * 'init' functionality is back on '__init__' * finalizer is back too --- README.md | 2 +- TODO.md | 1 + stm32pio/app.py | 41 ++++++----- stm32pio/lib.py | 57 ++++++++++----- stm32pio/tests/test.py | 156 +++++++++++++++++++++++------------------ 5 files changed, 154 insertions(+), 103 deletions(-) diff --git a/README.md b/README.md index f9f1071..e196aa7 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ to see help. ## Testing Since ver. 0.45 there are some unit-tests in file `stm32pio/tests/test.py` (based on the unittest module). Run ```shell script -stm32pio-repo/ $ python3 -m unittest discover -v +stm32pio-repo/ $ python3 -m unittest -b -v ``` or ```shell script diff --git a/TODO.md b/TODO.md index 68b910e..945747b 100644 --- a/TODO.md +++ b/TODO.md @@ -24,3 +24,4 @@ - [x] New argparse algo cause now we have config file - [ ] Update `.ioc` file - [ ] `str(path)` -> `path` were possible + - [ ] Check `start_editor()` for different input diff --git a/stm32pio/app.py b/stm32pio/app.py index 2babdfc..86bbb55 100755 --- a/stm32pio/app.py +++ b/stm32pio/app.py @@ -34,10 +34,10 @@ def parse_args(args: list) -> Optional[argparse.Namespace]: "proceeding") # Common subparsers options - for p in [parser_new, parser_generate, parser_clean, parser_init]: + for p in [parser_init, parser_new, parser_generate, parser_clean]: p.add_argument('-d', '--directory', dest='project_path', default=pathlib.Path.cwd(), help="path to the project (current directory, if not given)") - for p in [parser_new, parser_init]: + for p in [parser_init, parser_new]: p.add_argument('-b', '--board', dest='board', help="PlatformIO name of the board", required=False) for p in [parser_new, parser_generate]: p.add_argument('--start-editor', dest='editor', help="use specified editor to open PlatformIO project (e.g. " @@ -77,35 +77,40 @@ def main(sys_argv: list = sys.argv[1:]) -> int: import stm32pio.lib # import the module after sys.path modification try: - project = stm32pio.lib.Stm32pio(args.project_path) - - if args.subcommand == 'init' or args.subcommand == 'new' or args.subcommand == 'generate': - project.init(board=args.board if 'board' in args else None) - if (args.subcommand == 'init' or args.subcommand == 'new') and project.config.get('project', 'board') == '': + if args.subcommand == 'init': + stm32pio.lib.Stm32pio(args.project_path, parameters={'board': args.board if 'board' in args else None}) + if 'board' not in args: logger.warning("STM32 board is not specified, it will be needed on PlatformIO project creation") - if args.subcommand == 'init': - logger.info('stm32pio project has been initialized. You can now edit parameters in stm32pio.ini file') - project.save_config() - if args.subcommand == 'new' or args.subcommand == 'generate': + elif args.subcommand == 'new': + if 'board' in args: + project = stm32pio.lib.Stm32pio(args.project_path, parameters={'board': args.board}) + else: + raise Exception("STM32 board is not specified, it is needed for PlatformIO project creation") project.generate_code() - if args.subcommand == 'new': - project.pio_init() - project.patch() - project.save_config() + project.pio_init() + project.patch() + if args.with_build: + project.pio_build() + if args.editor: + project.start_editor(args.editor) + elif args.subcommand == 'generate': + project = stm32pio.lib.Stm32pio(args.project_path, save_on_destruction=False) + project.generate_code() if args.with_build: project.pio_build() if args.editor: project.start_editor(args.editor) - if args.subcommand == 'clean': + elif args.subcommand == 'clean': + project = stm32pio.lib.Stm32pio(args.project_path, save_on_destruction=False) project.clean() # library is designed to throw the exception in bad cases so we catch here globally except Exception as e: - logger.error(repr(e)) - if logger.level <= logging.DEBUG: # verbose + logger.error(e) + if logger.getEffectiveLevel() <= logging.DEBUG: # verbose traceback.print_exception(*sys.exc_info()) return -1 diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 648ce66..aa1d2a8 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -5,8 +5,10 @@ import enum import configparser import string +import sys import tempfile -# import weakref +import traceback +import weakref import stm32pio.settings @@ -25,44 +27,65 @@ class ProjectState(enum.IntEnum): BUILT = enum.auto() -# NUM_OF_STATES = len(list(ProjectState)) - - class Stm32pio: """ Main class """ - def __init__(self, dirty_path: str): + def __init__(self, dirty_path: str, parameters=None, save_on_destruction=True): + if parameters is None: + parameters = {} + self.project_path = self._resolve_project_path(dirty_path) self.config = self._load_settings_file() ioc_file = self._find_ioc_file() self.config.set('project', 'ioc_file', str(ioc_file)) - # self._finalizer = weakref.finalize(self, self.save_config) - - - def init(self, **kwargs): cubemx_script_template = string.Template(self.config.get('project', 'cubemx_script_content')) cubemx_script_content = cubemx_script_template.substitute(project_path=self.project_path, cubemx_ioc_full_filename=self.config.get('project', 'ioc_file')) self.config.set('project', 'cubemx_script_content', cubemx_script_content) board = '' - if 'board' in kwargs and kwargs['board'] is not None: + if 'board' in parameters and parameters['board'] is not None: try: - board = self._resolve_board(kwargs['board']) + board = self._resolve_board(parameters['board']) except Exception as e: logger.warning(e) self.config.set('project', 'board', board) elif self.config.get('project', 'board', fallback=None) is None: self.config.set('project', 'board', board) + if save_on_destruction: + self._finalizer = weakref.finalize(self, self.save_config) + + + # def init(self, **kwargs): + # cubemx_script_template = string.Template(self.config.get('project', 'cubemx_script_content')) + # cubemx_script_content = cubemx_script_template.substitute(project_path=self.project_path, + # cubemx_ioc_full_filename=self.config.get('project', 'ioc_file')) + # self.config.set('project', 'cubemx_script_content', cubemx_script_content) + # + # board = '' + # if 'board' in kwargs and kwargs['board'] is not None: + # try: + # board = self._resolve_board(kwargs['board']) + # except Exception as e: + # logger.warning(e) + # self.config.set('project', 'board', board) + # elif self.config.get('project', 'board', fallback=None) is None: + # self.config.set('project', 'board', board) + def save_config(self): - with self.project_path.joinpath('stm32pio.ini').open(mode='w') as config_file: - self.config.write(config_file) + try: + with self.project_path.joinpath('stm32pio.ini').open(mode='w') as config_file: + self.config.write(config_file) + except Exception as e: + logger.warning(f"Cannot save config: {e}") + if logger.getEffectiveLevel() <= logging.DEBUG: + traceback.print_exception(*sys.exc_info()) def get_state(self) -> ProjectState: @@ -128,7 +151,7 @@ def _find_ioc_file(self) -> pathlib.Path: logger.debug("Searching for any .ioc file...") candidates = list(self.project_path.glob('*.ioc')) if len(candidates) == 0: - raise FileNotFoundError("CubeMX project .ioc file") + raise FileNotFoundError("Not found: CubeMX project .ioc file") elif len(candidates) == 1: logger.debug(f"{candidates[0].name} is selected") return candidates[0] @@ -168,7 +191,7 @@ def _resolve_project_path(dirty_path: str) -> pathlib.Path: """ resolved_path = pathlib.Path(dirty_path).expanduser().resolve() if not resolved_path.exists(): - raise FileNotFoundError(resolved_path) + raise FileNotFoundError(f"Not found: {resolved_path}") else: return resolved_path @@ -259,8 +282,8 @@ def patch(self) -> None: platformio_ini_file = self.project_path.joinpath('platformio.ini') if platformio_ini_file.is_file(): with platformio_ini_file.open(mode='a') as f: - f.write(self.config.get('project', 'platformio_ini_patch_content') + '\n') - logger.info("'platformio.ini' patched") + f.write(self.config.get('project', 'platformio_ini_patch_content')) + logger.info("'platformio.ini' has been patched") else: logger.warning("'platformio.ini' file not found") diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index 9fc399f..25af8ae 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -1,3 +1,4 @@ +import unittest import configparser import pathlib import platform @@ -7,7 +8,6 @@ import time import inspect import sys -import unittest import stm32pio.app import stm32pio.settings @@ -22,7 +22,10 @@ temp_dir = tempfile.TemporaryDirectory() -fixture_path = pathlib.Path(temp_dir.name).joinpath(TEST_PROJECT_PATH.name) +FIXTURE_PATH = pathlib.Path(temp_dir.name).joinpath(TEST_PROJECT_PATH.name) + +STM32PIO_MAIN_SCRIPT = inspect.getfile(stm32pio.app) +PYTHON_EXEC = sys.executable class TestUnit(unittest.TestCase): @@ -32,47 +35,47 @@ class TestUnit(unittest.TestCase): def setUp(self) -> None: self.tearDown() - shutil.copytree(TEST_PROJECT_PATH, fixture_path) + shutil.copytree(TEST_PROJECT_PATH, FIXTURE_PATH) def tearDown(self) -> None: - shutil.rmtree(fixture_path, ignore_errors=True) + shutil.rmtree(FIXTURE_PATH, ignore_errors=True) def test_generate_code(self): """ Check whether files and folders have been created """ - project = stm32pio.lib.Stm32pio(fixture_path) + project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}) project.generate_code() # Assuming that the presence of these files indicates a success files_should_be_present = ['Src/main.c', 'Inc/main.h'] for file in files_should_be_present: with self.subTest(file_should_be_present=file, msg=f"{file} hasn't been created"): - self.assertEqual(fixture_path.joinpath(file).is_file(), True) + self.assertEqual(FIXTURE_PATH.joinpath(file).is_file(), True) def test_pio_init(self): """ Consider that existence of 'platformio.ini' file is displaying successful PlatformIO project initialization """ - project = stm32pio.lib.Stm32pio(fixture_path) - project.init(board=TEST_PROJECT_BOARD) + project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}) result = project.pio_init() self.assertEqual(result, 0, msg="Non-zero return code") - self.assertTrue(fixture_path.joinpath('platformio.ini').is_file(), msg="platformio.ini is not there") + self.assertTrue(FIXTURE_PATH.joinpath('platformio.ini').is_file(), msg="platformio.ini is not there") def test_patch(self): """ Compare contents of the patched string and the desired patch """ - project = stm32pio.lib.Stm32pio(fixture_path) + project = stm32pio.lib.Stm32pio(FIXTURE_PATH) test_content = "*** TEST PLATFORMIO.INI FILE ***" - fixture_path.joinpath('platformio.ini').write_text(test_content) + FIXTURE_PATH.joinpath('platformio.ini').write_text(test_content) project.patch() - # TODO: check 'include' deletion - after_patch_content = fixture_path.joinpath('platformio.ini').read_text() + self.assertFalse(FIXTURE_PATH.joinpath('include').is_dir(), msg="'include' has not been deleted") + + after_patch_content = FIXTURE_PATH.joinpath('platformio.ini').read_text() self.assertEqual(after_patch_content[:len(test_content)], test_content, msg="Initial content of platformio.ini is corrupted") @@ -84,8 +87,7 @@ def test_build_should_raise(self): """ Build an empty project so PlatformIO should return non-zero code and we, in turn, should throw the exception """ - project = stm32pio.lib.Stm32pio(fixture_path) - project.init(board=TEST_PROJECT_BOARD) + project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}) project.pio_init() with self.assertRaisesRegex(Exception, "PlatformIO build error", @@ -96,7 +98,7 @@ def test_run_editor(self): """ Call the editors """ - project = stm32pio.lib.Stm32pio(fixture_path) + project = stm32pio.lib.Stm32pio(FIXTURE_PATH) editors = { 'atom': { @@ -117,8 +119,13 @@ def test_run_editor(self): } for command, name in editors.items(): - # TODO: add Windows - editor_exists = True if subprocess.run(['command', '-v', command]).returncode == 0 else False + if platform.system() == 'Windows': + command_str = f"where {command} /q" + else: + command_str = f"command -v {command}" + editor_exists = True if subprocess.run(command_str, shell=True, stdout=subprocess.PIPE, + stderr=subprocess.PIPE).returncode == 0\ + else False if editor_exists: with self.subTest(command=command, name=name[platform.system()]): project.start_editor(command) @@ -140,25 +147,25 @@ def test_init_path_not_found_should_raise(self): """ path_does_not_exist = 'does_not_exist' - not_existing_path = fixture_path.joinpath(path_does_not_exist) + not_existing_path = FIXTURE_PATH.joinpath(path_does_not_exist) with self.assertRaises(FileNotFoundError, msg="FileNotFoundError was not raised") as cm: stm32pio.lib.Stm32pio(not_existing_path) self.assertIn(path_does_not_exist, str(cm.exception), msg="Exception doesn't contain a description") def test_save_config(self): - project = stm32pio.lib.Stm32pio(fixture_path) - project.init(board=TEST_PROJECT_BOARD) + project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}) project.save_config() - self.assertTrue(fixture_path.joinpath('stm32pio.ini').is_file(), msg="'stm32pio.ini' file hasn't been created") + self.assertTrue(FIXTURE_PATH.joinpath('stm32pio.ini').is_file(), msg="'stm32pio.ini' file hasn't been created") config = configparser.ConfigParser() - config.read(str(fixture_path.joinpath('stm32pio.ini'))) + config.read(str(FIXTURE_PATH.joinpath('stm32pio.ini'))) for section, parameters in stm32pio.settings.config_default.items(): for option, value in parameters.items(): with self.subTest(section=section, option=option, msg="Section/key is not found in saved config file"): self.assertNotEqual(config.get(section, option, fallback="Not found"), "Not found") - self.assertEqual(config.get('project', 'board', fallback="Not found"), TEST_PROJECT_BOARD) + self.assertEqual(config.get('project', 'board', fallback="Not found"), TEST_PROJECT_BOARD, + msg="'board' has not been set") class TestIntegration(unittest.TestCase): @@ -168,17 +175,35 @@ class TestIntegration(unittest.TestCase): def setUp(self) -> None: self.tearDown() - shutil.copytree(TEST_PROJECT_PATH, fixture_path) + shutil.copytree(TEST_PROJECT_PATH, FIXTURE_PATH) def tearDown(self) -> None: - shutil.rmtree(fixture_path, ignore_errors=True) + shutil.rmtree(FIXTURE_PATH, ignore_errors=True) + + def test_config_prioritites(self): + custom_content = "SOME CUSTOM CONTENT" + + config = configparser.ConfigParser() + config.read_dict({ + 'project': { + 'platformio_ini_patch_content': custom_content + } + }) + with FIXTURE_PATH.joinpath('stm32pio.ini').open(mode='w') as config_file: + config.write(config_file) + + project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}) + project.pio_init() + project.patch() + + after_patch_content = FIXTURE_PATH.joinpath('platformio.ini').read_text() + self.assertIn(custom_content, after_patch_content, msg="Patch content is not from user config") def test_build(self): """ Initialize a new project and try to build it """ - project = stm32pio.lib.Stm32pio(fixture_path) - project.init(board=TEST_PROJECT_BOARD) + project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}) project.generate_code() project.pio_init() project.patch() @@ -193,18 +218,17 @@ def test_regenerate_code(self): hardware features and some new files) """ - project = stm32pio.lib.Stm32pio(fixture_path) + project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}) # Generate a new project ... - project.init(board=TEST_PROJECT_BOARD) project.generate_code() project.pio_init() project.patch() # ... change it: - test_file_1 = fixture_path.joinpath('Src', 'main.c') + test_file_1 = FIXTURE_PATH.joinpath('Src', 'main.c') test_content_1 = "*** TEST STRING 1 ***\n" - test_file_2 = fixture_path.joinpath('Inc', 'my_header.h') + test_file_2 = FIXTURE_PATH.joinpath('Inc', 'my_header.h') test_content_2 = "*** TEST STRING 2 ***\n" # - add some sample string inside CubeMX' /* BEGIN - END */ block main_c_content = test_file_1.read_text() @@ -233,24 +257,10 @@ class TestCLI(unittest.TestCase): def setUp(self) -> None: self.tearDown() - shutil.copytree(TEST_PROJECT_PATH, fixture_path) + shutil.copytree(TEST_PROJECT_PATH, FIXTURE_PATH) def tearDown(self) -> None: - shutil.rmtree(fixture_path, ignore_errors=True) - - def test_init(self): - return_code = stm32pio.app.main(sys_argv=['init', '-d', str(fixture_path), '-b', TEST_PROJECT_BOARD]) - self.assertEqual(return_code, 0, msg="Non-zero return code") - - self.assertTrue(fixture_path.joinpath('stm32pio.ini').is_file(), msg="'stm32pio.ini' file hasn't been created") - - config = configparser.ConfigParser() - config.read(str(fixture_path.joinpath('stm32pio.ini'))) - for section, parameters in stm32pio.settings.config_default.items(): - for option, value in parameters.items(): - with self.subTest(section=section, option=option, msg="Section/key is not found in saved config file"): - self.assertNotEqual(config.get(section, option, fallback="Not found"), "Not found") - self.assertEqual(config.get('project', 'board', fallback="Not found"), TEST_PROJECT_BOARD) + shutil.rmtree(FIXTURE_PATH, ignore_errors=True) def test_clean(self): """ @@ -258,13 +268,13 @@ def test_clean(self): """ # Create files and folders - file_should_be_deleted = fixture_path.joinpath('file.should.be.deleted') - dir_should_be_deleted = fixture_path.joinpath('dir.should.be.deleted') + file_should_be_deleted = FIXTURE_PATH.joinpath('file.should.be.deleted') + dir_should_be_deleted = FIXTURE_PATH.joinpath('dir.should.be.deleted') file_should_be_deleted.touch(exist_ok=False) dir_should_be_deleted.mkdir(exist_ok=False) # Clean - return_code = stm32pio.app.main(sys_argv=['clean', '-d', str(fixture_path)]) + return_code = stm32pio.app.main(sys_argv=['clean', '-d', str(FIXTURE_PATH)]) self.assertEqual(return_code, 0, msg="Non-zero return code") # Look for remaining items @@ -272,34 +282,35 @@ def test_clean(self): self.assertFalse(dir_should_be_deleted.is_dir(), msg=f"{dir_should_be_deleted} is still there") # And .ioc file should be preserved - self.assertTrue(fixture_path.joinpath(f"{fixture_path.name}.ioc").is_file(), msg="Missing .ioc file") + self.assertTrue(FIXTURE_PATH.joinpath(f"{FIXTURE_PATH.name}.ioc").is_file(), msg="Missing .ioc file") def test_new(self): """ Successful build is the best indicator that all went right so we use '--with-build' option """ - return_code = stm32pio.app.main(sys_argv=['new', '-d', str(fixture_path), '-b', TEST_PROJECT_BOARD, '--with-build']) - self.assertEqual(return_code, 0, msg="Non-zero return code") + return_code = stm32pio.app.main(sys_argv=['new', '-d', str(FIXTURE_PATH), '-b', TEST_PROJECT_BOARD, + '--with-build']) + self.assertEqual(return_code, 0, msg="Non-zero return code") # .ioc file should be preserved - self.assertTrue(fixture_path.joinpath(f"{fixture_path.name}.ioc").is_file(), msg="Missing .ioc file") + self.assertTrue(FIXTURE_PATH.joinpath(f"{FIXTURE_PATH.name}.ioc").is_file(), msg="Missing .ioc file") def test_generate(self): """ """ - return_code = stm32pio.app.main(sys_argv=['generate', '-d', str(fixture_path)]) + return_code = stm32pio.app.main(sys_argv=['generate', '-d', str(FIXTURE_PATH)]) self.assertEqual(return_code, 0, msg="Non-zero return code") inc_dir = 'Inc' src_dir = 'Src' - self.assertTrue(fixture_path.joinpath(inc_dir).is_dir(), msg=f"Missing '{inc_dir}'") - self.assertTrue(fixture_path.joinpath(src_dir).is_dir(), msg=f"Missing '{src_dir}'") - self.assertFalse(len(list(fixture_path.joinpath(inc_dir).iterdir())) == 0, msg=f"'{inc_dir}' is empty") - self.assertFalse(len(list(fixture_path.joinpath(src_dir).iterdir())) == 0, msg=f"'{src_dir}' is empty") + self.assertTrue(FIXTURE_PATH.joinpath(inc_dir).is_dir(), msg=f"Missing '{inc_dir}'") + self.assertTrue(FIXTURE_PATH.joinpath(src_dir).is_dir(), msg=f"Missing '{src_dir}'") + self.assertFalse(len(list(FIXTURE_PATH.joinpath(inc_dir).iterdir())) == 0, msg=f"'{inc_dir}' is empty") + self.assertFalse(len(list(FIXTURE_PATH.joinpath(src_dir).iterdir())) == 0, msg=f"'{src_dir}' is empty") # .ioc file should be preserved - self.assertTrue(fixture_path.joinpath(f"{fixture_path.name}.ioc").is_file(), msg="Missing .ioc file") + self.assertTrue(FIXTURE_PATH.joinpath(f"{FIXTURE_PATH.name}.ioc").is_file(), msg="Missing .ioc file") def test_incorrect_path(self): """ @@ -314,7 +325,7 @@ def test_no_ioc_file(self): """ """ - dir_with_no_ioc_file = fixture_path.joinpath('dir.with.no.ioc.file') + dir_with_no_ioc_file = FIXTURE_PATH.joinpath('dir.with.no.ioc.file') dir_with_no_ioc_file.mkdir(exist_ok=False) with self.assertLogs(level='ERROR') as logs: @@ -332,7 +343,7 @@ def test_verbose(self): # get the current python executable (no need to guess whether it's python or python3 and so on) python_exec = sys.executable - result = subprocess.run([python_exec, stm32pio_exec, '-v', 'generate', '-d', fixture_path], encoding='utf-8', + result = subprocess.run([python_exec, stm32pio_exec, '-v', 'generate', '-d', FIXTURE_PATH], encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE) self.assertEqual(result.returncode, 0, msg="Non-zero return code") @@ -344,10 +355,7 @@ def test_non_verbose(self): """ """ - stm32pio_exec = inspect.getfile(stm32pio.app) - python_exec = sys.executable - - result = subprocess.run([python_exec, stm32pio_exec, 'generate', '-d', fixture_path], encoding='utf-8', + result = subprocess.run([PYTHON_EXEC, STM32PIO_MAIN_SCRIPT, 'generate', '-d', FIXTURE_PATH], encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE) self.assertEqual(result.returncode, 0, msg="Non-zero return code") @@ -355,6 +363,20 @@ def test_non_verbose(self): self.assertNotIn('DEBUG', result.stdout, msg="Verbose logging output has been enabled on stdout") self.assertNotIn('Starting STM32CubeMX', result.stdout, msg="STM32CubeMX printed its logs") + def test_init(self): + subprocess.run([PYTHON_EXEC, STM32PIO_MAIN_SCRIPT, 'init', '-d', FIXTURE_PATH, '-b', TEST_PROJECT_BOARD]) + + self.assertTrue(FIXTURE_PATH.joinpath('stm32pio.ini').is_file(), msg="'stm32pio.ini' file hasn't been created") + + config = configparser.ConfigParser() + config.read(str(FIXTURE_PATH.joinpath('stm32pio.ini'))) + for section, parameters in stm32pio.settings.config_default.items(): + for option, value in parameters.items(): + with self.subTest(section=section, option=option, msg="Section/key is not found in saved config file"): + self.assertIsNotNone(config.get(section, option, fallback=None)) + self.assertEqual(config.get('project', 'board', fallback="Not found"), TEST_PROJECT_BOARD, + msg="'board' has not been set") + def tearDownModule(): """ From b0f70dc03f8b76b8ea04a1dbe4c5b85aa0ec01d5 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sun, 24 Nov 2019 16:17:13 +0300 Subject: [PATCH 17/26] v0.9-alpha: * add '--start-editor' option for 'init' too * refactored 'get_state()' --- stm32pio/app.py | 28 +++++++++++--------- stm32pio/lib.py | 69 +++++++++++++++++++++---------------------------- 2 files changed, 45 insertions(+), 52 deletions(-) diff --git a/stm32pio/app.py b/stm32pio/app.py index 86bbb55..44c118f 100755 --- a/stm32pio/app.py +++ b/stm32pio/app.py @@ -23,8 +23,8 @@ def parse_args(args: list) -> Optional[argparse.Namespace]: parser.add_argument('--version', action='version', version=f"stm32pio v{__version__}") parser.add_argument('-v', '--verbose', help="enable verbose output (default: INFO)", action='count', required=False) - subparsers = parser.add_subparsers(dest='subcommand', title='subcommands', - description="valid subcommands", help="modes of operation") + subparsers = parser.add_subparsers(dest='subcommand', title='subcommands', description="valid subcommands", + help="modes of operation") parser_new = subparsers.add_parser('new', help="generate CubeMX code, create PlatformIO project") parser_generate = subparsers.add_parser('generate', help="generate CubeMX code") @@ -35,14 +35,15 @@ def parse_args(args: list) -> Optional[argparse.Namespace]: # Common subparsers options for p in [parser_init, parser_new, parser_generate, parser_clean]: - p.add_argument('-d', '--directory', dest='project_path', default=pathlib.Path.cwd(), + p.add_argument('-d', '--directory', dest='project_path', default=pathlib.Path.cwd(), required=True, help="path to the project (current directory, if not given)") for p in [parser_init, parser_new]: - p.add_argument('-b', '--board', dest='board', help="PlatformIO name of the board", required=False) + p.add_argument('-b', '--board', dest='board', required=False, help="PlatformIO name of the board") + for p in [parser_init, parser_new, parser_generate]: + p.add_argument('--start-editor', dest='editor', required=False, + help="use specified editor to open PlatformIO project (e.g. subl, code, atom, etc.)") for p in [parser_new, parser_generate]: - p.add_argument('--start-editor', dest='editor', help="use specified editor to open PlatformIO project (e.g. " - "subl, code, atom, etc.)", required=False) - p.add_argument('--with-build', action='store_true', help="build a project after generation", required=False) + p.add_argument('--with-build', action='store_true', required=False, help="build a project after generation") # Show help and exit if no arguments were given if len(args) == 0: @@ -78,15 +79,18 @@ def main(sys_argv: list = sys.argv[1:]) -> int: try: if args.subcommand == 'init': - stm32pio.lib.Stm32pio(args.project_path, parameters={'board': args.board if 'board' in args else None}) - if 'board' not in args: - logger.warning("STM32 board is not specified, it will be needed on PlatformIO project creation") + project = stm32pio.lib.Stm32pio(args.project_path, parameters={'board': args.board}) + if not args.board: + logger.warning("STM32 PlatformIO board is not specified, it will be needed on PlatformIO project " + "creation") + if args.editor: + project.start_editor(args.editor) elif args.subcommand == 'new': - if 'board' in args: + if 'board' in args and args.board is not None: project = stm32pio.lib.Stm32pio(args.project_path, parameters={'board': args.board}) else: - raise Exception("STM32 board is not specified, it is needed for PlatformIO project creation") + raise Exception("STM32 PlatformIO board is not specified, it is needed for PlatformIO project creation") project.generate_code() project.pio_init() project.patch() diff --git a/stm32pio/lib.py b/stm32pio/lib.py index aa1d2a8..f15e628 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -1,3 +1,4 @@ +import collections import logging import pathlib import shutil @@ -61,23 +62,6 @@ def __init__(self, dirty_path: str, parameters=None, save_on_destruction=True): self._finalizer = weakref.finalize(self, self.save_config) - # def init(self, **kwargs): - # cubemx_script_template = string.Template(self.config.get('project', 'cubemx_script_content')) - # cubemx_script_content = cubemx_script_template.substitute(project_path=self.project_path, - # cubemx_ioc_full_filename=self.config.get('project', 'ioc_file')) - # self.config.set('project', 'cubemx_script_content', cubemx_script_content) - # - # board = '' - # if 'board' in kwargs and kwargs['board'] is not None: - # try: - # board = self._resolve_board(kwargs['board']) - # except Exception as e: - # logger.warning(e) - # self.config.set('project', 'board', board) - # elif self.config.get('project', 'board', fallback=None) is None: - # self.config.set('project', 'board', board) - - def save_config(self): try: with self.project_path.joinpath('stm32pio.ini').open(mode='w') as config_file: @@ -101,30 +85,35 @@ def get_state(self) -> ProjectState: logger.debug("Calculating project state...") logger.debug(f"Project content: {[item.name for item in self.project_path.iterdir()]}") - states_conditions = { - ProjectState.UNDEFINED: [True], - ProjectState.INITIALIZED: [self.project_path.joinpath('stm32pio.ini').is_file()], - ProjectState.GENERATED: [self.project_path.joinpath('Inc').is_dir() and - len(list(self.project_path.joinpath('Inc').iterdir())) > 0, - self.project_path.joinpath('Src').is_dir() and - len(list(self.project_path.joinpath('Src').iterdir())) > 0], - ProjectState.PIO_INITIALIZED: [self.project_path.joinpath('platformio.ini').is_file() and - len(self.project_path.joinpath('platformio.ini').read_text()) > 0], - ProjectState.PIO_INI_PATCHED: [self.project_path.joinpath('platformio.ini').is_file() and - self.config.get('project', 'platformio_ini_patch_content') in - self.project_path.joinpath('platformio.ini').read_text()], - ProjectState.BUILT: [self.project_path.joinpath('.pio').is_dir() and - any([item.is_file() for item in self.project_path.joinpath('.pio').rglob('*firmware*')])] - } - - # Use (1,0) instead of (True,False) because on debug printing it looks cleaner - conditions_results = [1 if all(conditions is True for conditions in states_conditions[state]) else 0 - for state in ProjectState] + states_conditions = collections.OrderedDict() + states_conditions[ProjectState.UNDEFINED] = [True] + states_conditions[ProjectState.INITIALIZED] = [self.project_path.joinpath('stm32pio.ini').is_file()] + states_conditions[ProjectState.GENERATED] = [self.project_path.joinpath('Inc').is_dir() and + len(list(self.project_path.joinpath('Inc').iterdir())) > 0, + self.project_path.joinpath('Src').is_dir() and + len(list(self.project_path.joinpath('Src').iterdir())) > 0] + states_conditions[ProjectState.PIO_INITIALIZED] = [ + self.project_path.joinpath('platformio.ini').is_file() and + len(self.project_path.joinpath('platformio.ini').read_text()) > 0] + states_conditions[ProjectState.PIO_INI_PATCHED] = [ + self.project_path.joinpath('platformio.ini').is_file() and + self.config.get('project', 'platformio_ini_patch_content') in + self.project_path.joinpath('platformio.ini').read_text(), + not self.project_path.joinpath('include').is_dir()] + states_conditions[ProjectState.BUILT] = [ + self.project_path.joinpath('.pio').is_dir() and + any([item.is_file() for item in self.project_path.joinpath('.pio').rglob('*firmware*')])] + + # Use (1,0) instead of (True,False) because on debug printing it looks better + conditions_results = [] + for state, conditions in states_conditions.items(): + conditions_results.append(1 if all(condition is True for condition in conditions) else 0) + # Put away unnecessary processing as the string still will be formed even if the logging level doesn't allow # propagation of this message if logger.getEffectiveLevel() <= logging.DEBUG: states_info_str = '\n'.join(f"{state.name:20}{conditions_results[state.value-1]}" for state in ProjectState) - logger.debug(f"Determined states: {states_info_str}") + logger.debug(f"Determined states:\n{states_info_str}") last_true_index = 0 # UNDEFINED is always True for index, value in enumerate(conditions_results): @@ -207,7 +196,7 @@ def _resolve_board(self, board: str) -> str: # result = subprocess.run(['platformio', 'boards'], encoding='utf-8', capture_output=True) if result.returncode == 0: if board not in result.stdout.split(): - raise Exception("wrong STM32 board. Run 'platformio boards' for possible names") + raise Exception("wrong PlatformIO STM32 board. Run 'platformio boards' for possible names") else: return board else: @@ -250,7 +239,7 @@ def pio_init(self) -> int: logger.info("starting PlatformIO project initialization...") - command_arr = [self.config.get('app', 'platformio_cmd'), 'init', '-d', self.project_path, '-b', self.config.get('project', 'board'), + command_arr = [self.config.get('app', 'platformio_cmd'), 'init', '-d', str(self.project_path), '-b', self.config.get('project', 'board'), '-O', 'framework=stm32cube'] if logger.getEffectiveLevel() > logging.DEBUG: command_arr.append('--silent') @@ -300,7 +289,7 @@ def start_editor(self, editor_command: str) -> None: editor_command: editor command (as we start in the terminal) """ - logger.info("starting an editor...") + logger.info(f"starting an editor '{editor_command}'...") try: subprocess.run([editor_command, self.project_path], check=True) From 7b3f6d7d645fd49e7c5242b9018af97c3b8c39ef Mon Sep 17 00:00:00 2001 From: usserr Date: Sun, 24 Nov 2019 17:34:40 +0300 Subject: [PATCH 18/26] v0.9-beta: * fix parameters detection * comply with Windows on tests and in library itself --- stm32pio/app.py | 5 ++--- stm32pio/lib.py | 11 +++++++---- stm32pio/tests/test.py | 14 ++++++++------ 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/stm32pio/app.py b/stm32pio/app.py index 44c118f..8f65325 100755 --- a/stm32pio/app.py +++ b/stm32pio/app.py @@ -87,9 +87,8 @@ def main(sys_argv: list = sys.argv[1:]) -> int: project.start_editor(args.editor) elif args.subcommand == 'new': - if 'board' in args and args.board is not None: - project = stm32pio.lib.Stm32pio(args.project_path, parameters={'board': args.board}) - else: + project = stm32pio.lib.Stm32pio(args.project_path, parameters={'board': args.board}) + if project.config.get('project', 'board') == '': raise Exception("STM32 PlatformIO board is not specified, it is needed for PlatformIO project creation") project.generate_code() project.pio_init() diff --git a/stm32pio/lib.py b/stm32pio/lib.py index f15e628..3a71a7b 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -208,13 +208,15 @@ def generate_code(self) -> None: Call STM32CubeMX app as a 'java -jar' file with the automatically prearranged 'cubemx-script' file """ + cubemx_script_file, cubemx_script_name = tempfile.mkstemp() + # buffering=0 leads to the immediate flushing on writing - with tempfile.NamedTemporaryFile(buffering=0) as cubemx_script: + with open(cubemx_script_file, mode='w+b', buffering=0) as cubemx_script: cubemx_script.write(self.config.get('project', 'cubemx_script_content').encode()) logger.info("starting to generate a code from the CubeMX .ioc file...") command_arr = [self.config.get('app', 'java_cmd'), '-jar', self.config.get('app', 'cubemx_cmd'), '-q', - cubemx_script.name, '-s'] + cubemx_script_name, '-s'] if logger.getEffectiveLevel() <= logging.DEBUG: result = subprocess.run(command_arr) else: @@ -228,6 +230,7 @@ def generate_code(self) -> None: "Try to enable a verbose output or generate a code from the CubeMX itself.") raise Exception("code generation error") + pathlib.Path(cubemx_script_name).unlink() def pio_init(self) -> int: """ @@ -292,7 +295,7 @@ def start_editor(self, editor_command: str) -> None: logger.info(f"starting an editor '{editor_command}'...") try: - subprocess.run([editor_command, self.project_path], check=True) + subprocess.run([editor_command, str(self.project_path)], check=True) except subprocess.CalledProcessError as e: logger.error(f"Failed to start the editor {editor_command}: {e.stderr}") @@ -311,7 +314,7 @@ def pio_build(self) -> int: logger.error("no 'platformio.ini' file, build is impossible") return -1 - command_arr = [self.config.get('app', 'platformio_cmd'), 'run', '-d', self.project_path] + command_arr = [self.config.get('app', 'platformio_cmd'), 'run', '-d', str(self.project_path)] if logger.getEffectiveLevel() > logging.DEBUG: command_arr.append('--silent') result = subprocess.run(command_arr) diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index 25af8ae..9eef52b 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -180,7 +180,7 @@ def setUp(self) -> None: def tearDown(self) -> None: shutil.rmtree(FIXTURE_PATH, ignore_errors=True) - def test_config_prioritites(self): + def test_config_priorities(self): custom_content = "SOME CUSTOM CONTENT" config = configparser.ConfigParser() @@ -315,10 +315,12 @@ def test_generate(self): def test_incorrect_path(self): """ """ + path_not_exist = pathlib.Path('path/does/not/exist') + with self.assertLogs(level='ERROR') as logs: - return_code = stm32pio.app.main(sys_argv=['init', '-d', '~/path/does/not/exist']) + return_code = stm32pio.app.main(sys_argv=['init', '-d', str(path_not_exist)]) self.assertNotEqual(return_code, 0, msg='Return code should be non-zero') - self.assertTrue(next(True for item in logs.output if 'path/does/not/exist' in item), + self.assertTrue(next(True for item in logs.output if str(path_not_exist) in item), # TODO: fix to not raise on not found msg="ERROR logging message hasn't been printed") def test_no_ioc_file(self): @@ -343,7 +345,7 @@ def test_verbose(self): # get the current python executable (no need to guess whether it's python or python3 and so on) python_exec = sys.executable - result = subprocess.run([python_exec, stm32pio_exec, '-v', 'generate', '-d', FIXTURE_PATH], encoding='utf-8', + result = subprocess.run([python_exec, stm32pio_exec, '-v', 'generate', '-d', str(FIXTURE_PATH)], encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE) self.assertEqual(result.returncode, 0, msg="Non-zero return code") @@ -355,7 +357,7 @@ def test_non_verbose(self): """ """ - result = subprocess.run([PYTHON_EXEC, STM32PIO_MAIN_SCRIPT, 'generate', '-d', FIXTURE_PATH], encoding='utf-8', + result = subprocess.run([PYTHON_EXEC, STM32PIO_MAIN_SCRIPT, 'generate', '-d', str(FIXTURE_PATH)], encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE) self.assertEqual(result.returncode, 0, msg="Non-zero return code") @@ -364,7 +366,7 @@ def test_non_verbose(self): self.assertNotIn('Starting STM32CubeMX', result.stdout, msg="STM32CubeMX printed its logs") def test_init(self): - subprocess.run([PYTHON_EXEC, STM32PIO_MAIN_SCRIPT, 'init', '-d', FIXTURE_PATH, '-b', TEST_PROJECT_BOARD]) + subprocess.run([PYTHON_EXEC, STM32PIO_MAIN_SCRIPT, 'init', '-d', str(FIXTURE_PATH), '-b', TEST_PROJECT_BOARD]) self.assertTrue(FIXTURE_PATH.joinpath('stm32pio.ini').is_file(), msg="'stm32pio.ini' file hasn't been created") From 9ca7c5a1ffa935f961f79983ee43f75c462d36d1 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Fri, 29 Nov 2019 18:11:10 +0300 Subject: [PATCH 19/26] v0.9-beta: start to fill the documentation and comments --- TODO.md | 3 + stm32pio/tests/test.py | 146 +++++++++++++++++++++-------------------- 2 files changed, 77 insertions(+), 72 deletions(-) diff --git a/TODO.md b/TODO.md index 945747b..00a7014 100644 --- a/TODO.md +++ b/TODO.md @@ -25,3 +25,6 @@ - [ ] Update `.ioc` file - [ ] `str(path)` -> `path` were possible - [ ] Check `start_editor()` for different input + - [ ] Test on Python 3.6 + - [ ] Test `get_state()` (as sequence of states) + - [ ] Remake `get_state()` as property value (read-only getter) diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index 9eef52b..4778ea8 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -14,32 +14,45 @@ import stm32pio.lib +STM32PIO_MAIN_SCRIPT = inspect.getfile(stm32pio.app) # absolute path to the main stm32pio script +# absolute path to the Python executable (no need to guess whether it's python or python3 and so on) +PYTHON_EXEC = sys.executable + # Test data TEST_PROJECT_PATH = pathlib.Path('stm32pio-test-project').resolve() if not TEST_PROJECT_PATH.is_dir() or not TEST_PROJECT_PATH.joinpath('stm32pio-test-project.ioc').is_file(): raise FileNotFoundError("No test project is present") +# Make sure you have F0 framework installed (try to run code generation from STM32CubeMX manually at least once before +# proceeding) TEST_PROJECT_BOARD = 'nucleo_f031k6' +# Instantiate a temporary folder on every fixture run. It is used across all tests and is deleted on shutdown temp_dir = tempfile.TemporaryDirectory() FIXTURE_PATH = pathlib.Path(temp_dir.name).joinpath(TEST_PROJECT_PATH.name) -STM32PIO_MAIN_SCRIPT = inspect.getfile(stm32pio.app) -PYTHON_EXEC = sys.executable - -class TestUnit(unittest.TestCase): - """ - - """ - - def setUp(self) -> None: - self.tearDown() +class CustomTestCase(unittest.TestCase): + def setUp(self): + """ + Copy the test project from the repo to our temp directory + """ + shutil.rmtree(FIXTURE_PATH, ignore_errors=True) shutil.copytree(TEST_PROJECT_PATH, FIXTURE_PATH) - def tearDown(self) -> None: + def tearDown(self): + """ + Clean the temp directory + """ shutil.rmtree(FIXTURE_PATH, ignore_errors=True) + +class TestUnit(CustomTestCase): + """ + Test the single method. As we at some point decided to use a class instead of the set of scattered functions we need + to do some preparations for almost every test (e.g. instantiate the class, create the PlatformIO project, etc.) + """ + def test_generate_code(self): """ Check whether files and folders have been created @@ -47,7 +60,7 @@ def test_generate_code(self): project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}) project.generate_code() - # Assuming that the presence of these files indicates a success + # Assuming that the presence of these files indicating a success files_should_be_present = ['Src/main.c', 'Inc/main.h'] for file in files_should_be_present: with self.subTest(file_should_be_present=file, msg=f"{file} hasn't been created"): @@ -55,7 +68,7 @@ def test_generate_code(self): def test_pio_init(self): """ - Consider that existence of 'platformio.ini' file is displaying successful PlatformIO project initialization + Consider that existence of 'platformio.ini' file showing a successful PlatformIO project initialization """ project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}) result = project.pio_init() @@ -65,12 +78,12 @@ def test_pio_init(self): def test_patch(self): """ - Compare contents of the patched string and the desired patch + Compare contents of the patched string and the patch itself """ - project = stm32pio.lib.Stm32pio(FIXTURE_PATH) test_content = "*** TEST PLATFORMIO.INI FILE ***" FIXTURE_PATH.joinpath('platformio.ini').write_text(test_content) + project = stm32pio.lib.Stm32pio(FIXTURE_PATH) project.patch() self.assertFalse(FIXTURE_PATH.joinpath('include').is_dir(), msg="'include' has not been deleted") @@ -90,8 +103,7 @@ def test_build_should_raise(self): project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}) project.pio_init() - with self.assertRaisesRegex(Exception, "PlatformIO build error", - msg="Build error exception hadn't been raised"): + with self.assertRaisesRegex(Exception, "PlatformIO build error", msg="Build exception hadn't been raised"): project.pio_build() def test_run_editor(self): @@ -119,41 +131,52 @@ def test_run_editor(self): } for command, name in editors.items(): + # Look for the command presence in the system so we test only installed editors if platform.system() == 'Windows': command_str = f"where {command} /q" else: command_str = f"command -v {command}" - editor_exists = True if subprocess.run(command_str, shell=True, stdout=subprocess.PIPE, - stderr=subprocess.PIPE).returncode == 0\ - else False + editor_exists = False + if subprocess.run(command_str, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).returncode == 0: + editor_exists = True + if editor_exists: with self.subTest(command=command, name=name[platform.system()]): project.start_editor(command) + time.sleep(1) # wait a little bit for app to start + + command_arr = ['ps', '-A'] if platform.system() == 'Windows': - # "encoding='utf-8'" is for "a bytes-like object is required, not 'str'" in "assertIn" - result = subprocess.run(['wmic', 'process', 'get', 'description'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8') - else: - result = subprocess.run(['ps', '-A'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, - encoding='utf-8') - # Or, for Python 3.7 and above: - # result = subprocess.run(['ps', '-A'], capture_output=True, encoding='utf-8') + command_arr = ['wmic', 'process', 'get', 'description'] + # "encoding='utf-8'" is for "a bytes-like object is required, not 'str'" in "assertIn" + result = subprocess.run(command_arr, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + encoding='utf-8') + # Or, for Python 3.7 and above: + # result = subprocess.run(command_arr, capture_output=True, encoding='utf-8') self.assertIn(name[platform.system()], result.stdout) def test_init_path_not_found_should_raise(self): """ Pass non-existing path and expect the error """ - path_does_not_exist = 'does_not_exist' + path_does_not_exist_name = 'does_not_exist' - not_existing_path = FIXTURE_PATH.joinpath(path_does_not_exist) + path_does_not_exist = FIXTURE_PATH.joinpath(path_does_not_exist_name) + # 'cm' is for context manager with self.assertRaises(FileNotFoundError, msg="FileNotFoundError was not raised") as cm: - stm32pio.lib.Stm32pio(not_existing_path) - self.assertIn(path_does_not_exist, str(cm.exception), msg="Exception doesn't contain a description") + stm32pio.lib.Stm32pio(path_does_not_exist) + self.assertIn(path_does_not_exist_name, str(cm.exception), msg="Exception doesn't contain a description") def test_save_config(self): + """ + Explicitly save the config to file and look did that actually happen and whether all the information was + preserved + """ + + # 'board' is non-default, 'project'-section parameter project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}) + project.save_config() self.assertTrue(FIXTURE_PATH.joinpath('stm32pio.ini').is_file(), msg="'stm32pio.ini' file hasn't been created") @@ -168,36 +191,36 @@ def test_save_config(self): msg="'board' has not been set") -class TestIntegration(unittest.TestCase): +class TestIntegration(CustomTestCase): """ - + Sequence of methods that should work seamlessly """ - def setUp(self) -> None: - self.tearDown() - shutil.copytree(TEST_PROJECT_PATH, FIXTURE_PATH) - - def tearDown(self) -> None: - shutil.rmtree(FIXTURE_PATH, ignore_errors=True) - def test_config_priorities(self): + """ + Test the compliance with priorities when reading the parameters + """ + custom_content = "SOME CUSTOM CONTENT" + # Create test config config = configparser.ConfigParser() config.read_dict({ 'project': { 'platformio_ini_patch_content': custom_content } }) + # ... save it with FIXTURE_PATH.joinpath('stm32pio.ini').open(mode='w') as config_file: config.write(config_file) + # On project creation we should get the CLI-provided value as superseding to the saved one project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}) project.pio_init() project.patch() after_patch_content = FIXTURE_PATH.joinpath('platformio.ini').read_text() - self.assertIn(custom_content, after_patch_content, msg="Patch content is not from user config") + self.assertIn(custom_content, after_patch_content, msg="Patch content is not from CLI argument") def test_build(self): """ @@ -250,23 +273,13 @@ def test_regenerate_code(self): msg=f"User content hasn't been preserved after regeneration in {test_file_2}") -class TestCLI(unittest.TestCase): +class TestCLI(CustomTestCase): """ - + Some tests to mimic the behavior of end-user tasks (CLI commands such as 'new', 'clean', etc.). Run main function + passing the arguments to it but sometimes even run as subprocess (to capture actual STDOUT/STDERR output) """ - def setUp(self) -> None: - self.tearDown() - shutil.copytree(TEST_PROJECT_PATH, FIXTURE_PATH) - - def tearDown(self) -> None: - shutil.rmtree(FIXTURE_PATH, ignore_errors=True) - def test_clean(self): - """ - Dangerous test actually... - """ - # Create files and folders file_should_be_deleted = FIXTURE_PATH.joinpath('file.should.be.deleted') dir_should_be_deleted = FIXTURE_PATH.joinpath('dir.should.be.deleted') @@ -296,8 +309,6 @@ def test_new(self): self.assertTrue(FIXTURE_PATH.joinpath(f"{FIXTURE_PATH.name}.ioc").is_file(), msg="Missing .ioc file") def test_generate(self): - """ - """ return_code = stm32pio.app.main(sys_argv=['generate', '-d', str(FIXTURE_PATH)]) self.assertEqual(return_code, 0, msg="Non-zero return code") @@ -312,21 +323,16 @@ def test_generate(self): # .ioc file should be preserved self.assertTrue(FIXTURE_PATH.joinpath(f"{FIXTURE_PATH.name}.ioc").is_file(), msg="Missing .ioc file") - def test_incorrect_path(self): - """ - """ + def test_incorrect_path_should_log_error(self): path_not_exist = pathlib.Path('path/does/not/exist') with self.assertLogs(level='ERROR') as logs: return_code = stm32pio.app.main(sys_argv=['init', '-d', str(path_not_exist)]) self.assertNotEqual(return_code, 0, msg='Return code should be non-zero') self.assertTrue(next(True for item in logs.output if str(path_not_exist) in item), # TODO: fix to not raise on not found - msg="ERROR logging message hasn't been printed") - - def test_no_ioc_file(self): - """ - """ + msg="'ERROR' logging message hasn't been printed") + def test_no_ioc_file_should_log_error(self): dir_with_no_ioc_file = FIXTURE_PATH.joinpath('dir.with.no.ioc.file') dir_with_no_ioc_file.mkdir(exist_ok=False) @@ -334,18 +340,14 @@ def test_no_ioc_file(self): return_code = stm32pio.app.main(sys_argv=['init', '-d', str(dir_with_no_ioc_file)]) self.assertNotEqual(return_code, 0, msg='Return code should be non-zero') self.assertTrue(next(True for item in logs.output if "CubeMX project .ioc file" in item), - msg="ERROR logging message hasn't been printed") + msg="'ERROR' logging message hasn't been printed") def test_verbose(self): """ - Run as subprocess (can be done as assertLogs(), see above) + Run as subprocess to capture the full output. Check for both 'DEBUG' logging messages and STM32CubeMX CLI output """ - stm32pio_exec = inspect.getfile(stm32pio.app) # get the path to the main stm32pio script - # get the current python executable (no need to guess whether it's python or python3 and so on) - python_exec = sys.executable - - result = subprocess.run([python_exec, stm32pio_exec, '-v', 'generate', '-d', str(FIXTURE_PATH)], encoding='utf-8', + result = subprocess.run([PYTHON_EXEC, STM32PIO_MAIN_SCRIPT, '-v', 'generate', '-d', str(FIXTURE_PATH)], encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE) self.assertEqual(result.returncode, 0, msg="Non-zero return code") From c7c44db4e536e1b951ce4f5ae582ccc5742c5570 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sat, 30 Nov 2019 17:29:50 +0300 Subject: [PATCH 20/26] v0.9-alpha: * more of docs * root logger is now local, not 'logging' root --- TODO.md | 2 +- stm32pio/app.py | 32 +++++++++++++++++------ stm32pio/settings.py | 16 ++++++------ stm32pio/tests/test.py | 59 ++++++++++++++++++++++++++---------------- 4 files changed, 69 insertions(+), 40 deletions(-) diff --git a/TODO.md b/TODO.md index 00a7014..944ce51 100644 --- a/TODO.md +++ b/TODO.md @@ -26,5 +26,5 @@ - [ ] `str(path)` -> `path` were possible - [ ] Check `start_editor()` for different input - [ ] Test on Python 3.6 - - [ ] Test `get_state()` (as sequence of states) + - [ ] Test `get_state()` (as sequence of states (see scratch.py)) - [ ] Remake `get_state()` as property value (read-only getter) diff --git a/stm32pio/app.py b/stm32pio/app.py index 8f65325..87c2929 100755 --- a/stm32pio/app.py +++ b/stm32pio/app.py @@ -13,12 +13,19 @@ def parse_args(args: list) -> Optional[argparse.Namespace]: """ + Dedicated function to parse the arguments given via the CLI + Args: + args: list of strings + + Returns: + argparse.Namespace or None if no arguments were given """ parser = argparse.ArgumentParser(description="Automation of creating and updating STM32CubeMX-PlatformIO projects. " - "Requirements: Python 3.6+, STM32CubeMX, Java, PlatformIO CLI. Edit " - "settings.py to set path to the STM32CubeMX (if default doesn't work)") + "Requirements: Python 3.6+, STM32CubeMX, Java, PlatformIO CLI. Run " + "'init' command to create settings file and set the path to " + "STM32CubeMX and other tools (if defaults doesn't work)") # Global arguments (there is also an automatically added '-h, --help' option) parser.add_argument('--version', action='version', version=f"stm32pio v{__version__}") parser.add_argument('-v', '--verbose', help="enable verbose output (default: INFO)", action='count', required=False) @@ -26,12 +33,12 @@ def parse_args(args: list) -> Optional[argparse.Namespace]: subparsers = parser.add_subparsers(dest='subcommand', title='subcommands', description="valid subcommands", help="modes of operation") + parser_init = subparsers.add_parser('init', help="create config .ini file so you can tweak parameters before " + "proceeding") parser_new = subparsers.add_parser('new', help="generate CubeMX code, create PlatformIO project") parser_generate = subparsers.add_parser('generate', help="generate CubeMX code") parser_clean = subparsers.add_parser('clean', help="clean-up the project (WARNING: it deletes ALL content of " "'path' except the .ioc file)") - parser_init = subparsers.add_parser('init', help="create config .ini file so you can tweak parameters before " - "proceeding") # Common subparsers options for p in [parser_init, parser_new, parser_generate, parser_clean]: @@ -45,7 +52,6 @@ def parse_args(args: list) -> Optional[argparse.Namespace]: for p in [parser_new, parser_generate]: p.add_argument('--with-build', action='store_true', required=False, help="build a project after generation") - # Show help and exit if no arguments were given if len(args) == 0: parser.print_help() return None @@ -55,24 +61,34 @@ def parse_args(args: list) -> Optional[argparse.Namespace]: def main(sys_argv: list = sys.argv[1:]) -> int: """ + Can be used as high-level wrapper to do complete tasks + Example: + ret_code = stm32pio.app.main(sys_argv=['new', '-d', '~/path/to/project', '-b', 'nucleo_f031k6', '--with-build']) + + Args: + sys_argv: list of strings """ args = parse_args(sys_argv) + # Show help and exit if no arguments were given if args is None or args.subcommand is None: print("\nNo arguments were given, exiting...") return 0 # Logger instance goes through the whole program. - # Currently only 2 levels of verbosity through the '-v' option are counted (INFO and DEBUG) + # Currently only 2 levels of verbosity through the '-v' option are counted (INFO (default) and DEBUG (-v)) logger = logging.getLogger('stm32pio') + handler = logging.StreamHandler() if args.verbose: - logging.basicConfig(format="%(levelname)-8s %(funcName)-26s %(message)s", level=logging.DEBUG) logger.setLevel(logging.DEBUG) + handler.setFormatter(logging.Formatter("%(levelname)-8s %(funcName)-26s %(message)s")) + logger.addHandler(handler) logger.debug("debug logging enabled") else: - logging.basicConfig(format="%(levelname)-8s %(message)s", level=logging.INFO) logger.setLevel(logging.INFO) + handler.setFormatter(logging.Formatter("%(levelname)-8s %(message)s")) + logger.addHandler(handler) # Main routine import stm32pio.lib # import the module after sys.path modification diff --git a/stm32pio/settings.py b/stm32pio/settings.py index 2866864..89faa43 100644 --- a/stm32pio/settings.py +++ b/stm32pio/settings.py @@ -10,25 +10,25 @@ # (default is OK) How do you start Java from the command line? (edit if Java not in PATH) 'java_cmd': 'java', - # (default is OK) How do you start PlatformIO from the command line? (edit if not in PATH, check - # https://docs.platformio.org/en/latest/installation.html#install-shell-commands) + # (default is OK) How do you start PlatformIO from the command line? (edit if not in PATH, if you use PlatformIO + # IDE check https://docs.platformio.org/en/latest/installation.html#install-shell-commands) 'platformio_cmd': 'platformio', - # (default is OK) We trying to guess STM32CubeMX location. You can just avoid this and hard-code it. - # Note that STM32CubeMX will be invoked as 'java -jar CUBEMX' + # (default is OK) Trying to guess the STM32CubeMX location. STM actually had changed the installation path + # several times already. Note that STM32CubeMX will be invoked as 'java -jar CUBEMX' 'cubemx_cmd': # macOS default: 'Applications' folder "/Applications/STMicroelectronics/STM32CubeMX.app/Contents/Resources/STM32CubeMX" if my_os == 'Darwin' else - # Linux (Ubuntu) default: + # Linux (Ubuntu) default: home directory pathlib.Path.home().joinpath("STM32CubeMX/STM32CubeMX") if my_os == 'Linux' else - # Windows default: + # Windows default: Program Files "C:/Program Files/STMicroelectronics/STM32Cube/STM32CubeMX/STM32CubeMX.exe" if my_os == 'Windows' else None }, project={ - # (default is OK) see CubeMX user manual PDF to get other useful options + # (default is OK) See CubeMX user manual PDF (UM1718) to get other useful options 'cubemx_script_content': "config load $cubemx_ioc_full_filename\ngenerate code $project_path\nexit", - # override the defaults to comply with CubeMX project structure + # Override the defaults to comply with CubeMX project structure 'platformio_ini_patch_content': "[platformio]\ninclude_dir = Inc\nsrc_dir = Src\n" } ) diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index 4778ea8..21e972d 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -47,6 +47,13 @@ def tearDown(self): shutil.rmtree(FIXTURE_PATH, ignore_errors=True) +def tearDownModule(): + """ + Clean up after yourself + """ + temp_dir.cleanup() + + class TestUnit(CustomTestCase): """ Test the single method. As we at some point decided to use a class instead of the set of scattered functions we need @@ -57,7 +64,8 @@ def test_generate_code(self): """ Check whether files and folders have been created """ - project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}) + project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}, + save_on_destruction=False) project.generate_code() # Assuming that the presence of these files indicating a success @@ -70,7 +78,8 @@ def test_pio_init(self): """ Consider that existence of 'platformio.ini' file showing a successful PlatformIO project initialization """ - project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}) + project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}, + save_on_destruction=False) result = project.pio_init() self.assertEqual(result, 0, msg="Non-zero return code") @@ -83,7 +92,7 @@ def test_patch(self): test_content = "*** TEST PLATFORMIO.INI FILE ***" FIXTURE_PATH.joinpath('platformio.ini').write_text(test_content) - project = stm32pio.lib.Stm32pio(FIXTURE_PATH) + project = stm32pio.lib.Stm32pio(FIXTURE_PATH, save_on_destruction=False) project.patch() self.assertFalse(FIXTURE_PATH.joinpath('include').is_dir(), msg="'include' has not been deleted") @@ -100,7 +109,8 @@ def test_build_should_raise(self): """ Build an empty project so PlatformIO should return non-zero code and we, in turn, should throw the exception """ - project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}) + project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}, + save_on_destruction=False) project.pio_init() with self.assertRaisesRegex(Exception, "PlatformIO build error", msg="Build exception hadn't been raised"): @@ -110,7 +120,7 @@ def test_run_editor(self): """ Call the editors """ - project = stm32pio.lib.Stm32pio(FIXTURE_PATH) + project = stm32pio.lib.Stm32pio(FIXTURE_PATH, save_on_destruction=False) editors = { 'atom': { @@ -165,7 +175,7 @@ def test_init_path_not_found_should_raise(self): path_does_not_exist = FIXTURE_PATH.joinpath(path_does_not_exist_name) # 'cm' is for context manager with self.assertRaises(FileNotFoundError, msg="FileNotFoundError was not raised") as cm: - stm32pio.lib.Stm32pio(path_does_not_exist) + stm32pio.lib.Stm32pio(path_does_not_exist, save_on_destruction=False) self.assertIn(path_does_not_exist_name, str(cm.exception), msg="Exception doesn't contain a description") def test_save_config(self): @@ -175,7 +185,8 @@ def test_save_config(self): """ # 'board' is non-default, 'project'-section parameter - project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}) + project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}, + save_on_destruction=False) project.save_config() @@ -215,7 +226,8 @@ def test_config_priorities(self): config.write(config_file) # On project creation we should get the CLI-provided value as superseding to the saved one - project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}) + project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}, + save_on_destruction=False) project.pio_init() project.patch() @@ -226,7 +238,8 @@ def test_build(self): """ Initialize a new project and try to build it """ - project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}) + project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}, + save_on_destruction=False) project.generate_code() project.pio_init() project.patch() @@ -241,7 +254,8 @@ def test_regenerate_code(self): hardware features and some new files) """ - project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}) + project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}, + save_on_destruction=False) # Generate a new project ... project.generate_code() @@ -329,7 +343,7 @@ def test_incorrect_path_should_log_error(self): with self.assertLogs(level='ERROR') as logs: return_code = stm32pio.app.main(sys_argv=['init', '-d', str(path_not_exist)]) self.assertNotEqual(return_code, 0, msg='Return code should be non-zero') - self.assertTrue(next(True for item in logs.output if str(path_not_exist) in item), # TODO: fix to not raise on not found + self.assertTrue(next((True for item in logs.output if str(path_not_exist) in item), False), msg="'ERROR' logging message hasn't been printed") def test_no_ioc_file_should_log_error(self): @@ -339,7 +353,7 @@ def test_no_ioc_file_should_log_error(self): with self.assertLogs(level='ERROR') as logs: return_code = stm32pio.app.main(sys_argv=['init', '-d', str(dir_with_no_ioc_file)]) self.assertNotEqual(return_code, 0, msg='Return code should be non-zero') - self.assertTrue(next(True for item in logs.output if "CubeMX project .ioc file" in item), + self.assertTrue(next((True for item in logs.output if "CubeMX project .ioc file" in item), False), msg="'ERROR' logging message hasn't been printed") def test_verbose(self): @@ -347,8 +361,8 @@ def test_verbose(self): Run as subprocess to capture the full output. Check for both 'DEBUG' logging messages and STM32CubeMX CLI output """ - result = subprocess.run([PYTHON_EXEC, STM32PIO_MAIN_SCRIPT, '-v', 'generate', '-d', str(FIXTURE_PATH)], encoding='utf-8', - stdout=subprocess.PIPE, stderr=subprocess.PIPE) + result = subprocess.run([PYTHON_EXEC, STM32PIO_MAIN_SCRIPT, '-v', 'generate', '-d', str(FIXTURE_PATH)], + encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE) self.assertEqual(result.returncode, 0, msg="Non-zero return code") # Somehow stderr and not stdout contains the actual output @@ -357,10 +371,12 @@ def test_verbose(self): def test_non_verbose(self): """ + Run as subprocess to capture the full output. We should not see any 'DEBUG' logging messages or STM32CubeMX CLI + output """ - result = subprocess.run([PYTHON_EXEC, STM32PIO_MAIN_SCRIPT, 'generate', '-d', str(FIXTURE_PATH)], encoding='utf-8', - stdout=subprocess.PIPE, stderr=subprocess.PIPE) + result = subprocess.run([PYTHON_EXEC, STM32PIO_MAIN_SCRIPT, 'generate', '-d', str(FIXTURE_PATH)], + encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE) self.assertEqual(result.returncode, 0, msg="Non-zero return code") self.assertNotIn('DEBUG', result.stderr, msg="Verbose logging output has been enabled on stderr") @@ -368,6 +384,10 @@ def test_non_verbose(self): self.assertNotIn('Starting STM32CubeMX', result.stdout, msg="STM32CubeMX printed its logs") def test_init(self): + """ + Check for config creation and parameters presence + """ + subprocess.run([PYTHON_EXEC, STM32PIO_MAIN_SCRIPT, 'init', '-d', str(FIXTURE_PATH), '-b', TEST_PROJECT_BOARD]) self.assertTrue(FIXTURE_PATH.joinpath('stm32pio.ini').is_file(), msg="'stm32pio.ini' file hasn't been created") @@ -382,12 +402,5 @@ def test_init(self): msg="'board' has not been set") -def tearDownModule(): - """ - Clean up after yourself - """ - temp_dir.cleanup() - - if __name__ == '__main__': unittest.main() From dad2a6f83d1fa7cf41582e315c6c56c3935a96e9 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Mon, 2 Dec 2019 01:27:08 +0300 Subject: [PATCH 21/26] v0.9-beta: * more of docs * refactor some code, add/remove logs, cleaning, etc... --- CHANGELOG | 2 +- TODO.md | 15 ++--- stm32pio/lib.py | 149 +++++++++++++++++++++++++++++------------ stm32pio/settings.py | 2 + stm32pio/tests/test.py | 30 +++++---- 5 files changed, 135 insertions(+), 63 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 58f40a7..8eae406 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -100,5 +100,5 @@ stm32pio changelog: - Changed: actualized .ioc file and clean-up the code according to the latest STM32CubeMX version (5.3.0 at the moment) - Changed: revised and improved util module - ver. 0.9 (11.19): + ver. 0.9 (12.19): - New: tested with Python3 version of PlatformIO diff --git a/TODO.md b/TODO.md index 944ce51..be5bf7b 100644 --- a/TODO.md +++ b/TODO.md @@ -5,6 +5,7 @@ - [ ] Add more checks, for example when updating the project (`generate` command), check for boards matching and so on... - [x] Function annotations - [ ] GUI. For example, drop the folder into small window (with checkboxes corresponding with CLI options) and get the output. At left is a list of recent projects + - [ ] GUI. Indicate the progress as states goes forward during the run (see `scratch.py`) - [x] Remade as Class (constructor `__init__(project_path)`) - [x] Config file for every project instead of the `settings.py` (but we still sill be storing the default parameters there) - [x] Test CLI (integration testing) @@ -17,14 +18,12 @@ - [x] Do not require matching of the project folder and .ioc file names (use first .ioc file found) - [x] Remove casts to string where we can use path-like objects - [x] Settings string templates and multi line - - [ ] Smart `start_editor` test (detect editors in system, maybe use unittest `skipIf` decorator) - - [ ] Maybe split tests to avoid long names (e.g. => `stm32pio.tests.test_unit` (but then we will have `stm32pio.tests.test_unit.Test` or so...) - - [ ] For GUI: indicate progress as states goes forward (see `scratch.py`) + - [x] Smart `start_editor` test (detect editors in system, maybe use unittest `skipIf` decorator) - [x] `init` command - [x] New argparse algo cause now we have config file - [ ] Update `.ioc` file - - [ ] `str(path)` -> `path` were possible - - [ ] Check `start_editor()` for different input - - [ ] Test on Python 3.6 - - [ ] Test `get_state()` (as sequence of states (see scratch.py)) - - [ ] Remake `get_state()` as property value (read-only getter) + - [x] `str(path)` -> `path` were possible + - [x] Check `start_editor()` for different input + - [x] Test on Python 3.6 + - [ ] Test for `get_state()` (as sequence of states (see scratch.py)) + - [ ] Remake `get_state()` as property value (read-only getter with decorator) diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 3a71a7b..85faa0b 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -1,3 +1,7 @@ +""" +Main library +""" + import collections import logging import pathlib @@ -13,12 +17,26 @@ import stm32pio.settings +# Child logger logger = logging.getLogger('stm32pio.util') @enum.unique class ProjectState(enum.IntEnum): """ + Codes indicating a project state at the moment. Should be the sequence of incrementing integers to be suited for + state determining algorithm + + Hint: Files/folders to be present on every project state: + UNDEFINED: use this state to indicate none of the states below. Also, when we do not have any .ioc file the + Stm32pio class cannot be instantiated (constructor raises an exception) + INITIALIZED: ['project.ioc', 'stm32pio.ini'] + GENERATED: ['Inc', 'Src', 'project.ioc', 'stm32pio.ini'] + PIO_INITIALIZED (on case-sensitive FS): ['test', 'include', 'Inc', 'platformio.ini', '.gitignore', 'Src', 'lib', + 'project.ioc', '.travis.yml', 'src'] + PIO_INI_PATCHED: ['test', 'Inc', 'platformio.ini', '.gitignore', 'Src', 'lib', 'project.ioc', '.travis.yml'] + BUILT: same as above + '.pio' folder with build artifacts (such as .pio/build/nucleo_f031k6/firmware.bin, + .pio/build/nucleo_f031k6/firmware.elf) """ UNDEFINED = enum.auto() INITIALIZED = enum.auto() @@ -30,10 +48,28 @@ class ProjectState(enum.IntEnum): class Stm32pio: """ - Main class + Main class. + + Represents a single project, encapsulating file system path to the project (main mandatory identifier) and some + parameters in a configparser .ini file. As stm32pio can be installed via pip and has no global config we also + storing global parameters (such as Java or STM32CubeMX invoking commands) in this config .ini file so the user can + specify settings on a per-project base. The config can be saved in a non-disturbing way automatically on the + instance destruction (e.g. by garbage collecting it) (use save_on_destruction=True flag), otherwise user should + explicitly save the config if he wants to (using save_config() method). + + The typical life cycle consists of project creation, passing mandatory 'dirty_path' argument. If also 'parameters' + dictionary is specified also these settings are processed (white-list approach is used so we set only those + parameters that are listed in the constructor code) (currently only 'board' parameter is included). Then it is + possible to perform API operations. WARNING. Please be careful with the 'clean' method as it deletes all the content + of the project directory except the main .ioc file. + + Args: + dirty_path (str): path to the project + parameters (dict): additional parameters to set on initialization stage + save_on_destruction (bool): register or not the finalizer that saves the config to file """ - def __init__(self, dirty_path: str, parameters=None, save_on_destruction=True): + def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction: bool = True): if parameters is None: parameters = {} @@ -62,32 +98,37 @@ def __init__(self, dirty_path: str, parameters=None, save_on_destruction=True): self._finalizer = weakref.finalize(self, self.save_config) - def save_config(self): + def save_config(self) -> int: + """ + Tries to save the configparser config to file and gently log if error occurs + """ + try: - with self.project_path.joinpath('stm32pio.ini').open(mode='w') as config_file: + with self.project_path.joinpath(stm32pio.settings.config_file_name).open(mode='w') as config_file: self.config.write(config_file) + logger.debug("stm32pio.ini config file has been saved") + return 0 except Exception as e: logger.warning(f"Cannot save config: {e}") if logger.getEffectiveLevel() <= logging.DEBUG: traceback.print_exception(*sys.exc_info()) + return -1 - def get_state(self) -> ProjectState: + @property + def state(self) -> ProjectState: """ - Hint: Files/folders to be present on every project state: - generated: ['Inc', 'Src', 'cubemx-script', 'stm32pio-test-project.ioc'] - pio initted: ['test', 'include', 'Inc', 'platformio.ini', '.gitignore', 'Src', 'cubemx-script', 'lib', 'stm32pio-test-project.ioc', '.travis.yml', 'src'] - patched: ['test', 'Inc', 'platformio.ini', '.gitignore', 'Src', 'cubemx-script', 'lib', 'stm32pio-test-project.ioc', '.travis.yml'] - built: ['.pio', 'test', 'Inc', 'platformio.ini', '.gitignore', 'Src', 'cubemx-script', 'lib', 'stm32pio-test-project.ioc', '.travis.yml'] + - .pio/build/nucleo_f031k6/firmware.bin, .pio/build/nucleo_f031k6/firmware.elf + Property returning the current state of the project. Calculated at every request. """ - logger.debug("Calculating project state...") + logger.debug("Calculating the project state...") logger.debug(f"Project content: {[item.name for item in self.project_path.iterdir()]}") + # Fill the ordered dictionary with conditions results states_conditions = collections.OrderedDict() states_conditions[ProjectState.UNDEFINED] = [True] - states_conditions[ProjectState.INITIALIZED] = [self.project_path.joinpath('stm32pio.ini').is_file()] + states_conditions[ProjectState.INITIALIZED] = [ + self.project_path.joinpath(stm32pio.settings.config_file_name).is_file()] states_conditions[ProjectState.GENERATED] = [self.project_path.joinpath('Inc').is_dir() and len(list(self.project_path.joinpath('Inc').iterdir())) > 0, self.project_path.joinpath('Src').is_dir() and @@ -115,13 +156,18 @@ def get_state(self) -> ProjectState: states_info_str = '\n'.join(f"{state.name:20}{conditions_results[state.value-1]}" for state in ProjectState) logger.debug(f"Determined states:\n{states_info_str}") - last_true_index = 0 # UNDEFINED is always True + # Search for a consecutive raw of 1's and find the last of them. For example, if the array is + # [1,1,0,1,0,0] + # ^ + last_true_index = 0 # UNDEFINED is always True, use as a start value for index, value in enumerate(conditions_results): if value == 1: last_true_index = index else: break + # Fall back to the UNDEFINED state if we have breaks in conditions results array. For example, in [1,1,0,1,0,0] + # we still return UNDEFINED as it doesn't look like a correct combination of files actually project_state = ProjectState.UNDEFINED if 1 not in conditions_results[last_true_index + 1:]: project_state = ProjectState(last_true_index + 1) @@ -131,6 +177,8 @@ def get_state(self) -> ProjectState: def _find_ioc_file(self) -> pathlib.Path: """ + Find and return an .ioc file. If there are more than one, return first. If no .ioc file is present raise + FileNotFoundError exception """ ioc_file = self.config.get('project', 'ioc_file', fallback=None) @@ -151,10 +199,13 @@ def _find_ioc_file(self) -> pathlib.Path: def _load_settings_file(self) -> configparser.ConfigParser: """ + Prepare configparser config for the project. First, read the default config and then mask these values with user + ones """ - # logger.debug("Searching for any .ioc file...") - stm32pio_ini = self.project_path.joinpath('stm32pio.ini') - # if stm32pio_ini.is_file(): + + logger.debug(f"Searching for {stm32pio.settings.config_file_name}...") + stm32pio_ini = self.project_path.joinpath(stm32pio.settings.config_file_name) + config = configparser.ConfigParser() # Fill with default values @@ -162,10 +213,15 @@ def _load_settings_file(self) -> configparser.ConfigParser: # Then override by user values (if exist) config.read(str(stm32pio_ini)) - # for section in config.sections(): - # print('=========== ' + section + ' ===========') - # for item in config.items(section): - # print(item) + # Put away unnecessary processing as the string still will be formed even if the logging level doesn't allow + # propagation of this message + if logger.getEffectiveLevel() <= logging.DEBUG: + debug_str = 'Resolved config:' + for section in config.sections(): + debug_str += (f"\n=========== {section} ===========\n") + for value in config.items(section): + debug_str += f"{value}\n" + logger.debug(debug_str) return config @@ -176,7 +232,7 @@ def _resolve_project_path(dirty_path: str) -> pathlib.Path: Handle 'path/to/proj' and 'path/to/proj/', '.' (current directory) and other cases Args: - dirty_path: some directory in the filesystem + dirty_path (str): some directory in the filesystem """ resolved_path = pathlib.Path(dirty_path).expanduser().resolve() if not resolved_path.exists(): @@ -187,8 +243,12 @@ def _resolve_project_path(dirty_path: str) -> pathlib.Path: def _resolve_board(self, board: str) -> str: """ + Check if given board is a correct board name in the PlatformIO database + Returns: + same board that has been given, raise an exception otherwise """ + logger.debug("searching for PlatformIO board...") result = subprocess.run([self.config.get('app', 'platformio_cmd'), 'boards'], encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -205,18 +265,20 @@ def _resolve_board(self, board: str) -> str: def generate_code(self) -> None: """ - Call STM32CubeMX app as a 'java -jar' file with the automatically prearranged 'cubemx-script' file + Call STM32CubeMX app as a 'java -jar' file to generate the code from the .ioc file. Pass commands to the + STM32CubeMX in a temp file """ + # Use mkstemp() instead of higher-level API for compatibility with Windows (see tempfile docs for more details) cubemx_script_file, cubemx_script_name = tempfile.mkstemp() # buffering=0 leads to the immediate flushing on writing with open(cubemx_script_file, mode='w+b', buffering=0) as cubemx_script: - cubemx_script.write(self.config.get('project', 'cubemx_script_content').encode()) + cubemx_script.write(self.config.get('project', 'cubemx_script_content').encode()) # encode since mode='w+b' logger.info("starting to generate a code from the CubeMX .ioc file...") command_arr = [self.config.get('app', 'java_cmd'), '-jar', self.config.get('app', 'cubemx_cmd'), '-q', - cubemx_script_name, '-s'] + cubemx_script_name, '-s'] # -q: read commands from file, -s: silent performance if logger.getEffectiveLevel() <= logging.DEBUG: result = subprocess.run(command_arr) else: @@ -227,37 +289,37 @@ def generate_code(self) -> None: logger.info("successful code generation") else: logger.error(f"code generation error (CubeMX return code is {result.returncode}).\n" - "Try to enable a verbose output or generate a code from the CubeMX itself.") + "Enable a verbose output or try to generate a code from the CubeMX itself.") raise Exception("code generation error") pathlib.Path(cubemx_script_name).unlink() + def pio_init(self) -> int: """ - Call PlatformIO CLI to initialize a new project - - Args: - board: string displaying PlatformIO name of MCU/board (from 'pio boards' command) + Call PlatformIO CLI to initialize a new project. It uses parameters (path, board) collected before so the + confirmation of the data presence is on a user """ logger.info("starting PlatformIO project initialization...") - command_arr = [self.config.get('app', 'platformio_cmd'), 'init', '-d', str(self.project_path), '-b', self.config.get('project', 'board'), - '-O', 'framework=stm32cube'] + # TODO: check whether there is already a platformio.ini file and warn in this case + + # TODO: move out to settings a 'framework' option + command_arr = [self.config.get('app', 'platformio_cmd'), 'init', '-d', str(self.project_path), '-b', + self.config.get('project', 'board'), '-O', 'framework=stm32cube'] if logger.getEffectiveLevel() > logging.DEBUG: command_arr.append('--silent') - error_msg = "PlatformIO project initialization error" - result = subprocess.run(command_arr, encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + error_msg = "PlatformIO project initialization error" if result.returncode == 0: - # PlatformIO returns 0 even on some errors ('platformio.ini' wasn't created, e.g. no '--board' argument) - if 'ERROR' in result.stdout.upper(): - print(result.stdout) - raise Exception(error_msg) - if 'ERROR' in result.stderr.upper(): - print(result.stderr) - raise Exception(error_msg) + # PlatformIO returns 0 even on some errors (e.g. no '--board' argument) + for output in [result.stdout, result.stderr]: + if 'ERROR' in output.upper(): + logger.error(output) + raise Exception(error_msg) logger.info("successful PlatformIO project initialization") return result.returncode else: @@ -271,15 +333,18 @@ def patch(self) -> None: logger.debug("patching 'platformio.ini' file...") + # TODO: check whether there is already a patched platformio.ini file, warn in this case and do not proceed + platformio_ini_file = self.project_path.joinpath('platformio.ini') if platformio_ini_file.is_file(): with platformio_ini_file.open(mode='a') as f: f.write(self.config.get('project', 'platformio_ini_patch_content')) logger.info("'platformio.ini' has been patched") else: - logger.warning("'platformio.ini' file not found") + raise Exception("'platformio.ini' file not found, the project cannot be patched successfully") shutil.rmtree(self.project_path.joinpath('include'), ignore_errors=True) + if not self.project_path.joinpath('SRC').is_dir(): # check for case sensitive file system shutil.rmtree(self.project_path.joinpath('src'), ignore_errors=True) diff --git a/stm32pio/settings.py b/stm32pio/settings.py index 89faa43..87fd926 100644 --- a/stm32pio/settings.py +++ b/stm32pio/settings.py @@ -32,3 +32,5 @@ 'platformio_ini_patch_content': "[platformio]\ninclude_dir = Inc\nsrc_dir = Src\n" } ) + +config_file_name = 'stm32pio.ini' diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index 21e972d..1a2b26c 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -1,3 +1,8 @@ +""" +Note: pyenv is recommended to use for testing with different Python versions +https://www.tecmint.com/pyenv-install-and-manage-multiple-python-versions-in-linux/ +""" + import unittest import configparser import pathlib @@ -31,8 +36,15 @@ temp_dir = tempfile.TemporaryDirectory() FIXTURE_PATH = pathlib.Path(temp_dir.name).joinpath(TEST_PROJECT_PATH.name) +def tearDownModule(): + temp_dir.cleanup() + class CustomTestCase(unittest.TestCase): + """ + These pre- and post-tasks are common for all test cases + """ + def setUp(self): """ Copy the test project from the repo to our temp directory @@ -47,13 +59,6 @@ def tearDown(self): shutil.rmtree(FIXTURE_PATH, ignore_errors=True) -def tearDownModule(): - """ - Clean up after yourself - """ - temp_dir.cleanup() - - class TestUnit(CustomTestCase): """ Test the single method. As we at some point decided to use a class instead of the set of scattered functions we need @@ -190,10 +195,11 @@ def test_save_config(self): project.save_config() - self.assertTrue(FIXTURE_PATH.joinpath('stm32pio.ini').is_file(), msg="'stm32pio.ini' file hasn't been created") + self.assertTrue(FIXTURE_PATH.joinpath(stm32pio.settings.config_file_name).is_file(), + msg=f"{stm32pio.settings.config_file_name} file hasn't been created") config = configparser.ConfigParser() - config.read(str(FIXTURE_PATH.joinpath('stm32pio.ini'))) + config.read(str(FIXTURE_PATH.joinpath(stm32pio.settings.config_file_name))) for section, parameters in stm32pio.settings.config_default.items(): for option, value in parameters.items(): with self.subTest(section=section, option=option, msg="Section/key is not found in saved config file"): @@ -222,7 +228,7 @@ def test_config_priorities(self): } }) # ... save it - with FIXTURE_PATH.joinpath('stm32pio.ini').open(mode='w') as config_file: + with FIXTURE_PATH.joinpath(stm32pio.settings.config_file_name).open(mode='w') as config_file: config.write(config_file) # On project creation we should get the CLI-provided value as superseding to the saved one @@ -390,10 +396,10 @@ def test_init(self): subprocess.run([PYTHON_EXEC, STM32PIO_MAIN_SCRIPT, 'init', '-d', str(FIXTURE_PATH), '-b', TEST_PROJECT_BOARD]) - self.assertTrue(FIXTURE_PATH.joinpath('stm32pio.ini').is_file(), msg="'stm32pio.ini' file hasn't been created") + self.assertTrue(FIXTURE_PATH.joinpath(stm32pio.settings.config_file_name).is_file(), msg=f"{stm32pio.settings.config}_file_name file hasn't been created") config = configparser.ConfigParser() - config.read(str(FIXTURE_PATH.joinpath('stm32pio.ini'))) + config.read(str(FIXTURE_PATH.joinpath(stm32pio.settings.config_file_name))) for section, parameters in stm32pio.settings.config_default.items(): for option, value in parameters.items(): with self.subTest(section=section, option=option, msg="Section/key is not found in saved config file"): From d5b855a9c2b6813458d4bdac7cba67f82d28a4ea Mon Sep 17 00:00:00 2001 From: ussserrr Date: Mon, 2 Dec 2019 20:11:49 +0300 Subject: [PATCH 22/26] v0.9-beta: docs, docs, docs... --- CHANGELOG | 21 +++++++++++++++++++++ README.md | 38 +++++++++++++++++++++----------------- TODO.md | 2 ++ stm32pio/app.py | 2 +- stm32pio/lib.py | 19 +++++++++++++------ stm32pio/tests/test.py | 10 ++++++---- 6 files changed, 64 insertions(+), 28 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8eae406..e04336b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -102,3 +102,24 @@ stm32pio changelog: ver. 0.9 (12.19): - New: tested with Python3 version of PlatformIO + - New: '__main__.py' file (to run the app as module (python -m stm32pio)) + - New: 'init' subcommand (initialize the project only, useful for the preliminary tweaking) + - New: introducing the OOP pattern: we have now a Stm32pio class representing a single project (project path as a main identifier) + - New: projects now have a config file stm32pio.ini where the user can set the variety of parameters + - New: 'state' property calculating the estimated project state on every request to itself (beta). It is the concept for future releases + - New: STM32CubeMX is now started more silently (without a splash screen) + - New: add integration and CLI tests (sort of) + - New: testing with different Python versions using pyenv (3.6+ target) + - New: more typing annotations + - Fixed: the app has been failed to start as 'python app.py' (modify sys.path to fix) + - Changed: 'main' function is now fully modular: can be run from anywhere with given CLI arguments (will be piped forward to be parsed via 'argparse') + - Changed: rename stm32pio.py -> app.py (stm32pio is the name of the package as a whole) + - Changed: rename util.py -> lib.py (means main library) + - Changed: logging is now more modular: we do not set global 'basicConfig' and specify separated loggers for each module instead + - Changed: more clear description of steps to do for each user subcommand by the code + - Changed: get rid of 'print' calls leaving only logging messages (easy to turn on/off the console output in the outer code) + - Changed: re-imagined API behavior: where to raise exceptions, where to return values and so on + - Changed: more clean API, e.g. move out the board resolving procedure and so on + - Changed: test fixture is now moved out from the repo and is created temporarily on every test run + - Changed: actualized .ioc file for the latest STM32CubeMX version (5.3.0 at the moment) + - Changed: improved help, docs, comments diff --git a/README.md b/README.md index e196aa7..508ed76 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,17 @@ # stm32pio Small cross-platform Python app that can create and update [PlatformIO](https://platformio.org) projects from [STM32CubeMX](https://www.st.com/en/development-tools/stm32cubemx.html) `.ioc` files. +It uses STM32CubeMX to generate a HAL-framework based code and creates PlatformIO project with the compatible `stm32cube` framework specified. + ![Logo](/screenshots/logo.png) ## Features - Start the new project in a single directory using only an `.ioc` file - Update existing project after changing hardware options from CubeMX - - Clean-up the project (WARNING: it deletes ALL content of 'path' except the `.ioc` file!) + - Clean-up the project (WARNING: it deletes ALL content of project path except the `.ioc` file!) - *[optional]* Automatically run your favorite editor in the end - - *[optional]* Make an initial build of the project + - *[optional]* Automatically make an initial build of the project ## Requirements: @@ -17,9 +19,9 @@ Small cross-platform Python app that can create and update [PlatformIO](https:// - Python 3.6+ - For usage: - macOS, Linux, Windows - - STM32CubeMX (all recent versions) with downloaded necessary frameworks (F0, F1, etc.) + - STM32CubeMX (all recent versions) with desired downloaded frameworks (F0, F1, etc.) - Java CLI (JRE) (likely is already installed if STM32CubeMX works) - - PlatformIO CLI + - PlatformIO CLI (already presented if you have installed PlatformIO via some package manager or need to be installed as the command line extension from IDE) A general recommendation there would be to try to generate and build a code manually (via the CubeMX GUI and PlatformIO CLI or IDE) at least once before using stm32pio to make sure that all tools are working properly. @@ -37,20 +39,24 @@ $ pip3 uninstall stm32pio ## Usage Basically, you need to follow such a pattern: - 1. Create CubeMX project, set-up hardware configuration - 2. Run stm32pio that automatically invoke CubeMX to generate the code, create PlatformIO project, patch an '.ini' file and so on - 3. Work on the project in your editor, compile/upload/debug it + 1. Create CubeMX project, set-up your hardware configuration + 2. Run stm32pio that automatically invoke CubeMX to generate the code, create PlatformIO project, patch a 'platformio.ini' file and so on + 3. Work on the project in your editor, compile/upload/debug etc. 4. Edit the configuration in CubeMX when necessary, then run stm32pio to regenerate the code. Refer to Example section on more detailed steps. -stm32pio will create an accessory file `cubemx-script`'` in your project directory that contains commands passed to CubeMX. You can safely delete it (it will be created again on the next run) or edit corresponding to your goals. +On the first run stm32pio will create a config file `stm32pio.ini`, syntax of which is similar to the `platformio.ini`. You can also create this config without any following operations by initializing the project: +```shell script +$ stm32pio init -d path/to/project +``` +It may be useful to tweak some parameters before proceeding. The structure of the config is separated in two sections: `app` and `project`. Options of the first one is related to the global settings such as commands to invoke different instruments though they can be adjusted on the per-project base while the second section contains of project-related parameters. -Check `settings.py` to make sure that all user-specific parameters are valid. Run +You can always run ```shell script $ python3 app.py --help ``` -to see help. +to see help on available commands. ## Example @@ -67,11 +73,11 @@ to see help. 5. Run `platformio boards` (`pio boards`) or go to [boards](https://docs.platformio.org/en/latest/boards) to list all supported devices. Pick one and use its ID as a `-b` argument (for example, `nucleo_f031k6`) 6. All done! You can now run ```shell script - $ python3 stm32pio.py new -d path/to/cubemx/project/ -b nucleo_f031k6 --start-editor=code --with-build + $ python3 app.py new -d path/to/cubemx/project/ -b nucleo_f031k6 --start-editor=code --with-build ``` - to complete generation, start the Visual Studio Code editor with opened folder and compile the project (as an example, not required). Make sure you have all tools in PATH (`java` (or set its path in `settings.py`), `python`, editor). You can use shorter form if you are already located in the project directory (also using shebang alias): + to complete generation, start the Visual Studio Code editor with opened folder and compile the project (as an example, not required). Make sure you have all tools in PATH (`java` (or set its path in `stm32pio.ini`), `platformio`, `python`, editor). You can use shorter form if you are already located in the project directory (also using shebang alias): ```shell script - path/to/cubemx/project/ $ stm32pio.py new -b nucleo_f031k6 + path/to/cubemx/project/ $ stm32pio new -b nucleo_f031k6 ``` 7. If you will be in need to update hardware configuration in the future, make all necessary stuff in CubeMX and run `generate` command in a similar way: ```shell script @@ -89,16 +95,14 @@ or ```shell script stm32pio-repo/ $ python3 -m stm32pio.tests.test -b -v ``` -to test the app. It uses STM32F0 framework to generate and build a code from the `stm32pio/tests/stm32pio-test-project/stm32pio-test-project.ioc` file. +to test the app. It uses STM32F0 framework to generate and build a code from the test `stm32pio/tests/stm32pio-test-project/stm32pio-test-project.ioc` project file. For specific test you may use ```shell script stm32pio-repo/ $ python3 -m unittest stm32pio.tests.test.TestCLI -b -v ``` -It's fine to fail an editor test as you not necessarily should have all the editors on your machine. - -CI is hard to implement for all target OSes during the requirement to have all tools (PlatformIO, Java, CubeMX, etc.) installed during the test. For example, ST doesn't even provide a direct link to CubeMX for downloading +CI is hard to implement for all target OSes during the requirement to have all tools (PlatformIO, Java, CubeMX, etc.) installed during the test. For example, ST doesn't even provide a direct link to the CubeMX for downloading. ## Restrictions diff --git a/TODO.md b/TODO.md index be5bf7b..ff2826d 100644 --- a/TODO.md +++ b/TODO.md @@ -27,3 +27,5 @@ - [x] Test on Python 3.6 - [ ] Test for `get_state()` (as sequence of states (see scratch.py)) - [ ] Remake `get_state()` as property value (read-only getter with decorator) + - [ ] If the project path is a unique identifier of the project in our code maybe we can remake `Stm32pio` class as a subclass of `pathlib.Path` and then reference it like `self` and not `self.project_path`. It would be more consistent also, as now `project_path` is perceived like any other config parameter that somehow is appeared to exist outside of a config instance but then it will be a core identifier, a truly `self` value. + - [ ] Try to invoke stm32pio as modeule (-m), from different paths... diff --git a/stm32pio/app.py b/stm32pio/app.py index 87c2929..9d1bc82 100755 --- a/stm32pio/app.py +++ b/stm32pio/app.py @@ -73,7 +73,7 @@ def main(sys_argv: list = sys.argv[1:]) -> int: args = parse_args(sys_argv) # Show help and exit if no arguments were given if args is None or args.subcommand is None: - print("\nNo arguments were given, exiting...") + print("\nNo arguments were given, exiting...") # TODO: replace 'print' with 'logger.info' return 0 # Logger instance goes through the whole program. diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 85faa0b..4c1ae8f 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -218,7 +218,7 @@ def _load_settings_file(self) -> configparser.ConfigParser: if logger.getEffectiveLevel() <= logging.DEBUG: debug_str = 'Resolved config:' for section in config.sections(): - debug_str += (f"\n=========== {section} ===========\n") + debug_str += f"\n=========== {section} ===========\n" for value in config.items(section): debug_str += f"{value}\n" logger.debug(debug_str) @@ -349,20 +349,26 @@ def patch(self) -> None: shutil.rmtree(self.project_path.joinpath('src'), ignore_errors=True) - def start_editor(self, editor_command: str) -> None: + def start_editor(self, editor_command: str) -> int: """ Start the editor specified by 'editor_command' with the project opened Args: - editor_command: editor command (as we start in the terminal) + editor_command: editor command as we start it in the terminal. Note that only single-word command is + currently supported + + Returns: + return code of the editor on success, -1 otherwise """ logger.info(f"starting an editor '{editor_command}'...") try: - subprocess.run([editor_command, str(self.project_path)], check=True) + result = subprocess.run([editor_command, str(self.project_path)], check=True) + return result.returncode if result.returncode != -1 else 0 except subprocess.CalledProcessError as e: logger.error(f"Failed to start the editor {editor_command}: {e.stderr}") + return -1 def pio_build(self) -> int: @@ -373,6 +379,7 @@ def pio_build(self) -> int: 0 if success, raise an exception otherwise """ + # TODO: do we need this check? PlatformIO can handle it by itself if self.project_path.joinpath('platformio.ini').is_file(): logger.info("starting PlatformIO build...") else: @@ -382,13 +389,13 @@ def pio_build(self) -> int: command_arr = [self.config.get('app', 'platformio_cmd'), 'run', '-d', str(self.project_path)] if logger.getEffectiveLevel() > logging.DEBUG: command_arr.append('--silent') + result = subprocess.run(command_arr) if result.returncode == 0: logger.info("successful PlatformIO build") - return result.returncode else: logger.error("PlatformIO build error") - raise Exception("PlatformIO build error") + return result.returncode def clean(self) -> None: diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index 1a2b26c..4c587af 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -110,7 +110,7 @@ def test_patch(self): stm32pio.settings.config_default['project']['platformio_ini_patch_content'], msg="Patch content is not as expected") - def test_build_should_raise(self): + def test_build_should_handle_error(self): """ Build an empty project so PlatformIO should return non-zero code and we, in turn, should throw the exception """ @@ -118,8 +118,9 @@ def test_build_should_raise(self): save_on_destruction=False) project.pio_init() - with self.assertRaisesRegex(Exception, "PlatformIO build error", msg="Build exception hadn't been raised"): - project.pio_build() + self.assertEqual(project.pio_build(), -1, msg="Build error was not been indicated") + # with self.assertRaisesRegex(Exception, "PlatformIO build error", msg="Build exception hadn't been raised"): + # project.pio_build() def test_run_editor(self): """ @@ -396,7 +397,8 @@ def test_init(self): subprocess.run([PYTHON_EXEC, STM32PIO_MAIN_SCRIPT, 'init', '-d', str(FIXTURE_PATH), '-b', TEST_PROJECT_BOARD]) - self.assertTrue(FIXTURE_PATH.joinpath(stm32pio.settings.config_file_name).is_file(), msg=f"{stm32pio.settings.config}_file_name file hasn't been created") + self.assertTrue(FIXTURE_PATH.joinpath(stm32pio.settings.config_file_name).is_file(), + msg=f"{stm32pio.settings.config_file_name} file hasn't been created") config = configparser.ConfigParser() config.read(str(FIXTURE_PATH.joinpath(stm32pio.settings.config_file_name))) From 28386c55d95c077ed7959e74cf93a0898a758422 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Tue, 3 Dec 2019 18:52:28 +0300 Subject: [PATCH 23/26] v0.9: * fix test_save_config() test, improve some others * testing on Windows * simplify pio_build() * up .ioc file version * more docs, comments... --- CHANGELOG | 8 ++-- README.md | 19 ++++---- TODO.md | 5 +- .../stm32pio-test-project.ioc | 8 ++-- stm32pio/app.py | 17 +++---- stm32pio/lib.py | 42 ++++++++-------- stm32pio/tests/test.py | 48 +++++++++++++------ 7 files changed, 84 insertions(+), 63 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index e04336b..9549bac 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -110,6 +110,7 @@ stm32pio changelog: - New: STM32CubeMX is now started more silently (without a splash screen) - New: add integration and CLI tests (sort of) - New: testing with different Python versions using pyenv (3.6+ target) + - New: 'run_editor' test is now preliminary automatically check whether an editor is installed on the machine - New: more typing annotations - Fixed: the app has been failed to start as 'python app.py' (modify sys.path to fix) - Changed: 'main' function is now fully modular: can be run from anywhere with given CLI arguments (will be piped forward to be parsed via 'argparse') @@ -119,7 +120,8 @@ stm32pio changelog: - Changed: more clear description of steps to do for each user subcommand by the code - Changed: get rid of 'print' calls leaving only logging messages (easy to turn on/off the console output in the outer code) - Changed: re-imagined API behavior: where to raise exceptions, where to return values and so on - - Changed: more clean API, e.g. move out the board resolving procedure and so on - - Changed: test fixture is now moved out from the repo and is created temporarily on every test run - - Changed: actualized .ioc file for the latest STM32CubeMX version (5.3.0 at the moment) + - Changed: more clean API, e.g. move out the board resolving procedure from the 'pio_init' method and so on + - Changed: test fixture is now moved out from the repo and is deployed temporarily on every test run + - Changed: set-up and tear-down stages are now done using 'unittest' API + - Changed: actualized .ioc file for the latest STM32CubeMX version (5.4.0 at the moment) - Changed: improved help, docs, comments diff --git a/README.md b/README.md index 508ed76..3aeb876 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,11 @@ It uses STM32CubeMX to generate a HAL-framework based code and creates PlatformI ## Requirements: - For this app: - - Python 3.6+ + - Python 3.6 and above - For usage: - macOS, Linux, Windows - STM32CubeMX (all recent versions) with desired downloaded frameworks (F0, F1, etc.) - - Java CLI (JRE) (likely is already installed if STM32CubeMX works) + - Java CLI (JRE) (likely is already installed if the STM32CubeMX is working) - PlatformIO CLI (already presented if you have installed PlatformIO via some package manager or need to be installed as the command line extension from IDE) A general recommendation there would be to try to generate and build a code manually (via the CubeMX GUI and PlatformIO CLI or IDE) at least once before using stm32pio to make sure that all tools are working properly. @@ -50,7 +50,7 @@ On the first run stm32pio will create a config file `stm32pio.ini`, syntax of wh ```shell script $ stm32pio init -d path/to/project ``` -It may be useful to tweak some parameters before proceeding. The structure of the config is separated in two sections: `app` and `project`. Options of the first one is related to the global settings such as commands to invoke different instruments though they can be adjusted on the per-project base while the second section contains of project-related parameters. +It may be useful to tweak some parameters before proceeding. The structure of the config is separated in two sections: `app` and `project`. Options of the first one is related to the global settings such as commands to invoke different instruments though they can be adjusted on the per-project base while the second section contains of project-related parameters. See the comments in the `stm32pio/settings.py` file for parameters description. You can always run ```shell script @@ -58,6 +58,8 @@ $ python3 app.py --help ``` to see help on available commands. +You can also use stm32pio as a package and embed it in your own application. See `stm32pio/app.py` to see how to implement this. Basically you need to import `stm32pio.lib` module (where the main `Stm32pio` class resides), set up a logger and you are good to go. If you need higher-level API similar to the CLI version use `main()` function in `app.py` passing the same CLI arguments to it. + ## Example 1. Run CubeMX, choose MCU/board, do all necessary stuff @@ -87,7 +89,7 @@ to see help on available commands. ## Testing -Since ver. 0.45 there are some unit-tests in file `stm32pio/tests/test.py` (based on the unittest module). Run +Since ver. 0.45 there are some tests in file `stm32pio/tests/test.py` (based on the unittest module). Run ```shell script stm32pio-repo/ $ python3 -m unittest -b -v ``` @@ -95,11 +97,12 @@ or ```shell script stm32pio-repo/ $ python3 -m stm32pio.tests.test -b -v ``` -to test the app. It uses STM32F0 framework to generate and build a code from the test `stm32pio/tests/stm32pio-test-project/stm32pio-test-project.ioc` project file. +to test the app. It uses STM32F0 framework to generate and build a code from the test `stm32pio-test-project/stm32pio-test-project.ioc` project file. Please make sure that the test project folder is clean (i.e. contains only an .ioc file) before running the test. -For specific test you may use +For specific test suite or case you can use ```shell script -stm32pio-repo/ $ python3 -m unittest stm32pio.tests.test.TestCLI -b -v +stm32pio-repo/ $ python3 -m unittest stm32pio.tests.test.TestIntegration -b -v +stm32pio-repo/ $ python3 -m unittest stm32pio.tests.test.TestCLI.test_verbose -b -v ``` CI is hard to implement for all target OSes during the requirement to have all tools (PlatformIO, Java, CubeMX, etc.) installed during the test. For example, ST doesn't even provide a direct link to the CubeMX for downloading. @@ -107,7 +110,7 @@ CI is hard to implement for all target OSes during the requirement to have all t ## Restrictions - The tool doesn't check for different parameters compatibility, e.g. CPU frequency, memory sizes and so on. It simply eases your workflow with these 2 programs (PlatformIO and STM32CubeMX) a little bit. - - CubeMX middlewares don't support yet because it's hard to be prepared for every possible configuration. You need to manually adjust them to build appropriately. For example, FreeRTOS can be added via PlatformIO' `lib` feature or be directly compiled in its own directory using `lib_extra_dirs` option: + - CubeMX middlewares are not supported yet because it's hard to be prepared for every possible configuration. You need to manually adjust them to build appropriately. For example, FreeRTOS can be added via PlatformIO' `lib` feature or be directly compiled in its own directory using `lib_extra_dirs` option: ```ini lib_extra_dirs = Middlewares/Third_Party/FreeRTOS ``` diff --git a/TODO.md b/TODO.md index ff2826d..2a79040 100644 --- a/TODO.md +++ b/TODO.md @@ -26,6 +26,7 @@ - [x] Check `start_editor()` for different input - [x] Test on Python 3.6 - [ ] Test for `get_state()` (as sequence of states (see scratch.py)) - - [ ] Remake `get_state()` as property value (read-only getter with decorator) + - [x] Remake `get_state()` as property value (read-only getter with decorator) - [ ] If the project path is a unique identifier of the project in our code maybe we can remake `Stm32pio` class as a subclass of `pathlib.Path` and then reference it like `self` and not `self.project_path`. It would be more consistent also, as now `project_path` is perceived like any other config parameter that somehow is appeared to exist outside of a config instance but then it will be a core identifier, a truly `self` value. - - [ ] Try to invoke stm32pio as modeule (-m), from different paths... + - [x] Try to invoke stm32pio as module (-m), from different paths... + - [ ] Logs format test (see prepared regular expressions) diff --git a/stm32pio-test-project/stm32pio-test-project.ioc b/stm32pio-test-project/stm32pio-test-project.ioc index b005e21..834d9de 100644 --- a/stm32pio-test-project/stm32pio-test-project.ioc +++ b/stm32pio-test-project/stm32pio-test-project.ioc @@ -19,8 +19,8 @@ Mcu.PinsNb=6 Mcu.ThirdPartyNb=0 Mcu.UserConstants= Mcu.UserName=STM32F031K6Tx -MxCube.Version=5.3.0 -MxDb.Version=DB.5.0.30 +MxCube.Version=5.4.0 +MxDb.Version=DB.5.0.40 NVIC.ForceEnableDMAVector=true NVIC.HardFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false NVIC.NonMaskableInt_IRQn=true\:0\:0\:false\:false\:true\:false\:false @@ -59,7 +59,7 @@ PF0-OSC_IN.Locked=true PF0-OSC_IN.Mode=HSE-External-Clock-Source PF0-OSC_IN.Signal=RCC_OSC_IN PinOutPanel.RotationAngle=0 -ProjectManager.AskForMigrate=false +ProjectManager.AskForMigrate=true ProjectManager.BackupPrevious=false ProjectManager.CompilerOptimize=6 ProjectManager.ComputerToolchain=false @@ -68,7 +68,7 @@ ProjectManager.CustomerFirmwarePackage= ProjectManager.DefaultFWLocation=true ProjectManager.DeletePrevious=true ProjectManager.DeviceId=STM32F031K6Tx -ProjectManager.FirmwarePackage=STM32Cube FW_F0 V1.10.1 +ProjectManager.FirmwarePackage=STM32Cube FW_F0 V1.11.0 ProjectManager.FreePins=false ProjectManager.HalAssertFull=false ProjectManager.HeapSize=0x200 diff --git a/stm32pio/app.py b/stm32pio/app.py index 9d1bc82..f7fd620 100755 --- a/stm32pio/app.py +++ b/stm32pio/app.py @@ -71,24 +71,24 @@ def main(sys_argv: list = sys.argv[1:]) -> int: """ args = parse_args(sys_argv) - # Show help and exit if no arguments were given - if args is None or args.subcommand is None: - print("\nNo arguments were given, exiting...") # TODO: replace 'print' with 'logger.info' - return 0 # Logger instance goes through the whole program. # Currently only 2 levels of verbosity through the '-v' option are counted (INFO (default) and DEBUG (-v)) logger = logging.getLogger('stm32pio') handler = logging.StreamHandler() - if args.verbose: + logger.addHandler(handler) + if args is not None and args.verbose: logger.setLevel(logging.DEBUG) handler.setFormatter(logging.Formatter("%(levelname)-8s %(funcName)-26s %(message)s")) - logger.addHandler(handler) logger.debug("debug logging enabled") - else: + elif args is not None: logger.setLevel(logging.INFO) handler.setFormatter(logging.Formatter("%(levelname)-8s %(message)s")) - logger.addHandler(handler) + else: + logger.setLevel(logging.INFO) + handler.setFormatter(logging.Formatter("%(message)s")) + logger.info("\nNo arguments were given, exiting...") + return 0 # Main routine import stm32pio.lib # import the module after sys.path modification @@ -99,6 +99,7 @@ def main(sys_argv: list = sys.argv[1:]) -> int: if not args.board: logger.warning("STM32 PlatformIO board is not specified, it will be needed on PlatformIO project " "creation") + logger.info('project has been initialized. You can now edit stm32pio.ini config file') if args.editor: project.start_editor(args.editor) diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 4c1ae8f..9666108 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -109,7 +109,7 @@ def save_config(self) -> int: logger.debug("stm32pio.ini config file has been saved") return 0 except Exception as e: - logger.warning(f"Cannot save config: {e}") + logger.warning(f"cannot save config: {e}") if logger.getEffectiveLevel() <= logging.DEBUG: traceback.print_exception(*sys.exc_info()) return -1 @@ -121,8 +121,8 @@ def state(self) -> ProjectState: Property returning the current state of the project. Calculated at every request. """ - logger.debug("Calculating the project state...") - logger.debug(f"Project content: {[item.name for item in self.project_path.iterdir()]}") + logger.debug("calculating the project state...") + logger.debug(f"project content: {[item.name for item in self.project_path.iterdir()]}") # Fill the ordered dictionary with conditions results states_conditions = collections.OrderedDict() @@ -154,7 +154,7 @@ def state(self) -> ProjectState: # propagation of this message if logger.getEffectiveLevel() <= logging.DEBUG: states_info_str = '\n'.join(f"{state.name:20}{conditions_results[state.value-1]}" for state in ProjectState) - logger.debug(f"Determined states:\n{states_info_str}") + logger.debug(f"determined states:\n{states_info_str}") # Search for a consecutive raw of 1's and find the last of them. For example, if the array is # [1,1,0,1,0,0] @@ -185,7 +185,7 @@ def _find_ioc_file(self) -> pathlib.Path: if ioc_file: return pathlib.Path(ioc_file).resolve() else: - logger.debug("Searching for any .ioc file...") + logger.debug("searching for any .ioc file...") candidates = list(self.project_path.glob('*.ioc')) if len(candidates) == 0: raise FileNotFoundError("Not found: CubeMX project .ioc file") @@ -193,7 +193,7 @@ def _find_ioc_file(self) -> pathlib.Path: logger.debug(f"{candidates[0].name} is selected") return candidates[0] else: - logger.warning(f"There are multiple .ioc files, {candidates[0].name} is selected") + logger.warning(f"there are multiple .ioc files, {candidates[0].name} is selected") return candidates[0] @@ -203,7 +203,7 @@ def _load_settings_file(self) -> configparser.ConfigParser: ones """ - logger.debug(f"Searching for {stm32pio.settings.config_file_name}...") + logger.debug(f"searching for {stm32pio.settings.config_file_name}...") stm32pio_ini = self.project_path.joinpath(stm32pio.settings.config_file_name) config = configparser.ConfigParser() @@ -216,7 +216,7 @@ def _load_settings_file(self) -> configparser.ConfigParser: # Put away unnecessary processing as the string still will be formed even if the logging level doesn't allow # propagation of this message if logger.getEffectiveLevel() <= logging.DEBUG: - debug_str = 'Resolved config:' + debug_str = 'resolved config:' for section in config.sections(): debug_str += f"\n=========== {section} ===========\n" for value in config.items(section): @@ -236,7 +236,7 @@ def _resolve_project_path(dirty_path: str) -> pathlib.Path: """ resolved_path = pathlib.Path(dirty_path).expanduser().resolve() if not resolved_path.exists(): - raise FileNotFoundError(f"Not found: {resolved_path}") + raise FileNotFoundError(f"not found: {resolved_path}") else: return resolved_path @@ -258,6 +258,7 @@ def _resolve_board(self, board: str) -> str: if board not in result.stdout.split(): raise Exception("wrong PlatformIO STM32 board. Run 'platformio boards' for possible names") else: + logger.debug(f"PlatformIO board {board} was found") return board else: raise Exception("failed to search for PlatformIO boards") @@ -305,7 +306,7 @@ def pio_init(self) -> int: # TODO: check whether there is already a platformio.ini file and warn in this case - # TODO: move out to settings a 'framework' option + # TODO: move out to config a 'framework' option and to settings a 'platformio.ini' file name command_arr = [self.config.get('app', 'platformio_cmd'), 'init', '-d', str(self.project_path), '-b', self.config.get('project', 'board'), '-O', 'framework=stm32cube'] if logger.getEffectiveLevel() > logging.DEBUG: @@ -333,11 +334,10 @@ def patch(self) -> None: logger.debug("patching 'platformio.ini' file...") - # TODO: check whether there is already a patched platformio.ini file, warn in this case and do not proceed - platformio_ini_file = self.project_path.joinpath('platformio.ini') if platformio_ini_file.is_file(): with platformio_ini_file.open(mode='a') as f: + # TODO: check whether there is already a patched platformio.ini file, warn in this case and do not proceed f.write(self.config.get('project', 'platformio_ini_patch_content')) logger.info("'platformio.ini' has been patched") else: @@ -364,28 +364,24 @@ def start_editor(self, editor_command: str) -> int: logger.info(f"starting an editor '{editor_command}'...") try: - result = subprocess.run([editor_command, str(self.project_path)], check=True) + # result = subprocess.run([editor_command, str(self.project_path)], check=True) + # TODO: need to clarify + result = subprocess.run(f"{editor_command} {str(self.project_path)}", check=True, shell=True) return result.returncode if result.returncode != -1 else 0 except subprocess.CalledProcessError as e: - logger.error(f"Failed to start the editor {editor_command}: {e.stderr}") + logger.error(f"failed to start the editor {editor_command}: {e.stderr}") return -1 def pio_build(self) -> int: """ - Initiate a build of the PlatformIO project by the PlatformIO ('run' command) + Initiate a build of the PlatformIO project by the PlatformIO ('run' command). PlatformIO prints error message + by itself to the STDERR so there is a no need to catch it and outputs by us Returns: 0 if success, raise an exception otherwise """ - # TODO: do we need this check? PlatformIO can handle it by itself - if self.project_path.joinpath('platformio.ini').is_file(): - logger.info("starting PlatformIO build...") - else: - logger.error("no 'platformio.ini' file, build is impossible") - return -1 - command_arr = [self.config.get('app', 'platformio_cmd'), 'run', '-d', str(self.project_path)] if logger.getEffectiveLevel() > logging.DEBUG: command_arr.append('--silent') @@ -400,7 +396,7 @@ def pio_build(self) -> int: def clean(self) -> None: """ - Clean-up the project folder and preserve only an '.ioc' file + Clean-up the project folder preserving only an '.ioc' file """ for child in self.project_path.iterdir(): diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index 4c587af..693ef73 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -35,9 +35,10 @@ # Instantiate a temporary folder on every fixture run. It is used across all tests and is deleted on shutdown temp_dir = tempfile.TemporaryDirectory() FIXTURE_PATH = pathlib.Path(temp_dir.name).joinpath(TEST_PROJECT_PATH.name) +print(f"Temp fixture path: {FIXTURE_PATH}") -def tearDownModule(): - temp_dir.cleanup() +# def tearDownModule(): +# temp_dir.cleanup() class CustomTestCase(unittest.TestCase): @@ -47,7 +48,8 @@ class CustomTestCase(unittest.TestCase): def setUp(self): """ - Copy the test project from the repo to our temp directory + Copy the test project from the repo to our temp directory. WARNING: make sure the test project folder is clean + (i.e. contains only an .ioc file) before running the test """ shutil.rmtree(FIXTURE_PATH, ignore_errors=True) shutil.copytree(TEST_PROJECT_PATH, FIXTURE_PATH) @@ -112,15 +114,16 @@ def test_patch(self): def test_build_should_handle_error(self): """ - Build an empty project so PlatformIO should return non-zero code and we, in turn, should throw the exception + Build an empty project so PlatformIO should return the error """ project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}, save_on_destruction=False) project.pio_init() - self.assertEqual(project.pio_build(), -1, msg="Build error was not been indicated") - # with self.assertRaisesRegex(Exception, "PlatformIO build error", msg="Build exception hadn't been raised"): - # project.pio_build() + with self.assertLogs(level='ERROR') as logs: + self.assertNotEqual(project.pio_build(), 0, msg="Build error was not been indicated") + self.assertTrue(next((True for item in logs.output if "PlatformIO build error" in item), False), + msg="Error message does not match") def test_run_editor(self): """ @@ -179,10 +182,9 @@ def test_init_path_not_found_should_raise(self): path_does_not_exist_name = 'does_not_exist' path_does_not_exist = FIXTURE_PATH.joinpath(path_does_not_exist_name) - # 'cm' is for context manager - with self.assertRaises(FileNotFoundError, msg="FileNotFoundError was not raised") as cm: + with self.assertRaisesRegex(FileNotFoundError, path_does_not_exist_name, + msg="FileNotFoundError was not raised or doesn't contain a description"): stm32pio.lib.Stm32pio(path_does_not_exist, save_on_destruction=False) - self.assertIn(path_does_not_exist_name, str(cm.exception), msg="Exception doesn't contain a description") def test_save_config(self): """ @@ -219,27 +221,34 @@ def test_config_priorities(self): Test the compliance with priorities when reading the parameters """ - custom_content = "SOME CUSTOM CONTENT" + config_parameter_user_value = "SOME CUSTOM CONTENT" + cli_parameter_user_value = 'nucleo_f429zi' # Create test config config = configparser.ConfigParser() config.read_dict({ 'project': { - 'platformio_ini_patch_content': custom_content + 'platformio_ini_patch_content': config_parameter_user_value, + 'board': TEST_PROJECT_BOARD } }) # ... save it with FIXTURE_PATH.joinpath(stm32pio.settings.config_file_name).open(mode='w') as config_file: config.write(config_file) - # On project creation we should get the CLI-provided value as superseding to the saved one - project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}, + # On project creation we should interpret the CLI-provided values as superseding to the saved ones and + # saved ones as superseding to the default ones + project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': cli_parameter_user_value}, save_on_destruction=False) project.pio_init() project.patch() + # Actually, we can parse platformio.ini via configparser but this is simpler after_patch_content = FIXTURE_PATH.joinpath('platformio.ini').read_text() - self.assertIn(custom_content, after_patch_content, msg="Patch content is not from CLI argument") + self.assertIn(config_parameter_user_value, after_patch_content, + msg="User config parameter has not been prioritized over the default one") + self.assertIn(cli_parameter_user_value, after_patch_content, + msg="User CLI parameter has not been prioritized over the saved one") def test_build(self): """ @@ -363,6 +372,15 @@ def test_no_ioc_file_should_log_error(self): self.assertTrue(next((True for item in logs.output if "CubeMX project .ioc file" in item), False), msg="'ERROR' logging message hasn't been printed") + # TODO: test logs format + # non-verbose: + # ^(?=(DEBUG|INFO|WARNING|ERROR|CRITICAL) {0,4})(?=.{8} [^ ]) + # + # verbose: + # ^(?=(DEBUG|INFO|WARNING|ERROR|CRITICAL) {0,4})(?=.{8} .{26} [^ ]) + # + # we can actually get possible function names in that 26-width block + def test_verbose(self): """ Run as subprocess to capture the full output. Check for both 'DEBUG' logging messages and STM32CubeMX CLI output From f59b6279f2b3705c8dca4f762d19603920bd86f7 Mon Sep 17 00:00:00 2001 From: usserr Date: Tue, 3 Dec 2019 22:04:00 +0300 Subject: [PATCH 24/26] v0.9: * continue to test on different platforms --- stm32pio/lib.py | 4 ++-- stm32pio/tests/test.py | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 9666108..471ead3 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -364,9 +364,9 @@ def start_editor(self, editor_command: str) -> int: logger.info(f"starting an editor '{editor_command}'...") try: - # result = subprocess.run([editor_command, str(self.project_path)], check=True) + result = subprocess.run([editor_command, str(self.project_path)], check=True) # TODO: need to clarify - result = subprocess.run(f"{editor_command} {str(self.project_path)}", check=True, shell=True) + # result = subprocess.run(f"{editor_command} {str(self.project_path)}", check=True, shell=True) return result.returncode if result.returncode != -1 else 0 except subprocess.CalledProcessError as e: logger.error(f"failed to start the editor {editor_command}: {e.stderr}") diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index 693ef73..fd3201b 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -31,14 +31,11 @@ # proceeding) TEST_PROJECT_BOARD = 'nucleo_f031k6' - # Instantiate a temporary folder on every fixture run. It is used across all tests and is deleted on shutdown temp_dir = tempfile.TemporaryDirectory() FIXTURE_PATH = pathlib.Path(temp_dir.name).joinpath(TEST_PROJECT_PATH.name) -print(f"Temp fixture path: {FIXTURE_PATH}") +print(f"Temp test fixture path: {FIXTURE_PATH}") -# def tearDownModule(): -# temp_dir.cleanup() class CustomTestCase(unittest.TestCase): From 8774f3d32452e1387aa16f46e120c3605b2f101e Mon Sep 17 00:00:00 2001 From: ussserrr Date: Tue, 3 Dec 2019 22:13:25 +0300 Subject: [PATCH 25/26] v0.9: * add 'coverage' instructions --- stm32pio/tests/test.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index fd3201b..e83b78c 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -1,6 +1,10 @@ """ -Note: pyenv is recommended to use for testing with different Python versions +Note: 'pyenv' is recommended to use for testing with different Python versions https://www.tecmint.com/pyenv-install-and-manage-multiple-python-versions-in-linux/ + +To get test coverage use 'coverage': + $ coverage run -m stm32pio.tests.test -b + $ coverage html """ import unittest From 8c563484f9b7d60e557c163c03ffdb3a11476320 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Wed, 4 Dec 2019 15:08:57 +0300 Subject: [PATCH 26/26] v0.9: release --- README.md | 16 +++++++++------- TODO.md | 6 ++++-- stm32pio/lib.py | 20 ++++++++++---------- stm32pio/tests/test.py | 41 +++++++++++++++++++++++++++-------------- 4 files changed, 50 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 3aeb876..d860d95 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # stm32pio Small cross-platform Python app that can create and update [PlatformIO](https://platformio.org) projects from [STM32CubeMX](https://www.st.com/en/development-tools/stm32cubemx.html) `.ioc` files. -It uses STM32CubeMX to generate a HAL-framework based code and creates PlatformIO project with the compatible `stm32cube` framework specified. +It uses STM32CubeMX to generate a HAL-framework based code and alongside creates PlatformIO project with the compatible `stm32cube` framework specified. ![Logo](/screenshots/logo.png) @@ -23,7 +23,7 @@ It uses STM32CubeMX to generate a HAL-framework based code and creates PlatformI - Java CLI (JRE) (likely is already installed if the STM32CubeMX is working) - PlatformIO CLI (already presented if you have installed PlatformIO via some package manager or need to be installed as the command line extension from IDE) -A general recommendation there would be to try to generate and build a code manually (via the CubeMX GUI and PlatformIO CLI or IDE) at least once before using stm32pio to make sure that all tools are working properly. +A general recommendation there would be to try to generate and build a code manually (via the CubeMX GUI and PlatformIO CLI or IDE) at least once before using stm32pio to make sure that all tools are working properly without any "glue". ## Installation @@ -50,7 +50,7 @@ On the first run stm32pio will create a config file `stm32pio.ini`, syntax of wh ```shell script $ stm32pio init -d path/to/project ``` -It may be useful to tweak some parameters before proceeding. The structure of the config is separated in two sections: `app` and `project`. Options of the first one is related to the global settings such as commands to invoke different instruments though they can be adjusted on the per-project base while the second section contains of project-related parameters. See the comments in the `stm32pio/settings.py` file for parameters description. +It may be useful to tweak some parameters before proceeding. The structure of the config is separated in two sections: `app` and `project`. Options of the first one is related to the global settings such as commands to invoke different instruments though they can be adjusted on the per-project base while the second section contains of project-related parameters. See the comments in the [`settings.py`](/stm32pio/settings.py) file for parameters description. You can always run ```shell script @@ -58,7 +58,7 @@ $ python3 app.py --help ``` to see help on available commands. -You can also use stm32pio as a package and embed it in your own application. See `stm32pio/app.py` to see how to implement this. Basically you need to import `stm32pio.lib` module (where the main `Stm32pio` class resides), set up a logger and you are good to go. If you need higher-level API similar to the CLI version use `main()` function in `app.py` passing the same CLI arguments to it. +You can also use stm32pio as a package and embed it in your own application. See [`app.py`](/stm32pio/app.py) to see how to implement this. Basically you need to import `stm32pio.lib` module (where the main `Stm32pio` class resides), set up a logger and you are good to go. If you need higher-level API similar to the CLI version use `main()` function in `app.py` passing the same CLI arguments to it. ## Example @@ -89,7 +89,7 @@ You can also use stm32pio as a package and embed it in your own application. See ## Testing -Since ver. 0.45 there are some tests in file `stm32pio/tests/test.py` (based on the unittest module). Run +Since ver. 0.45 there are some tests in file [`test.py`](/stm32pio/tests/test.py) (based on the unittest module). Run ```shell script stm32pio-repo/ $ python3 -m unittest -b -v ``` @@ -97,7 +97,7 @@ or ```shell script stm32pio-repo/ $ python3 -m stm32pio.tests.test -b -v ``` -to test the app. It uses STM32F0 framework to generate and build a code from the test `stm32pio-test-project/stm32pio-test-project.ioc` project file. Please make sure that the test project folder is clean (i.e. contains only an .ioc file) before running the test. +to test the app. It uses STM32F0 framework to generate and build a code from the test [`stm32pio-test-project.ioc`](/stm32pio-test-project/stm32pio-test-project.ioc) project file. Please make sure that the test project folder is clean (i.e. contains only an .ioc file) before running the test. For specific test suite or case you can use ```shell script @@ -105,7 +105,9 @@ stm32pio-repo/ $ python3 -m unittest stm32pio.tests.test.TestIntegration -b -v stm32pio-repo/ $ python3 -m unittest stm32pio.tests.test.TestCLI.test_verbose -b -v ``` -CI is hard to implement for all target OSes during the requirement to have all tools (PlatformIO, Java, CubeMX, etc.) installed during the test. For example, ST doesn't even provide a direct link to the CubeMX for downloading. +While testing was performed on different Python and OS versions, some older Windows versions had shown some 'glitches' and instability. [WinError 5] and others had appeared on such tests like `test_run_edtor` and on `tempfile` clean-up processes. So be ready to face off with them. + +CI is hard to implement for all target OSes during the requirement to have all the tools (PlatformIO, Java, CubeMX, etc.) installed during the test. For example, ST doesn't even provide a direct link to the CubeMX for downloading. ## Restrictions diff --git a/TODO.md b/TODO.md index 2a79040..8c618e8 100644 --- a/TODO.md +++ b/TODO.md @@ -21,12 +21,14 @@ - [x] Smart `start_editor` test (detect editors in system, maybe use unittest `skipIf` decorator) - [x] `init` command - [x] New argparse algo cause now we have config file - - [ ] Update `.ioc` file + - [x] Update `.ioc` file - [x] `str(path)` -> `path` were possible - [x] Check `start_editor()` for different input - - [x] Test on Python 3.6 + - [x] Test on Python 3.6 (pyenv) - [ ] Test for `get_state()` (as sequence of states (see scratch.py)) - [x] Remake `get_state()` as property value (read-only getter with decorator) - [ ] If the project path is a unique identifier of the project in our code maybe we can remake `Stm32pio` class as a subclass of `pathlib.Path` and then reference it like `self` and not `self.project_path`. It would be more consistent also, as now `project_path` is perceived like any other config parameter that somehow is appeared to exist outside of a config instance but then it will be a core identifier, a truly `self` value. - [x] Try to invoke stm32pio as module (-m), from different paths... - [ ] Logs format test (see prepared regular expressions) + - [ ] Some non-intrusive installation test (may be some sort of temp virtualenv...) + - [ ] We look for some snippets of strings in logs and output for the testing code but we hard-code them and this is not good diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 471ead3..6b18ae6 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -100,7 +100,7 @@ def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction def save_config(self) -> int: """ - Tries to save the configparser config to file and gently log if error occurs + Tries to save the configparser config to file and gently log if any error occurs """ try: @@ -109,7 +109,7 @@ def save_config(self) -> int: logger.debug("stm32pio.ini config file has been saved") return 0 except Exception as e: - logger.warning(f"cannot save config: {e}") + logger.warning(f"cannot save the config: {e}") if logger.getEffectiveLevel() <= logging.DEBUG: traceback.print_exception(*sys.exc_info()) return -1 @@ -187,7 +187,7 @@ def _find_ioc_file(self) -> pathlib.Path: else: logger.debug("searching for any .ioc file...") candidates = list(self.project_path.glob('*.ioc')) - if len(candidates) == 0: + if len(candidates) == 0: # good candidate for the new Python 3.8 assignment expressions feature :) raise FileNotFoundError("Not found: CubeMX project .ioc file") elif len(candidates) == 1: logger.debug(f"{candidates[0].name} is selected") @@ -337,7 +337,8 @@ def patch(self) -> None: platformio_ini_file = self.project_path.joinpath('platformio.ini') if platformio_ini_file.is_file(): with platformio_ini_file.open(mode='a') as f: - # TODO: check whether there is already a patched platformio.ini file, warn in this case and do not proceed + # TODO: check whether there is already a patched platformio.ini file, warn in this case and do not + # proceed f.write(self.config.get('project', 'platformio_ini_patch_content')) logger.info("'platformio.ini' has been patched") else: @@ -354,8 +355,7 @@ def start_editor(self, editor_command: str) -> int: Start the editor specified by 'editor_command' with the project opened Args: - editor_command: editor command as we start it in the terminal. Note that only single-word command is - currently supported + editor_command: editor command as we start it in the terminal Returns: return code of the editor on success, -1 otherwise @@ -364,9 +364,9 @@ def start_editor(self, editor_command: str) -> int: logger.info(f"starting an editor '{editor_command}'...") try: - result = subprocess.run([editor_command, str(self.project_path)], check=True) - # TODO: need to clarify - # result = subprocess.run(f"{editor_command} {str(self.project_path)}", check=True, shell=True) + # Works unstable on some Windows 7 systems, but correct on latest Win7 and Win10... + # result = subprocess.run([editor_command, str(self.project_path)], check=True) + result = subprocess.run(f"{editor_command} {str(self.project_path)}", check=True, shell=True) return result.returncode if result.returncode != -1 else 0 except subprocess.CalledProcessError as e: logger.error(f"failed to start the editor {editor_command}: {e.stderr}") @@ -376,7 +376,7 @@ def start_editor(self, editor_command: str) -> int: def pio_build(self) -> int: """ Initiate a build of the PlatformIO project by the PlatformIO ('run' command). PlatformIO prints error message - by itself to the STDERR so there is a no need to catch it and outputs by us + by itself to the STDERR so there is no need to catch it and outputs by us Returns: 0 if success, raise an exception otherwise diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index e83b78c..3730f94 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -1,5 +1,5 @@ """ -Note: 'pyenv' is recommended to use for testing with different Python versions +'pyenv' is recommended to use for testing with different Python versions https://www.tecmint.com/pyenv-install-and-manage-multiple-python-versions-in-linux/ To get test coverage use 'coverage': @@ -65,12 +65,13 @@ def tearDown(self): class TestUnit(CustomTestCase): """ Test the single method. As we at some point decided to use a class instead of the set of scattered functions we need - to do some preparations for almost every test (e.g. instantiate the class, create the PlatformIO project, etc.) + to do some preparations for almost every test (e.g. instantiate the class, create the PlatformIO project, etc.), + though """ def test_generate_code(self): """ - Check whether files and folders have been created + Check whether files and folders have been created by STM32CubeMX """ project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}, save_on_destruction=False) @@ -84,7 +85,8 @@ def test_generate_code(self): def test_pio_init(self): """ - Consider that existence of 'platformio.ini' file showing a successful PlatformIO project initialization + Consider that existence of 'platformio.ini' file showing a successful PlatformIO project initialization. The + last one has another traces those can be checked too but we are interested only in a 'platformio.ini' anyway """ project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}, save_on_destruction=False) @@ -92,6 +94,7 @@ def test_pio_init(self): self.assertEqual(result, 0, msg="Non-zero return code") self.assertTrue(FIXTURE_PATH.joinpath('platformio.ini').is_file(), msg="platformio.ini is not there") + # TODO: check that platformio.ini is a correct configparser file and is not empty def test_patch(self): """ @@ -122,7 +125,7 @@ def test_build_should_handle_error(self): project.pio_init() with self.assertLogs(level='ERROR') as logs: - self.assertNotEqual(project.pio_build(), 0, msg="Build error was not been indicated") + self.assertNotEqual(project.pio_build(), 0, msg="Build error was not indicated") self.assertTrue(next((True for item in logs.output if "PlatformIO build error" in item), False), msg="Error message does not match") @@ -192,7 +195,6 @@ def test_save_config(self): Explicitly save the config to file and look did that actually happen and whether all the information was preserved """ - # 'board' is non-default, 'project'-section parameter project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}, save_on_destruction=False) @@ -267,10 +269,9 @@ def test_build(self): def test_regenerate_code(self): """ - Simulate new project creation, its changing and CubeMX code re-generation (for example, after adding new - hardware features and some new files) + Simulate a new project creation, its changing and CubeMX code re-generation (for example, after adding new + hardware features and some new files by a user) """ - project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}, save_on_destruction=False) @@ -332,10 +333,11 @@ def test_new(self): """ Successful build is the best indicator that all went right so we use '--with-build' option """ + return_code = stm32pio.app.main(sys_argv=['new', '-d', str(FIXTURE_PATH), '-b', TEST_PROJECT_BOARD, '--with-build']) - self.assertEqual(return_code, 0, msg="Non-zero return code") + # .ioc file should be preserved self.assertTrue(FIXTURE_PATH.joinpath(f"{FIXTURE_PATH.name}.ioc").is_file(), msg="Missing .ioc file") @@ -355,6 +357,10 @@ def test_generate(self): self.assertTrue(FIXTURE_PATH.joinpath(f"{FIXTURE_PATH.name}.ioc").is_file(), msg="Missing .ioc file") def test_incorrect_path_should_log_error(self): + """ + We should see an error log message and non-zero return code + """ + path_not_exist = pathlib.Path('path/does/not/exist') with self.assertLogs(level='ERROR') as logs: @@ -364,6 +370,10 @@ def test_incorrect_path_should_log_error(self): msg="'ERROR' logging message hasn't been printed") def test_no_ioc_file_should_log_error(self): + """ + We should see an error log message and non-zero return code + """ + dir_with_no_ioc_file = FIXTURE_PATH.joinpath('dir.with.no.ioc.file') dir_with_no_ioc_file.mkdir(exist_ok=False) @@ -374,7 +384,7 @@ def test_no_ioc_file_should_log_error(self): msg="'ERROR' logging message hasn't been printed") # TODO: test logs format - # non-verbose: + # non-verbose should satisfy: # ^(?=(DEBUG|INFO|WARNING|ERROR|CRITICAL) {0,4})(?=.{8} [^ ]) # # verbose: @@ -391,8 +401,9 @@ def test_verbose(self): encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE) self.assertEqual(result.returncode, 0, msg="Non-zero return code") - # Somehow stderr and not stdout contains the actual output - self.assertIn('DEBUG', result.stderr, msg="Verbose logging output hasn't been enabled on stderr") + # Somehow stderr and not stdout contains the actual output but we check both + self.assertTrue('DEBUG' in result.stderr or 'DEBUG' in result.stdout, + msg="Verbose logging output hasn't been enabled on stderr") self.assertIn('Starting STM32CubeMX', result.stdout, msg="STM32CubeMX didn't print its logs") def test_non_verbose(self): @@ -414,7 +425,9 @@ def test_init(self): Check for config creation and parameters presence """ - subprocess.run([PYTHON_EXEC, STM32PIO_MAIN_SCRIPT, 'init', '-d', str(FIXTURE_PATH), '-b', TEST_PROJECT_BOARD]) + result = subprocess.run([PYTHON_EXEC, STM32PIO_MAIN_SCRIPT, 'init', '-d', str(FIXTURE_PATH), + '-b', TEST_PROJECT_BOARD]) + self.assertEqual(result.returncode, 0, msg="Non-zero return code") self.assertTrue(FIXTURE_PATH.joinpath(stm32pio.settings.config_file_name).is_file(), msg=f"{stm32pio.settings.config_file_name} file hasn't been created")