Unit Testing for Embedded // Resolve #408

This commit is contained in:
Ivan Kravets
2016-06-15 14:10:42 +03:00
parent f5727af40e
commit bb22a1297b
15 changed files with 725 additions and 37 deletions

View File

@ -10,7 +10,8 @@ PlatformIO 3.0
* Decentralized architecture for development platforms: "platform.json",
semantic versioning, package dependencies, embedded board configs, isolated
build scripts
(`issue #479 <https://github.com/platformio/platformio/issues/479>`_)
* Unit Testing for Embedded (`docs <http://docs.platformio.org/en/latest/platforms/unit_testing.html>`__)
(`issue #408 <https://github.com/platformio/platformio/issues/408>`_)
PlatformIO 2.0
--------------

View File

@ -69,6 +69,10 @@ Allows to override :ref:`projectconf` option :ref:`projectconf_pio_envs_dir`.
Allows to override :ref:`projectconf` option :ref:`projectconf_pio_data_dir`.
.. envvar:: PLATFORMIO_TEST_DIR
Allows to override :ref:`projectconf` option :ref:`projectconf_pio_test_dir`.
Building
--------

View File

@ -111,6 +111,7 @@ Contents
platforms/embedded_boards
frameworks/index
platforms/custom_platform_and_board
platforms/unit_testing
.. toctree::
:caption: Library Manager

View File

