Add Support for local ("PC") unit tests // Resolve #519

This commit is contained in:
Ivan Kravets
2016-08-10 15:50:01 +03:00
parent 9177c6f210
commit a395b171e3
11 changed files with 269 additions and 88 deletions

View File

@ -9,8 +9,9 @@ PlatformIO 3.0
* PlatformIO Plus * PlatformIO Plus
+ `Unit Testing <http://docs.platformio.org/en/latest/unit_testing.html>`__ for Embedded + Local and Embedded `Unit Testing <http://docs.platformio.org/en/latest/unit_testing.html>`__
(`issue #408 <https://github.com/platformio/platformio/issues/408>`_) (`issue #408 <https://github.com/platformio/platformio/issues/408>`_,
`issue #519 <https://github.com/platformio/platformio/issues/519>`_)
* Decentralized Development Platforms * Decentralized Development Platforms

View File

@ -656,7 +656,7 @@ Multiple dependencies are allowed (multi-lines).
.. code-block:: ini .. code-block:: ini
[env:***] [env:myenv]
lib_deps = lib_deps =
LIBRARY_1 LIBRARY_1
LIBRARY_2 LIBRARY_2
@ -784,6 +784,53 @@ Finder. More details :ref:`ldf_compat_mode`.
By default, this value is set to ``lib_compat_mode = 1`` and means that LDF By default, this value is set to ``lib_compat_mode = 1`` and means that LDF
will check only for framework compatibility. will check only for framework compatibility.
Test options
~~~~~~~~~~~~
.. contents::
:local:
.. _projectconf_test_ignore:
``test_ignore``
^^^^^^^^^^^^^^^
.. versionadded:: 3.0
.. seealso::
Please make sure to read :ref:`unit_testing` guide first.
Ignore tests where the name matches specified patterns.
More than one pattern is allowed (multi-lines). Also, you can ignore some
tests using :option:`platformio test --ignore` command.
.. list-table::
:header-rows: 1
* - Pattern
- Meaning
* - ``*``
- matches everything
* - ``?``
- matches any single character
* - ``[seq]``
- matches any character in seq
* - ``[!seq]``
- matches any character not in seq
**Example**
.. code-block:: ini
[env:myenv]
test_ignore =
mytest*
test[13]
----------- -----------
.. _projectconf_examples: .. _projectconf_examples:

View File

