diff --git a/.github/workflows/fuzzy_compile_weekly.yml b/.github/workflows/fuzzy_compile_weekly.yml index 396cf709..635e2043 100644 --- a/.github/workflows/fuzzy_compile_weekly.yml +++ b/.github/workflows/fuzzy_compile_weekly.yml @@ -97,7 +97,7 @@ jobs: - name: Generate Sophios Python API Workflows (*.py -> *.wic) if: always() - run: cd sophios/ && python -c 'import sophios; import sophios.plugins; sophios.plugins.blindly_execute_python_workflows()' + run: cd sophios/ && pytest -k test_compile_python_workflows - name: Generate Sophios Validation Jsonschema if: always() @@ -108,7 +108,7 @@ jobs: # WIC Python API workflows as well as the WIC Python API itself. - name: Validate Sophios Python API Workflows (*.py -> *.wic) if: always() - run: cd sophios/ && python -c 'import sophios; import sophios.plugins; sophios.plugins.blindly_execute_python_workflows()' + run: cd sophios/ && pytest -k test_compile_python_workflows # Since a randomly chosen subschema is used every time, repeat 10X for more coverage. diff --git a/.github/workflows/lint_and_test.yml b/.github/workflows/lint_and_test.yml index f5548654..2b55f62f 100644 --- a/.github/workflows/lint_and_test.yml +++ b/.github/workflows/lint_and_test.yml @@ -179,7 +179,7 @@ jobs: - name: Generate Sophios Python API Workflows (*.py -> *.wic) if: always() - run: cd sophios/ && python -c 'import sophios; import sophios.plugins; sophios.plugins.blindly_execute_python_workflows()' + run: cd sophios/ && pytest -k test_compile_python_workflows - name: Generate Sophios Validation Jsonschema if: always() @@ -190,7 +190,7 @@ jobs: # Sophios Python API workflows as well as the Sophios Python API itself. - name: Validate sophios Python API Workflows (*.py -> *.wic) if: always() - run: cd sophios/ && python -c 'import sophios; import sophios.plugins; sophios.plugins.blindly_execute_python_workflows()' + run: cd sophios/ && pytest -k test_compile_python_workflows - name: Build Documentation if: always() diff --git a/.github/workflows/lint_and_test_macos.yml b/.github/workflows/lint_and_test_macos.yml index 5307a693..22c9ba38 100644 --- a/.github/workflows/lint_and_test_macos.yml +++ b/.github/workflows/lint_and_test_macos.yml @@ -103,7 +103,7 @@ jobs: - name: Generate Sophios Python API Workflows (*.py -> *.wic) if: always() - run: cd sophios/ && python -c 'import sophios; import sophios.plugins; sophios.plugins.blindly_execute_python_workflows()' + run: cd sophios/ && pytest -k test_compile_python_workflows - name: Generate Sophios Validation Jsonschema if: always() @@ -114,7 +114,7 @@ jobs: # Sophios Python API workflows as well as the Sophios Python API itself. - name: Validate Sophios Python API Workflows (*.py -> *.wic) if: always() - run: cd sophios/ && python -c 'import sophios; import sophios.plugins; sophios.plugins.blindly_execute_python_workflows()' + run: cd sophios/ && pytest -k test_compile_python_workflows - name: Build Documentation if: always() diff --git a/.github/workflows/run_workflows.yml b/.github/workflows/run_workflows.yml index f1ffb20a..8a39f4e0 100644 --- a/.github/workflows/run_workflows.yml +++ b/.github/workflows/run_workflows.yml @@ -157,7 +157,7 @@ jobs: - name: Generate Sophios Python API Workflows (*.py -> *.wic) if: always() - run: cd sophios/ && python -c 'import sophios; import sophios.plugins; sophios.plugins.blindly_execute_python_workflows()' + run: cd sophios/ && pytest -k test_compile_python_workflows - name: Generate Sophios Validation Jsonschema if: always() @@ -168,7 +168,7 @@ jobs: # WIC Python API workflows as well as the WIC Python API itself. - name: Validate Sophios Python API Workflows (*.py -> *.wic) if: always() - run: cd sophios/ && python -c 'import sophios; import sophios.plugins; sophios.plugins.blindly_execute_python_workflows()' + run: cd sophios/ && pytest -k test_compile_python_workflows - name: cwl-docker-extract (i.e. recursively docker pull) if: always() diff --git a/.github/workflows/run_workflows_weekly.yml b/.github/workflows/run_workflows_weekly.yml index bc86555a..20f041f1 100644 --- a/.github/workflows/run_workflows_weekly.yml +++ b/.github/workflows/run_workflows_weekly.yml @@ -120,7 +120,7 @@ jobs: - name: Generate Sophios Python API Workflows (*.py -> *.wic) if: always() - run: cd sophios/ && python -c 'import sophios; import sophios.plugins; sophios.plugins.blindly_execute_python_workflows()' + run: cd sophios/ && pytest -k test_compile_python_workflows - name: Generate Sophios Validation Jsonschema if: always() @@ -131,7 +131,7 @@ jobs: # Sophios Python API workflows as well as the WIC Python API itself. - name: Validate Sophios Python API Workflows (*.py -> *.wic) if: always() - run: cd sophios/ && python -c 'import sophios; import sophios.plugins; sophios.plugins.blindly_execute_python_workflows()' + run: cd sophios/ && pytest -k test_compile_python_workflows - name: cwl-docker-extract (i.e. recursively docker pull) if: always() diff --git a/src/sophios/plugins.py b/src/sophios/plugins.py index a1bb08ed..bada3845 100644 --- a/src/sophios/plugins.py +++ b/src/sophios/plugins.py @@ -466,69 +466,3 @@ def get_yml_paths(config: Json) -> Dict[str, Dict[str, Path]]: def get_py_paths(config: Json) -> Dict[str, Dict[str, Path]]: return get_workflow_paths(config, 'py') - - -def blindly_execute_python_workflows() -> None: - """This function imports (read: blindly executes) all python files in 'search_paths_wic' - The python files are assumed to have a top-level workflow() function - which returns a sophios.api.pythonapi.Workflow object. - The python files should NOT call the .run() method! - (from any code path that is automatically executed on import) - """ - # I hope u like Remote Code Execution vulnerabilities! - # See https://en.wikipedia.org/wiki/Arithmetical_hierarchy - from sophios.api import pythonapi # pylint: disable=C0415:import-outside-toplevel - # Since this is completely different test path we have to copy - # default .txt files to default global_config.json - config_file = Path().home()/'wic'/'global_config.json' - global_config = io.read_config_from_disk(config_file) - pythonapi.global_config = get_tools_cwl(global_config) # Use path fallback in the CI - paths = get_py_paths(global_config) - # Above we are assuming that config is default - paths_tuples = [(path_str, path) - for namespace, paths_dict in paths.items() - for path_str, path in paths_dict.items()] - any_import_errors = False - for path_stem, path in paths_tuples: - if 'mm-workflows' in str(path) or 'docs/tutorials/' in str(path): - # Exclude paths that only contain 'regular' python files. - continue - # NOTE: Use anything (unique?) for the python_module_name. - try: - module = import_python_file(path_stem, path) - # Let's require all python API files to define a function, say - # def workflow() -> Workflow - # so we can programmatically call it here: - retval: pythonapi.Workflow = module.workflow() # no arguments - # which allows us to programmatically call Workflow methods: - compiler_info = retval.compile() # hopefully retval is actually a Workflow object! - # But since this is python (i.e. not Haskell) that in no way eliminates - # the above security considerations. - - # This lets us use path.parent to write a *.wic file in the - # auto-discovery path, and thus reuse the existing wic CI - retval.write_ast_to_disk(path.parent) - - # Programmatically blacklist subworkflows from running in config_ci.json - # (Again, because subworkflows are missing inputs and cannot run.) - config_ci = path.parent / 'config_ci.json' - json_contents = {} - if config_ci.exists(): - with open(config_ci, mode='r', encoding='utf-8') as r: - json_contents = json.load(r) - run_blacklist: list[str] = json_contents.get('run_blacklist', []) - # Use [1:] for proper subworkflows only - subworkflows: list[pythonapi.Workflow] = retval.flatten_subworkflows()[1:] - run_blacklist += [wf.process_name for wf in subworkflows] - json_contents['run_blacklist'] = run_blacklist - with open(config_ci, mode='w', encoding='utf-8') as f: - json.dump(json_contents, f) - - except Exception as e: - any_import_errors = True - if sys.version_info >= (3, 10): - traceback.print_exception(type(e), value=e, tb=None) - else: - traceback.print_exception(etype=type(e), value=e, tb=None) - if any_import_errors: - sys.exit(1) # Make sure the CI fails diff --git a/tests/test_compile_python_workflows.py b/tests/test_compile_python_workflows.py new file mode 100644 index 00000000..8ed80bb9 --- /dev/null +++ b/tests/test_compile_python_workflows.py @@ -0,0 +1,73 @@ +import json +import sys +import traceback +from pathlib import Path + +import sophios +import sophios.plugins +from sophios import input_output as io +from sophios.python_cwl_adapter import import_python_file + + +def test_compile_python_workflows() -> None: + """This function imports (read: blindly executes) all python files in 'search_paths_wic' + The python files are assumed to have a top-level workflow() function + which returns a sophios.api.pythonapi.Workflow object. + The python files should NOT call the .run() method! + (from any code path that is automatically executed on import) + """ + from sophios.api import pythonapi # pylint: disable=C0415:import-outside-toplevel + # Since this is completely different test path we have to copy + # default .txt files to default global_config.json + config_file = Path().home()/'wic'/'global_config.json' + global_config = io.read_config_from_disk(config_file) + pythonapi.global_config = sophios.plugins.get_tools_cwl(global_config) # Use path fallback in the CI + paths = sophios.plugins.get_py_paths(global_config) + # Above we are assuming that config is default + paths_tuples = [(path_str, path) + for namespace, paths_dict in paths.items() + for path_str, path in paths_dict.items()] + any_import_errors = False + for path_stem, path in paths_tuples: + if 'mm-workflows' in str(path) or 'docs/tutorials/' in str(path): + # Exclude paths that only contain 'regular' python files. + continue + # NOTE: Use anything (unique?) for the python_module_name. + try: + module = import_python_file(path_stem, path) + # Let's require all python API files to define a function, say + # def workflow() -> Workflow + # so we can programmatically call it here: + retval: pythonapi.Workflow = module.workflow() # no arguments + # which allows us to programmatically call Workflow methods: + compiler_info = retval.compile() # hopefully retval is actually a Workflow object! + # But since this is python (i.e. not Haskell) that in no way eliminates + # the above security considerations. + + # This lets us use path.parent to write a *.wic file in the + # auto-discovery path, and thus reuse the existing wic CI + retval.write_ast_to_disk(path.parent) + + # Programmatically blacklist subworkflows from running in config_ci.json + # (Again, because subworkflows are missing inputs and cannot run.) + config_ci = path.parent / 'config_ci.json' + json_contents = {} + if config_ci.exists(): + with open(config_ci, mode='r', encoding='utf-8') as r: + json_contents = json.load(r) + run_blacklist: list[str] = json_contents.get('run_blacklist', []) + # Use [1:] for proper subworkflows only + subworkflows: list[pythonapi.Workflow] = retval.flatten_subworkflows()[1:] + run_blacklist += [wf.process_name for wf in subworkflows] + json_contents['run_blacklist'] = run_blacklist + with open(config_ci, mode='w', encoding='utf-8') as f: + json.dump(json_contents, f) + + except Exception as e: + any_import_errors = True + if sys.version_info >= (3, 10): + traceback.print_exception(type(e), value=e, tb=None) + else: + traceback.print_exception(etype=type(e), value=e, tb=None) + if any_import_errors: + sys.exit(1) # Make sure the CI fails