Chapter 13Lesson 02~140 minutes

Metrics: Reliability, Maintainability, Security, Complexity, and Size: Guided Hands-On Workflow

Capture a metric baseline, make one controlled revision that increases structural complexity and adds uncovered code, reanalyze, compare raw measures and ratings, and explain every changed and unchanged signal from evidence.

Controlled revisionWeb APINew vs overallCoverageTask evidence

Learning objectives

  • Create a disposable Python project with a reproducible baseline revision, tests, coverage report, and full Git history.
  • Capture a mode-aware baseline metric snapshot only after the corresponding Compute Engine task succeeds.
  • Predict the direction of structural and coverage measures before adding a deliberately complex, untested function.
  • Reanalyze the changed revision and compare raw measures, ratings, New Code, and Overall Code without assuming every metric should move.
  • Explain why complexity or LOC can increase while reliability/security ratings stay unchanged, and why maintainability rating can remain stable even if remediation effort changes.
  • Preserve task IDs, metric snapshots, source revisions, coverage evidence, and credential cleanup as one auditable chain.

1. Lab contract and dated assumptions

The lab uses a tiny Python project because it can produce deterministic structural and coverage changes without requiring a commercial feature. It does not aim for a particular letter rating. The experiment is successful when the evidence explains the observed values—even if an analyzer/rule update changes an issue count.

Mandatory local/free baseline
  • SonarQube Community Build 26.9.0.129388 at http://localhost:9000.
  • SonarScanner CLI 8.1.0.6389 with default JRE auto-provisioning where supported.
  • Python 3.11+; pytest and pytest-cov.
  • Disposable project key academy-sq-ch13.
  • Full local Git history.
  • SONAR_TOKEN: project-analysis token used only by the scanner.
  • SONAR_API_TOKEN: short-lived lab user token with Browse permission for read-only metric API calls.
  • SONAR_ADMIN_TOKEN: optional short-lived project-admin token used only to pin the Specific-analysis New Code baseline.
  • No third-party plugin, paid CI, enterprise identity, commercial edition, external database, Kubernetes, or hosted service is required.
Secret handling: never echo, commit, or place token values in sonar-project.properties. The scanner reads SONAR_TOKEN from the environment; API calls use bearer authentication. Revoke disposable tokens after the lab.

2. Create the fixture and baseline revision

mkdir -p academy-sq-ch13/src academy-sq-ch13/tests academy-sq-ch13/evidence
cd academy-sq-ch13
git init
git config user.name "Academy Learner"
git config user.email "academy@example.invalid"

cat > .gitignore <<'EOF'
.venv/
.scannerwork/
.coverage
coverage.xml
__pycache__/
.pytest_cache/
evidence/
EOF

python -m venv .venv
# Linux/macOS
. .venv/bin/activate
# Windows PowerShell equivalent: .\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install pytest pytest-cov

cat > src/pricing.py <<'PY'
def subtotal(lines):
    total = 0.0
    for quantity, unit_price in lines:
        if quantity < 0 or unit_price < 0:
            raise ValueError("negative input")
        total += quantity * unit_price
    return round(total, 2)


def shipping(total, expedited=False):
    if expedited:
        return 15.0
    if total >= 100:
        return 0.0
    return 6.0


def invoice_total(lines, tax_rate=0.0, expedited=False):
    base = subtotal(lines)
    tax = base * tax_rate
    return round(base + tax + shipping(base, expedited), 2)
PY

cat > tests/test_pricing.py <<'PY'
import pytest
from src.pricing import invoice_total, shipping, subtotal


def test_subtotal_and_validation():
    assert subtotal([(2, 10.0), (1, 5.5)]) == 25.5
    with pytest.raises(ValueError):
        subtotal([(-1, 10.0)])


def test_shipping():
    assert shipping(50.0) == 6.0
    assert shipping(100.0) == 0.0
    assert shipping(50.0, expedited=True) == 15.0


def test_invoice_total():
    assert invoice_total([(2, 50.0)], 0.10) == 110.0