@ -22,19 +22,30 @@ of one or more MCU program modules together with associated control data,
usage procedures, and operating procedures, are tested to determine whether usage procedures, and operating procedures, are tested to determine whether
they are fit for use. Unit testing finds problems early in the development cycle. they are fit for use. Unit testing finds problems early in the development cycle.
PlatformIO Test System is very interesting for embedded development. PlatformIO Test System supports 2 different test types:
It allows you to write tests locally and run them directly on the target
device (hardware unit testing). Also, you will be able to run the same tests 1. **Local Test** - *[host, native]*, process test on the host machine
on the different target devices (:ref:`embedded_boards`). using :ref:`platform_native`.
2. **Embedded Test** - *[remote, hardware]*, prepare special firmware for the
target device and upload it. Run test on the embedded device and collect
results. Process test results on the host machine.
You will be able to run the same test on the different target devices
(:ref:`embedded_boards`).
PlatformIO Test System consists of: PlatformIO Test System consists of:
* Project builder * Project builder
* Test builder * Test builder
* Firmware uploader * Firmware uploader (is used only for embedded test)
* Test processor * Test processor
There is special command :ref:`cmd_test` to run tests from PlatformIO Project. There is special command :ref:`cmd_test` to run tests from PlatformIO Project.
It allows to process specific environments or to ignore some tests using
"Glob patterns".
Also, is possible to ignore some tests for specific environment using
:ref:`projectconf_test_ignore` option from :ref:`projectconf`.
.. contents:: .. contents::
@ -55,38 +66,74 @@ PlatformIO Test System design is based on a few isolated components:
Workflow Workflow
-------- --------
1. Create PlatformIO project using :ref:`cmd_init` command. 1. Create PlatformIO project using :ref:`cmd_init` command. For Local Unit
Testing (on the host machine), need to use :ref:`platform_native`.
.. code-block:: ini
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter, extra scripting
; Upload options: custom port, speed and extra flags
; Library options: dependencies, extra library storages
;
; Please visit documentation for the other options and examples
; http://docs.platformio.org/en/stable/projectconf.html
;
; Embedded platforms
;
[env:uno]
platform = atmelavr
framework = arduino
board = uno
[env:nodemcu]
platform = espressif
framework = arduino
board = nodemcuv2
;
; Local (PC, native) platforms
;
[env:local]
platform = native
2. Place source code of main program to ``src`` directory. 2. Place source code of main program to ``src`` directory.
3. Wrap ``main()`` or ``setup()/loop()`` methods of main program in ``UNIT_TEST`` 3. Wrap ``main()`` or ``setup()/loop()`` methods of main program in ``UNIT_TEST``
guard: guard:
.. code-block:: c .. code-block:: c
/** /**
* Arduino Wiring-based Framework * Arduino Wiring-based Framework
*/ */
#ifndef UNIT_TEST #ifndef UNIT_TEST
void setup () { #include <Arduino.h>
void setup () {
// some code... // some code...
} }
void loop () { void loop () {
// some code... // some code...
} }
#endif #endif
/** /**
* Generic C/C++ * Generic C/C++
*/ */
#ifndef UNIT_TEST #ifndef UNIT_TEST
int main() { int main(int argc, char **argv) {
// setup code... // setup code...
while (1) { while (1) {
// loop code... // loop code...
} }
} return 0
#endif }
#endif
4. Create ``test`` directory in the root of project. See :ref:`projectconf_pio_test_dir`. 4. Create ``test`` directory in the root of project. See :ref:`projectconf_pio_test_dir`.
5. Write test using :ref:`unit_testing_api`. The each test is a small 5. Write test using :ref:`unit_testing_api`. The each test is a small
@ -183,8 +230,8 @@ The summary of `Unity Test API <https://github.com/ThrowTheSwitch/Unity#unity-te
- ``TEST_ASSERT_EQUAL_MEMORY(expected, actual, len)`` - ``TEST_ASSERT_EQUAL_MEMORY(expected, actual, len)``
Example Test "Blink" Project
------- --------------------
1. Please follow to :ref:`quickstart` and create "Blink Project". According 1. Please follow to :ref:`quickstart` and create "Blink Project". According
to the Unit Testing :ref:`unit_testing_design` it is the **Main program**. to the Unit Testing :ref:`unit_testing_design` it is the **Main program**.
@ -215,8 +262,15 @@ Source files
.. code-block:: ini .. code-block:: ini
; Project Configuration File ; PlatformIO Project Configuration File
; Docs: http://docs.platformio.org/en/latest/projectconf.html ;
; Build options: build flags, source filter, extra scripting
; Upload options: custom port, speed and extra flags
; Library options: dependencies, extra library storages
;
; Please visit documentation for the other options and examples
; http://docs.platformio.org/en/stable/projectconf.html
[env:uno] [env:uno]
platform = atmelavr platform = atmelavr
@ -393,7 +447,11 @@ Test results
test:*/env:uno PASSED test:*/env:uno PASSED
========================= [PASSED] Took 13.35 seconds ======================== ========================= [PASSED] Took 13.35 seconds ========================
------- Examples
--------
* `Embedded: Wiring Blink <https://github.com/platformio/platformio-examples/tree/develop/unit-testing/wiring-blink>`_
* `Local & Embedded: Calculator <https://github.com/platformio/platformio-examples/tree/develop/unit-testing/calculator>`_
For the other examples and source code please follow to For the other examples and source code please follow to
`PlatformIO Unit Testing Examples <https://github.com/platformio/platformio-examples/tree/feature/platformio-30/unit-testing>`_ repository. `PlatformIO Unit Testing Examples <hhttps://github.com/platformio/platformio-examples/tree/develop/unit-testing>`_ repository.

View File

@ -45,10 +45,12 @@ Options
Process specified environments. More details :option:`platformio run --environment` Process specified environments. More details :option:`platformio run --environment`
.. option:: .. option::
--skip -i, --ignore
Skip over tests where the name matches specified patterns. More than one Ignore tests where the name matches specified patterns. More than one
option/pattern is allowed. pattern is allowed. If you need to ignore some tests for the specific
environment, please take a look at :ref:`projectconf_test_ignore` option from
:ref:`projectconf`.
.. list-table:: .. list-table::
:header-rows: 1 :header-rows: 1
@ -68,7 +70,7 @@ option/pattern is allowed.
* - ``[!seq]`` * - ``[!seq]``
- matches any character not in seq - matches any character not in seq
For example, ``platformio test --skip "mytest*" -i "test[13]"`` For example, ``platformio test --ignore "mytest*" -i "test[13]"``
.. option:: .. option::
--upload-port --upload-port

View File

@ -14,7 +14,7 @@
import sys import sys
VERSION = (3, 0, "0a3") VERSION = (3, 0, "0a4")
__version__ = ".".join([str(s) for s in VERSION]) __version__ = ".".join([str(s) for s in VERSION])
__title__ = "platformio" __title__ = "platformio"

View File

@ -43,6 +43,14 @@ FRAMEWORK_PARAMETERS = {
"serial_flush": "Serial.flush()", "serial_flush": "Serial.flush()",
"serial_begin": "Serial.begin(9600)", "serial_begin": "Serial.begin(9600)",
"serial_end": "Serial.end()" "serial_end": "Serial.end()"
},
"native": {
"framework": "stdio.h",
"serial_obj": "",
"serial_putc": "putchar(a)",
"serial_flush": "fflush(stdout)",
"serial_begin": "",
"serial_end": ""
} }
} }
@ -114,7 +122,10 @@ void output_complete(void)
print("Warning: Could not remove temporary file '%s'. " print("Warning: Could not remove temporary file '%s'. "
"Please remove it manually." % file_) "Please remove it manually." % file_)
framework = env.subst("$PIOFRAMEWORK").lower() if env['PIOPLATFORM'] == "native":
framework = "native"
else:
framework = env.subst("$PIOFRAMEWORK").lower()
if framework not in FRAMEWORK_PARAMETERS: if framework not in FRAMEWORK_PARAMETERS:
env.Exit("Error: %s framework doesn't support testing feature!" % env.Exit("Error: %s framework doesn't support testing feature!" %
framework) framework)

