Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e3acfe7
fix: IzhikevichNodes and CSRMNodes accept multi-dimensional shapes; C…
Hananel-Hazan Oct 29, 2026
1c4c098
fix: LocalConnection batch > 1; MaxPool connections without decay or …
Hananel-Hazan Oct 29, 2026
926e60d
fix: sparse MulticompartmentConnection learns and runs with batch > 1…
Hananel-Hazan Oct 29, 2026
3a2f53f
fix: MSTDP/MSTDPET/Rmax state cleared on reset; Rmax and MCC MSTDPET …
Hananel-Hazan Oct 29, 2026
65723f4
fix: preserve the mean-field weight parameter during normalization (#…
AHMETHAKANBEZIR1 Oct 29, 2026
5924712
fix: reset and resize the CSRM connection spike history (#815)
AHMETHAKANBEZIR1 Oct 29, 2026
5d72052
test: goldens and invariants after adopting #813 and #815
Hananel-Hazan Oct 29, 2026
5725c1a
fix: CSRM spike window saved in the state_dict; MulticompartmentConne…
Hananel-Hazan Oct 29, 2026
c5ee6e9
fix: MeanFieldConnection keeps batch samples apart and applies weight…
Hananel-Hazan Oct 29, 2026
d239424
fix: MeanField feature, Normalization/Updating sub-features, adaptati…
Hananel-Hazan Oct 29, 2026
0983682
test: known-bug list becomes FIXED_BUGS; workarounds removed; Multico…
Hananel-Hazan Oct 29, 2026
ce98f93
docs: CHANGELOG for the 0.3.5 bug fixes, results changes listed first
Hananel-Hazan Oct 29, 2026
02219bf
fix: issues found by independent review of PR #831
Hananel-Hazan Oct 29, 2026
fa0847d
ci: close test-guard bypasses found by independent review (WIP part 1)
Hananel-Hazan Oct 29, 2026
0493dda
test: op-count workloads for every Nodes class
Hananel-Hazan Oct 29, 2026
1a792d8
ci: close the test-guard bypasses found by independent review (part 2…
Hananel-Hazan Oct 29, 2026
6cdd49f
Merge master (#831 squash-merged) into ci-guard-hardening
Hananel-Hazan Oct 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# Pull requests that touch these files need a maintainer's approval (branch
# protection: "Require review from Code Owners"). They hold recorded results,
# the tests that pin formulas to their papers, and the CI that runs them; a
# pull request must not edit them to make itself pass.
/test/regression/ @Hananel-Hazan
/test/api_surface.json @Hananel-Hazan
/test/regen_api_surface.py @Hananel-Hazan
/test/test_api_surface.py @Hananel-Hazan
/test/network/test_all_classes.py @Hananel-Hazan
/test/network/test_learning_rule_specs.py @Hananel-Hazan
/test/network/test_mstdp_florian.py @Hananel-Hazan
/test/network/test_perf_equivalence.py @Hananel-Hazan
/.github/ @Hananel-Hazan
# the tests and the settings that decide which tests run, and the CI that runs
# them; a pull request must not edit them to make itself pass.
/test/ @Hananel-Hazan
/.github/ @Hananel-Hazan
/pyproject.toml @Hananel-Hazan
/poetry.lock @Hananel-Hazan
/pytest.ini @Hananel-Hazan
/.pytest.ini @Hananel-Hazan
/tox.ini @Hananel-Hazan
/setup.cfg @Hananel-Hazan
/.coveragerc @Hananel-Hazan
/conftest.py @Hananel-Hazan
31 changes: 28 additions & 3 deletions .github/scripts/bench_ab.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@

The report (Markdown) is written to DIR/bench_ab.md, appended to --summary if
given (GitHub's $GITHUB_STEP_SUMMARY), and the raw numbers to DIR/bench_ab.json.
The exit status is always 0: this is a non-required check and a human decides;
the verdict, or a TOOL ERROR line, is in the report.
Without --fail-above the exit status is always 0 and a human decides; the
verdict, or a TOOL ERROR line, is in the report. With --fail-above X the exit
status is 1 when any workload's median ratio is above X and its interval's lower
end is above 1.5 (a large, clear slowdown, far outside the no-change spread of
0.90 to 1.12 per round measured on GitHub runners), or when the tool failed; CI
uses X = 2.
"""

from __future__ import annotations
Expand Down Expand Up @@ -318,6 +322,12 @@ def main() -> int:
ap.add_argument("--time", type=int, default=100, help="time steps per run")
ap.add_argument("--threads", type=int, default=2)
ap.add_argument("--threshold", type=float, default=1.25)
ap.add_argument(
"--fail-above",
type=float,
default=None,
help="exit 1 when a median ratio is above this and the interval's lower end above 1.5",
)
ap.add_argument("--min-seconds", type=float, default=1.0)
ap.add_argument("--only", default=",".join(WORKLOADS))
ap.add_argument("--timeout", type=int, default=900, help="seconds per process")
Expand Down Expand Up @@ -383,10 +393,25 @@ def log(msg):
indent=1,
)
)
failing = []
if args.fail_above is not None:
failing = [
r["workload"]
for r in rows
if r["ratio_median"] > args.fail_above and r["ratio_ci95"][0] > 1.5
]
if error:
failing.append("tool error")
report += (
f"\n**Failed:** {', '.join(failing)} (median above {args.fail_above}x "
"the base, or the tool failed).\n"
if failing
else f"\nRequired limit: no workload above {args.fail_above}x the base.\n"
)
if args.summary:
with open(args.summary, "a") as f:
f.write(report)
return 0
return 1 if failing else 0


if __name__ == "__main__":
Expand Down
13 changes: 10 additions & 3 deletions .github/scripts/coverage_floor.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,19 @@
import sys
import xml.etree.ElementTree as ET

# Measured on CI? No: measured locally on 2026-10-24 with torch 2.14.1 CPU after
# Measured on GitHub CI (build job, Python 3.13, CPU torch), run 40798879593,
# 2026-10-28.
# One point under the coverage measured on GitHub CI (build job, Python 3.13, CPU
# torch): network and learning in run 40798879593, the others in run 40804291700
# (master 33913c7), both 2026-10-28.
FLOORS = {
"network": 89.6, # 90.6% measured
"learning": 94.2, # 95.2% measured
"models": 98.3, # 99.3% measured
"encoding": 73.6, # 74.6% measured
"conversion": 58.8, # 59.8% measured
"analysis": 32.8, # 33.8% measured
"environment": 26.3, # 27.3% measured
"pipeline": 19.8, # 20.8% measured
"datasets": 20.6, # 21.6% measured
}
# Cobertura filenames are relative to the <source> element, which pytest-cov sets to
# the bindsnet package directory, so "network/nodes.py", not "bindsnet/network/...".
Expand Down
72 changes: 53 additions & 19 deletions .github/scripts/fails_before_fix.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@
- on the head commit as checked out.

It passes when at least one test fails (or cannot be collected) on the base and
passes on the head. A test file that cannot be imported on the base (for
example, it imports a function the pull request adds) counts as failing there.
passes on the head. Only a test that ran on the base and failed there counts
(any exception in its body, including an assertion or "DID NOT RAISE"). A test
that cannot be imported or set up on the base (for example, it imports a
function the pull request adds) shows no bug and does not count; a pull request
that adds a feature needs the maintainer label. The summary shows each counted
test's failure on the base, because a test can also fail for reasons unrelated
to the bug, and a maintainer must read it.

It also passes, with a note, when the pull request does not touch any
``bindsnet/*.py`` file (documentation, dependencies, workflows), or when the
Expand Down Expand Up @@ -53,8 +58,11 @@ def changed(base, head, path, filters="ACMR"):
def run_pytest(tree, files, xml):
"""Run pytest on ``files`` inside ``tree`` with ``tree`` first on the path.

Returns {test id: "pass" | "fail"} and {module: "error"} for files that
could not be collected. Skipped and xfailed tests are left out.
Returns {test id: ("pass" | "fail" | "error", message)} and the modules that
could not be collected. "fail" means the test ran and failed (any exception
in its body); "error" means set-up or collection failed. Skipped and xfailed
tests are left out. The settings come from the tree's pyproject.toml only, so
an added pytest.ini or setup.cfg cannot change what runs.
"""
env = dict(os.environ, PYTHONPATH=str(tree))
where = subprocess.run(
Expand All @@ -69,7 +77,8 @@ def run_pytest(tree, files, xml):
raise RuntimeError(f"bindsnet imported from {where}, not from {tree}")
subprocess.run(
[sys.executable, "-m", "pytest", "-q", "-p", "no:cacheprovider"]
+ ["--continue-on-collection-errors", f"--junitxml={xml}", *files],
+ ["-c", "pyproject.toml", "--continue-on-collection-errors"]
+ [f"--junitxml={xml}", *files],
cwd=tree,
env=env,
)
Expand All @@ -82,7 +91,12 @@ def run_pytest(tree, files, xml):
test_id = f"{case.get('classname')}::{case.get('name')}"
if "skipped" in tags:
continue
results[test_id] = "fail" if tags & {"failure", "error"} else "pass"
for kind, tag in (("fail", "failure"), ("error", "error"), ("pass", None)):
child = case.find(tag) if tag else None
if tag is None or child is not None:
message = (child.get("message") or "") if child is not None else ""
results[test_id] = (kind, message.splitlines()[0][:200] if message else "")
break
return results, broken


Expand All @@ -106,6 +120,10 @@ def finish(code, message):
f.write(text)
return code

# Commit ids, so "master" or "HEAD" mean the same inside the base worktree.
args.base = git("rev-parse", args.base).strip()
args.head = git("rev-parse", args.head).strip()

labels = {label.strip() for label in args.labels.split(",") if label.strip()}
if args.skip_label in labels:
return finish(0, f"Skipped: a maintainer added the label `{args.skip_label}`.")
Expand Down Expand Up @@ -137,26 +155,42 @@ def finish(code, message):
subprocess.run(["git", "worktree", "remove", "--force", str(base_tree)])
shutil.rmtree(work, ignore_errors=True)

def failed_on_base(test_id):
def collection_broken(test_id):
module = test_id.split("::")[0]
if any(module == m or module.startswith(m + ".") for m in broken):
return True
return on_base.get(test_id) == "fail"

shown = [t for t, r in on_head.items() if r == "pass" and failed_on_base(t)]
return any(module == m or module.startswith(m + ".") for m in broken)

passing = [t for t, (kind, _) in on_head.items() if kind == "pass"]
# Evidence: the test ran on the base and failed there. A test that could not
# be imported or set up on the base (for example, it imports a name the PR
# adds) shows nothing about a bug; it is listed for the maintainer instead.
shown = [t for t in passing if on_base.get(t, ("", ""))[0] == "fail"]
unshown = [
t
for t in passing
if t not in shown and (collection_broken(t) or on_base.get(t, ("",))[0] == "error")
]
if unshown:
lines.append(
"Not counted (could not be collected or set up on the base, so they "
"show no bug): " + ", ".join(f"`{t}`" for t in sorted(unshown)[:20])
)
lines.append("")
if shown:
lines.append("Tests that fail on the base and pass with this change:")
lines += [f"- `{t}`" for t in sorted(shown)[:50]]
lines.append(
"Tests that fail on the base and pass with this change, with the "
"base failure (a maintainer checks that it is the bug):"
)
lines += [f"- `{t}`: {on_base[t][1]}" for t in sorted(shown)[:50]]
if len(shown) > 50:
lines.append(f"- ... and {len(shown) - 50} more")
return finish(0, "\n**Passed.**")
return finish(
1,
"**Failed:** every test in the changed test files gives the same result "
"with and without the library change, so none of them shows the bug "
"this pull request fixes. Add a test that fails on `master` and passes "
"with this change. If this is not a bug fix (a refactor or a speed-up), "
f"a maintainer can add the label `{args.skip_label}`.",
"**Failed:** no test in the changed test files ran and failed on the "
"base while passing with this change, so none of them shows the bug this "
"pull request fixes. Add a test that fails on `master` and passes with "
"this change. If this is not a bug fix (a refactor, a speed-up, or a new "
f"feature), a maintainer can add the label `{args.skip_label}`.",
)


Expand Down
Loading
Loading