Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
2 changes: 2 additions & 0 deletions .github/workflows/test_artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ jobs:

- name: "Determine test dependencies"
id: test_deps
# Reads the committed consumer graph directly (no fetch/configure); kept
# in sync by test_consumer_graph_drift.yml.
shell: bash
env:
CHANGED_PROJECTS: ${{ inputs.changed_projects }}
Expand Down
53 changes: 53 additions & 0 deletions .github/workflows/test_consumer_graph_drift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Copyright Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT

# Regenerates the consumer graph from a full-tree configure and fails if it
# differs from the committed test_tools/therock_consumer_graph.json. Runs only
# when graph-affecting files change, so the configure cost stays off the per-PR
# path (see RFC0013 "CI integration").

name: Consumer Graph Drift Check

on:
pull_request:
paths:
- "**/CMakeLists.txt"
- "cmake/therock_emit_consumer_graph.cmake"
- "cmake/therock_subproject.cmake"
- "test_tools/therock_consumer_graph.json"
- ".github/workflows/test_consumer_graph_drift.yml"

permissions:
contents: read

jobs:
consumer_graph_drift:
name: "Consumer graph drift check"
runs-on: ubuntu-24.04
steps:
- name: Checking out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Setting up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: 3.12

- name: Install Python deps
run: pip install -r requirements.txt

- name: Fetch sources
# All subproject sources must be present for the graph to be complete.
run: python3 ./build_tools/fetch_sources.py --jobs 12 --depth 1

- name: Configure (emits the consumer graph)
# Configure only, no build. gfx94X-dcgpu enables every subproject; some
# (hipSPARSELt, rocprofiler-compute) are excluded on gfx110X, which would
# drop their edges.
run: |
cmake -B build -S . -GNinja \
-DTHEROCK_ENABLE_ALL=ON \
-DTHEROCK_AMDGPU_FAMILIES=gfx94X-dcgpu

- name: Check committed consumer graph is up to date
run: python3 ./build_tools/github_actions/check_consumer_graph_drift.py --check
4 changes: 4 additions & 0 deletions .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ jobs:
working-directory: build_tools/scan_tools
run: python -m pytest -vv

- name: Test test_tools
working-directory: test_tools
run: python -m pytest -vv

# TODO: upload to AWS S3 instead (so reports are hosted and don't require
# a download step to view).
# We could also use https://about.codecov.io/ instead of custom infra.
Expand Down
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -556,3 +556,6 @@ therock_subproject_merge_compile_commands()

# Write artifact→subprojects manifest for Python scripts to use
therock_write_subproject_manifest()

include(therock_emit_consumer_graph)
therock_emit_consumer_graph("${CMAKE_BINARY_DIR}/therock_consumer_graph.json")
124 changes: 124 additions & 0 deletions build_tools/github_actions/check_consumer_graph_drift.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
# Copyright Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
"""Drift check for the committed consumer graph.

Keeps test_tools/therock_consumer_graph.json in sync with a freshly emitted graph
(written to build/ by a configure). `--check` compares the two, failing on any
edge change; `--write` overwrites the committed copy. Both normalize (sorted keys
and consumer lists, 2-space indent, trailing newline) so only genuine
dependency-edge changes register, not the emitter's registration order.
"""

import argparse
import difflib
import json
import os
import sys
from pathlib import Path

_REPO_ROOT = Path(__file__).resolve().parents[2]
_GRAPH_FILENAME = "therock_consumer_graph.json"
_COMMITTED_PATH = _REPO_ROOT / "test_tools" / _GRAPH_FILENAME
# Which build tree holds the freshly emitted graph; overridable via --build-dir.
_DEFAULT_BUILD_DIR = Path(os.environ.get("THEROCK_BUILD_DIR", _REPO_ROOT / "build"))


