forked from platformio/platformio-core
458 lines
12 KiB
Python
458 lines
12 KiB
Python
# Copyright (c) 2019-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.
|
|
|
|
# pylint: disable=redefined-outer-name
|
|
|
|
import json
|
|
import sys
|
|
from os.path import isfile, join
|
|
|
|
import pytest
|
|
|
|
from platformio import fs
|
|
from platformio.commands.check.command import cli as cmd_check
|
|
|
|
DEFAULT_CONFIG = """
|
|
[env:native]
|
|
platform = native
|
|
"""
|
|
|
|
TEST_CODE = """
|
|
#include <stdlib.h>
|
|
|
|
void run_defects() {
|
|
/* Freeing a pointer twice */
|
|
int* doubleFreePi = (int*)malloc(sizeof(int));
|
|
*doubleFreePi=2;
|
|
free(doubleFreePi);
|
|
free(doubleFreePi); /* High */
|
|
|
|
/* Reading uninitialized memory */
|
|
int* uninitializedPi = (int*)malloc(sizeof(int));
|
|
*uninitializedPi++; /* High + Medium*/
|
|
free(uninitializedPi);
|
|
|
|
/* Delete instead of delete [] */
|
|
int* wrongDeletePi = new int[10];
|
|
wrongDeletePi++;
|
|
delete wrongDeletePi; /* High */
|
|
|
|
/* Index out of bounds */
|
|
int arr[10];
|
|
for(int i=0; i < 11; i++) {
|
|
arr[i] = 0; /* High */
|
|
}
|
|
}
|
|
|
|
int main() {
|
|
int uninitializedVar; /* Low */
|
|
run_defects();
|
|
}
|
|
"""
|
|
|
|
EXPECTED_ERRORS = 4
|
|
EXPECTED_WARNINGS = 1
|
|
EXPECTED_STYLE = 1
|
|
EXPECTED_DEFECTS = EXPECTED_ERRORS + EXPECTED_WARNINGS + EXPECTED_STYLE
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def check_dir(tmpdir_factory):
|
|
tmpdir = tmpdir_factory.mktemp("project")
|
|
tmpdir.join("platformio.ini").write(DEFAULT_CONFIG)
|
|
tmpdir.mkdir("src").join("main.cpp").write(TEST_CODE)
|
|
return tmpdir
|
|
|
|
|
|
def count_defects(output):
|
|
error, warning, style = 0, 0, 0
|
|
for l in output.split("\n"):
|
|
if "[high:error]" in l:
|
|
error += 1
|
|
elif "[medium:warning]" in l:
|
|
warning += 1
|
|
elif "[low:style]" in l:
|
|
style += 1
|
|
return error, warning, style
|
|
|
|
|
|
def test_check_cli_output(clirunner, check_dir):
|
|
result = clirunner.invoke(cmd_check, ["--project-dir", str(check_dir)])
|
|
|
|
errors, warnings, style = count_defects(result.output)
|
|
|
|
assert result.exit_code == 0
|
|
assert errors + warnings + style == EXPECTED_DEFECTS
|
|
|
|
|
|
def test_check_json_output(clirunner, check_dir):
|
|
result = clirunner.invoke(
|
|
cmd_check, ["--project-dir", str(check_dir), "--json-output"]
|
|
)
|
|
output = json.loads(result.stdout.strip())
|
|
|
|
assert isinstance(output, list)
|
|
assert len(output[0].get("defects", [])) == EXPECTED_DEFECTS
|
|
|
|
|
|
def test_check_tool_defines_passed(clirunner, check_dir):
|
|
result = clirunner.invoke(cmd_check, ["--project-dir", str(check_dir), "--verbose"])
|
|
output = result.output
|
|
|
|
assert "PLATFORMIO=" in output
|
|
assert "__GNUC__" in output
|
|
|
|
|
|
def test_check_severity_threshold(clirunner, check_dir):
|
|
result = clirunner.invoke(
|
|
cmd_check, ["--project-dir", str(check_dir), "--severity=high"]
|
|
)
|
|
|
|
errors, warnings, style = count_defects(result.output)
|
|
|
|
assert result.exit_code == 0
|
|
assert errors == EXPECTED_ERRORS
|
|
assert warnings == 0
|
|
assert style == 0
|
|
|
|
|
|
def test_check_includes_passed(clirunner, check_dir):
|
|
result = clirunner.invoke(cmd_check, ["--project-dir", str(check_dir), "--verbose"])
|
|
output = result.output
|
|
|
|
inc_count = 0
|
|
for l in output.split("\n"):
|
|
if l.startswith("Includes:"):
|
|
inc_count = l.count("-I")
|
|
|
|
# at least 1 include path for default mode
|
|
assert inc_count > 1
|
|
|
|
|
|
def test_check_silent_mode(clirunner, check_dir):
|
|
result = clirunner.invoke(cmd_check, ["--project-dir", str(check_dir), "--silent"])
|
|
|
|
errors, warnings, style = count_defects(result.output)
|
|
|
|
assert result.exit_code == 0
|
|
assert errors == EXPECTED_ERRORS
|
|
assert warnings == 0
|
|
assert style == 0
|
|
|
|
|
|
def test_check_custom_pattern_absolute_path(clirunner, tmpdir_factory):
|
|
project_dir = tmpdir_factory.mktemp("project")
|
|
project_dir.join("platformio.ini").write(DEFAULT_CONFIG)
|
|
|
|
check_dir = tmpdir_factory.mktemp("custom_src_dir")
|
|
check_dir.join("main.cpp").write(TEST_CODE)
|
|
|
|
result = clirunner.invoke(
|
|
cmd_check, ["--project-dir", str(project_dir), "--pattern=" + str(check_dir)]
|
|
)
|
|
|
|
errors, warnings, style = count_defects(result.output)
|
|
|
|
assert result.exit_code == 0
|
|
assert errors == EXPECTED_ERRORS
|
|
assert warnings == EXPECTED_WARNINGS
|
|
assert style == EXPECTED_STYLE
|
|
|
|
|
|
def test_check_custom_pattern_relative_path(clirunner, tmpdir_factory):
|
|
tmpdir = tmpdir_factory.mktemp("project")
|
|
tmpdir.join("platformio.ini").write(DEFAULT_CONFIG)
|
|
|
|
tmpdir.mkdir("app").join("main.cpp").write(TEST_CODE)
|
|
tmpdir.mkdir("prj").join("test.cpp").write(TEST_CODE)
|
|
|
|
result = clirunner.invoke(
|
|
cmd_check, ["--project-dir", str(tmpdir), "--pattern=app", "--pattern=prj"]
|
|
)
|
|
|
|
errors, warnings, style = count_defects(result.output)
|
|
|
|
assert result.exit_code == 0
|
|
assert errors + warnings + style == EXPECTED_DEFECTS * 2
|
|
|
|
|
|
def test_check_no_source_files(clirunner, tmpdir):
|
|
tmpdir.join("platformio.ini").write(DEFAULT_CONFIG)
|
|
tmpdir.mkdir("src")
|
|
|
|
result = clirunner.invoke(cmd_check, ["--project-dir", str(tmpdir)])
|
|
|
|
errors, warnings, style = count_defects(result.output)
|
|
|
|
assert result.exit_code != 0
|
|
assert errors == 0
|
|
assert warnings == 0
|
|
assert style == 0
|
|
|
|
|
|
def test_check_bad_flag_passed(clirunner, check_dir):
|
|
result = clirunner.invoke(
|
|
cmd_check, ["--project-dir", str(check_dir), '"--flags=--UNKNOWN"']
|
|
)
|
|
|
|
errors, warnings, style = count_defects(result.output)
|
|
|
|
assert result.exit_code != 0
|
|
assert errors == 0
|
|
assert warnings == 0
|
|
assert style == 0
|
|
|
|
|
|
def test_check_success_if_no_errors(clirunner, tmpdir):
|
|
tmpdir.join("platformio.ini").write(DEFAULT_CONFIG)
|
|
tmpdir.mkdir("src").join("main.c").write(
|
|
"""
|
|
#include <stdlib.h>
|
|
|
|
void unused_function(){
|
|
int unusedVar = 0;
|
|
int* iP = &unusedVar;
|
|
*iP++;
|
|
}
|
|
|
|
int main() {
|
|
}
|
|
"""
|
|
)
|
|
|
|
result = clirunner.invoke(cmd_check, ["--project-dir", str(tmpdir)])
|
|
|
|
errors, warnings, style = count_defects(result.output)
|
|
|
|
assert "[PASSED]" in result.output
|
|
assert result.exit_code == 0
|
|
assert errors == 0
|
|
assert warnings == 1
|
|
assert style == 1
|
|
|
|
|
|
def test_check_individual_flags_passed(clirunner, tmpdir):
|
|
config = DEFAULT_CONFIG + "\ncheck_tool = cppcheck, clangtidy, pvs-studio"
|
|
config += """\ncheck_flags =
|
|
cppcheck: --std=c++11
|
|
clangtidy: --fix-errors
|
|
pvs-studio: --analysis-mode=4
|
|
"""
|
|
tmpdir.join("platformio.ini").write(config)
|
|
tmpdir.mkdir("src").join("main.cpp").write(TEST_CODE)
|
|
result = clirunner.invoke(cmd_check, ["--project-dir", str(tmpdir), "-v"])
|
|
|
|
clang_flags_found = cppcheck_flags_found = pvs_flags_found = False
|
|
for l in result.output.split("\n"):
|
|
if "--fix" in l and "clang-tidy" in l and "--std=c++11" not in l:
|
|
clang_flags_found = True
|
|
elif "--std=c++11" in l and "cppcheck" in l and "--fix" not in l:
|
|
cppcheck_flags_found = True
|
|
elif (
|
|
"--analysis-mode=4" in l and "pvs-studio" in l.lower() and "--fix" not in l
|
|
):
|
|
pvs_flags_found = True
|
|
|
|
assert clang_flags_found
|
|
assert cppcheck_flags_found
|
|
assert pvs_flags_found
|
|
|
|
|
|
def test_check_cppcheck_misra_addon(clirunner, check_dir):
|
|
check_dir.join("misra.json").write(
|
|
"""
|
|
{
|
|
"script": "addons/misra.py",
|
|
"args": ["--rule-texts=rules.txt"]
|
|
}
|
|
"""
|
|
)
|
|
|
|
check_dir.join("rules.txt").write(
|
|
"""
|
|
Appendix A Summary of guidelines
|
|
Rule 3.1 Required
|
|
R3.1 text.
|
|
Rule 4.1 Required
|
|
R4.1 text.
|
|
Rule 10.4 Mandatory
|
|
R10.4 text.
|
|
Rule 11.5 Advisory
|
|
R11.5 text.
|
|
Rule 15.5 Advisory
|
|
R15.5 text.
|
|
Rule 15.6 Required
|
|
R15.6 text.
|
|
Rule 17.7 Required
|
|
R17.7 text.
|
|
Rule 20.1 Advisory
|
|
R20.1 text.
|
|
Rule 21.3 Required
|
|
R21.3 Found MISRA defect
|
|
Rule 21.4
|
|
R21.4 text.
|
|
"""
|
|
)
|
|
|
|
result = clirunner.invoke(
|
|
cmd_check, ["--project-dir", str(check_dir), "--flags=--addon=misra.json"]
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "R21.3 Found MISRA defect" in result.output
|
|
assert not isfile(join(str(check_dir), "src", "main.cpp.dump"))
|
|
|
|
|
|
def test_check_fails_on_defects_only_with_flag(clirunner, tmpdir):
|
|
config = DEFAULT_CONFIG + "\ncheck_tool = cppcheck, clangtidy"
|
|
tmpdir.join("platformio.ini").write(config)
|
|
tmpdir.mkdir("src").join("main.cpp").write(TEST_CODE)
|
|
|
|
default_result = clirunner.invoke(cmd_check, ["--project-dir", str(tmpdir)])
|
|
|
|
result_with_flag = clirunner.invoke(
|
|
cmd_check, ["--project-dir", str(tmpdir), "--fail-on-defect=high"]
|
|
)
|
|
|
|
assert default_result.exit_code == 0
|
|
assert result_with_flag.exit_code != 0
|
|
|
|
|
|
def test_check_fails_on_defects_only_on_specified_level(clirunner, tmpdir):
|
|
config = DEFAULT_CONFIG + "\ncheck_tool = cppcheck, clangtidy"
|
|
tmpdir.join("platformio.ini").write(config)
|
|
tmpdir.mkdir("src").join("main.c").write(
|
|
"""
|
|
#include <stdlib.h>
|
|
|
|
void unused_function(){
|
|
int unusedVar = 0;
|
|
int* iP = &unusedVar;
|
|
*iP++;
|
|
}
|
|
|
|
int main() {
|
|
}
|
|
"""
|
|
)
|
|
|
|
high_result = clirunner.invoke(
|
|
cmd_check, ["--project-dir", str(tmpdir), "--fail-on-defect=high"]
|
|
)
|
|
|
|
low_result = clirunner.invoke(
|
|
cmd_check, ["--project-dir", str(tmpdir), "--fail-on-defect=low"]
|
|
)
|
|
|
|
assert high_result.exit_code == 0
|
|
assert low_result.exit_code != 0
|
|
|
|
|
|
def test_check_pvs_studio_free_license(clirunner, tmpdir):
|
|
config = """
|
|
[env:test]
|
|
platform = teensy
|
|
board = teensy35
|
|
framework = arduino
|
|
check_tool = pvs-studio
|
|
"""
|
|
code = (
|
|
"""// This is an open source non-commercial project. Dear PVS-Studio, please check it.
|
|
// PVS-Studio Static Code Analyzer for C, C++, C#, and Java: http://www.viva64.com
|
|
"""
|
|
+ TEST_CODE
|
|
)
|
|
|
|
tmpdir.join("platformio.ini").write(config)
|
|
tmpdir.mkdir("src").join("main.c").write(code)
|
|
|
|
result = clirunner.invoke(
|
|
cmd_check, ["--project-dir", str(tmpdir), "--fail-on-defect=high", "-v"]
|
|
)
|
|
|
|
errors, warnings, style = count_defects(result.output)
|
|
|
|
assert result.exit_code != 0
|
|
assert errors != 0
|
|
assert warnings != 0
|
|
assert style == 0
|
|
|
|
|
|
def test_check_embedded_platform_all_tools(clirunner, validate_cliresult, tmpdir):
|
|
config = """
|
|
[env:test]
|
|
platform = ststm32
|
|
board = nucleo_f401re
|
|
framework = %s
|
|
check_tool = %s
|
|
"""
|
|
# tmpdir.join("platformio.ini").write(config)
|
|
tmpdir.mkdir("src").join("main.c").write(
|
|
"""// This is an open source non-commercial project. Dear PVS-Studio, please check it.
|
|
// PVS-Studio Static Code Analyzer for C, C++, C#, and Java: http://www.viva64.com
|
|
#include <stdlib.h>
|
|
|
|
void unused_function(int val){
|
|
int unusedVar = 0;
|
|
int* iP = &unusedVar;
|
|
*iP++;
|
|
}
|
|
|
|
int main() {
|
|
}
|
|
"""
|
|
)
|
|
|
|
frameworks = ["arduino", "stm32cube"]
|
|
if sys.version_info[0] == 3:
|
|
# Zephyr only supports Python 3
|
|
frameworks.append("zephyr")
|
|
|
|
for framework in frameworks:
|
|
for tool in ("cppcheck", "clangtidy", "pvs-studio"):
|
|
tmpdir.join("platformio.ini").write(config % (framework, tool))
|
|
result = clirunner.invoke(cmd_check, ["--project-dir", str(tmpdir)])
|
|
validate_cliresult(result)
|
|
defects = sum(count_defects(result.output))
|
|
assert result.exit_code == 0 and defects > 0, "Failed %s with %s" % (
|
|
framework,
|
|
tool,
|
|
)
|
|
|
|
|
|
def test_check_skip_includes_from_packages(clirunner, tmpdir):
|
|
config = """
|
|
[env:test]
|
|
platform = nordicnrf52
|
|
board = nrf52_dk
|
|
framework = arduino
|
|
"""
|
|
|
|
tmpdir.join("platformio.ini").write(config)
|
|
tmpdir.mkdir("src").join("main.c").write(TEST_CODE)
|
|
|
|
result = clirunner.invoke(
|
|
cmd_check, ["--project-dir", str(tmpdir), "--skip-packages", "-v"]
|
|
)
|
|
|
|
output = result.output
|
|
|
|
project_path = fs.to_unix_path(str(tmpdir))
|
|
for l in output.split("\n"):
|
|
if not l.startswith("Includes:"):
|
|
continue
|
|
for inc in l.split(" "):
|
|
if inc.startswith("-I") and project_path not in inc:
|
|
pytest.fail("Detected an include path from packages: " + inc)
|