mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(skills): remotion-to-hyperframes corpus T4 (5/7)
Adds the escape-hatch tier — lint-only fixtures that test the skill's
ability to refuse translation cleanly when it sees patterns that don't map
to HF's seek-driven model.
Cases (8 total):
01-use-state.tsx blocker: r2hf/use-state
02-use-effect-deps.tsx blocker: r2hf/use-effect-deps (multi-line body
with internal commas — regression target for
the regex bug fix in PR 2)
03-async-metadata.tsx blocker: r2hf/async-metadata
04-third-party-react.tsx blocker: r2hf/third-party-react-ui (@mui/material)
05-lambda-config.tsx blocker: r2hf/lambda-import
06-warnings-only.tsx warnings: delayRender / useCallback / useMemo
(no blockers — translates after dropping wrappers)
07-custom-hook.tsx warning: r2hf/custom-hook (pure useFadeIn)
08-mixed.tsx multiple blockers + warnings (aggregate test)
Each case documents:
- The Remotion pattern it demonstrates
- Why it's a blocker / warning / info
- What the skill should do (refuse / drop-and-translate / translate-as-is)
Validation harness (validate.sh):
Runs lint_source.py against each case, asserts:
- Each expected blocker rule fires with severity="blocker"
- Each expected warning rule fires with severity="warning"
- lint_source.py exit code is 1 when blockers expected, 0 otherwise
T4 has no renders to diff. The skill is graded on lint correctness — that's
the gate that decides whether to translate or recommend the runtime interop
pattern from PR #214.
Result: 8/8 cases pass.
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
# Tier 4 — escape-hatch
|
||||
|
||||
## What it tests
|
||||
|
||||
T4 is the **lint-only** tier. There are no renders to diff — the skill is
|
||||
graded on whether it correctly _refuses_ to translate each case (and
|
||||
recommends the runtime interop pattern from PR #214 instead) or, where
|
||||
appropriate, translates after dropping warning-level decorations.
|
||||
|
||||
Each `cases/*.tsx` file is a minimal Remotion composition that
|
||||
demonstrates one specific pattern. The skill should:
|
||||
|
||||
1. Run `scripts/lint_source.py` over the source.
|
||||
2. Compare the JSON output to `expected.json` for that case.
|
||||
3. Take the documented `skill_action`:
|
||||
- `refuse_translation_recommend_interop` — print the rationale + link to
|
||||
the PR #214 interop guide; do not produce HF output.
|
||||
- `drop_lambda_code_translate_remainder_if_clean` — drop the
|
||||
`@remotion/lambda` code with a note; translate the rest only if no
|
||||
other blockers are present.
|
||||
- `translate_after_dropping_wrappers` — translate normally; drop
|
||||
`useCallback` / `useMemo` / `delayRender` wrappers.
|
||||
- `inline_hook_body_if_pure` — inline the custom hook's body if it's a
|
||||
pure derivation of `useCurrentFrame`; otherwise bow out.
|
||||
|
||||
## Cases
|
||||
|
||||
| # | File | Expected finding | Notes |
|
||||
| --- | -------------------------- | ----------------------------------- | ----------------------------------------------- |
|
||||
| 01 | `01-use-state.tsx` | blocker `r2hf/use-state` | useState driving animation |
|
||||
| 02 | `02-use-effect-deps.tsx` | blocker `r2hf/use-effect-deps` | useEffect/useLayoutEffect with non-empty deps |
|
||||
| 03 | `03-async-metadata.tsx` | blocker `r2hf/async-metadata` | calculateMetadata returns a Promise |
|
||||
| 04 | `04-third-party-react.tsx` | blocker `r2hf/third-party-react-ui` | imports `@mui/material` |
|
||||
| 05 | `05-lambda-config.tsx` | warning `r2hf/lambda-import` | imports `@remotion/lambda` — drops, translates |
|
||||
| 06 | `06-warnings-only.tsx` | warnings only | delayRender / useCallback / useMemo |
|
||||
| 07 | `07-custom-hook.tsx` | warning `r2hf/custom-hook` | locally-defined `useFadeIn` (export const form) |
|
||||
| 08 | `08-mixed.tsx` | 3 blockers + 1 warning | aggregate-findings test |
|
||||
|
||||
## Validation
|
||||
|
||||
```bash
|
||||
./validate.sh
|
||||
```
|
||||
|
||||
The script runs `lint_source.py` against each case and asserts:
|
||||
|
||||
- Each expected blocker rule fires with severity `blocker`.
|
||||
- Each expected warning rule fires with severity `warning` (or stronger).
|
||||
- `lint_source.py`'s exit code is 1 when blockers are expected, 0 otherwise.
|
||||
|
||||
T4 passes when every case matches its expected output. No renders involved.
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
// T4 case 01 — useState drives animation.
|
||||
//
|
||||
// Should be detected by lint_source.py as blocker r2hf/use-state.
|
||||
// The skill should refuse to translate and recommend the runtime interop
|
||||
// pattern from PR #214.
|
||||
//
|
||||
// Why this is a blocker: useState is React's component-local mutable state.
|
||||
// HF's seek-driven model produces deterministic frames from a single time
|
||||
// value — there's no per-frame React render cycle to update state on.
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { AbsoluteFill, useCurrentFrame } from "remotion";
|
||||
|
||||
export const StateDriven: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const [hue, setHue] = useState(0);
|
||||
|
||||
// Even if this looks innocuous, the setHue call breaks determinism: HF
|
||||
// can't reproduce React state mutations across seeks.
|
||||
if (frame % 30 === 0 && hue < 360) {
|
||||
setHue((h) => h + 30);
|
||||
}
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ background: `hsl(${hue}, 80%, 50%)` }}>
|
||||
<div>frame {frame}</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
// T4 case 02 — useEffect with non-empty deps performs side effects per render.
|
||||
//
|
||||
// Should be detected by lint_source.py as blocker r2hf/use-effect-deps.
|
||||
// The skill should refuse to translate.
|
||||
//
|
||||
// Why this is a blocker: side effects (network, DOM mutation outside the
|
||||
// rendered tree, timers) don't translate to a seek-driven model. HF assumes
|
||||
// the page is fully rendered and pure between seeks.
|
||||
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { AbsoluteFill, useCurrentFrame } from "remotion";
|
||||
|
||||
export const SideEffectDriven: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx?.fillRect(frame, frame, 10, 10);
|
||||
}, [frame]);
|
||||
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
<canvas ref={canvasRef} width={1280} height={720} />
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
// T4 case 03 — calculateMetadata returns a Promise.
|
||||
//
|
||||
// Should be detected by lint_source.py as blocker r2hf/async-metadata.
|
||||
// The skill should refuse to translate.
|
||||
//
|
||||
// Why this is a blocker: HF needs the composition's duration, dimensions,
|
||||
// and props known up-front to produce HTML and seed the timeline. Async
|
||||
// metadata fetched from a server at render time has no equivalent in HF —
|
||||
// the metadata would need to be resolved at build time before the HTML is
|
||||
// authored.
|
||||
|
||||
import React from "react";
|
||||
import { AbsoluteFill, useCurrentFrame } from "remotion";
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const AsyncMetadataDriven: React.FC<Props> = ({ text }) => {
|
||||
const frame = useCurrentFrame();
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
<div>
|
||||
{text} · frame {frame}
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
export const calculateMetadata = async ({ props }: { props: Props }) => {
|
||||
const response = await fetch(
|
||||
`https://api.example.com/duration?text=${encodeURIComponent(props.text)}`,
|
||||
);
|
||||
const { durationInFrames } = await response.json();
|
||||
return {
|
||||
durationInFrames,
|
||||
fps: 30,
|
||||
};
|
||||
};
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// T4 case 04 — Imports from a third-party React UI library.
|
||||
//
|
||||
// Should be detected by lint_source.py as blocker r2hf/third-party-react-ui.
|
||||
// The skill should refuse to translate.
|
||||
//
|
||||
// Why this is a blocker: a Material-UI Button (or any React UI library
|
||||
// component) is a React-only abstraction with internal hooks, refs, and
|
||||
// theme provider context. Translating it to HTML+CSS would require
|
||||
// re-implementing the design system, which is out of scope for a video
|
||||
// translation skill. Use the runtime interop pattern from PR #214 to keep
|
||||
// these components rendering through Remotion's React tree.
|
||||
|
||||
import React from "react";
|
||||
import { Button } from "@mui/material";
|
||||
import { AbsoluteFill, useCurrentFrame, interpolate } from "remotion";
|
||||
|
||||
export const MuiDriven: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const opacity = interpolate(frame, [0, 30], [0, 1], { extrapolateRight: "clamp" });
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center" }}>
|
||||
<div style={{ opacity }}>
|
||||
<Button variant="contained" color="primary">
|
||||
Click me · frame {frame}
|
||||
</Button>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
// T4 case 05 — Imports @remotion/lambda for distributed rendering config.
|
||||
//
|
||||
// Should be detected by lint_source.py as warning r2hf/lambda-import.
|
||||
// The skill drops the Lambda code with a note (HF runs single-machine
|
||||
// today) and translates the rest of the composition.
|
||||
//
|
||||
// Why this is a warning, not a blocker: @remotion/lambda config is
|
||||
// orthogonal to the rendered composition — it's deployment configuration,
|
||||
// not animation logic. Treating it as a hard blocker would refuse
|
||||
// translation for compositions that are otherwise clean. The skill drops
|
||||
// the Lambda calls in step 3 (Generate) and writes a TRANSLATION_NOTES.md
|
||||
// entry so the user knows to set up HF rendering separately.
|
||||
|
||||
import React from "react";
|
||||
import { renderMediaOnLambda } from "@remotion/lambda";
|
||||
import { AbsoluteFill, useCurrentFrame, interpolate } from "remotion";
|
||||
|
||||
export const LambdaConfigured: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const opacity = interpolate(frame, [0, 30], [0, 1]);
|
||||
return (
|
||||
<AbsoluteFill style={{ opacity }}>
|
||||
<div>frame {frame}</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Rendered at scale via Lambda — no HF equivalent.
|
||||
export async function renderViaLambda() {
|
||||
return renderMediaOnLambda({
|
||||
region: "us-east-1",
|
||||
functionName: "remotion-render",
|
||||
composition: "LambdaConfigured",
|
||||
serveUrl: "https://example.com/bundle",
|
||||
inputProps: {},
|
||||
codec: "h264",
|
||||
});
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
// T4 case 06 — Patterns that warn but don't block.
|
||||
//
|
||||
// Should be detected by lint_source.py with:
|
||||
// - r2hf/delay-render (warning) — drop the call; HF handles asset readiness
|
||||
// - r2hf/use-callback (warning) — decorative, drop the wrapper
|
||||
// - r2hf/use-memo (warning) — decorative, drop the wrapper
|
||||
//
|
||||
// 0 blockers expected — the skill should still translate this composition
|
||||
// after dropping the wrappers. delayRender is paired with continueRender via
|
||||
// an empty-deps useEffect (mount-once side effect), which doesn't trip the
|
||||
// use-effect-deps blocker.
|
||||
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { AbsoluteFill, delayRender, continueRender, useCurrentFrame, interpolate } from "remotion";
|
||||
|
||||
const handle = delayRender();
|
||||
// Resolve the handle once at module load — no per-frame side effects.
|
||||
queueMicrotask(() => continueRender(handle));
|
||||
|
||||
export const WarningsOnly: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
|
||||
// useCallback / useMemo — decorative for render-perf in React, no equivalent
|
||||
// needed in the seek-driven HF model.
|
||||
const opacity = useMemo(
|
||||
() => interpolate(frame, [0, 30], [0, 1], { extrapolateRight: "clamp" }),
|
||||
[frame],
|
||||
);
|
||||
const onMount = useCallback(() => {}, []);
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ opacity }} onClick={onMount}>
|
||||
<div>frame {frame}</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
// T4 case 07 — Locally-defined custom hook.
|
||||
//
|
||||
// Should be detected by lint_source.py as warning r2hf/custom-hook.
|
||||
// 0 blockers expected — the skill can attempt translation if the hook body
|
||||
// is pure (derives from props/frame alone).
|
||||
//
|
||||
// Why this is a warning: custom hooks vary widely in what they do. Some are
|
||||
// pure derivations of useCurrentFrame (translatable — inline the body); some
|
||||
// wrap useState/useEffect (blocker — but those will be caught by the other
|
||||
// rules independently). The warning prompts the agent to inspect the body.
|
||||
|
||||
import React from "react";
|
||||
import { AbsoluteFill, useCurrentFrame, interpolate } from "remotion";
|
||||
|
||||
// Custom hook — pure derivation from frame, no state. Translates fine.
|
||||
function useFadeIn(durationInFrames: number) {
|
||||
const frame = useCurrentFrame();
|
||||
return interpolate(frame, [0, durationInFrames], [0, 1], { extrapolateRight: "clamp" });
|
||||
}
|
||||
|
||||
export const CustomHookDriven: React.FC = () => {
|
||||
const opacity = useFadeIn(30);
|
||||
return (
|
||||
<AbsoluteFill style={{ opacity }}>
|
||||
<div>fading in</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
// T4 case 08 — Multiple blockers + multiple warnings in one file.
|
||||
//
|
||||
// Should report:
|
||||
// blockers: r2hf/use-state, r2hf/use-effect-deps, r2hf/third-party-react-ui
|
||||
// warnings: r2hf/use-callback (also r2hf/delay-render via the import chain
|
||||
// would only fire if delayRender is actually called)
|
||||
//
|
||||
// Tests that the linter aggregates findings correctly and does not stop at
|
||||
// the first blocker.
|
||||
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { AbsoluteFill, useCurrentFrame } from "remotion";
|
||||
import { Card } from "@chakra-ui/react";
|
||||
|
||||
interface Item {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const MixedBlockers: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const [items, setItems] = useState<Item[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/items")
|
||||
.then((r) => r.json())
|
||||
.then(setItems);
|
||||
}, [frame]);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
setItems((prev) => [...prev, { id: String(prev.length), label: "new" }]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AbsoluteFill onClick={onClick}>
|
||||
{items.map((item) => (
|
||||
<Card key={item.id}>{item.label}</Card>
|
||||
))}
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"tier": 4,
|
||||
"name": "escape-hatch",
|
||||
"description": "Lint-only fixture set. Each case demonstrates a Remotion pattern the skill cannot or should not translate cleanly. The skill is graded on whether lint_source.py emits the right finding for each case — there are no renders to compare. T4 passes when every case triggers its expected rule and no others.",
|
||||
"cases": [
|
||||
{
|
||||
"file": "01-use-state.tsx",
|
||||
"expected": {
|
||||
"blockers": [{ "rule": "r2hf/use-state", "min_count": 1 }],
|
||||
"warnings": [],
|
||||
"skill_action": "refuse_translation_recommend_interop"
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "02-use-effect-deps.tsx",
|
||||
"expected": {
|
||||
"blockers": [{ "rule": "r2hf/use-effect-deps", "min_count": 1 }],
|
||||
"warnings": [],
|
||||
"skill_action": "refuse_translation_recommend_interop"
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "03-async-metadata.tsx",
|
||||
"expected": {
|
||||
"blockers": [{ "rule": "r2hf/async-metadata", "min_count": 1 }],
|
||||
"warnings": [],
|
||||
"skill_action": "refuse_translation_recommend_interop"
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "04-third-party-react.tsx",
|
||||
"expected": {
|
||||
"blockers": [{ "rule": "r2hf/third-party-react-ui", "min_count": 1 }],
|
||||
"warnings": [],
|
||||
"skill_action": "refuse_translation_recommend_interop"
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "05-lambda-config.tsx",
|
||||
"expected": {
|
||||
"blockers": [],
|
||||
"warnings": [{ "rule": "r2hf/lambda-import", "min_count": 1 }],
|
||||
"skill_action": "drop_lambda_code_translate_remainder_if_clean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "06-warnings-only.tsx",
|
||||
"expected": {
|
||||
"blockers": [],
|
||||
"warnings": [
|
||||
{ "rule": "r2hf/delay-render", "min_count": 1 },
|
||||
{ "rule": "r2hf/use-callback", "min_count": 1 },
|
||||
{ "rule": "r2hf/use-memo", "min_count": 1 }
|
||||
],
|
||||
"skill_action": "translate_after_dropping_wrappers"
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "07-custom-hook.tsx",
|
||||
"expected": {
|
||||
"blockers": [],
|
||||
"warnings": [{ "rule": "r2hf/custom-hook", "min_count": 1 }],
|
||||
"skill_action": "inline_hook_body_if_pure"
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "08-mixed.tsx",
|
||||
"expected": {
|
||||
"blockers": [
|
||||
{ "rule": "r2hf/use-state", "min_count": 1 },
|
||||
{ "rule": "r2hf/use-effect-deps", "min_count": 1 },
|
||||
{ "rule": "r2hf/third-party-react-ui", "min_count": 1 }
|
||||
],
|
||||
"warnings": [{ "rule": "r2hf/use-callback", "min_count": 1 }],
|
||||
"skill_action": "refuse_translation_recommend_interop"
|
||||
}
|
||||
}
|
||||
],
|
||||
"totals": {
|
||||
"expected_blocker_cases": 5,
|
||||
"expected_warning_only_cases": 3,
|
||||
"expected_total_blocker_findings_min": 7,
|
||||
"expected_total_warning_findings_min": 6
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
# validate.sh — assert lint_source.py output matches expected.json for every T4 case.
|
||||
#
|
||||
# T4 has no renders to diff. The skill is graded on whether it correctly
|
||||
# refuses to translate each case (or drops only the lambda config in case 5,
|
||||
# or warns appropriately in cases 6 and 7).
|
||||
#
|
||||
# Usage:
|
||||
# ./validate.sh
|
||||
# Exit 0 on pass.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
THIS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SCRIPTS_DIR="$(cd "$THIS_DIR/../../../scripts" && pwd)"
|
||||
EXPECTED="$THIS_DIR/expected.json"
|
||||
|
||||
if [[ ! -f "$SCRIPTS_DIR/lint_source.py" ]]; then
|
||||
echo "error: lint_source.py not found at $SCRIPTS_DIR/lint_source.py" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ ! -f "$EXPECTED" ]]; then
|
||||
echo "error: expected.json not found at $EXPECTED" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Drive lint_file() in-process so the per-case overhead is one Python startup,
|
||||
# not N (8 cases × ~80 ms forking python3 was the dominant cost).
|
||||
SCRIPTS_DIR="$SCRIPTS_DIR" \
|
||||
THIS_DIR="$THIS_DIR" \
|
||||
EXPECTED="$EXPECTED" \
|
||||
python3 <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
scripts_dir = Path(os.environ["SCRIPTS_DIR"])
|
||||
this_dir = Path(os.environ["THIS_DIR"])
|
||||
expected_path = Path(os.environ["EXPECTED"])
|
||||
cases_dir = this_dir / "cases"
|
||||
|
||||
sys.path.insert(0, str(scripts_dir))
|
||||
from lint_source import BLOCKER, WARNING, lint_file # noqa: E402
|
||||
|
||||
expected = json.loads(expected_path.read_text())
|
||||
|
||||
fails: list[str] = []
|
||||
passes: list[str] = []
|
||||
|
||||
for case in expected["cases"]:
|
||||
file_name = case["file"]
|
||||
fixture = cases_dir / file_name
|
||||
if not fixture.exists():
|
||||
fails.append(f"{file_name}: fixture missing at {fixture}")
|
||||
continue
|
||||
|
||||
findings = lint_file(fixture)
|
||||
rule_counts: Counter[str] = Counter()
|
||||
severity_by_rule: dict[str, str] = {}
|
||||
for f in findings:
|
||||
rule_counts[f.rule] += 1
|
||||
severity_by_rule[f.rule] = f.severity
|
||||
|
||||
case_failed = False
|
||||
|
||||
def assert_rule(expected_entry, expected_severity_floor, kind):
|
||||
global case_failed
|
||||
rule = expected_entry["rule"]
|
||||
min_count = expected_entry["min_count"]
|
||||
actual = rule_counts[rule]
|
||||
actual_severity = severity_by_rule.get(rule)
|
||||
if actual < min_count:
|
||||
fails.append(f"{file_name}: expected >={min_count} {kind} findings of rule {rule}, got {actual}")
|
||||
case_failed = True
|
||||
elif actual_severity not in expected_severity_floor:
|
||||
fails.append(
|
||||
f"{file_name}: rule {rule} found but severity={actual_severity!r} (expected {kind})"
|
||||
)
|
||||
case_failed = True
|
||||
|
||||
for entry in case["expected"]["blockers"]:
|
||||
assert_rule(entry, {BLOCKER}, "blocker")
|
||||
for entry in case["expected"]["warnings"]:
|
||||
assert_rule(entry, {WARNING, BLOCKER}, "warning")
|
||||
|
||||
# Implied lint exit code: 1 when blockers are expected, 0 otherwise.
|
||||
has_blockers = any(f.severity == BLOCKER for f in findings)
|
||||
expected_has_blockers = bool(case["expected"]["blockers"])
|
||||
if has_blockers != expected_has_blockers:
|
||||
fails.append(
|
||||
f"{file_name}: implied lint exit {1 if has_blockers else 0}, "
|
||||
f"expected {1 if expected_has_blockers else 0} (blockers expected: {expected_has_blockers})"
|
||||
)
|
||||
case_failed = True
|
||||
|
||||
if not case_failed:
|
||||
passes.append(file_name)
|
||||
|
||||
print(f"Passed: {len(passes)}")
|
||||
for name in passes:
|
||||
print(f" ✓ {name}")
|
||||
if fails:
|
||||
print(f"Failed: {len(fails)}")
|
||||
for msg in fails:
|
||||
print(f" ✗ {msg}")
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
PY
|
||||
Reference in New Issue
Block a user