def normalize(path: Path) -> str:
"""Return the graph as canonical JSON: sorted keys and consumer lists,
2-space indent, trailing newline. Used by both --check and --write."""
graph = json.loads(path.read_text())
norm = {
key: {"consumers": sorted(graph[key].get("consumers", []))}
for key in sorted(graph)
}
return json.dumps(norm, indent=2, sort_keys=True) + "\n"


def check(committed_path: Path, emitted_path: Path) -> int:
"""Fail (return 1) if the committed graph differs from the emitted one."""
committed = normalize(committed_path)
emitted = normalize(emitted_path)

if committed == emitted:
print("Consumer graph is up to date.")
return 0

print("::error::test_tools/therock_consumer_graph.json is out of date.")
print(
"A dependency edge changed but the committed consumer graph was not "
"regenerated."
)
print("Fix: run a full-tree configure, then regenerate and re-commit:")
print(" python3 ./build_tools/fetch_sources.py")
print(
" cmake -B build -GNinja -DTHEROCK_ENABLE_ALL=ON "
"-DTHEROCK_AMDGPU_FAMILIES=gfx94X-dcgpu"
)
Comment on lines +54 to +57

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also update the recommended family to gfx94X-dcgpu here if addressing my related comment

print(
" python3 ./build_tools/github_actions/check_consumer_graph_drift.py --write"
)
print(" git add test_tools/therock_consumer_graph.json")
sys.stdout.writelines(
difflib.unified_diff(
committed.splitlines(keepends=True),
emitted.splitlines(keepends=True),
fromfile="committed",
tofile="regenerated",
)
)
return 1


def write(committed_path: Path, emitted_path: Path) -> int:
"""Overwrite the committed graph with the normalized emitted graph."""
committed_path.write_text(normalize(emitted_path))
print(f"Wrote normalized consumer graph to {committed_path}.")
return 0


def main(argv: list[str]) -> int:
parser = argparse.ArgumentParser(description=__doc__)
mode = parser.add_mutually_exclusive_group(required=True)
mode.add_argument(
"--check",
action="store_true",
help="Fail if the committed graph differs from build/'s emitted graph.",
)
mode.add_argument(
"--write",
action="store_true",
help="Normalize build/'s emitted graph over the committed copy.",
)
parser.add_argument(
"--committed",
type=Path,
default=_COMMITTED_PATH,
help="Path to the committed consumer graph (default: test_tools/).",
)
parser.add_argument(
"--build-dir",
type=Path,
default=_DEFAULT_BUILD_DIR,
help=(
"Build directory holding the freshly emitted graph "
"(default: $THEROCK_BUILD_DIR or build/). Ignored if --emitted is set."
),
)
parser.add_argument(
"--emitted",
type=Path,
default=None,
help="Explicit path to the emitted graph (overrides --build-dir).",
)
args = parser.parse_args(argv)

emitted = args.emitted or (args.build_dir / _GRAPH_FILENAME)

if args.write:
return write(args.committed, emitted)
return check(args.committed, emitted)


if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
# Copyright Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
"""Unit tests for check_consumer_graph_drift.py"""

import json
import sys
import tempfile
import unittest
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent.parent))

from check_consumer_graph_drift import check, normalize, write


class TestNormalize(unittest.TestCase):
def _write(self, obj) -> Path:
path = Path(tempfile.mkdtemp()) / "graph.json"
path.write_text(json.dumps(obj))
return path

def test_normalize_is_order_insensitive(self) -> None:
# Same edges, different key/consumer order -> identical normalized text.
a = self._write({"b": {"consumers": ["z", "a"]}, "a": {"consumers": []}})
b = self._write({"a": {"consumers": []}, "b": {"consumers": ["a", "z"]}})
self.assertEqual(normalize(a), normalize(b))

def test_normalize_has_trailing_newline_and_indent(self) -> None:
text = normalize(self._write({"a": {"consumers": ["b"]}}))
self.assertTrue(text.endswith("\n"))
self.assertIn(' "a"', text) # 2-space indent

