mirror of
https://github.com/SCST-project/scst.git
synced 2026-10-22 06:02:33 +00:00
.github/workflows: Run Sparse and Smatch with KUnit
Run isolated tooling checks and separate Sparse and Smatch Make targets on pushes to master and manual dispatch. Use the pinned kernel with KUnit enabled and preserve full logs, policy and tool provenance. During the source-fix rollout, completed source findings are advisory. Tool, build, parser and coverage failures remain blocking, including an empty scan or missing KUnit inputs. Local Make targets stay strict. Validate the current run's report together with Make's exit status, since Make maps both findings and incomplete analysis to status 2. Prepare the runner and container for Coccinelle and Cppcheck as well, but leave their matrix entries commented out. Cover all four report formats and GNU Make exit mapping with isolated tooling fixtures.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
name: Static analyzers
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
tooling:
|
||||
name: Tooling
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check analyzer tooling
|
||||
run: make lint-test
|
||||
|
||||
native:
|
||||
name: ${{ matrix.analyzer }} with KUnit
|
||||
needs: tooling
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
analyzer:
|
||||
- sparse
|
||||
- smatch
|
||||
# Supported by the runner; enable after reviewing native coverage.
|
||||
# - coccicheck
|
||||
# - cppcheck
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Build pinned analyzer environment
|
||||
run: |
|
||||
docker build -f scripts/ci/analyzers.Dockerfile -t scst-analyzers:local . \
|
||||
2>&1 | tee "$RUNNER_TEMP/analyzer-image.log"
|
||||
- name: Run make ${{ matrix.analyzer }}
|
||||
env:
|
||||
ANALYZER: ${{ matrix.analyzer }}
|
||||
run: |
|
||||
source scripts/ci/kernel.env
|
||||
# Temporary rollout mode: only completed source findings are advisory.
|
||||
# Tool, build and coverage failures still fail this job.
|
||||
docker run --rm --init --network=none --cap-drop=ALL \
|
||||
--security-opt=no-new-privileges --user "$(id -u):$(id -g)" \
|
||||
--env HOME=/tmp --env GITHUB_STEP_SUMMARY=/scst/.cache/analyzer-summary \
|
||||
--env "KDIR=/usr/src/kernels/$KERNEL_BUILD.x86_64" \
|
||||
--mount "type=bind,src=$PWD,dst=/scst" scst-analyzers:local \
|
||||
python3 -B scripts/ci/run-analyzer.py "$ANALYZER"
|
||||
- name: Publish summary
|
||||
if: always()
|
||||
run: |
|
||||
if [[ -f .cache/analyzer-summary ]]; then
|
||||
cat .cache/analyzer-summary >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
- name: Save complete analyzer evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.analyzer }}-evidence
|
||||
retention-days: 7
|
||||
path: |
|
||||
${{ runner.temp }}/analyzer-image.log
|
||||
.cache/lint/
|
||||
.cache/analyzer-summary
|
||||
@@ -9,6 +9,7 @@ Workflows live in [`.github/workflows`](../.github/workflows). PR checks target
|
||||
| [CI](../.github/workflows/ci.yml) | Clang build and DEB packaging | PR, push, manual |
|
||||
| [Packages](../.github/workflows/rpm.yml) | Ubuntu DEBs, RPMs and a published package snapshot | Push to master only |
|
||||
| [Regression](../.github/workflows/run_regression_tests.yaml) | Module and patched-kernel builds across the kernel matrix | PR, push |
|
||||
| [Static analyzers](../.github/workflows/analyzers.yml) | Sparse/Smatch with KUnit sources, tooling checks and full reports | Push to master, manual |
|
||||
| [KUnit](../.github/workflows/kunit.yml) | Kernel unit tests in QEMU | PR, push, manual |
|
||||
| [Ginkgo](../.github/workflows/ginkgo.yml) | Local and TCP iSCSI component tests in QEMU | PR, push, manual |
|
||||
| [Coverity](../.github/workflows/coverity.yml) | Static analysis | Push, upstream repository only |
|
||||
@@ -39,6 +40,10 @@ kernel diagnostics and incomplete reports fail the run. Package artifacts
|
||||
are attached to the Packages workflow; upstream master builds also update
|
||||
the `master-rpm` prerelease snapshot.
|
||||
|
||||
Analyzer jobs retain full reports in CI artifacts. See the
|
||||
[analyzer guide](../scripts/README.md#analyzer-ci) for reproduction and the
|
||||
temporary nonblocking policy for source findings.
|
||||
|
||||
## Local reproduction
|
||||
|
||||
Use the commands in the relevant workflow for build and package checks;
|
||||
|
||||
@@ -232,6 +232,19 @@ analysis. Source findings return 1 and incomplete analysis returns 2; Make
|
||||
maps both to 2. Full logs and the summary are in `.cache/lint/cppcheck.{log,json}`;
|
||||
the summary links to captured commands, native XML and checker coverage.
|
||||
|
||||
## Analyzer CI
|
||||
|
||||
The [workflow](../.github/workflows/analyzers.yml) runs on pushes to `master`
|
||||
and manual dispatch. Separate jobs run `make sparse` and `make smatch` with
|
||||
`KUNIT_TEST=y`. Reports in `.cache/lint/` are kept for seven days. Use the
|
||||
commands above to run either analyzer locally. The runner and image also
|
||||
support Coccinelle and Cppcheck; their matrix entries remain commented out.
|
||||
|
||||
CI temporarily reports completed source findings without blocking while fixes
|
||||
land. Build/tool failures, incomplete analysis and missing KUnit coverage
|
||||
still fail. This policy lives in `ci/run-analyzer.py`; local Make targets
|
||||
always fail on findings.
|
||||
|
||||
## Dependencies and tooling tests
|
||||
|
||||
Shared analyzer tooling requires Linux, Git and Python 3.10+, using only the
|
||||
|
||||
@@ -3,7 +3,7 @@ FROM fedora:44@sha256:43b29f65a41eb9c35e1cd5323e3bdf3b655c2357a9f4f1ff2f9c2798e5
|
||||
RUN dnf install -y --setopt=install_weak_deps=False \
|
||||
gcc make git python3 perl elfutils-libelf-devel openssl openssl-devel \
|
||||
sqlite-devel flex bison pkgconf-pkg-config diffutils findutils \
|
||||
curl-minimal xz tar gzip dwarves && dnf clean all
|
||||
curl-minimal xz tar gzip dwarves coccinelle cppcheck && dnf clean all
|
||||
|
||||
COPY scripts/ci/kernel.env scripts/ci/kernel.SHA256SUMS /opt/scst-ci/
|
||||
COPY scripts/ci/analyzers.env scripts/ci/install-analyzers /opt/scst-ci/
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
"""Run one analyzer Make target with CI coverage and failure checks."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
from analyzer_policy import source_policy # noqa: E402
|
||||
from lint_common import copy_output, entrypoint, run # noqa: E402
|
||||
|
||||
TOOLS = {"sparse": "sparse", "smatch": "smatch", "coccicheck": "spatch", "cppcheck": "cppcheck"}
|
||||
|
||||
|
||||
def complete_native_report(report):
|
||||
tool = report.get("tool")
|
||||
if tool in {"sparse", "smatch"}:
|
||||
return (bool(report.get("translation_units")) and report.get("make_status") == 0 and
|
||||
report.get("errors") == 0 and not report.get("unchecked_files") and
|
||||
not report.get("unrecorded_checks"))
|
||||
if tool == "coccicheck":
|
||||
rules = report.get("rules")
|
||||
return (isinstance(rules, list) and bool(rules) and all(
|
||||
row.get("complete") is True and row.get("native_status") == 0 and
|
||||
not row.get("failures") and row.get("handled") for row in rules))
|
||||
if tool == "cppcheck":
|
||||
profiles = report.get("profiles")
|
||||
return (report.get("kernel_make_status") == 0 and
|
||||
report.get("userspace_make_status") == 0 and
|
||||
not report.get("unchecked_files") and not report.get("unrecorded_checks") and
|
||||
isinstance(profiles, list) and len(profiles) == 2 and
|
||||
{row.get("profile") for row in profiles} == {"kernel", "userspace"} and all(
|
||||
row.get("complete") is True and row.get("native_status") in {0, 3} and
|
||||
not row.get("failures") and row.get("translation_units") and
|
||||
row.get("checked_files") for row in profiles))
|
||||
return False
|
||||
|
||||
|
||||
def digest(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def job_status(make_status, report):
|
||||
"""Only a completed scan with source findings may become advisory."""
|
||||
status = report.get("exit_code")
|
||||
if (report.get("tool") not in TOOLS or
|
||||
type(status) is not int or status not in (0, 1) or
|
||||
make_status != (2 if status else 0) or
|
||||
report.get("outcome") != ("findings" if status else "clean") or
|
||||
not complete_native_report(report) or
|
||||
not isinstance(report.get("checked_files"), list)):
|
||||
return 2
|
||||
remaining = report.get("remaining")
|
||||
if type(remaining) is not int or remaining < 0 or bool(status) != bool(remaining):
|
||||
return 2
|
||||
tool = report["tool"]
|
||||
scope = source_policy(tool)
|
||||
excluded = {rule["path"] for rule in scope["exclude_directories"]}
|
||||
roots = [root for root in scope["source_roots"] if root not in excluded]
|
||||
checked = report["checked_files"]
|
||||
if (any(not any(path.startswith(root + "/") for path in checked) for root in roots) or
|
||||
not any("/kunit/" in path for path in checked)):
|
||||
print("CI requires analysis records for every module component and KUnit sources, "
|
||||
"plus userspace where selected by policy.",
|
||||
file=sys.stderr)
|
||||
return 2
|
||||
# Temporary rollout policy: completed source findings are advisory.
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("tool", choices=tuple(TOOLS))
|
||||
args = parser.parse_args()
|
||||
kernel = Path(os.environ.get("SCST_LINT_KDIR") or os.environ.get("KDIR", ""))
|
||||
if not (kernel / "Module.symvers").is_file():
|
||||
raise ValueError("Set KDIR to a prepared kernel build tree with Module.symvers")
|
||||
config = (kernel / ".config").read_text()
|
||||
if not any(line in {"CONFIG_KUNIT=y", "CONFIG_KUNIT=m"} for line in config.splitlines()):
|
||||
raise ValueError("The CI analysis kernel must enable CONFIG_KUNIT")
|
||||
executable = shutil.which(TOOLS[args.tool])
|
||||
if not executable:
|
||||
raise ValueError(f"{args.tool} is not installed")
|
||||
directory = ROOT / ".cache/lint"
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
provenance = Path("/usr/local/share/scst-ci") / (args.tool + "-source.txt")
|
||||
environment = {
|
||||
"commit": subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip(),
|
||||
"kernel": str(kernel.resolve()), "kernel_config_sha256": digest(kernel / ".config"),
|
||||
"tool": executable, "tool_sha256": digest(Path(executable)),
|
||||
"version": subprocess.check_output([executable, "--version"], text=True).strip(),
|
||||
"source_provenance": provenance.read_text() if provenance.exists() else "unavailable",
|
||||
"kunit_test": "y",
|
||||
}
|
||||
environment_text = json.dumps(environment, indent=2) + "\n"
|
||||
(directory / f"{args.tool}.environment.json").write_text(environment_text)
|
||||
print(environment_text, flush=True)
|
||||
(directory / "kernel.config").write_text(config)
|
||||
models = provenance.with_name("smatch-data.sha256")
|
||||
if args.tool == "smatch" and models.exists():
|
||||
shutil.copyfile(models, directory / "smatch-data.sha256")
|
||||
policy = subprocess.check_output([sys.executable, "-B", str(ROOT / "scripts/analyzer_policy.py"),
|
||||
"show", args.tool, "--format=json"], text=True)
|
||||
(directory / f"{args.tool}.policy.json").write_text(policy)
|
||||
print(policy, flush=True)
|
||||
identity = str(uuid.uuid4())
|
||||
env = dict(os.environ, KDIR=str(kernel.resolve()), SCST_LINT_KDIR=str(kernel.resolve()),
|
||||
KUNIT_TEST="y", KUNIT_LABEL="ALL", DIFFBR="", LINT_DEFER_REPORTS="1",
|
||||
SCST_LINT_RUN_ID=identity)
|
||||
report = {}
|
||||
try:
|
||||
# Make maps both findings and driver failures to 2. The current run's
|
||||
# report distinguishes them; neither a failed make nor a report alone
|
||||
# is sufficient to classify the run as completed source findings.
|
||||
result = run(["make", "-j2", args.tool, f"KDIR={kernel.resolve()}",
|
||||
"KUNIT_TEST=y", "KUNIT_LABEL=ALL", "DIFFBR=", "WERROR=n"], cwd=ROOT, env=env)
|
||||
finally:
|
||||
path = directory / f"{args.tool}.json"
|
||||
if path.exists():
|
||||
candidate = json.loads(path.read_text())
|
||||
if (isinstance(candidate, dict) and candidate.get("run_id") == identity and
|
||||
candidate.get("tool") == args.tool):
|
||||
report = candidate
|
||||
log = directory / f"{args.tool}.log"
|
||||
if log.exists():
|
||||
print(f"\nFull report: {log}", flush=True)
|
||||
copy_output(log, sys.stdout.buffer)
|
||||
status = job_status(result.returncode, report)
|
||||
if status == 0 and report["exit_code"] == 1:
|
||||
print(f"::warning::{args.tool}: {report['remaining']} source finding(s); "
|
||||
"temporary report mode, analysis completed.")
|
||||
summary = (f"{args.tool}: outcome={report.get('outcome', 'missing')}, "
|
||||
f"remaining={report.get('remaining', 'unknown')}, "
|
||||
f"make exit={result.returncode}, job exit={status}\n")
|
||||
print(summary, end="")
|
||||
if os.environ.get("GITHUB_STEP_SUMMARY"):
|
||||
with Path(os.environ["GITHUB_STEP_SUMMARY"]).open("a") as stream:
|
||||
stream.write(summary)
|
||||
return status
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(entrypoint(main))
|
||||
@@ -0,0 +1,153 @@
|
||||
"""Exercise Make exit mapping and CI reports without a build or container."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
class AnalyzerCiTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
temporary = tempfile.TemporaryDirectory(prefix="scst-analyzer-ci-")
|
||||
self.addCleanup(temporary.cleanup)
|
||||
self.root = Path(temporary.name)
|
||||
repository = Path(__file__).resolve().parents[2]
|
||||
shutil.copytree(repository / "scripts", self.root / "scripts",
|
||||
ignore=shutil.ignore_patterns("__pycache__"))
|
||||
self.bin = self.root / "bin"
|
||||
self.bin.mkdir()
|
||||
(self.bin / "make").symlink_to(shutil.which("make"))
|
||||
# Exercise real GNU Make's mapping of recipe failures to status 2.
|
||||
# The fixture driver below produces reports without compiling SCST.
|
||||
(self.root / "Makefile").write_text(
|
||||
"SHELL := /bin/sh\n.PHONY: sparse smatch coccicheck cppcheck\n"
|
||||
"sparse smatch coccicheck cppcheck:\n"
|
||||
f"\t@{shlex.quote(sys.executable)} -B scripts/sparse_smatch/smp.py $@\n"
|
||||
"\t@exit $(FIXTURE_MAKE_TAIL_EXIT)\n")
|
||||
for name in ("git", "sparse", "smatch", "spatch", "cppcheck"):
|
||||
path = self.bin / name
|
||||
path.write_text(f"#!{sys.executable}\nprint('fixture-version')\n")
|
||||
path.chmod(0o755)
|
||||
kernel = self.root / "kernel"
|
||||
kernel.mkdir()
|
||||
(kernel / "Module.symvers").touch()
|
||||
(kernel / ".config").write_text("CONFIG_KUNIT=y\n")
|
||||
self.env = dict(os.environ, KDIR=str(kernel), SCST_LINT_KDIR=str(kernel),
|
||||
PATH=str(self.bin), MAKEFLAGS="", MFLAGS="", MAKEOVERRIDES="",
|
||||
GNUMAKEFLAGS="", FIXTURE_STATUS="0", FIXTURE_MAKE_TAIL_EXIT="0",
|
||||
FIXTURE_OUTCOME="clean", FIXTURE_COVERAGE="full")
|
||||
self.env.pop("GITHUB_STEP_SUMMARY", None)
|
||||
driver = self.root / "scripts/sparse_smatch/smp.py"
|
||||
driver.write_text('''import json, os, sys
|
||||
from pathlib import Path
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
assert os.environ['KUNIT_TEST'] == 'y'
|
||||
assert os.environ['KUNIT_LABEL'] == 'ALL'
|
||||
assert os.environ['DIFFBR'] == ''
|
||||
assert os.environ['LINT_DEFER_REPORTS'] == '1'
|
||||
status = int(os.environ['FIXTURE_STATUS'])
|
||||
directory = root / '.cache/lint'
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
tool = sys.argv[1]
|
||||
if os.environ.get('FIXTURE_NO_REPORT'):
|
||||
sys.exit(2)
|
||||
checked = [p + '/fixture.c' for p in ('scst', 'iscsi-scst',
|
||||
'qla2x00t-32gbit/qla2x00-target', 'srpt', 'fcst', 'scst_local')]
|
||||
checked += ['scst/src/kunit/fixture.c']
|
||||
if tool in ('coccicheck', 'cppcheck'):
|
||||
checked += ['usr/fileio/fixture.c', 'iscsi-scst/usr/fixture.c']
|
||||
if os.environ['FIXTURE_COVERAGE'] != 'full':
|
||||
checked = checked[:1]
|
||||
report = dict(tool=os.environ.get('FIXTURE_REPORT_TOOL', tool), run_id=os.environ['SCST_LINT_RUN_ID'],
|
||||
outcome=os.environ['FIXTURE_OUTCOME'], exit_code=status, make_status=0,
|
||||
errors=0, unchecked_files=[], unrecorded_checks=[],
|
||||
translation_units=len(checked), checked_files=checked,
|
||||
remaining=1 if status == 1 else 0)
|
||||
report['rules'] = [dict(complete=True, native_status=0, failures=[], handled=checked)]
|
||||
report['kernel_make_status'] = report['userspace_make_status'] = 0
|
||||
report['profiles'] = [dict(profile=profile, complete=True, native_status=0, failures=[],
|
||||
translation_units=1, checked_files=checked) for profile in ('kernel', 'userspace')]
|
||||
if os.environ.get('FIXTURE_NATIVE_INCOMPLETE'):
|
||||
report['rules'][0]['complete'] = False
|
||||
report['profiles'][0]['complete'] = False
|
||||
if os.environ.get('FIXTURE_STALE'):
|
||||
report['run_id'] = 'previous run'
|
||||
(directory / (tool + '.json')).write_text(json.dumps(report))
|
||||
(directory / (tool + '.log')).write_text('raw diagnostic fixture\\n')
|
||||
print('summary fixture')
|
||||
sys.exit(status)
|
||||
''')
|
||||
|
||||
def run_ci(self, tool="sparse"):
|
||||
return subprocess.run([sys.executable, "-B", str(self.root / "scripts/ci/run-analyzer.py"),
|
||||
tool], cwd=self.root, env=self.env,
|
||||
capture_output=True, text=True, timeout=10)
|
||||
|
||||
def test_only_completed_findings_can_be_advisory(self):
|
||||
for tool in ("sparse", "smatch", "coccicheck", "cppcheck"):
|
||||
for status, outcome, expected in (
|
||||
(0, "clean", 0), (1, "findings", 0), (2, "incomplete", 2),
|
||||
(1, "incomplete", 2), (0, "skipped", 2), (0, "findings", 2)):
|
||||
with self.subTest(tool=tool, status=status, outcome=outcome):
|
||||
self.env.update(FIXTURE_STATUS=str(status), FIXTURE_OUTCOME=outcome)
|
||||
result = self.run_ci(tool)
|
||||
self.assertEqual(result.returncode, expected, result.stdout + result.stderr)
|
||||
self.assertEqual(result.stdout.count("raw diagnostic fixture"), 1)
|
||||
self.assertEqual("::warning::" in result.stdout,
|
||||
outcome == "findings" and expected == 0)
|
||||
self.assertIn(f"make exit={2 if status else 0}", result.stdout)
|
||||
|
||||
def test_make_failure_after_clean_analysis_cannot_pass(self):
|
||||
self.env["FIXTURE_MAKE_TAIL_EXIT"] = "7"
|
||||
result = self.run_ci()
|
||||
self.assertEqual(result.returncode, 2, result.stdout + result.stderr)
|
||||
self.assertIn("make exit=2, job exit=2", result.stdout)
|
||||
|
||||
def test_basic_analyzers_cannot_downgrade_incomplete_native_reports(self):
|
||||
self.env.update(FIXTURE_STATUS="1", FIXTURE_OUTCOME="findings", FIXTURE_NATIVE_INCOMPLETE="1")
|
||||
for tool in ("coccicheck", "cppcheck"):
|
||||
with self.subTest(tool=tool):
|
||||
result = self.run_ci(tool)
|
||||
self.assertEqual(result.returncode, 2, result.stdout + result.stderr)
|
||||
self.assertNotIn("::warning::", result.stdout)
|
||||
|
||||
def test_make_failure_without_a_new_report_cannot_reuse_previous_results(self):
|
||||
self.assertEqual(self.run_ci().returncode, 0)
|
||||
self.env["FIXTURE_NO_REPORT"] = "1"
|
||||
result = self.run_ci()
|
||||
self.assertEqual(result.returncode, 2, result.stdout + result.stderr)
|
||||
self.assertNotIn("raw diagnostic fixture", result.stdout)
|
||||
|
||||
def test_report_from_another_tool_is_not_accepted(self):
|
||||
self.env["FIXTURE_REPORT_TOOL"] = "smatch"
|
||||
result = self.run_ci("sparse")
|
||||
self.assertEqual(result.returncode, 2, result.stdout + result.stderr)
|
||||
self.assertNotIn("raw diagnostic fixture", result.stdout)
|
||||
|
||||
def test_missing_components_or_kunit_cannot_pass(self):
|
||||
self.env["FIXTURE_COVERAGE"] = "partial"
|
||||
result = self.run_ci()
|
||||
self.assertEqual(result.returncode, 2, result.stdout + result.stderr)
|
||||
self.assertIn("every module component and KUnit", result.stderr)
|
||||
|
||||
def test_previous_report_is_not_printed_or_accepted(self):
|
||||
self.env["FIXTURE_STALE"] = "1"
|
||||
result = self.run_ci()
|
||||
self.assertEqual(result.returncode, 2, result.stdout + result.stderr)
|
||||
self.assertNotIn("raw diagnostic fixture", result.stdout)
|
||||
|
||||
def test_failed_preparation_does_not_dump_old_logs(self):
|
||||
self.assertEqual(self.run_ci().returncode, 0)
|
||||
(self.root / "kernel/.config").write_text("# CONFIG_KUNIT is not set\n")
|
||||
result = self.run_ci()
|
||||
self.assertEqual(result.returncode, 2, result.stdout + result.stderr)
|
||||
self.assertNotIn("raw diagnostic fixture", result.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user