forked from platformio/platformio-core
Using hardware Simulators for Unit Testing // Issue #4238
This commit is contained in:
@ -45,9 +45,10 @@ Please check `Migration guide from 5.x to 6.0 <https://docs.platformio.org/en/la
|
|||||||
* **Unit Testing**
|
* **Unit Testing**
|
||||||
|
|
||||||
- Refactored from scratch `Unit Testing <https://docs.platformio.org/en/latest/advanced/unit-testing/index.html>`_ solution and its documentation
|
- Refactored from scratch `Unit Testing <https://docs.platformio.org/en/latest/advanced/unit-testing/index.html>`_ solution and its documentation
|
||||||
- New `Test Hierarchies <https://docs.platformio.org/en/latest/advanced/unit-testing/structure.html>`_ (`issue #4135 <https://github.com/platformio/platformio-core/issues/4135>`_)
|
- New: `Test Hierarchies <https://docs.platformio.org/en/latest/advanced/unit-testing/structure.html>`_ (`issue #4135 <https://github.com/platformio/platformio-core/issues/4135>`_)
|
||||||
- New `Custom Testing Framework <https://docs.platformio.org/en/latest/advanced/unit-testing/frameworks/custom/index.html>`_
|
- New: `Custom Testing Framework <https://docs.platformio.org/en/latest/advanced/unit-testing/frameworks/custom/index.html>`_
|
||||||
- New "test" `build configuration <https://docs.platformio.org/en/latest/projectconf/build_configurations.html>`__
|
- New: Using hardware `Simulators <https://docs.platformio.org/en/latest/advanced/unit-testing/simulators/index.html>`__ for Unit Testing
|
||||||
|
- Added a new "test" `build configuration <https://docs.platformio.org/en/latest/projectconf/build_configurations.html>`__
|
||||||
- Added support for the ``socket://`` and ``rfc2217://`` protocols using `test_port <https://docs.platformio.org/en/latest/projectconf/section_env_test.html#test-port>`__ option (`issue #4229 <https://github.com/platformio/platformio-core/issues/4229>`_)
|
- Added support for the ``socket://`` and ``rfc2217://`` protocols using `test_port <https://docs.platformio.org/en/latest/projectconf/section_env_test.html#test-port>`__ option (`issue #4229 <https://github.com/platformio/platformio-core/issues/4229>`_)
|
||||||
- Added support for a `Custom Unity Library <https://docs.platformio.org/en/latest/advanced/unit-testing/frameworks/custom/examples/custom_unity_library.html>`__ (`issue #3980 <https://github.com/platformio/platformio-core/issues/3980>`_)
|
- Added support for a `Custom Unity Library <https://docs.platformio.org/en/latest/advanced/unit-testing/frameworks/custom/examples/custom_unity_library.html>`__ (`issue #3980 <https://github.com/platformio/platformio-core/issues/3980>`_)
|
||||||
- Generate reports in JUnit and JSON formats using the `pio test --output-format <https://docs.platformio.org/en/latest/core/userguide/cmd_test.html#cmdoption-pio-test-output-format>`__ option (`issue #2891 <https://github.com/platformio/platformio-core/issues/2891>`_)
|
- Generate reports in JUnit and JSON formats using the `pio test --output-format <https://docs.platformio.org/en/latest/core/userguide/cmd_test.html#cmdoption-pio-test-output-format>`__ option (`issue #2891 <https://github.com/platformio/platformio-core/issues/2891>`_)
|
||||||
|
@ -40,7 +40,7 @@ PlatformIO Core
|
|||||||
.. image:: https://raw.githubusercontent.com/platformio/platformio-web/develop/app/images/platformio-ide-laptop.png
|
.. image:: https://raw.githubusercontent.com/platformio/platformio-web/develop/app/images/platformio-ide-laptop.png
|
||||||
:target: https://platformio.org?utm_source=github&utm_medium=core
|
:target: https://platformio.org?utm_source=github&utm_medium=core
|
||||||
|
|
||||||
`PlatformIO <https://platformio.org>`_ is a professional collaborative platform for safety-critical and declarative embedded development.
|
`PlatformIO <https://platformio.org>`_ is a professional collaborative platform for embedded development.
|
||||||
|
|
||||||
**A place where Developers and Teams have true Freedom! No more vendor lock-in!**
|
**A place where Developers and Teams have true Freedom! No more vendor lock-in!**
|
||||||
|
|
||||||
|
2
docs
2
docs
Submodule docs updated: c5de0701f6...e12174e655
@ -687,6 +687,15 @@ ProjectOptions = OrderedDict(
|
|||||||
type=click.BOOL,
|
type=click.BOOL,
|
||||||
default=False,
|
default=False,
|
||||||
),
|
),
|
||||||
|
ConfigEnvOption(
|
||||||
|
group="test",
|
||||||
|
name="test_testing_command",
|
||||||
|
multiple=True,
|
||||||
|
description=(
|
||||||
|
"A custom testing command that runs test cases "
|
||||||
|
"and returns results to the standard output"
|
||||||
|
),
|
||||||
|
),
|
||||||
# Debug
|
# Debug
|
||||||
ConfigEnvOption(
|
ConfigEnvOption(
|
||||||
group="debug",
|
group="debug",
|
||||||
|
@ -144,11 +144,17 @@ class TestRunnerBase:
|
|||||||
return None
|
return None
|
||||||
click.secho("Testing...", bold=self.options.verbose)
|
click.secho("Testing...", bold=self.options.verbose)
|
||||||
test_port = self.get_test_port()
|
test_port = self.get_test_port()
|
||||||
serial_conds = [self.platform.is_embedded(), test_port and "://" in test_port]
|
program_conds = [
|
||||||
|
not self.platform.is_embedded()
|
||||||
|
and (not test_port or "://" not in test_port),
|
||||||
|
self.project_config.get(
|
||||||
|
f"env:{self.test_suite.env_name}", "test_testing_command"
|
||||||
|
),
|
||||||
|
]
|
||||||
reader = (
|
reader = (
|
||||||
SerialTestOutputReader(self)
|
ProgramTestOutputReader(self)
|
||||||
if any(serial_conds)
|
if any(program_conds)
|
||||||
else ProgramTestOutputReader(self)
|
else SerialTestOutputReader(self)
|
||||||
)
|
)
|
||||||
return reader.begin()
|
return reader.begin()
|
||||||
|
|
||||||
|
@ -12,28 +12,88 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
|
||||||
from platformio import proc
|
from platformio.compat import IS_WINDOWS, get_filesystem_encoding, get_locale_encoding
|
||||||
from platformio.test.exception import UnitTestError
|
from platformio.test.exception import UnitTestError
|
||||||
|
|
||||||
|
|
||||||
|
class ProgramProcessProtocol(asyncio.SubprocessProtocol):
|
||||||
|
def __init__(self, test_runner, exit_future):
|
||||||
|
self.test_runner = test_runner
|
||||||
|
self.exit_future = exit_future
|
||||||
|
|
||||||
|
def pipe_data_received(self, _, data):
|
||||||
|
try:
|
||||||
|
data = data.decode(get_locale_encoding() or get_filesystem_encoding())
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
data = data.decode("latin-1")
|
||||||
|
self.test_runner.on_test_output(data)
|
||||||
|
if self.test_runner.test_suite.is_finished():
|
||||||
|
self._stop_testing()
|
||||||
|
|
||||||
|
def process_exited(self):
|
||||||
|
self._stop_testing()
|
||||||
|
|
||||||
|
def _stop_testing(self):
|
||||||
|
if not self.exit_future.done():
|
||||||
|
self.exit_future.set_result(True)
|
||||||
|
|
||||||
|
|
||||||
class ProgramTestOutputReader:
|
class ProgramTestOutputReader:
|
||||||
|
|
||||||
|
KILLING_TIMEOUT = 5 # seconds
|
||||||
|
|
||||||
def __init__(self, test_runner):
|
def __init__(self, test_runner):
|
||||||
self.test_runner = test_runner
|
self.test_runner = test_runner
|
||||||
|
self.aio_loop = (
|
||||||
def begin(self):
|
asyncio.ProactorEventLoop() if IS_WINDOWS else asyncio.new_event_loop()
|
||||||
build_dir = self.test_runner.project_config.get("platformio", "build_dir")
|
|
||||||
result = proc.exec_command(
|
|
||||||
[os.path.join(build_dir, self.test_runner.test_suite.env_name, "program")],
|
|
||||||
stdout=proc.LineBufferedAsyncPipe(self.test_runner.on_test_output),
|
|
||||||
stderr=proc.LineBufferedAsyncPipe(self.test_runner.on_test_output),
|
|
||||||
)
|
)
|
||||||
if result["returncode"] == 0:
|
asyncio.set_event_loop(self.aio_loop)
|
||||||
return True
|
|
||||||
|
def get_testing_command(self):
|
||||||
|
custom_testing_command = self.test_runner.project_config.get(
|
||||||
|
f"env:{self.test_runner.test_suite.env_name}", "test_testing_command"
|
||||||
|
)
|
||||||
|
if custom_testing_command:
|
||||||
|
return custom_testing_command
|
||||||
|
build_dir = self.test_runner.project_config.get("platformio", "build_dir")
|
||||||
|
return [
|
||||||
|
os.path.join(build_dir, self.test_runner.test_suite.env_name, "program")
|
||||||
|
]
|
||||||
|
|
||||||
|
async def gather_results(self):
|
||||||
|
exit_future = asyncio.Future(loop=self.aio_loop)
|
||||||
|
transport, _ = await self.aio_loop.subprocess_exec(
|
||||||
|
lambda: ProgramProcessProtocol(self.test_runner, exit_future),
|
||||||
|
*self.get_testing_command(),
|
||||||
|
stdin=None,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
await exit_future
|
||||||
|
last_return_code = transport.get_returncode()
|
||||||
|
transport.close()
|
||||||
|
|
||||||
|
# wait until subprocess will be killed
|
||||||
|
start = time.time()
|
||||||
|
while (
|
||||||
|
start > (time.time() - self.KILLING_TIMEOUT)
|
||||||
|
and transport.get_returncode() is None
|
||||||
|
):
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
if last_return_code:
|
||||||
|
self.raise_for_status(last_return_code)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def raise_for_status(return_code):
|
||||||
try:
|
try:
|
||||||
sig = signal.Signals(abs(result["returncode"]))
|
sig = signal.Signals(abs(return_code))
|
||||||
try:
|
try:
|
||||||
signal_description = signal.strsignal(sig)
|
signal_description = signal.strsignal(sig)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@ -42,4 +102,11 @@ class ProgramTestOutputReader:
|
|||||||
f"Program received signal {sig.name} ({signal_description})"
|
f"Program received signal {sig.name} ({signal_description})"
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise UnitTestError("Program errored with %d code" % result["returncode"])
|
raise UnitTestError("Program errored with %d code" % return_code)
|
||||||
|
|
||||||
|
def begin(self):
|
||||||
|
try:
|
||||||
|
self.aio_loop.run_until_complete(self.gather_results())
|
||||||
|
finally:
|
||||||
|
self.aio_loop.run_until_complete(self.aio_loop.shutdown_asyncgens())
|
||||||
|
self.aio_loop.close()
|
||||||
|
@ -13,9 +13,12 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from platformio import proc
|
from platformio import proc
|
||||||
from platformio.test.command import test_cmd as pio_test_cmd
|
from platformio.test.command import test_cmd as pio_test_cmd
|
||||||
|
|
||||||
@ -212,6 +215,69 @@ int main(int argc, char *argv[]) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
sys.platform == "win32", reason="runs only on Unix (issue with SimAVR)"
|
||||||
|
)
|
||||||
|
def test_custom_testing_command(clirunner, validate_cliresult, tmp_path: Path):
|
||||||
|
project_dir = tmp_path / "project"
|
||||||
|
project_dir.mkdir()
|
||||||
|
(project_dir / "platformio.ini").write_text(
|
||||||
|
"""
|
||||||
|
[env:uno]
|
||||||
|
platform = atmelavr
|
||||||
|
framework = arduino
|
||||||
|
board = uno
|
||||||
|
|
||||||
|
platform_packages =
|
||||||
|
platformio/tool-simavr @ ^1
|
||||||
|
test_speed = 9600
|
||||||
|
test_testing_command =
|
||||||
|
${platformio.packages_dir}/tool-simavr/bin/simavr
|
||||||
|
-m
|
||||||
|
atmega328p
|
||||||
|
-f
|
||||||
|
16000000L
|
||||||
|
${platformio.build_dir}/${this.__env__}/firmware.elf
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
test_dir = project_dir / "test" / "test_dummy"
|
||||||
|
test_dir.mkdir(parents=True)
|
||||||
|
(test_dir / "test_main.cpp").write_text(
|
||||||
|
"""
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <unity.h>
|
||||||
|
|
||||||
|
void setUp(void) {
|
||||||
|
// set stuff up here
|
||||||
|
}
|
||||||
|
|
||||||
|
void tearDown(void) {
|
||||||
|
// clean stuff up here
|
||||||
|
}
|
||||||
|
|
||||||
|
void dummy_test(void) {
|
||||||
|
TEST_ASSERT_EQUAL(1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
UNITY_BEGIN();
|
||||||
|
RUN_TEST(dummy_test);
|
||||||
|
UNITY_END();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = clirunner.invoke(
|
||||||
|
pio_test_cmd,
|
||||||
|
["-d", str(project_dir), "--without-uploading"],
|
||||||
|
)
|
||||||
|
validate_cliresult(result)
|
||||||
|
assert "dummy_test" in result.output
|
||||||
|
|
||||||
|
|
||||||
def test_unity_setup_teardown(clirunner, validate_cliresult, tmpdir):
|
def test_unity_setup_teardown(clirunner, validate_cliresult, tmpdir):
|
||||||
project_dir = tmpdir.mkdir("project")
|
project_dir = tmpdir.mkdir("project")
|
||||||
project_dir.join("platformio.ini").write(
|
project_dir.join("platformio.ini").write(
|
||||||
|
Reference in New Issue
Block a user