Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion .github/workflows/regression.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ jobs:
path: build
- run: chmod +x build/opentaint
- name: Clone project
if: matrix.kind == 'git'
run: |
git clone --filter=blob:none "${{ matrix.git }}" project-root
git -C project-root checkout "${{ matrix.head }}"
Expand All @@ -302,7 +303,7 @@ jobs:
set +e
python scripts/run_analysis.py \
--build-dir build \
--project-dir project-root \
--project-dir "${{ matrix.kind == 'git' && 'project-root' || matrix.source }}" \
--results-dir results-bundle \
--max-memory "${{ matrix.max_memory }}" \
--timeout "${{ matrix.compilation_timeout }}" \
Expand Down
56 changes: 53 additions & 3 deletions scripts/generate_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,21 @@
restricts the project set (substring match against project name). Optional
--misses-only restricts to (project, ref) pairs flagged as cache misses.

A project entry is either:

* ``kind: git`` (the default) — cloned from ``git`` at ``head`` by the
workflow, then built by the autobuilder (``opentaint compile``).
* ``kind: local`` — a buildable project vendored in this repo at ``source``.
The workflow skips the clone and points the runner at ``source`` directly;
``opentaint compile`` builds it exactly like a cloned project. Local cells
carry ``git: ""`` and the constant ``head: "local"`` (kit-content changes
invalidate the cache via the test-system SHA, already part of every key).

Output JSON shape (printed to stdout):

{"include": [
{"project": "spring-petclinic", "git": "...", "head": "...",
{"project": "spring-petclinic", "kind": "git",
"git": "...", "head": "...", "source": "",
"java_version": "17", "max_memory": "8G", "compilation_timeout": "1200",
"ref_kind": "base", "analyzer_sha": "<sha>"},
...
Expand All @@ -27,6 +38,12 @@
DEFAULT_JAVA = "17"
DEFAULT_MEMORY = "8G"
DEFAULT_COMPILATION_TIMEOUT = "1200"
DEFAULT_KIND = "git"

# Cache-key head for local projects. A constant (no `/` or space, so it is a
# valid cache_key component); kit-content changes still invalidate the cache
# through the test-system SHA, which is part of every key.
LOCAL_HEAD_SENTINEL = "local"


def _matches_filter(name: str, patterns: list[str]) -> bool:
Expand All @@ -50,6 +67,36 @@ def _normalise_scan_flags(raw) -> list[str]:
return [str(token) for token in raw]


def _resolve_identity(repo: dict) -> tuple[str, str, str, str]:
"""Return ``(kind, git, head, source)`` for one repo entry, validating the
fields required by its kind.

* ``git`` projects must supply ``git`` and ``head``.
* ``local`` projects must supply ``source``; they carry ``git: ""`` and the
constant ``head`` sentinel.

Raises ``ValueError`` on an unknown kind or a missing required field, so a
misconfigured entry fails the workflow fast instead of producing a matrix
cell that breaks further downstream.
"""
name = repo.get("name", "<unnamed>")
kind = str(repo.get("kind", DEFAULT_KIND))
if kind == "git":
git = repo.get("git")
head = repo.get("head")
if not git or not head:
raise ValueError(
f"{name}: git project requires both 'git' and 'head'")
return kind, str(git), str(head), ""
if kind == "local":
source = repo.get("source")
if not source:
raise ValueError(f"{name}: local project requires 'source'")
return kind, "", LOCAL_HEAD_SENTINEL, str(source)
raise ValueError(
f"{name}: unknown kind {kind!r} (expected 'git' or 'local')")


def _load_misses(path: str | None) -> set[tuple[str, str]]:
if not path:
return set()
Expand All @@ -71,13 +118,16 @@ def build_matrix(repos_path: Path, base_sha: str, new_sha: str,
name = repo["name"]
if not _matches_filter(name, projects_filter):
continue
kind, git, head, source = _resolve_identity(repo)
for ref_kind, sha in refs:
if misses_only and (name, ref_kind) not in misses:
continue
include.append({
"project": name,
"git": repo["git"],
"head": repo["head"],
"kind": kind,
"git": git,
"head": head,
"source": source,
"java_version": str(repo.get("java-version", DEFAULT_JAVA)),
"max_memory": str(repo.get("max-memory", DEFAULT_MEMORY)),
"compilation_timeout": str(
Expand Down
66 changes: 66 additions & 0 deletions tests/test_scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,72 @@ def test_matrix_scan_flags_rejects_non_list(tmp_path):
generate_matrix.build_matrix(repos, "AAA", "AAA", [], None)


# ── generate_matrix: local (kind) projects ───────────────────────────────────

def test_matrix_git_entry_emits_kind_and_empty_source(tmp_path):
repos = _write_repos(tmp_path, (
"repositories:\n"
" - name: demo\n"
" git: https://example.com/demo.git\n"
" head: deadbeef\n"
))
m = generate_matrix.build_matrix(repos, "AAA", "AAA", [], None)
cell = m["include"][0]
assert cell["kind"] == "git"
assert cell["git"] == "https://example.com/demo.git"
assert cell["head"] == "deadbeef"
assert cell["source"] == ""


def test_matrix_local_entry_shape(tmp_path):
repos = _write_repos(tmp_path, (
"repositories:\n"
" - name: repro-01\n"
" kind: local\n"
" source: projects/repro-kits/01-reflect-method-invoke\n"
" java-version: 21\n"
))
m = generate_matrix.build_matrix(repos, "AAA", "AAA", [], None)
cell = m["include"][0]
assert cell["kind"] == "local"
assert cell["source"] == "projects/repro-kits/01-reflect-method-invoke"
# Local cells clone nothing and use the constant head sentinel.
assert cell["git"] == ""
assert cell["head"] == generate_matrix.LOCAL_HEAD_SENTINEL == "local"
assert cell["java_version"] == "21"


def test_matrix_local_missing_source_raises(tmp_path):
repos = _write_repos(tmp_path, (
"repositories:\n"
" - name: repro-01\n"
" kind: local\n"
))
with pytest.raises(ValueError):
generate_matrix.build_matrix(repos, "AAA", "AAA", [], None)


def test_matrix_git_missing_head_raises(tmp_path):
repos = _write_repos(tmp_path, (
"repositories:\n"
" - name: demo\n"
" git: https://example.com/demo.git\n"
))
with pytest.raises(ValueError):
generate_matrix.build_matrix(repos, "AAA", "AAA", [], None)


def test_matrix_unknown_kind_raises(tmp_path):
repos = _write_repos(tmp_path, (
"repositories:\n"
" - name: demo\n"
" kind: svn\n"
" source: whatever\n"
))
with pytest.raises(ValueError):
generate_matrix.build_matrix(repos, "AAA", "AAA", [], None)


# ── run_analysis: scan-flag expansion ────────────────────────────────

def test_expand_scan_flags_substitutes_ext(tmp_path):
Expand Down