├── .gitattributes ├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── environment.yml ├── example_src ├── main.mojo └── my_package │ ├── __init__.mojo │ ├── __init__.py │ ├── fibonacci.mojo │ ├── fibonacci.py │ └── random_tensor.mojo ├── example_tests └── my_package │ ├── my_test.mojo │ ├── test_fibonacci.mojo │ ├── test_fibonacci.py │ ├── test_fire.🔥 │ └── test_random_tensor.mojo ├── pyproject.toml └── pytest_mojo ├── __init__.py └── plugin.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # GitHub syntax highlighting 2 | pixi.lock linguist-language=YAML linguist-generated=true 3 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: ["push"] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-22.04 8 | 9 | steps: 10 | - name: Check out repository code 11 | uses: actions/checkout@v2 12 | 13 | - name: Set up Conda 14 | uses: conda-incubator/setup-miniconda@v3 15 | with: 16 | environment-file: environment.yml 17 | activate-environment: pytest-mojo 18 | 19 | - name: Integration Tests 20 | shell: bash -el {0} 21 | run: | 22 | conda info 23 | export PYTHONPATH=/home/runner/work/mojo-pytest/mojo-pytest/example_src 24 | make test 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pytest.out 2 | 3 | .vscode/ 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # poetry 102 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 106 | #poetry.lock 107 | 108 | # pdm 109 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 110 | #pdm.lock 111 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 112 | # in version control. 113 | # https://pdm.fming.dev/#use-with-ide 114 | .pdm.toml 115 | 116 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 117 | __pypackages__/ 118 | 119 | # Celery stuff 120 | celerybeat-schedule 121 | celerybeat.pid 122 | 123 | # SageMath parsed files 124 | *.sage.py 125 | 126 | # Environments 127 | .env 128 | .venv 129 | env/ 130 | venv/ 131 | ENV/ 132 | env.bak/ 133 | venv.bak/ 134 | 135 | # Spyder project settings 136 | .spyderproject 137 | .spyproject 138 | 139 | # Rope project settings 140 | .ropeproject 141 | 142 | # mkdocs documentation 143 | /site 144 | 145 | # mypy 146 | .mypy_cache/ 147 | .dmypy.json 148 | dmypy.json 149 | 150 | # Pyre type checker 151 | .pyre/ 152 | 153 | # pytype static type analyzer 154 | .pytype/ 155 | 156 | # Cython debug symbols 157 | cython_debug/ 158 | 159 | # PyCharm 160 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 161 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 162 | # and can be added to the global gitignore or merged into this file. For a more nuclear 163 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 164 | #.idea/ 165 | # pixi environments 166 | .pixi 167 | *.egg-info 168 | # magic environments 169 | .magic 170 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Alex G Rice 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | test: 4 | pwd 5 | mojo --version 6 | 7 | # run tests for example_src/ project (they should all pass) 8 | pytest --mojo-include example_src/ example_tests/ > pytest.out 9 | cat pytest.out 10 | # check test collection (this count needs to be updated manually when tests are updated) 11 | grep "collected 6 items" pytest.out 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mojo-pytest 2 | 3 | [![Run Tests](https://github.com/guidorice/mojo-pytest/actions/workflows/test.yml/badge.svg)](https://github.com/guidorice/mojo-pytest/actions/workflows/test.yml) 4 | 5 | [Mojo🔥](https://docs.modular.com/mojo/manual) language test runner plugin for [pytest](https://docs.pytest.org). Try it for 6 | your mixed Python and Mojo codebases! 7 | 8 | ## Design 9 | 10 | This package implements a `pytest` plugin to discover and run Mojo tests, alongside your Python tests. Although `pytest` 11 | does not have any awareness of Mojo source or package structure, `pytest` is extensible. In summary, `plugin.py` calls 12 | `mojo test` in a sub-process and parses the outputs and exit codes. 13 | 14 | ## Usage 15 | 16 | 1. Create your Mojo tests according to the manual: https://docs.modular.com/mojo/tools/testing . 17 | 18 | 2. Install Mojo, Python, `pytest` and this `pytest-mojo` plugin using the [conda](https://docs.anaconda.com/miniconda/) 19 | [environment.yml](environment.yml) file. This can alternatively be done with the [magic](https://docs.modular.com/magic/) 20 | package manager, but [conda](https://docs.anaconda.com/miniconda/) is easier for this use case. 21 | 22 | ```shell 23 | # use conda to install mojo, python, and the pytest-mojo plugin. 24 | $ conda env create -n foo-project -f environment.yml 25 | 26 | # verify environment 27 | $ conda activate foo-project 28 | 29 | $ mojo --version 30 | mojo 25.3.0 (d430567f) 31 | 32 | $ python --version 33 | Python 3.13.3 34 | 35 | $ conda list pytest 36 | ... 37 | pytest 8.3.5 pyhd8ed1ab_0 conda-forge 38 | pytest-mojo 24.6 pypi_0 pypi 39 | pytest-xdist 3.6.1 pyhd8ed1ab_1 conda-forge 40 | ``` 41 | 42 | Summary: it is a requirement is to have Python and Mojo sharing the same runtime and packages and 43 | [conda](https://docs.anaconda.com/miniconda/) is the easiest way to accomplish that. 44 | 45 | 3. See the example project for one possible filesystem layout: 46 | - `example_src/` has it's tests in the `example_tests/` folder. 47 | - Remember the [Mojo manual](https://docs.modular.com/mojo/manual) explains that tests are allowed to be in the 48 | same folder as Mojo code, or different folder, or even as Mojo code in docstrings! So this example project 49 | is just one possibility. 50 | 4. Mojo tests and Python tests are all run via `pytest`! Use the plugin's `--mojo-include` option to include your 51 | Mojo packages. 52 | 53 | ```shell 54 | # this example_src/ contains a Python package which is also called from Mojo, 55 | # so we must add it using PYTHONPATH. Please note that the full path may be required! 56 | $ export PYTHONPATH=/Users/you/project/example_src/ 57 | 58 | # Use the plugin's --mojo-include option to tell mojo where to find `my_package` 59 | $ pytest --mojo-include example_src/ example_tests/ 60 | 61 | ======================== test session starts ========================= 62 | platform darwin -- Python 3.13.3, pytest-8.3.5, pluggy-1.5.0 63 | rootdir: /Users/guidorice/dev/mojo/mojo-pytest 64 | configfile: pyproject.toml 65 | plugins: mojo-25.3, xdist-3.6.1 66 | collected 6 items 67 | 68 | example_tests/my_package/my_test.mojo . [ 16%] 69 | example_tests/my_package/test_fibonacci.mojo .. [ 50%] 70 | example_tests/my_package/test_fibonacci.py . [ 66%] 71 | example_tests/my_package/test_fire.🔥 . [ 83%] 72 | example_tests/my_package/test_random_tensor.mojo . [100%] 73 | 74 | ========================= 6 passed in 13.34s ========================= 75 | ``` 76 | 77 | 👆🏽 Notice how your Python tests are run alongside your Mojo tests. 78 | 🚀 [Add `-n auto` for a multiprocessing speedup with pytest-xdist](https://github.com/guidorice/mojo-pytest/wiki#2024-07-17-here-is-a-performance-tip). 79 | 80 | 5. Mojo binary packages are also supported with `--mojo-include`. For example, this could be used in a CI/CD script: 81 | 82 | ```shell 83 | $ mojo package example_src/my_package -o build/my_package.mojopkg # or .📦 84 | $ pytest --mojo-include build/ example_tests/ 85 | ... 86 | ... (same pytest output as above) 87 | ... 88 | ``` 89 | 90 | 91 | ## Example Project 92 | 93 | In the `example_src/` directory is a Mojo package with a couple of modules. There is also a Python module, which we call 94 | in two ways (from `pytest`, and from Mojo). Here is an overview: 95 | 96 | ```shell 97 | example_src 98 | ├── main.mojo # main entry point. run with `mojo example_src/main.mojo` 99 | └── my_package 100 | ├── __init__.mojo # this is both Mojo package, and a Python package. 101 | ├── __init__.py 102 | ├── fibonacci.mojo # Mojo implementation 103 | ├── fibonacci.py # Python implementation 104 | └── random_tensor.mojo # random tensor stuff 105 | 106 | example_tests 107 | └── my_package 108 | ├── my_test.mojo # files can be named xxx_test as well as test_xxx. 109 | ├── test_fibonacci.mojo # tests the Mojo impl and the Python impl. 110 | ├── test_fibonacci.py # tests the Python impl (pure Python). 111 | ├── test_fire.🔥 # tests are collected for fire extension too. 112 | └── test_random_tensor.mojo # tests the Mojo impl. 113 | ``` 114 | 115 | ## Links 116 | 117 | - If you experience slowness, see this 118 | [tip about using multiprocessing]( https://github.com/guidorice/mojo-pytest/wiki#2024-07-17-here-is-a-performance-tip) with `pytest`. 119 | - Writing tests in Mojo: https://docs.modular.com/mojo/tools/testing 120 | - Non-Python tests in `pytest`: https://pytest.org/en/latest/example/nonpython.html#non-python-tests 121 | - C test runner: https://pytest-c-testrunner.readthedocs.io 122 | - `pytest` docs: https://docs.pytest.org 123 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: pytest-mojo 2 | channels: 3 | - https://conda.modular.com/max 4 | - conda-forge 5 | - defaults 6 | dependencies: 7 | - python>=3.11 8 | # modular MAX installs mojo as well 9 | - max=25.3 10 | - pytest>=7.4 11 | # python-xdist is optional: https://github.com/guidorice/mojo-pytest/wiki#2024-07-17-here-is-a-performance-tip 12 | - pytest-xdist 13 | - pip 14 | - pip: 15 | 16 | # pip install from github 17 | # - git+https://github.com/guidorice/mojo-pytest.git 18 | 19 | # or pip install from filesystem (editable package), uses pyproject.toml 20 | - -e . 21 | -------------------------------------------------------------------------------- /example_src/main.mojo: -------------------------------------------------------------------------------- 1 | from my_package.random_tensor import random_tensor 2 | from my_package.fibonacci import fibonacci 3 | 4 | 5 | def main(): 6 | print(random_tensor[DType.float64]()) 7 | 8 | print("fibonacci sequence:") 9 | for n in range(2, 11): 10 | print(fibonacci(n)) 11 | -------------------------------------------------------------------------------- /example_src/my_package/__init__.mojo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guidorice/mojo-pytest/12f95f5dd7c80e582ad1deb3777e186b41558115/example_src/my_package/__init__.mojo -------------------------------------------------------------------------------- /example_src/my_package/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guidorice/mojo-pytest/12f95f5dd7c80e582ad1deb3777e186b41558115/example_src/my_package/__init__.py -------------------------------------------------------------------------------- /example_src/my_package/fibonacci.mojo: -------------------------------------------------------------------------------- 1 | fn fibonacci(n: Int) -> Int: 2 | """ 3 | The Nth Fibonacci number. 4 | """ 5 | if n <= 1: 6 | return n 7 | a, b = 0, 1 8 | for _ in range(2, n + 1): 9 | a, b = b, a + b 10 | return b 11 | -------------------------------------------------------------------------------- /example_src/my_package/fibonacci.py: -------------------------------------------------------------------------------- 1 | def fibonacci(n: int) -> int: 2 | """ 3 | The Nth Fibonacci number. 4 | """ 5 | if n <= 1: 6 | return n 7 | a, b = 0, 1 8 | for _ in range(2, n + 1): 9 | a, b = b, a + b 10 | return b 11 | -------------------------------------------------------------------------------- /example_src/my_package/random_tensor.mojo: -------------------------------------------------------------------------------- 1 | import random 2 | from tensor import Tensor 3 | 4 | 5 | fn random_tensor[dtype: DType]() -> Tensor[dtype]: 6 | """ 7 | Generate a random 100 x 1 tensor of the specified floating point type. 8 | """ 9 | constrained[dtype.is_floating_point(), "dtype must be floating point"]() 10 | a = Tensor[dtype](100, 1) 11 | random.rand(a.unsafe_ptr(), a.num_elements()) 12 | return a 13 | -------------------------------------------------------------------------------- /example_tests/my_package/my_test.mojo: -------------------------------------------------------------------------------- 1 | from testing import assert_true 2 | 3 | 4 | def my_test(): 5 | """ 6 | Tests that modules named with ***_test (suffix) are also discovered. 7 | """ 8 | assert_true(True) 9 | -------------------------------------------------------------------------------- /example_tests/my_package/test_fibonacci.mojo: -------------------------------------------------------------------------------- 1 | from python import Python 2 | from testing import assert_equal 3 | from my_package.fibonacci import fibonacci 4 | 5 | 6 | def test_fibonacci(): 7 | """ 8 | Test fibonacci 10th number. 9 | """ 10 | expect = 55 11 | got = fibonacci(10) 12 | assert_equal(got, expect) 13 | 14 | 15 | def test_fibonacci_reference(): 16 | """ 17 | Test mojo fibonacci versus python "reference" implementation. 18 | """ 19 | py = Python.import_module("my_package.fibonacci") 20 | for n in range(0, 10): 21 | expect = py.fibonacci(n) 22 | got = fibonacci(n) 23 | assert_equal(got, expect) 24 | -------------------------------------------------------------------------------- /example_tests/my_package/test_fibonacci.py: -------------------------------------------------------------------------------- 1 | from my_package.fibonacci import fibonacci 2 | 3 | 4 | def test_fibonacci(): 5 | """ 6 | Tests fibonacci module (py) using python. 7 | """ 8 | assert fibonacci(10) == 55 9 | -------------------------------------------------------------------------------- /example_tests/my_package/test_fire.🔥: -------------------------------------------------------------------------------- 1 | from testing import assert_true 2 | 3 | 4 | def test_emoji(): 5 | """ 6 | Testing the discovery of this file extension. 7 | """ 8 | assert_true(True) 9 | -------------------------------------------------------------------------------- /example_tests/my_package/test_random_tensor.mojo: -------------------------------------------------------------------------------- 1 | from testing import assert_false 2 | from utils.numerics import isnan 3 | from my_package.random_tensor import random_tensor 4 | 5 | 6 | def test_random_tensor(): 7 | """ 8 | Validate the random_tensor module in my_package. 9 | """ 10 | alias T = DType.float64 11 | t = random_tensor[T]() 12 | sample_value = t[0] 13 | assert_false(isnan(sample_value)) 14 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "pytest-mojo" 3 | version = "v25.3" 4 | description = "Mojo🔥 language test runner plugin for pytest. (aka pytest-mojo)" 5 | authors = [{name = "Alex G Rice", email = "alex@ricegeo.dev"}] 6 | license = {file = "LICENSE"} 7 | requires-python = ">=3.10" 8 | 9 | [build-system] 10 | requires = ["setuptools>=45", "wheel"] 11 | build-backend = "setuptools.build_meta" 12 | 13 | [tool.setuptools] 14 | packages = ["pytest_mojo"] 15 | 16 | [project.entry-points.pytest11] 17 | "pytest_mojo" = "pytest_mojo.plugin" 18 | -------------------------------------------------------------------------------- /pytest_mojo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guidorice/mojo-pytest/12f95f5dd7c80e582ad1deb3777e186b41558115/pytest_mojo/__init__.py -------------------------------------------------------------------------------- /pytest_mojo/plugin.py: -------------------------------------------------------------------------------- 1 | import json 2 | import subprocess 3 | from pathlib import Path 4 | from typing import Any 5 | import shlex 6 | 7 | from pytest import File, Item, Package, Parser 8 | 9 | 10 | MOJO_TEST = ["mojo", "test", "--diagnostic-format", "json"] 11 | """ 12 | Mojo command to be run by this pytest plugin. 13 | """ 14 | 15 | TEST_PREFIX = "test_" 16 | """ 17 | Examples of test prefix: `test_something.mojo` or `test_xyz.🔥` 18 | """ 19 | 20 | TEST_SUFFIX = "_test" 21 | """ 22 | Examples of test suffix: `something_test.mojo` or `xyz_test.🔥` 23 | """ 24 | 25 | 26 | def pytest_collect_file(parent: Package, file_path: Path) -> File | None: 27 | """ 28 | Pytest hook 29 | """ 30 | if file_path.suffix in (".mojo", ".🔥") and ( 31 | file_path.stem.startswith(TEST_PREFIX) or file_path.stem.endswith(TEST_SUFFIX) 32 | ): 33 | return MojoTestFile.from_parent(parent, path=file_path) 34 | return None 35 | 36 | 37 | def pytest_addoption(parser: Parser): 38 | """ 39 | Pytest hook 40 | """ 41 | parser.addoption("--mojo-include", help="Mojo package include path.") 42 | 43 | 44 | class MojoTestFile(File): 45 | """ 46 | `mojo test --collect-only` the source file, then parse the stdout and exit code into one or more `MojoTestItem`. 47 | """ 48 | 49 | def collect(self): 50 | mojo_include_path = self.config.getoption("--mojo-include") 51 | mojo_src = str(self.path) 52 | shell_cmd = MOJO_TEST.copy() 53 | shell_cmd.append("--collect-only") 54 | if mojo_include_path: 55 | shell_cmd.extend(["-I", mojo_include_path]) 56 | shell_cmd.append(mojo_src) 57 | process = subprocess.run(shell_cmd, capture_output=True, text=True) 58 | 59 | # early-out of there was a mojo parser error (tests cannot be discovered in this case) 60 | if not process.stdout and process.returncode != 0: 61 | raise MojoTestException(process.stderr) 62 | 63 | # parsed collected tests and generate MojoTestItems for each child 64 | report = json.loads(process.stdout) 65 | for test_metadata in report.get("children", []): 66 | id = test_metadata.get("id", None) 67 | tokens = id.split("::") 68 | name = tokens[1] 69 | yield MojoTestItem.from_parent( 70 | self, 71 | name=name, 72 | spec=test_metadata, 73 | ) 74 | 75 | 76 | class MojoTestItem(Item): 77 | def __init__(self, *, name: str, parent, spec: dict[str, Any], **kwargs): 78 | super().__init__(name, parent, **kwargs) 79 | self.spec = spec 80 | 81 | def runtest(self): 82 | mojo_include_path = self.config.getoption("--mojo-include") 83 | shell_cmd = MOJO_TEST.copy() 84 | 85 | if mojo_include_path: 86 | shell_cmd.extend(["-I", mojo_include_path]) 87 | target = self.spec.get("id", None) 88 | 89 | # `mojo test`` apparently needs shell=True to work. 90 | shell_cmd.append(shlex.quote(target)) 91 | shell_cmd_str = " ".join(shell_cmd) 92 | process = subprocess.run(shell_cmd_str, capture_output=True, text=True, shell=True) 93 | 94 | # early-out of there was a mojo parser error (tests cannot be discovered in this case) 95 | print("stdout:", process.stdout) 96 | print("stderr:", process.stderr) 97 | 98 | if not process.stdout and process.returncode != 0: 99 | raise MojoTestException(process.stderr) 100 | 101 | report = json.loads(process.stdout) 102 | kind = report.get("kind", None) 103 | error = report.get("error", None) 104 | if error: 105 | raise MojoTestException(kind + ":"+ report.get("stdErr") + report.get("stdOut")) 106 | 107 | def repr_failure(self, excinfo): 108 | return str(excinfo) 109 | 110 | def reportinfo(self): 111 | line_num = self.spec.get("startLine", 0) 112 | return self.path, line_num, self.name 113 | 114 | 115 | class MojoTestException(Exception): 116 | pass 117 | --------------------------------------------------------------------------------