Files
platformio-core/platformio/test/runners/unity.py
2022-05-09 18:58:43 +03:00

291 lines
8.8 KiB
Python

# Copyright (c) 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.
import os
import re
import string
from pathlib import Path
import click
from platformio.test.exception import UnitTestSuiteError
from platformio.test.result import TestCase, TestCaseSource, TestStatus
from platformio.test.runners.base import TestRunnerBase
from platformio.util import strip_ansi_codes
class UnityTestRunner(TestRunnerBase):
EXTRA_LIB_DEPS = ["throwtheswitch/Unity@^2.5.2"]
# Example:
# test/test_foo.cpp:44:test_function_foo:FAIL: Expected 32 Was 33
TESTCASE_PARSE_RE = re.compile(
r"(?P<source_file>[^:]+):(?P<source_line>\d+):(?P<name>[^:]+):"
r"(?P<status>PASS|IGNORE|FAIL)(:\s*(?P<message>.+)$)?"
)
UNITY_CONFIG_H = """
#ifndef UNITY_CONFIG_H
#define UNITY_CONFIG_H
#ifndef NULL
#ifndef __cplusplus
#define NULL (void*)0
#else
#define NULL 0
#endif
#endif
#ifdef __cplusplus
extern "C"
{
#endif
void unityOutputStart(unsigned long);
void unityOutputChar(unsigned int);
void unityOutputFlush();
void unityOutputComplete();
#define UNITY_OUTPUT_START() unityOutputStart((unsigned long) $baudrate)
#define UNITY_OUTPUT_CHAR(c) unityOutputChar(c)
#define UNITY_OUTPUT_FLUSH() unityOutputFlush()
#define UNITY_OUTPUT_COMPLETE() unityOutputComplete()
#ifdef __cplusplus
}
#endif /* extern "C" */
#endif /* UNITY_CONFIG_H */
"""
UNITY_CONFIG_C = """
#include <unity_config.h>
#if !defined(UNITY_WEAK_ATTRIBUTE) && !defined(UNITY_WEAK_PRAGMA)
# if defined(__GNUC__) || defined(__ghs__) /* __GNUC__ includes clang */
# if !(defined(__WIN32__) && defined(__clang__)) && !defined(__TMS470__)
# define UNITY_WEAK_ATTRIBUTE __attribute__((weak))
# endif
# endif
#endif
#ifdef __cplusplus
extern "C"
{
#endif
#ifdef UNITY_WEAK_ATTRIBUTE
UNITY_WEAK_ATTRIBUTE void setUp(void) { }
UNITY_WEAK_ATTRIBUTE void tearDown(void) { }
UNITY_WEAK_ATTRIBUTE void suiteSetUp(void) { }
UNITY_WEAK_ATTRIBUTE int suiteTearDown(int num_failures) { return num_failures; }
#elif defined(UNITY_WEAK_PRAGMA)
#pragma weak setUp
void setUp(void) { }
#pragma weak tearDown
void tearDown(void) { }
#pragma weak suiteSetUp
void suiteSetUp(void) { }
#pragma weak suiteTearDown
int suiteTearDown(int num_failures) { return num_failures; }
#endif
#ifdef __cplusplus
}
#endif /* extern "C" */
$framework_config_code
"""
UNITY_FRAMEWORK_CONFIG = dict(
native=dict(
code="""
#include <stdio.h>
void unityOutputStart(unsigned long baudrate) { }
void unityOutputChar(unsigned int c) { putchar(c); }
void unityOutputFlush(void) { fflush(stdout); }
void unityOutputComplete(void) { }
""",
language="c",
),
arduino=dict(
code="""
#include <Arduino.h>
void unityOutputStart(unsigned long baudrate) { Serial.begin(baudrate); }
void unityOutputChar(unsigned int c) { Serial.write(c); }
void unityOutputFlush(void) { Serial.flush(); }
void unityOutputComplete(void) { Serial.end(); }
""",
language="cpp",
),
mbed=dict(
code="""
#include <mbed.h>
#if MBED_MAJOR_VERSION == 6
UnbufferedSerial pc(USBTX, USBRX);
#else
RawSerial pc(USBTX, USBRX);
#endif
void unityOutputStart(unsigned long baudrate) { pc.baud(baudrate); }
void unityOutputChar(unsigned int c) {
#if MBED_MAJOR_VERSION == 6
pc.write(&c, 1);
#else
pc.putc(c);
#endif
}
void unityOutputFlush(void) { }
void unityOutputComplete(void) { }
""",
language="cpp",
),
espidf=dict(
code="""
#include <stdio.h>
void unityOutputStart(unsigned long baudrate) { }
void unityOutputChar(unsigned int c) { putchar(c); }
void unityOutputFlush(void) { fflush(stdout); }
void unityOutputComplete(void) { }
""",
language="c",
),
zephyr=dict(
code="""
#include <sys/printk.h>
void unityOutputStart(unsigned long baudrate) { }
void unityOutputChar(unsigned int c) { printk("%c", c); }
void unityOutputFlush(void) { }
void unityOutputComplete(void) { }
""",
language="c",
),
legacy_custom_transport=dict(
code="""
#include <unittest_transport.h>
void unityOutputStart(unsigned long baudrate) { unittest_uart_begin(); }
void unityOutputChar(unsigned int c) { unittest_uart_putchar(c); }
void unityOutputFlush(void) { unittest_uart_flush(); }
void unityOutputComplete(void) { unittest_uart_end(); }
""",
language="cpp",
),
)
def get_unity_framework_config(self):
if not self.platform.is_embedded():
return self.UNITY_FRAMEWORK_CONFIG["native"]
if (
self.project_config.get(
f"env:{self.test_suite.env_name}", "test_transport", None
)
== "custom"
):
framework = "legacy_custom_transport"
else:
framework = (
self.project_config.get(f"env:{self.test_suite.env_name}", "framework")
or [None]
)[0]
if framework and framework in self.UNITY_FRAMEWORK_CONFIG:
return self.UNITY_FRAMEWORK_CONFIG[framework]
raise UnitTestSuiteError(
f"Could not find Unity configuration for the `{framework}` framework.\n"
"Learn how to create a custom Unity configuration at"
"https://docs.platformio.org/en/latest/advanced/"
"unit-testing/frameworks/unity.html"
)
def configure_build_env(self, env):
env.Append(CPPDEFINES=["UNITY_INCLUDE_CONFIG_H"])
if self.custom_unity_config_exists(
[env.subst(item) for item in (env.get("CPPPATH") or [])]
):
return env
env.Replace(
UNITY_CONFIG_DIR=os.path.join("$BUILD_DIR", "unity_config"),
BUILD_UNITY_CONFIG_DIR=os.path.join("$BUILD_DIR", "unity_config_build"),
)
env.Append(CPPPATH=["$UNITY_CONFIG_DIR"])
self.generate_unity_extras(env.subst("$UNITY_CONFIG_DIR"))
env.BuildSources("$BUILD_UNITY_CONFIG_DIR", "$UNITY_CONFIG_DIR")
return env
@staticmethod
def custom_unity_config_exists(include_dirs):
return any(
os.path.isfile(os.path.join(d, "unity_config.h")) for d in include_dirs
)
def generate_unity_extras(self, dst_dir):
dst_dir = Path(dst_dir)
dst_dir.mkdir(parents=True, exist_ok=True)
unity_h = dst_dir / "unity_config.h"
if not unity_h.is_file():
unity_h.write_text(
string.Template(self.UNITY_CONFIG_H).substitute(
baudrate=self.get_test_speed()
),
encoding="utf8",
)
framework_config = self.get_unity_framework_config()
unity_c = dst_dir / ("unity_config.%s" % framework_config.get("language", "c"))
if not unity_c.is_file():
unity_c.write_text(
string.Template(self.UNITY_CONFIG_C).substitute(
framework_config_code=framework_config["code"]
),
encoding="utf8",
)
def on_testing_line_output(self, line):
if self.options.verbose:
click.echo(line, nl=False)
line = strip_ansi_codes(line or "").strip()
if not line:
return
test_case = self.parse_test_case(line)
if test_case:
click.echo(test_case.humanize())
if all(s in line for s in ("Tests", "Failures", "Ignored")):
self.test_suite.on_finish()
def parse_test_case(self, line):
if not self.TESTCASE_PARSE_RE:
raise NotImplementedError()
line = line.strip()
if not line:
return None
match = self.TESTCASE_PARSE_RE.search(line)
if not match:
return None
data = match.groupdict()
source = None
if "source_file" in data:
source = TestCaseSource(
filename=data["source_file"], line=int(data.get("source_line"))
)
test_case = TestCase(
name=data.get("name").strip(),
status=TestStatus.from_string(data.get("status")),
message=(data.get("message") or "").strip() or None,
stdout=line,
source=source,
)
self.test_suite.add_case(test_case)
return test_case