PY

cat > sonar-project.properties <<'EOF'
sonar.projectKey=academy-sq-ch13
sonar.projectName=Academy SonarQube Chapter 13 Metrics Lab
sonar.sources=src
sonar.tests=tests
sonar.sourceEncoding=UTF-8
sonar.python.coverage.reportPaths=coverage.xml
EOF

git add .gitignore src tests sonar-project.properties
git commit -m "ch13 metric baseline"
git rev-parse HEAD | tee evidence/baseline-revision.txt
pytest --cov=src --cov-report=term-missing --cov-report=xml
cp coverage.xml evidence/baseline-coverage.xml

Before scanning, predict: baseline coverage should be high, LOC/complexity modest, duplication low, and there should be no reason to expect security/reliability ratings to degrade. Exact values are analyzer evidence, not constants to invent.

3. Run baseline analysis and wait for Compute Engine

Create the disposable project in the local UI if it does not yet exist and generate the project-analysis token. Keep SONAR_HOST_URL, SONAR_TOKEN, and API/admin tokens in the environment.

export SONAR_HOST_URL="http://localhost:9000"
export PROJECT_KEY="academy-sq-ch13"

sonar-scanner \
  -Dsonar.projectVersion="1.0.0" \
  -Dsonar.buildString="ch13-baseline"

cp .scannerwork/report-task.txt evidence/baseline-report-task.txt
cat .scannerwork/report-task.txt

export CE_TASK_ID="$(sed -n 's/^ceTaskId=//p' .scannerwork/report-task.txt)"
while :; do
  curl -fsS -H "Authorization: Bearer $SONAR_API_TOKEN" \
    "$SONAR_HOST_URL/api/ce/task?id=$CE_TASK_ID" > evidence/ce-current.json
  STATUS="$(python -c 'import json; print(json.load(open("evidence/ce-current.json"))["task"]["status"])')"
  echo "Compute Engine: $STATUS"
  case "$STATUS" in
    SUCCESS) break ;;
    FAILED|CANCELED) echo "Preserve evidence and diagnose before continuing"; exit 1 ;;
  esac
  sleep 2
done
cp evidence/ce-current.json evidence/baseline-ce-task.json

Scanner exit 0 proves the scanner completed its local/upload phase. The persisted metric state belongs to the successfully completed Compute Engine task, so measurements are taken only after that state is proven.

4. Create a mode-aware metric snapshot helper

The helper discovers metric keys from the running instance and requests only keys that exist. This prevents the lab from confusing MQR and Standard Experience.

cat > snapshot_metrics.py <<'PY'
import json, os, sys, urllib.parse, urllib.request

host = os.environ["SONAR_HOST_URL"].rstrip("/")
project = os.environ["PROJECT_KEY"]
token = os.environ["SONAR_API_TOKEN"]
outfile = sys.argv[1]


def get(path, params=None):
    url = host + path
    if params:
        url += "?" + urllib.parse.urlencode(params)
    req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
    with urllib.request.urlopen(req) as response:
        return json.load(response)

catalog = get("/api/metrics/search", {"ps": 500})
available = {m["key"] for m in catalog.get("metrics", [])}
core = [
    "ncloc", "lines", "complexity", "cognitive_complexity",
    "coverage", "line_coverage", "duplicated_lines_density",
    "new_lines", "new_coverage", "new_line_coverage",
    "new_duplicated_lines_density", "violations", "new_violations",
]
mqr = [
    "software_quality_reliability_issues",
    "software_quality_reliability_rating",
    "software_quality_security_issues",
    "software_quality_security_rating",
    "software_quality_maintainability_issues",
    "software_quality_maintainability_remediation_effort",
    "software_quality_maintainability_debt_ratio",
    "software_quality_maintainability_rating",
    "new_software_quality_maintainability_issues",
    "new_software_quality_maintainability_remediation_effort",
    "new_software_quality_maintainability_debt_ratio",
    "new_software_quality_maintainability_rating",
]
standard = [
    "bugs", "reliability_rating", "vulnerabilities", "security_rating",
    "code_smells", "sqale_index", "sqale_debt_ratio", "sqale_rating",
    "new_bugs", "new_reliability_rating", "new_vulnerabilities",
    "new_security_rating", "new_code_smells", "new_technical_debt",
    "new_sqale_debt_ratio", "new_maintainability_rating",
]
selected = [k for k in core + mqr + standard if k in available]
measures = get("/api/measures/component", {
    "component": project,
    "metricKeys": ",".join(selected),
})
result = {
    "project": project,
    "selected_metric_keys": selected,
    "measures": measures,
}
with open(outfile, "w", encoding="utf-8") as fh:
    json.dump(result, fh, indent=2, sort_keys=True)