View File

@ -80,9 +80,8 @@ def cli(ctx, # pylint: disable=R0913
click.echo("The next files/directories have been created in %s" % click.echo("The next files/directories have been created in %s" %
click.style( click.style(
project_dir, fg="cyan")) project_dir, fg="cyan"))
click.echo("%s - Project Configuration File" % click.echo("%s - Project Configuration File" % click.style(
click.style( "platformio.ini", fg="cyan"))
"platformio.ini", fg="cyan"))
click.echo("%s - Put your source files here" % click.style( click.echo("%s - Put your source files here" % click.style(
"src", fg="cyan")) "src", fg="cyan"))
click.echo("%s - Put here project specific (private) libraries" % click.echo("%s - Put here project specific (private) libraries" %
@ -273,10 +272,7 @@ def init_ci_conf(project_dir):
def init_cvs_ignore(project_dir): def init_cvs_ignore(project_dir):
ignore_path = join(project_dir, ".gitignore") ignore_path = join(project_dir, ".gitignore")
default = [ default = [".pioenvs\n", ".piolibdeps\n"]
".pioenvs\n",
".piolibdeps\n"
]
current = [] current = []
if isfile(ignore_path): if isfile(ignore_path):
with open(ignore_path) as fp: with open(ignore_path) as fp:

View File

@ -111,14 +111,14 @@ def cli(ctx, # pylint: disable=R0913,R0914
class EnvironmentProcessor(object): class EnvironmentProcessor(object):
KNOWN_OPTIONS = ("platform", "framework", "board", "board_mcu", KNOWN_OPTIONS = (
"board_f_cpu", "board_f_flash", "board_flash_mode", "platform", "framework", "board", "board_mcu", "board_f_cpu",
"build_flags", "src_build_flags", "build_unflags", "board_f_flash", "board_flash_mode", "build_flags", "src_build_flags",
"src_filter", "extra_script", "targets", "upload_port", "build_unflags", "src_filter", "extra_script", "targets",
"upload_protocol", "upload_speed", "upload_flags", "upload_port", "upload_protocol", "upload_speed", "upload_flags",
"upload_resetmethod", "lib_install", "lib_deps", "upload_resetmethod", "lib_install", "lib_deps", "lib_force",
"lib_force", "lib_ignore", "lib_extra_dirs", "lib_ignore", "lib_extra_dirs", "lib_ldf_mode", "lib_compat_mode",
"lib_ldf_mode", "lib_compat_mode", "piotest") "test_ignore", "piotest")
REMAPED_OPTIONS = {"framework": "pioframework", "platform": "pioplatform"} REMAPED_OPTIONS = {"framework": "pioframework", "platform": "pioplatform"}

View File

@ -30,7 +30,7 @@ from platformio.managers.platform import PlatformFactory
@click.command("test", short_help="Unit Testing") @click.command("test", short_help="Unit Testing")
@click.option("--environment", "-e", multiple=True, metavar="<environment>") @click.option("--environment", "-e", multiple=True, metavar="<environment>")
@click.option("--skip", multiple=True, metavar="<pattern>") @click.option("--ignore", "-i", multiple=True, metavar="<pattern>")
@click.option("--upload-port", metavar="<upload port>") @click.option("--upload-port", metavar="<upload port>")
@click.option( @click.option(
"-d", "-d",
@ -44,7 +44,7 @@ from platformio.managers.platform import PlatformFactory
resolve_path=True)) resolve_path=True))
@click.option("--verbose", "-v", is_flag=True) @click.option("--verbose", "-v", is_flag=True)
@click.pass_context @click.pass_context
def cli(ctx, environment, skip, upload_port, project_dir, verbose): def cli(ctx, environment, ignore, upload_port, project_dir, verbose):
with util.cd(project_dir): with util.cd(project_dir):
test_dir = util.get_projecttest_dir() test_dir = util.get_projecttest_dir()
if not isdir(test_dir): if not isdir(test_dir):
@ -59,19 +59,30 @@ def cli(ctx, environment, skip, upload_port, project_dir, verbose):
start_time = time() start_time = time()
results = [] results = []
for testname in test_names: for testname in test_names:
for envname in projectconf.sections(): for section in projectconf.sections():
if not envname.startswith("env:"): if not section.startswith("env:"):
continue continue
envname = envname[4:]
envname = section[4:]
if environment and envname not in environment: if environment and envname not in environment:
continue continue
# check skip patterns # check ignore patterns
if testname != "*" and any([fnmatch(testname, p) for p in skip]): _ignore = list(ignore)
if projectconf.has_option(section, "test_ignore"):
_ignore.extend([p.strip()
for p in projectconf.get(
section, "test_ignore").split("\n")
if p.strip()])
if testname != "*" and \
any([fnmatch(testname, p) for p in _ignore]):
results.append((None, testname, envname)) results.append((None, testname, envname))
continue continue
tp = TestProcessor(ctx, testname, envname, { cls = (LocalTestProcessor
if projectconf.get(section, "platform") == "native" else
EmbeddedTestProcessor)
tp = cls(ctx, testname, envname, {
"project_config": projectconf, "project_config": projectconf,
"project_dir": project_dir, "project_dir": project_dir,
"upload_port": upload_port, "upload_port": upload_port,
@ -93,7 +104,7 @@ def cli(ctx, environment, skip, upload_port, project_dir, verbose):
status_str = click.style("IGNORED", fg="yellow") status_str = click.style("IGNORED", fg="yellow")
click.echo( click.echo(
"test:%s/env:%s\t%s" % (click.style( "test:%s/env:%s\t[%s]" % (click.style(
testname, fg="yellow"), click.style( testname, fg="yellow"), click.style(
envname, fg="cyan"), status_str), envname, fg="cyan"), status_str),
err=status is False) err=status is False)
@ -108,10 +119,7 @@ def cli(ctx, environment, skip, upload_port, project_dir, verbose):
raise exception.ReturnErrorCode() raise exception.ReturnErrorCode()
class TestProcessor(object): class TestProcessorBase(object):
SERIAL_TIMEOUT = 600
SERIAL_BAUDRATE = 9600
def __init__(self, cmd_ctx, testname, envname, options): def __init__(self, cmd_ctx, testname, envname, options):
self.cmd_ctx = cmd_ctx self.cmd_ctx = cmd_ctx
@ -119,24 +127,16 @@ class TestProcessor(object):
self.test_name = testname self.test_name = testname
self.env_name = envname self.env_name = envname
self.options = options self.options = options
self._run_failed = False
def process(self): def print_progress(self, text, is_error=False):
self._progress("Building... (1/3)")
self._build_or_upload(["test"])
self._progress("Uploading... (2/3)")
self._build_or_upload(["test", "upload"])
self._progress("Testing... (3/3)")
sleep(1.0) # wait while board is starting...
return self._run_hardware_test()
def _progress(self, text, is_error=False):
print_header( print_header(
"[test::%s] %s" % (click.style( "[test::%s] %s" % (click.style(
self.test_name, fg="yellow", bold=True), text), self.test_name, fg="yellow", bold=True), text),
is_error=is_error) is_error=is_error)
click.echo() click.echo()
def _build_or_upload(self, target): def build_or_upload(self, target):
if self.test_name != "*": if self.test_name != "*":
self.cmd_ctx.meta['piotest'] = self.test_name self.cmd_ctx.meta['piotest'] = self.test_name
return self.cmd_ctx.invoke( return self.cmd_ctx.invoke(
@ -147,7 +147,54 @@ class TestProcessor(object):
environment=[self.env_name], environment=[self.env_name],
target=target) target=target)
def _run_hardware_test(self): def run(self):
raise NotImplementedError
def on_run_out(self, line):
if line.endswith(":PASS"):
click.echo("%s\t[%s]" % (line[:-5], click.style(
"PASSED", fg="green")))
elif ":FAIL:" in line:
self._run_failed = True
click.echo("%s\t[%s]" % (line, click.style("FAILED", fg="red")))
else:
click.echo(line)
class LocalTestProcessor(TestProcessorBase):
def process(self):
self.print_progress("Building... (1/2)")
self.build_or_upload(["test"])
self.print_progress("Testing... (2/2)")
return self.run()
def run(self):
with util.cd(self.options['project_dir']):
pioenvs_dir = util.get_projectpioenvs_dir()
result = util.exec_command(
[join(pioenvs_dir, self.env_name, "program")],
stdout=util.AsyncPipe(self.on_run_out),
stderr=util.AsyncPipe(self.on_run_out))
assert "returncode" in result
return result['returncode'] == 0 and not self._run_failed
class EmbeddedTestProcessor(TestProcessorBase):
SERIAL_TIMEOUT = 600
SERIAL_BAUDRATE = 9600
def process(self):
self.print_progress("Building... (1/3)")
self.build_or_upload(["test"])
self.print_progress("Uploading... (2/3)")
self.build_or_upload(["test", "upload"])
self.print_progress("Testing... (3/3)")
sleep(1.0) # wait while board is starting...
return self.run()
def run(self):
click.echo("If you don't see any output for the first 10 secs, " click.echo("If you don't see any output for the first 10 secs, "
"please reset board (press reset button)") "please reset board (press reset button)")
click.echo() click.echo()
@ -155,23 +202,15 @@ class TestProcessor(object):
self.get_serial_port(), self.get_serial_port(),
self.SERIAL_BAUDRATE, self.SERIAL_BAUDRATE,
timeout=self.SERIAL_TIMEOUT) timeout=self.SERIAL_TIMEOUT)
passed = True
while True: while True:
line = ser.readline().strip() line = ser.readline().strip()
if not line: if not line:
continue continue
if line.endswith(":PASS"): self.on_run_out(line)
click.echo("%s\t%s" % (line[:-5], click.style(
"PASSED", fg="green")))
elif ":FAIL:" in line:
passed = False
click.echo("%s\t%s" % (line, click.style("FAILED", fg="red")))
else:
click.echo(line)
if all([l in line for l in ("Tests", "Failures", "Ignored")]): if all([l in line for l in ("Tests", "Failures", "Ignored")]):
break break
ser.close() ser.close()
return passed return not self._run_failed
def get_serial_port(self): def get_serial_port(self):
config = self.options['project_config'] config = self.options['project_config']

View File

@ -362,14 +362,15 @@ class PlatformBase(PlatformPackagesMixin, PlatformRunMixin):
@property @property
def packages(self): def packages(self):
packages = self._manifest.get("packages", {}) if "packages" not in self._manifest:
if "tool-scons" not in packages: self._manifest['packages'] = {}
packages['tool-scons'] = { if "tool-scons" not in self._manifest['packages']:
self._manifest['packages']['tool-scons'] = {
"version": self._manifest.get("engines", {}).get( "version": self._manifest.get("engines", {}).get(
"scons", ">=2.3.0,<2.6.0"), "scons", ">=2.3.0,<2.6.0"),
"optional": False "optional": False
} }
return packages return self._manifest['packages']
def get_dir(self): def get_dir(self):
return dirname(self.manifest_path) return dirname(self.manifest_path)
@ -476,7 +477,7 @@ class PlatformBase(PlatformPackagesMixin, PlatformRunMixin):
if "test" in targets and "tool-unity" not in self.packages: if "test" in targets and "tool-unity" not in self.packages:
self.packages['tool-unity'] = { self.packages['tool-unity'] = {
"version": "~1.20302.0", "version": "~1.20302.1",
"optional": False "optional": False
} }

View File

@ -0,0 +1,26 @@
# Copyright 2014-present PlatformIO <contact@platformio.org>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from os.path import join
from platformio.commands.test import cli as cli_test
def test_local_env(clirunner, validate_cliresult):
result = clirunner.invoke(
cli_test,
["-d", join("examples", "unit-testing", "calculator"), "-e", "local"])
result.exit_code == -1
assert all(
[s in result.output for s in ("[PASSED]", "[IGNORED]", "[FAILED]")])