@ -0,0 +1,390 @@
.. Copyright 2014-present Ivan Kravets <me@ikravets.com>
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.
.. _unit_testing:
Unit Testing
============
`Unit Testing (wiki) <https://en.wikipedia.org/wiki/Unit_testing>`_
is a software testing method by which individual units of source code, sets
of one or more MCU program modules together with associated control data,
usage procedures, and operating procedures, are tested to determine whether
they are fit for use. Unit testing finds problems early in the development cycle.
PlatformIO Test System is very interesting for embedded development.
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
on the different target devices (:ref:`embedded_boards`).
PlatformIO Test System consists of:
* Project builder
* Test builder
* Firmware uploader
* Test processor
.. contents::
.. _unit_testing_design:
Design
------
PlatformIO Test System design is based on a few isolated components:
1. **Main program**. Contains the independent modules, procedures,
functions or methods that will be the target candidates (TC) for testing
2. **Unit test**. This a small independent program that is intended to
re-use TC from the main program and apply tests for them.
3. **Test processor**. The set of approaches and tools that will be used
to apply test for the environments from :ref:`projectconf`.
Workflow
--------
1. Create PlatformIO project using :ref:`cmd_init` command
2. Place source code of main program to ``src`` directory
3. Wrap ``main()`` or ``setup()/loop()`` methods of main program in ``UNIT_TEST``
guard:
.. code-block:: c
/**
* Arduino Wiring-based Framework
*/
#ifndef UNIT_TEST
void setup () {
// some code...
}
void loop () {
// some code...
}
#endif
/**
* Generic C/C++
*/
#ifndef UNIT_TEST
int main() {
// setup code...
while (1) {
// loop code...
}
}
#endif
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
independent program with own ``main()`` or ``setup()/loop()`` methods. Also,
test should start from ``UNITY_BEGIN()`` and finish with ``UNITY_END()``
6. Place test to ``test`` directory. If you have more than one test, split them
into sub-folders. For example, ``test/test_1/*.[c,cpp,h]``,
``test_N/*.[c,cpp,h]``, etc. If no such directory in ``test`` folder, then
PlatformIO Test System will treat the source code of ``test`` folder
as SINGLE test.
7. Run tests using :ref:`cmd_test` command.
.. _unit_testing_api:
Test API
--------
The summary of `Unity Test API <https://github.com/ThrowTheSwitch/Unity#unity-test-api>`_:
* `Running Tests <https://github.com/ThrowTheSwitch/Unity#running-tests>`_
- ``RUN_TEST(func, linenum)``
* `Ignoring Tests <https://github.com/ThrowTheSwitch/Unity#ignoring-tests>`_
- ``TEST_IGNORE()``
- ``TEST_IGNORE_MESSAGE (message)``
* `Aborting Tests <https://github.com/ThrowTheSwitch/Unity#aborting-tests>`_
- ``TEST_PROTECT()``
- ``TEST_ABORT()``
* `Basic Validity Tests <https://github.com/ThrowTheSwitch/Unity#basic-validity-tests>`_
- ``TEST_ASSERT_TRUE(condition)``
- ``TEST_ASSERT_FALSE(condition)``
- ``TEST_ASSERT(condition)``
- ``TEST_ASSERT_UNLESS(condition)``
- ``TEST_FAIL()``
- ``TEST_FAIL_MESSAGE(message)``
* `Numerical Assertions: Integers <https://github.com/ThrowTheSwitch/Unity#numerical-assertions-integers>`_
- ``TEST_ASSERT_EQUAL_INT(expected, actual)``
- ``TEST_ASSERT_EQUAL_INT8(expected, actual)``
- ``TEST_ASSERT_EQUAL_INT16(expected, actual)``
- ``TEST_ASSERT_EQUAL_INT32(expected, actual)``
- ``TEST_ASSERT_EQUAL_INT64(expected, actual)``
- ``TEST_ASSERT_EQUAL_UINT(expected, actual)``
- ``TEST_ASSERT_EQUAL_UINT8(expected, actual)``
- ``TEST_ASSERT_EQUAL_UINT16(expected, actual)``
- ``TEST_ASSERT_EQUAL_UINT32(expected, actual)``
- ``TEST_ASSERT_EQUAL_UINT64(expected, actual)``
- ``TEST_ASSERT_EQUAL_HEX(expected, actual)``
- ``TEST_ASSERT_EQUAL_HEX8(expected, actual)``
- ``TEST_ASSERT_EQUAL_HEX16(expected, actual)``
- ``TEST_ASSERT_EQUAL_HEX32(expected, actual)``
- ``TEST_ASSERT_EQUAL_HEX64(expected, actual)``
- ``TEST_ASSERT_EQUAL_HEX8_ARRAY(expected, actual, elements)``
- ``TEST_ASSERT_EQUAL(expected, actual)``
- ``TEST_ASSERT_INT_WITHIN(delta, expected, actual)``
* `Numerical Assertions: Bitwise <https://github.com/ThrowTheSwitch/Unity#numerical-assertions-bitwise>`_
- ``TEST_ASSERT_BITS(mask, expected, actual)``
- ``TEST_ASSERT_BITS_HIGH(mask, actual)``
- ``TEST_ASSERT_BITS_LOW(mask, actual)``
- ``TEST_ASSERT_BIT_HIGH(mask, actual)``
- ``TEST_ASSERT_BIT_LOW(mask, actual)``
* `Numerical Assertions: Floats <https://github.com/ThrowTheSwitch/Unity#numerical-assertions-floats>`_
- ``TEST_ASSERT_FLOAT_WITHIN(delta, expected, actual)``
- ``TEST_ASSERT_EQUAL_FLOAT(expected, actual)``
- ``TEST_ASSERT_EQUAL_DOUBLE(expected, actual)``
* `String Assertions <https://github.com/ThrowTheSwitch/Unity#string-assertions>`_
- ``TEST_ASSERT_EQUAL_STRING(expected, actual)``
- ``TEST_ASSERT_EQUAL_STRING_LEN(expected, actual, len)``
- ``TEST_ASSERT_EQUAL_STRING_MESSAGE(expected, actual, message)``
- ``TEST_ASSERT_EQUAL_STRING_LEN_MESSAGE(expected, actual, len, message)``
* `Pointer Assertions <https://github.com/ThrowTheSwitch/Unity#pointer-assertions>`_
- ``TEST_ASSERT_NULL(pointer)``
- ``TEST_ASSERT_NOT_NULL(pointer)``
* `Memory Assertions <https://github.com/ThrowTheSwitch/Unity#pointer-assertions>`_
- ``TEST_ASSERT_EQUAL_MEMORY(expected, actual, len)``
Example
-------
1. Please follow to :ref:`quickstart` and create "Blink Project". According
to the Unit Testing :ref:`unit_testing_design` it is the **Main program**
2. Create ``test`` directory in that project (on the same level as ``src``)
and place ``test_main.cpp`` file to it (the source code is located below)
3. Wrap ``setup()`` and ``loop()`` methods of main program in ``UNIT_TEST``
guard
4. Run tests using :ref:`cmd_test` command.
Project structure
~~~~~~~~~~~~~~~~~
.. code-block:: bash
project_dir
├── lib
│   └── readme.txt
├── platformio.ini
├── src
│   └── main.cpp
└── test
└── test_main.cpp
Source files
~~~~~~~~~~~~
* ``platformio.ini``
.. code-block:: ini
; Project Configuration File
; Docs: http://docs.platformio.org/en/latest/projectconf.html
[env:uno]
platform = atmelavr
framework = arduino
board = uno
[env:nodemcu]
platform = espressif
framework = arduino
board = nodemcu
[env:teensy31]
platform = teensy
framework = arduino
board = teensy31
* ``src/main.cpp``
.. code-block:: cpp
/*
* Blink
* Turns on an LED on for one second,
* then off for one second, repeatedly.
*/
#include "Arduino.h"
#ifndef UNIT_TEST // IMPORTANT LINE!
void setup()
{
// initialize LED digital pin as an output.
pinMode(LED_BUILTIN, OUTPUT);
}
void loop()
{
// turn the LED on (HIGH is the voltage level)
digitalWrite(LED_BUILTIN, HIGH);
// wait for a second
delay(1000);
// turn the LED off by making the voltage LOW
digitalWrite(LED_BUILTIN, LOW);
// wait for a second
delay(1000);
}
#endif // IMPORTANT LINE!
* ``test/test_main.cpp``
.. code-block:: cpp
#include <Arduino.h>
#include <unity.h>
#ifdef UNIT_TEST
// void setUp(void) {
// // set stuff up here
// }
// void tearDown(void) {
// // clean stuff up here
// }
void test_led_builtin_pin_number(void) {
TEST_ASSERT_EQUAL(LED_BUILTIN, 13);
}
void test_led_state_high(void) {
digitalWrite(LED_BUILTIN, HIGH);
TEST_ASSERT_EQUAL(digitalRead(LED_BUILTIN), HIGH);
}
void test_led_state_low(void) {
digitalWrite(LED_BUILTIN, LOW);
TEST_ASSERT_EQUAL(digitalRead(LED_BUILTIN), LOW);
}
void setup() {
UNITY_BEGIN(); // IMPORTANT LINE!
RUN_TEST(test_led_builtin_pin_number);
pinMode(LED_BUILTIN, OUTPUT);
}
uint8_t i = 0;
uint8_t max_blinks = 5;
void loop() {
if (i < max_blinks)
{
RUN_TEST(test_led_state_high);
delay(500);
RUN_TEST(test_led_state_low);
delay(500);
}
else if (i == max_blinks) {
UNITY_END(); // IMPORTANT LINE!
}
i++;
}
#endif
Test results
~~~~~~~~~~~~
.. code-block:: bash
> platformio test --environment uno
Collected 1 items
========================= [test::*] Building... (1/3) ==============================
[Wed Jun 15 00:27:42 2016] Processing uno (platform: atmelavr, board: uno, framework: arduino)
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
avr-g++ -o .pioenvs/uno/test/test_main.o -c -fno-exceptions -fno-threadsafe-statics -std=gnu++11 -g -Os -Wall -ffunction-sections -fdata-sections -mmcu=atmega328p -DF_CPU=16000000L -DPLATFORMIO=030000 -DARDUINO_ARCH_AVR -DARDUINO_AVR_UNO -DARDUINO=10608 -DUNIT_TEST -DUNITY_INCLUDE_CONFIG_H -I.pioenvs/uno/FrameworkArduino -I.pioenvs/uno/FrameworkArduinoVariant -Isrc -I.pioenvs/uno/UnityTestLib test/test_main.cpp
avr-g++ -o .pioenvs/uno/firmware.elf -Os -mmcu=atmega328p -Wl,--gc-sections,--relax .pioenvs/uno/src/main.o .pioenvs/uno/test/output_export.o .pioenvs/uno/test/test_main.o -L.pioenvs/uno -Wl,--start-group .pioenvs/uno/libUnityTestLib.a .pioenvs/uno/libFrameworkArduinoVariant.a .pioenvs/uno/libFrameworkArduino.a -lm -Wl,--end-group
avr-objcopy -O ihex -R .eeprom .pioenvs/uno/firmware.elf .pioenvs/uno/firmware.hex
avr-size --mcu=atmega328p -C -d .pioenvs/uno/firmware.elf
AVR Memory Usage
----------------
Device: atmega328p
Program: 4702 bytes (14.3% Full)
(.text + .data + .bootloader)
Data: 460 bytes (22.5% Full)
(.data + .bss + .noinit)
========================= [test::*] Uploading... (2/3) ==============================
[Wed Jun 15 00:27:43 2016] Processing uno (platform: atmelavr, board: uno, framework: arduino)
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
avr-g++ -o .pioenvs/uno/firmware.elf -Os -mmcu=atmega328p -Wl,--gc-sections,--relax .pioenvs/uno/src/main.o .pioenvs/uno/test/output_export.o .pioenvs/uno/test/test_main.o -L.pioenvs/uno -Wl,--start-group .pioenvs/uno/libUnityTestLib.a .pioenvs/uno/libFrameworkArduinoVariant.a .pioenvs/uno/libFrameworkArduino.a -lm -Wl,--end-group
MethodWrapper([".pioenvs/uno/firmware.elf"], [".pioenvs/uno/src/main.o", ".pioenvs/uno/test/output_export.o", ".pioenvs/uno/test/test_main.o"])
Check program size...
text data bss dec hex filename
4464 238 222 4924 133c .pioenvs/uno/firmware.elf
BeforeUpload(["upload"], [".pioenvs/uno/firmware.hex"])
Looking for upload port/disk...
avr-size --mcu=atmega328p -C -d .pioenvs/uno/firmware.elf
Auto-detected: /dev/cu.usbmodemFD131
avrdude -v -p atmega328p -C "/Users/ikravets/.platformio/packages/tool-avrdude/avrdude.conf" -c arduino -b 115200 -P "/dev/cu.usbmodemFD131" -D -U flash:w:.pioenvs/uno/firmware.hex:i
[...]
avrdude done. Thank you.
========================= [test::*] Testing... (3/3) =========================
If you do not see any output for the first 10 secs, please reset board (press reset button)
test/test_main.cpp:30:test_led_builtin_pin_number PASSED
test/test_main.cpp:41:test_led_state_high PASSED
test/test_main.cpp:43:test_led_state_low PASSED
test/test_main.cpp:41:test_led_state_high PASSED
test/test_main.cpp:43:test_led_state_low PASSED
test/test_main.cpp:41:test_led_state_high PASSED
test/test_main.cpp:43:test_led_state_low PASSED
test/test_main.cpp:41:test_led_state_high PASSED
test/test_main.cpp:43:test_led_state_low PASSED
test/test_main.cpp:41:test_led_state_high PASSED
test/test_main.cpp:43:test_led_state_low PASSED
-----------------------
11 Tests 0 Failures 0 Ignored
========================= [TEST SUMMARY] =====================================
test:*/env:uno PASSED
========================= [PASSED] Took 13.35 seconds ========================