print(f"wrote {outfile} with {len(selected)} supported metric keys")
PY

python snapshot_metrics.py evidence/baseline-metrics.json

Also record the instance’s visible Mode setting in evidence/instance-mode.txt. The helper’s available-key set is evidence, but the administrator-visible mode is the clearest governance record.

5. Pin the baseline for New Code comparison

Reuse the Chapter 12 control rather than inventing a new baseline rule. Identify the baseline analysis by buildString=ch13-baseline, record its analysis key, then set that key as the project’s Specific analysis baseline with a disposable project-admin token.

curl -fsS -H "Authorization: Bearer $SONAR_ADMIN_TOKEN" --get \
  --data-urlencode "project=$PROJECT_KEY" \
  --data-urlencode "ps=20" \
  "$SONAR_HOST_URL/api/project_analyses/search" \
  > evidence/project-analyses.json

export BASELINE_ANALYSIS_KEY="<analysis-key-for-buildString-ch13-baseline>"
curl -fsS -X POST -H "Authorization: Bearer $SONAR_ADMIN_TOKEN" \
  -d "project=$PROJECT_KEY" \
  -d "type=SPECIFIC_ANALYSIS" \
  -d "value=$BASELINE_ANALYSIS_KEY" \
  "$SONAR_HOST_URL/api/new_code_periods/set"

curl -fsS -H "Authorization: Bearer $SONAR_ADMIN_TOKEN" --get \
  --data-urlencode "project=$PROJECT_KEY" \
  "$SONAR_HOST_URL/api/new_code_periods/show" \
  > evidence/new-code-definition.json

6. Predict the controlled change before editing source

Metric family Prediction Why
ncloc Increase New non-comment source lines will be added.
complexity Increase The added function contains multiple decision branches.
cognitive_complexity Increase Nested/compound control flow increases comprehension burden.
coverage Decrease The new function is intentionally not covered by tests, while coverage import remains enabled.
new_coverage Low / below baseline The Specific-analysis New Code population contains the new uncovered function.
Duplication density No deliberate change The new code is not copied from existing blocks.
Reliability/security ratings Probably unchanged The change is intended to affect structure, not create a reliability/security issue; actual analyzer evidence wins.
Maintainability issues/effort/rating May change; rating may still stay in same band Active complexity-related rules can raise maintainability issues, but ratings are banded debt-ratio outputs.

7. Add a deliberately complex, untested function

cat >> src/pricing.py <<'PY'


def route_order(country, total, fragile, expedited, customer_tier):
    if country == "local":
        if expedited:
            if fragile:
                return "local-priority-fragile"
            return "local-priority"
        if total > 250:
            return "local-free"
        return "local-standard"
    elif country == "regional":
        if fragile and expedited:
            return "regional-secure-priority"
        if fragile:
            return "regional-secure"
        if expedited:
            return "regional-priority"
        return "regional-standard"
    else:
        if customer_tier == "vip":
            if expedited:
                return "international-vip-priority"
            return "international-vip"
        if total > 1000 and not fragile:
            return "international-consolidated"
        if fragile:
            return "international-secure"
        return "international-standard"
PY

git add src/pricing.py
git commit -m "add deliberately complex routing example"
git rev-parse HEAD | tee evidence/change-revision.txt
pytest --cov=src --cov-report=term-missing --cov-report=xml
cp coverage.xml evidence/change-coverage.xml

