Skip to content
Merged
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
1 change: 1 addition & 0 deletions docs/walkthrough/plug-into-fluid-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ fluid plugins list
• aws allowed
• gcp allowed
• local allowed
• odps allowed
• snowflake allowed
```

Expand Down
4 changes: 3 additions & 1 deletion docs/walkthrough/your-first-real-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ class GitLabCIScaffold(CustomScaffold):

name = "gitlab-ci"

# ── identity (surfaces to `fluid plugins list`) ─────────────
# ── identity (descriptive metadata for registry / marketplace tooling) ──
# NB: these fields do NOT appear in `fluid plugins list` — that command reads
# entry-point names + allow/block status only and never loads the plugin.

@classmethod
def get_plugin_info(cls) -> PluginMetadata:
Expand Down
97 changes: 97 additions & 0 deletions tests/unit/test_docs_honesty.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@
)
_GROUP_RE = re.compile(r"fluid_build\.[a-z_]+")

# Markdown fenced code blocks, and the FIRST token of a `fluid <command>` invocation
# inside one (the top-level subcommand; flags are skipped by the `[a-z]` start).
_FENCE_RE = re.compile(r"```[\w]*\n(.*?)```", re.DOTALL)
_FLUID_CMD_RE = re.compile(r"(?m)^\s*\$?\s*fluid\s+([a-z][a-z0-9-]*)\b")
# Columns the `fluid plugins` command provably cannot emit — it reads entry-point
# names + allow/block status only and never loads plugin code to read PluginMetadata.
_IMPOSSIBLE_PLUGINS_COLUMNS = ("VERSION", "AUTHOR", "DESCRIPTION")


def _docs() -> list:
files = []
Expand All @@ -55,6 +63,44 @@ def _docs() -> list:
return sorted(f for f in files if f.is_file())


def _fenced_blocks(text: str):
return _FENCE_RE.findall(text)


def _documented_fluid_commands() -> Set[Tuple[str, str]]:
"""Return {(doc_relpath, command)} for every `fluid <command>` in a code fence."""
out: Set[Tuple[str, str]] = set()
for doc in _docs():
rel = str(doc.relative_to(_REPO_ROOT))
for block in _fenced_blocks(doc.read_text(encoding="utf-8")):
for cmd in _FLUID_CMD_RE.findall(block):
out.add((rel, cmd))
return out


def _cli_top_level_commands():
"""The CLI's real top-level subcommands, or ``None`` if the CLI isn't installed.

The SDK is zero-dependency and its CI does not install the ``data-product-forge``
CLI, so this returns ``None`` there and the command-existence check skips.
"""
import importlib.util

if importlib.util.find_spec("fluid_build") is None:
return None
try:
from fluid_build.cli import build_parser

parser = build_parser()
cmds: Set[str] = set()
groups = getattr(parser, "_subparsers", None)
for action in (groups._group_actions if groups else []):
cmds |= set(getattr(action, "choices", {}) or {})
return cmds
except Exception:
return None


def _clean_name(token: str) -> str:
token = token.strip().strip("(),")
# strip an `as alias`
Expand Down Expand Up @@ -139,3 +185,54 @@ def test_documented_role_harnesses_exist_and_subclass_base(harness: str) -> None
cls = getattr(t, harness, None)
assert cls is not None, f"fluid_sdk.testing.{harness} is documented but missing"
assert issubclass(cls, t.PluginTestHarness)


# ── CLI command-example honesty ───────────────────────────────────────
# The import-only checks above are blind to CLI-command-example drift (a doc could
# show a `fluid <cmd>` that doesn't exist, or output a command can't produce). These
# two close that gap as far as is feasible for a CLI-less SDK CI.
#
# Borrowed: the argparse-introspection pattern (assert documented commands exist in
# the parser tree). We DIVERGE from clitest / cram / markdown-clitest (which run the
# binary) because this zero-dependency SDK does not — and should not — install the
# `data-product-forge` CLI in its test env.


def test_documented_cli_commands_exist() -> None:
"""Every `fluid <command>` shown in a doc code fence is a real CLI command.

Runs only when the CLI happens to be importable (e.g. a dev env with both
installed); skips with a clear reason in SDK-only CI — "validate where feasible".
"""
cli_cmds = _cli_top_level_commands()
if cli_cmds is None:
pytest.skip("data-product-forge CLI not installed; CLI-command-existence check skipped")
bad = sorted(
f"{doc}: `fluid {cmd}`" for doc, cmd in _documented_fluid_commands() if cmd not in cli_cmds
)
assert not bad, "Docs reference `fluid` commands that do not exist:\n " + "\n ".join(bad)


def test_fluid_plugins_output_advertises_no_impossible_columns() -> None:
"""A documented `fluid plugins` output block must not claim columns the command
cannot emit (VERSION / AUTHOR / DESCRIPTION).

Feasible WITHOUT the CLI installed, so it runs in SDK-only CI too — it pins the
exact output-drift class (a NAME/ROLE/VERSION/AUTHOR/DESCRIPTION table) that the
import-only gate is structurally blind to. A real header row carries several of
these together, so we require >= 2 in one block to avoid flagging incidental prose.
"""
offenders = []
for doc in _docs():
text = doc.read_text(encoding="utf-8")
if "fluid plugins" not in text:
continue
rel = str(doc.relative_to(_REPO_ROOT))
for block in _fenced_blocks(text):
present = [c for c in _IMPOSSIBLE_PLUGINS_COLUMNS if c in block]
if len(present) >= 2:
offenders.append(f"{rel}: a `fluid plugins` block advertises {present} columns")
assert not offenders, (
"`fluid plugins` reads entry-point names + allow/block status only — it cannot "
"print plugin version/author/description:\n " + "\n ".join(sorted(set(offenders)))
)