View File

@ -113,7 +113,7 @@ This option can be overridden by global environment variable
:envvar:`PLATFORMIO_ENVS_DIR`.
.. note::
If you have any problems with building your Project environmets which
If you have any problems with building your Project environments which
are defined in :ref:`projectconf`, then **TRY TO DELETE** this folder. In
this situation you will remove all cached files without any risk.
@ -130,6 +130,19 @@ project.
This option can be overridden by global environment variable
:envvar:`PLATFORMIO_DATA_DIR`.
.. _projectconf_pio_test_dir:
``test_dir``
^^^^^^^^^^^^
Directory for :ref:`unit_testing`.
A default value is ``test`` which means that folder is located in the root of
project.
This option can be overridden by global environment variable
:envvar:`PLATFORMIO_TEST_DIR`.
.. _projectconf_pio_env_default:
``env_default``

View File

@ -82,11 +82,10 @@ According to the table above the ID/TYPE for Teensy 3.1 is ``teensy31``. Also,
the ID for Arduino UNO is ``uno`` and for NodeMCU 1.0 (ESP-12E Module)
is ``nodemcuv2``.
Initialize Project
------------------
PlatformIO ecosystem contains huge database with pre-configured settings for the
PlatformIO ecosystem contains big database with pre-configured settings for the
most popular embedded boards. It helps you to forget about installing
toolchains, writing build scripts or configuring uploading process. Just tell
PlatformIO the Board ID and you will receive full working project with