Do not add a test for route_order() yet. The temporary lack of coverage is intentional laboratory evidence; the repository is synthetic and isolated. Chapter 14 will later focus on coverage ingestion and test-report semantics in depth.

8. Reanalyze and capture the second snapshot

sonar-scanner \
  -Dsonar.projectVersion="1.0.1" \
  -Dsonar.buildString="ch13-complex-change"

cp .scannerwork/report-task.txt evidence/change-report-task.txt
export CE_TASK_ID="$(sed -n 's/^ceTaskId=//p' .scannerwork/report-task.txt)"
while :; do
  curl -fsS -H "Authorization: Bearer $SONAR_API_TOKEN" \
    "$SONAR_HOST_URL/api/ce/task?id=$CE_TASK_ID" > evidence/ce-current.json
  STATUS="$(python -c 'import json; print(json.load(open("evidence/ce-current.json"))["task"]["status"])')"
  case "$STATUS" in SUCCESS) break ;; FAILED|CANCELED) exit 1 ;; esac
  sleep 2
done
cp evidence/ce-current.json evidence/change-ce-task.json
python snapshot_metrics.py evidence/change-metrics.json

9. Compare values without forcing a narrative

cat > compare_metrics.py <<'PY'
import json, sys


def values(path):
    data = json.load(open(path, encoding="utf-8"))
    measures = data["measures"]["component"].get("measures", [])
    out = {}
    for m in measures:
        if "value" in m:
            out[m["metric"]] = m["value"]
        elif "period" in m and "value" in m["period"]:
            out[m["metric"]] = m["period"]["value"]
        else:
            out[m["metric"]] = "<no-value>"
    return out

before = values(sys.argv[1])
after = values(sys.argv[2])
keys = sorted(set(before) | set(after))
print(f"{'metric':48} {'before':>18} {'after':>18}")
print("-" * 88)
for key in keys:
    print(f"{key:48} {str(before.get(key, '-')):>18} {str(after.get(key, '-')):>18}")
PY
python compare_metrics.py evidence/baseline-metrics.json evidence/change-metrics.json \
  | tee evidence/metric-delta.txt

Interpret in causal order. First prove revision and indexed source changed as intended. Then verify CE success. Then inspect raw structural measures. Only after that interpret issues, effort, ratios, and ratings. If a rating did not change, explain its derivation instead of calling the scanner “wrong.” If an unexpected issue appears, preserve its rule key and evidence and update the interpretation.

10. Small challenge: choose the right evidence layer

Your manager asks: “Complexity rose by 40%; did application performance become 40% worse?” Write a response that identifies the wrong inference, names the metric’s actual scope, and specifies the performance evidence that would be required. Then answer a second question: “Maintainability is still A, so can we ignore the new complexity?” Explain why the rating and diagnostic structural measures answer different questions.

Knowledge check

Why wait for Compute Engine before capturing measures?

The complexity rises and reliability rating stays A. Is that contradictory?

Why does the helper discover metric keys first?

Why can new coverage be much lower than overall coverage?

What should happen if a predicted maintainability issue does not appear?

Next lesson

Choose the right metric representation for the decision

Lesson 3 turns the experiment into reusable design rules for counts, ratios, ratings, trends, gates, and cross-project comparisons.

Official references and version notes

Version and compatibility note

Rechecked 2026-09-07. Mandatory executable examples target SonarQube Community Build 26.9.0.129388 and SonarScanner CLI 8.1.0.6389. New Community Build instances use MQR Mode by default, but upgraded instances can remain in Standard Experience; therefore examples discover available metric keys and record the actual instance mode rather than hard-coding one issue/rating vocabulary. Core structural keys used in both modes include ncloc, lines, complexity, cognitive_complexity, coverage, and duplicated_lines_density. MQR and Standard issue/rating families are treated as distinct. Re-check primary documentation and the instance’s built-in Web API before automating against another release.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.