def test_normalize_tolerates_missing_consumers_key(self) -> None:
# A node with no "consumers" key normalizes to an empty list, not a crash.
text = normalize(self._write({"a": {}}))
self.assertIn('"consumers": []', text)


class TestCheck(unittest.TestCase):
def _write(self, obj) -> Path:
path = Path(tempfile.mkdtemp()) / "graph.json"
path.write_text(json.dumps(obj))
return path

def test_check_passes_when_equal_modulo_order(self) -> None:
committed = self._write({"a": {"consumers": ["b", "c"]}})
emitted = self._write({"a": {"consumers": ["c", "b"]}})
self.assertEqual(check(committed, emitted), 0)

def test_check_fails_on_edge_change(self) -> None:
committed = self._write({"a": {"consumers": ["b"]}})
emitted = self._write({"a": {"consumers": ["b", "c"]}})
self.assertEqual(check(committed, emitted), 1)


class TestWrite(unittest.TestCase):
def test_write_makes_check_pass(self) -> None:
root = Path(tempfile.mkdtemp())
committed = root / "committed.json"
emitted = root / "emitted.json"
# Stale committed copy, differently-ordered emitted graph.
committed.write_text(json.dumps({"a": {"consumers": ["stale"]}}))
emitted.write_text(json.dumps({"a": {"consumers": ["c", "b"]}}))

self.assertEqual(write(committed, emitted), 0)
# After write, committed matches emitted and check passes.
self.assertEqual(check(committed, emitted), 0)
# And it was written in normalized form (sorted, trailing newline).
self.assertEqual(committed.read_text(), normalize(emitted))


if __name__ == "__main__":
unittest.main()
57 changes: 57 additions & 0 deletions cmake/therock_emit_consumer_graph.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Copyright Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT

# therock_emit_consumer_graph(output_file)
#
# Serializes the consumer graph (from the THEROCK_ALL_SUBPROJECTS /
# THEROCK_DIRECT_CONSUMERS_OF_* global properties) to JSON at <output_file>. Call
# at the end of the top-level CMakeLists.txt, after all subprojects are declared.
#
# Schema (reverse-dependency edges only):
# { "<subproject>": { "consumers": ["<consumer>", ...] }, ... }
#
# The emit writes build/therock_consumer_graph.json, which refreshes the committed
# copy at test_tools/therock_consumer_graph.json. The committed copy is read
# directly by CI (no configure on the hot path) and kept honest by the drift check
# in .github/workflows/test_consumer_graph_drift.yml. It is generated-only; never
# hand-edit it.
function(therock_emit_consumer_graph output_file)
get_property(_all GLOBAL PROPERTY THEROCK_ALL_SUBPROJECTS)

set(_json "{\n")
set(_first_proj TRUE)

foreach(_proj IN LISTS _all)
get_property(_consumers GLOBAL PROPERTY "THEROCK_DIRECT_CONSUMERS_OF_${_proj}")
if(_consumers)
list(REMOVE_DUPLICATES _consumers)
endif()

if(NOT _first_proj)
string(APPEND _json ",\n")
endif()
set(_first_proj FALSE)

string(TOLOWER "${_proj}" _key)

# Build JSON array of consumers.
set(_arr "")
set(_first_c TRUE)
foreach(_c IN LISTS _consumers)
string(TOLOWER "${_c}" _cv)
if(NOT _first_c)
string(APPEND _arr ", ")
endif()
set(_first_c FALSE)
string(APPEND _arr "\"${_cv}\"")
endforeach()

string(APPEND _json " \"${_key}\": {\n")
string(APPEND _json " \"consumers\": [${_arr}]\n")
string(APPEND _json " }")
endforeach()

string(APPEND _json "\n}\n")
file(WRITE "${output_file}" "${_json}")
message(STATUS "Wrote consumer graph to ${output_file}")
endfunction()
Loading
Loading