View File

@ -64,7 +64,9 @@ Pre-built targets:
--upload-port
Upload port of embedded board. To print all available ports use
:ref:`cmd_serialports` command
:ref:`cmd_serialports` command.
If upload port is not specified, PlatformIO will try to detect it automatically.
.. option::
-d, --project-dir

View File

@ -0,0 +1,68 @@
.. Copyright 2014-2016 Ivan Kravets <me@ikravets.com>
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.
.. _cmd_test:
platformio test
===============
.. contents::
Usage
-----
.. code-block:: bash
platformio test [OPTIONS]
Description
-----------
Run tests from PlatformIO based project. More details about PlatformIO
:ref:`unit_testing`.
This command allows you to apply the tests for the environments specified
in :ref:`projectconf`.
Options
-------
.. program:: platformio test
.. option::
-e, --environment
Process specified environments. More details :option:`platformio run --environment`
.. option::
--upload-port
Upload port of embedded board. To print all available ports use
:ref:`cmd_serialports` command.
If upload port is not specified, PlatformIO will try to detect it automatically.
.. option::
-d, --project-dir
Specify the path to project directory. By default, ``--project-dir`` is equal
to current working directory (``CWD``).
.. option::
-v, --verbose
Shows details about the results of processing environments. More details
:option:`platformio run --verbose`
Examples
--------
For the examples please follow to :ref:`unit_testing` page.

