blob: 5c6704fb58ca7c7f04b74bfbe4ca47fc2c8306cf [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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 datetime
import json
import types
from app import statistics
from app.statistics import IssueWindow
def _ts(d: datetime.date) -> float:
return datetime.datetime(d.year, d.month, d.day, tzinfo=datetime.timezone.utc).timestamp()
def test_is_open_at_window():
w = IssueWindow(datetime.date(2025, 1, 1), datetime.date(2025, 1, 10))
assert not w.is_open_at(datetime.date(2024, 12, 31)) # before opening
assert w.is_open_at(datetime.date(2025, 1, 1)) # opened that day
assert w.is_open_at(datetime.date(2025, 1, 9)) # mid-window
assert not w.is_open_at(datetime.date(2025, 1, 10)) # closed that day
assert not w.is_open_at(datetime.date(2025, 1, 11))
def test_is_open_at_still_open():
w = IssueWindow(datetime.date(2025, 1, 1), None)
assert not w.is_open_at(datetime.date(2024, 12, 31))
assert w.is_open_at(datetime.date(2025, 1, 1))
assert w.is_open_at(datetime.date(2030, 1, 1))
def test_debt_at_is_constant_plus_age():
w = IssueWindow(datetime.date(2025, 1, 1), None)
assert w.debt_at(datetime.date(2025, 1, 1)) == 100
assert w.debt_at(datetime.date(2025, 1, 6)) == 105
def test_week_end_dates_last_point_is_today():
now = datetime.datetime(2026, 6, 5, tzinfo=datetime.timezone.utc)
dates = statistics._week_end_dates(now, weeks=52)
assert len(dates) == 53
assert dates[-1] == now.date()
assert dates[0] == now.date() - datetime.timedelta(weeks=52)
assert dates[1] - dates[0] == datetime.timedelta(weeks=1)
def test_issue_window_reads_first_and_last_mailtime(tmp_path):
path = tmp_path / "thread.json"
path.write_text(json.dumps([
{"mailtime": _ts(datetime.date(2025, 1, 1))},
{"mailtime": _ts(datetime.date(2025, 1, 20))},
]))
assert statistics._issue_window(path, closed=False) == IssueWindow(
datetime.date(2025, 1, 1), None
)
assert statistics._issue_window(path, closed=True) == IssueWindow(
datetime.date(2025, 1, 1), datetime.date(2025, 1, 20)
)
def test_issue_window_skips_empty(tmp_path):
path = tmp_path / "empty.json"
path.write_text("[]")
assert statistics._issue_window(path, closed=False) is None
def test_issue_window_ignores_repeat_cve_pushed(tmp_path):
# only the first "was pushed to cve.org" notification should count; the
# later one must not drag the close date forward.
path = tmp_path / "thread.json"
path.write_text(json.dumps([
{"mailtime": _ts(datetime.date(2025, 1, 1)), "subj": "[SECURITY] something"},
{"mailtime": _ts(datetime.date(2025, 1, 20)), "subj": "CVE-2025-1 was pushed to cve.org"},
{"mailtime": _ts(datetime.date(2025, 6, 1)), "subj": "CVE-2025-1 was pushed to cve.org"},
]))
assert statistics._issue_window(path, closed=True) == IssueWindow(
datetime.date(2025, 1, 1), datetime.date(2025, 1, 20)
)
def _write_thread(path, opened, last=None):
path.parent.mkdir(parents=True, exist_ok=True)
emails = [{"mailtime": _ts(opened)}]
if last is not None:
emails.append({"mailtime": _ts(last)})
path.write_text(json.dumps(emails))
def test_compute_debt_chart_end_to_end(tmp_path, monkeypatch):
# proj1: one still-open issue, plus one issue closed within the past year.
# proj2: a non-issue (in archive) closed within the past year.
proj1_open = IssueWindow(datetime.date(2026, 1, 1), None)
proj1_closed = IssueWindow(datetime.date(2025, 8, 1), datetime.date(2026, 4, 1))
proj2_ni = IssueWindow(datetime.date(2025, 9, 1), datetime.date(2026, 3, 1))
_write_thread(tmp_path / "proj1" / "open.json", proj1_open.opened)
_write_thread(
tmp_path / "zzz-resolved" / "proj1" / "done.json",
proj1_closed.opened, proj1_closed.closed,
)
_write_thread(
tmp_path / "archive" / "zzz-non-issues" / "proj2" / "ni.json",
proj2_ni.opened, proj2_ni.closed,
)
fake_cfg = types.SimpleNamespace(data_dir_path=tmp_path)
monkeypatch.setattr(statistics.config, "get", lambda: fake_cfg)
# 'archive' is not itself a project; the two real projects are discovered
assert statistics.list_pmcs() == ["proj1", "proj2"]
now = datetime.datetime(2026, 6, 5, tzinfo=datetime.timezone.utc)
chart = statistics.compute_debt_chart(now=now, weeks=52)
assert chart["weeks"][-1] == "now"
boundaries = statistics._week_end_dates(now, weeks=52)
series = {s["name"]: s["values"] for s in chart["series"]}
assert set(series) == {"proj1", "proj2"}
def debt(windows, end):
return sum(w.debt_at(end) for w in windows if w.is_open_at(end))
expected1 = [debt([proj1_open, proj1_closed], e) for e in boundaries]
expected2 = [debt([proj2_ni], e) for e in boundaries]
assert series["proj1"] == expected1
assert series["proj2"] == expected2
# sanity: the open issue keeps proj1 non-zero at 'now'; proj2's only issue is closed
assert series["proj1"][-1] == 100 + (now.date() - proj1_open.opened).days
assert series["proj2"][-1] == 0
# restricting to a pmc set only returns those projects
restricted = statistics.compute_debt_chart(now=now, weeks=52, pmcs=["proj1"])
assert {s["name"] for s in restricted["series"]} == {"proj1"}
# an empty allow-list yields no series
assert statistics.compute_debt_chart(now=now, weeks=52, pmcs=[])["series"] == []