View File

@ -66,5 +66,6 @@ Commands
cmd_run
cmd_serialports
cmd_settings
cmd_test
cmd_update
cmd_upgrade

View File

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

View File

@ -32,6 +32,7 @@ commonvars.AddVariables(
("BUILD_SCRIPT",),
("EXTRA_SCRIPT",),
("PIOENV",),
("PIOTEST",),
("PLATFORM",),
# options

View File

@ -16,10 +16,10 @@ from __future__ import absolute_import
import atexit
from os import remove
from os.path import isdir, isfile, join
from os.path import isdir, isfile, join, sep
from string import Template
FRAMEWORKS_PARAMETERS = {
FRAMEWORK_PARAMETERS = {
"arduino": {
"framework": "Arduino.h",
"serial_obj": "",
@ -50,9 +50,6 @@ FRAMEWORKS_PARAMETERS = {
def ProcessTest(env):
test_dir = env.subst("$PROJECTTEST_DIR")
env.Append(
CPPDEFINES=[
"UNIT_TEST",
@ -63,19 +60,23 @@ def ProcessTest(env):
join("$BUILD_DIR", "UnityTestLib")
]
)
unitylib = env.BuildLibrary(
join("$BUILD_DIR", "UnityTestLib"),
env.DevPlatform().get_package_dir("tool-unity")
)
env.Prepend(LIBS=[unitylib])
test_dir = env.subst("$PROJECTTEST_DIR")
env.GenerateOutputReplacement(test_dir)
src_filter = None
if "PIOTEST" in env:
src_filter = "+<output_export.cpp>"
src_filter += " +<%s%s>" % (env['PIOTEST'], sep)
return env.LookupSources(
"$BUILDTEST_DIR", test_dir, duplicate=False)
"$BUILDTEST_DIR", test_dir, duplicate=False, src_filter=src_filter
)
def GenerateOutputReplacement(env, destination_dir):
@ -122,12 +123,12 @@ void output_complete(void)
"Please remove it manually." % file_)
framework = env.subst("$FRAMEWORK").lower()
if framework not in FRAMEWORKS_PARAMETERS.keys():
if framework not in FRAMEWORK_PARAMETERS.keys():
env.Exit(
"Error: %s framework doesn't support testing feature!" % framework)
else:
data = Template(TEMPLATECPP).substitute(
FRAMEWORKS_PARAMETERS[framework])
FRAMEWORK_PARAMETERS[framework])
tmp_file = join(destination_dir, "output_export.cpp")
with open(tmp_file, "w") as f:

View File

@ -41,19 +41,8 @@ from platformio.managers.platform import PlatformFactory
@click.pass_context
def cli(ctx, environment, target, upload_port, # pylint: disable=R0913,R0914
project_dir, verbose, disable_auto_clean):
assert check_project_envs(project_dir, environment)
with util.cd(project_dir):
config = util.get_project_config()
if not config.sections():
raise exception.ProjectEnvsNotAvailable()
known = set([s[4:] for s in config.sections()
if s.startswith("env:")])
unknown = set(environment) - known
if unknown:
raise exception.UnknownEnvNames(
", ".join(unknown), ", ".join(known))
# clean obsolete .pioenvs dir
if not disable_auto_clean:
try:
@ -66,6 +55,7 @@ def cli(ctx, environment, target, upload_port, # pylint: disable=R0913,R0914
fg="yellow"
)
config = util.get_project_config()
env_default = None
if config.has_option("platformio", "env_default"):
env_default = [
@ -94,6 +84,8 @@ def cli(ctx, environment, target, upload_port, # pylint: disable=R0913,R0914
options = {}
for k, v in config.items(section):
options[k] = v
if "piotest" not in options and "piotest" in ctx.meta:
options['piotest'] = ctx.meta['piotest']
ep = EnvironmentProcessor(
ctx, envname, options, target, upload_port, verbose)
@ -136,15 +128,12 @@ class EnvironmentProcessor(object):
result = self._run()
is_error = result['returncode'] != 0
summary_text = " Took %.2f seconds " % (time() - start_time)
half_line = "=" * ((terminal_width - len(summary_text) - 10) / 2)
click.echo("%s [%s]%s%s" % (
half_line,
(click.style(" ERROR ", fg="red", bold=True)
if is_error else click.style("SUCCESS", fg="green", bold=True)),
summary_text,
half_line
), err=is_error)
if is_error or "piotest_processor" not in self.cmd_ctx.meta:
print_header("[%s] Took %.2f seconds" % (
(click.style("ERROR", fg="red", bold=True) if is_error
else click.style("SUCCESS", fg="green", bold=True)),
time() - start_time
), is_error=is_error)
return not is_error
@ -254,6 +243,29 @@ def _clean_pioenvs_dir(pioenvs_dir):
f.write(proj_hash)
def print_header(label, is_error=False):
terminal_width, _ = click.get_terminal_size()
width = len(click.unstyle(label))
half_line = "=" * ((terminal_width - width - 2) / 2)
click.echo("%s %s %s" % (half_line, label, half_line), err=is_error)
def check_project_envs(project_dir, environments):
with util.cd(project_dir):
config = util.get_project_config()
if not config.sections():
raise exception.ProjectEnvsNotAvailable()
known = set([s[4:] for s in config.sections()
if s.startswith("env:")])
unknown = set(environments) - known
if unknown:
raise exception.UnknownEnvNames(
", ".join(unknown), ", ".join(known))
return True
def calculate_project_hash():
structure = []
for d in (util.get_projectsrc_dir(), util.get_projectlib_dir()):

188
platformio/commands/test.py Normal file
View File

@ -0,0 +1,188 @@
# Copyright 2014-present Ivan Kravets <me@ikravets.com>
#
# 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 import getcwd, listdir
from os.path import isdir, join
from time import sleep, time
import click
import serial
from platformio import exception, util
from platformio.commands.run import cli as cmd_run
from platformio.commands.run import check_project_envs, print_header
from platformio.managers.platform import PlatformFactory
@click.command("test", short_help="Unit Testing")
@click.option("--environment", "-e", multiple=True, metavar="<environment>")
@click.option("--upload-port", metavar="<upload port>")
@click.option("--project-dir", "-d", default=getcwd,
type=click.Path(exists=True, file_okay=False, dir_okay=True,
writable=True, resolve_path=True))
@click.option("--verbose", "-v", count=True, default=3)
@click.pass_context
def cli(ctx, environment, upload_port, # pylint: disable=R0913,R0914
project_dir, verbose):
assert check_project_envs(project_dir, environment)
with util.cd(project_dir):
test_dir = util.get_projecttest_dir()
if not isdir(test_dir):
raise exception.TestDirEmpty(test_dir)
config = util.get_project_config()
env_names = set(
[s[4:] for s in config.sections() if s.startswith("env:")])
test_names = []
for item in sorted(listdir(test_dir)):
if isdir(join(test_dir, item)):
test_names.append(item)
if not test_names:
test_names = ["*"]
click.echo("Collected %d items" % len(test_names))
click.echo()
start_time = time()
results = []
for testname in test_names:
for envname in env_names:
if environment and envname not in environment:
continue
tp = TestProcessor(ctx, testname, envname, {
"project_config": config,
"project_dir": project_dir,
"upload_port": upload_port,
"verbose": verbose
})
results.append((tp.process(), testname, envname))
click.echo()
print_header("[%s]" % click.style("TEST SUMMARY"))
passed = True
for result in results:
if not result[0]:
passed = False
click.echo("test:%s/env:%s\t%s" % (
click.style(result[1], fg="yellow"),
click.style(result[2], fg="cyan"),
click.style("PASSED" if passed else "FAILED", fg="green"
if passed else "red")), err=not passed)
print_header("[%s] Took %.2f seconds" % (
(click.style("PASSED", fg="green", bold=True) if passed
else click.style("FAILED", fg="red", bold=True)),
time() - start_time
), is_error=not passed)
if not passed:
raise exception.ReturnErrorCode()
class TestProcessor(object):
SERIAL_TIMEOUT = 600
SERIAL_BAUDRATE = 9600
def __init__(self, cmd_ctx, testname, envname, options):
self.cmd_ctx = cmd_ctx
self.cmd_ctx.meta['piotest_processor'] = True
self.test_name = testname
self.env_name = envname
self.options = options
def process(self):
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("[test::%s] %s" % (
click.style(self.test_name, fg="yellow", bold=True),
text
), is_error=is_error)
click.echo()
def _build_or_upload(self, target):
if self.test_name != "*":
self.cmd_ctx.meta['piotest'] = self.test_name
return self.cmd_ctx.invoke(
cmd_run, project_dir=self.options['project_dir'],
upload_port=self.options['upload_port'],
verbose=self.options['verbose'], environment=[self.env_name],
target=target
)
def _run_hardware_test(self):
click.echo("If you don't see any output for the first 10 secs, "
"please reset board (press reset button)")
click.echo()
ser = serial.Serial(self.get_serial_port(), self.SERIAL_BAUDRATE,
timeout=self.SERIAL_TIMEOUT)
passed = True
while True:
line = ser.readline().strip()
if not line:
continue
if line.endswith(":PASS"):
click.echo("%s\t%s" % (
line[:-5], click.style("PASSED", fg="green")))
elif ":FAIL:" in line:
passed = False
click.secho(line, fg="red")
else:
click.echo(line)
if all([l in line for l in ("Tests", "Failures", "Ignored")]):
break
ser.close()
return passed
def get_serial_port(self):
config = self.options['project_config']
envdata = {}
for k, v in config.items("env:" + self.env_name):
envdata[k] = v
# if upload port is specified manually
if self.options.get("upload_port", envdata.get("upload_port")):
return self.options.get("upload_port", envdata.get("upload_port"))
platform = envdata['platform']
version = None
if "@" in platform:
platform, version = platform.rsplit("@", 1)
p = PlatformFactory.newPlatform(platform, version)
bconfig = p.board_config(envdata['board'])
port = None
board_hwids = []
if "build.hwids" in bconfig:
board_hwids = bconfig.get("build.hwids")
for item in util.get_serialports():
if "VID:PID" not in item['hwid']:
continue
port = item['port']
for hwid in board_hwids:
hwid_str = ("%s:%s" % (hwid[0], hwid[1])).replace("0x", "")
if hwid_str in item['hwid']:
return port
if not port:
raise exception.PlatformioException(
"Please specify `upload_port` for environment or use "
"global `--upload-port` option.")
return port

View File

@ -197,6 +197,13 @@ class CIBuildEnvsEmpty(PlatformioException):
"predefined environments using `--project-conf` option"
class TestDirEmpty(PlatformioException):
MESSAGE = "Test directory '{0}' is empty. More details about Unit "\
"Testing:\n http://docs.platformio.org/en/latest/platforms/"\
"unit_testing.html"
class UpgradeError(PlatformioException):
MESSAGE = """{0}