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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
- name: Lint, anonymous steps
run: python -m codeupipe_lint.anonymous_steps
- name: Lint, invariants
run: python -m codeupipe_lint.invariants
run: python -m codeupipe_lint.invariants --strict
- name: Conformance
run: python conformance/run.py

Expand Down
5 changes: 5 additions & 0 deletions conformance/bench.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,12 @@ def python_key():
return "%d.%d" % sys.version_info[:2]


# cup: allow-plain the speed check of the conformance suite: it times the scenarios and compares them with the baseline
def load_baseline():
return json.loads(BASELINE.read_text()) if BASELINE.exists() else {}


# cup: allow-plain the speed check of the conformance suite: it times the scenarios and compares them with the baseline
def compare(baseline, current, tolerance=DEFAULT_TOLERANCE):
"""Every number that is not within ``tolerance`` percent of its baseline.

Expand All @@ -84,6 +86,7 @@ def compare(baseline, current, tolerance=DEFAULT_TOLERANCE):
return problems


# cup: allow-plain the speed check of the conformance suite: it times the scenarios and compares them with the baseline
def _report(problems, tolerance):
if not problems:
return "Speed check: every number is within %g%% of its baseline." % tolerance
Expand All @@ -101,6 +104,7 @@ def _report(problems, tolerance):
return "\n".join(lines)


# cup: allow-plain the speed check of the conformance suite: it times the scenarios and compares them with the baseline
def main(argv):
argv = list(argv)
record, check = "--record" in argv, "--check" in argv
Expand Down Expand Up @@ -167,6 +171,7 @@ def main(argv):
return 0


# cup: allow-plain the speed check of the conformance suite: it times the scenarios and compares them with the baseline
def markdown(scenarios, table, problems, refused=()):
targets = list(table)
lines = ["µs per run (best of 5 × 20,000, after 20,000 warmup runs); lower is better.", "",
Expand Down
8 changes: 8 additions & 0 deletions conformance/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def _typescript(target):
}


# cup: allow-plain the judge of the conformance suite: it runs each target and compares each result with what the case expects
def _node_runs_typescript():
"""Whether there is a Node that runs a .ts file as it is (22.18 or later)."""
node = shutil.which("node")
Expand Down Expand Up @@ -83,6 +84,7 @@ def _node_runs_typescript():
}


# cup: allow-plain the judge of the conformance suite: it runs each target and compares each result with what the case expects
def default_targets():
"""Every target that this machine can run. TypeScript needs Node, and the
static TypeScript target needs its package to be installed."""
Expand All @@ -93,6 +95,7 @@ def can_run(name):
return [name for name in TARGETS if can_run(name)]


# cup: allow-plain the judge of the conformance suite: it runs each target and compares each result with what the case expects
def load_cases():
cases = []
for path in sorted(CASES.glob("*.json")):
Expand All @@ -104,6 +107,7 @@ def load_cases():
return cases


# cup: allow-plain the judge of the conformance suite: it runs each target and compares each result with what the case expects
def run_target(name, spec, cases):
env = dict(os.environ)
env.update(spec["env"])
Expand All @@ -119,6 +123,7 @@ def run_target(name, spec, cases):
return None, f"unreadable runner output ({error}): {proc.stdout[:500]}"


# cup: allow-plain the judge of the conformance suite: it runs each target and compares each result with what the case expects
def _norm(value):
"""JSON equality: 1 == 1.0, but true is not 1."""
if isinstance(value, bool) or value is None or isinstance(value, str):
Expand All @@ -132,6 +137,7 @@ def _norm(value):
return value


# cup: allow-plain the judge of the conformance suite: it runs each target and compares each result with what the case expects
def judge(case, result):
"""Return (status, problems)."""
if result is None:
Expand Down Expand Up @@ -179,6 +185,7 @@ def judge(case, result):
return "pass", []


# cup: allow-plain the judge of the conformance suite: it runs each target and compares each result with what the case expects
def main(argv):
cases = load_cases()
names = argv or default_targets()
Expand Down Expand Up @@ -211,6 +218,7 @@ def main(argv):
return 1 if failed else 0


# cup: allow-plain the judge of the conformance suite: it runs each target and compares each result with what the case expects
def _markdown(cases, names, matrix, report):
mark = {"pass": "✅", "partial": "✅*", "FAIL": "❌", "unsupported": "➖", "—": "—"}
lines = ["| case | " + " | ".join(names) + " |", "|---|" + "---|" * len(names)]
Expand Down
15 changes: 15 additions & 0 deletions conformance/runners/python/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class Unsupported(Exception):
pass


# cup: allow-plain the conformance harness: it builds each case, runs it on a target and reports; it never judges
def _filter(spec, run_async=False):
op = spec["op"]
if op == "run_again":
Expand All @@ -61,6 +62,7 @@ def _filter(spec, run_async=False):
raise Unsupported(f"unknown filter op {op!r}")


# cup: allow-plain the conformance harness: it builds each case, runs it on a target and reports; it never judges
def _tap(spec):
if spec["op"] == "record":
return ops.RecordTap(spec["key"])
Expand All @@ -71,6 +73,7 @@ def _tap(spec):
raise Unsupported(f"unknown tap op {spec['op']!r}")


# cup: allow-plain the conformance harness: it builds each case, runs it on a target and reports; it never judges
def _predicate_source(name, when):
"""Valve predicates are generated as real predicate classes so static
lowering can recover their source (it refuses anything else)."""
Expand All @@ -87,6 +90,7 @@ def _predicate_source(name, when):
f"when_{name} = When_{name}()\n")


# cup: allow-plain the conformance harness: it builds each case, runs it on a target and reports; it never judges
def _predicates(steps, module_dir, module_name):
sources = []

Expand All @@ -107,6 +111,7 @@ def walk(items):
sys.path.remove(str(module_dir))


# cup: allow-plain the conformance harness: it builds each case, runs it on a target and reports; it never judges
def build(case, cls, predicates, names, run_async=False):
p = cls()
for step in case["steps"]:
Expand Down Expand Up @@ -147,6 +152,7 @@ def _named(steps, label):
return [label.get(step, step) for step in steps]


# cup: allow-plain the conformance harness: it builds each case, runs it on a target and reports; it never judges
def _named_hooks(events, label):
out = []
for event in events:
Expand All @@ -155,6 +161,7 @@ def _named_hooks(events, label):
return out


# cup: allow-plain the conformance harness: it builds each case, runs it on a target and reports; it never judges
def _dynamic(case, predicates, run_async=False):
names = {}
p = build(case, Pipeline, predicates, names, run_async)
Expand Down Expand Up @@ -197,6 +204,7 @@ def report(state):
return result


# cup: allow-plain the conformance harness: it builds each case, runs it on a target and reports; it never judges
def _load_export(p, lowering, observe, workdir):
"""Export ``p`` and import the package from disk, as a customer would."""
from codeupipe.compile import build_export_pipeline_v2
Expand All @@ -217,6 +225,7 @@ def _load_export(p, lowering, observe, workdir):
sys.path.remove(package_parent)


# cup: allow-plain the conformance harness: it builds each case, runs it on a target and reports; it never judges
def _static(case, predicates, lowering, workdir, observe=False):
# The compiled form always gets its State from the caller, so "given_state" needs nothing more.
names = {}
Expand Down Expand Up @@ -263,6 +272,7 @@ def report(one):
return result


# cup: allow-plain the conformance harness: it builds each case, runs it on a target and reports; it never judges
def run_case(case, target, index):
sink.reset()
tmp = pathlib.Path(tempfile.mkdtemp(prefix="cup_conf_"))
Expand Down Expand Up @@ -300,6 +310,7 @@ def _reference_write(data, key):
return data


# cup: allow-plain the conformance harness: it builds each case, runs it on a target and reports; it never judges
def _reference():
"""The reference workload: ten dict writes behind ten function calls,
with no framework. A pipeline's time is reported relative to it."""
Expand All @@ -309,6 +320,7 @@ def _reference():
return data


# cup: allow-plain the conformance harness: it builds each case, runs it on a target and reports; it never judges
def _timed(fn):
import time
t0 = time.perf_counter()
Expand All @@ -317,6 +329,7 @@ def _timed(fn):
return time.perf_counter() - t0


# cup: allow-plain the conformance harness: it builds each case, runs it on a target and reports; it never judges
def _measure(fn):
"""(µs per run, time relative to the reference workload).

Expand All @@ -339,6 +352,7 @@ def _measure(fn):
return best / BENCH_RUNS * 1e6, statistics.median(ratios)


# cup: allow-plain the conformance harness: it builds each case, runs it on a target and reports; it never judges
def bench(target, scenarios):
results = []
for index, case in enumerate(scenarios):
Expand Down Expand Up @@ -372,6 +386,7 @@ def bench(target, scenarios):
return results


# cup: allow-plain the conformance harness: it builds each case, runs it on a target and reports; it never judges
def main():
target = sys.argv[1]
if target.startswith("bench:"):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from ..rule_ids import unknown_rules


# cup: allow-plain the entry of the program: it reads the arguments, runs the pipeline and prints the result
def main(argv=None) -> int:
parser = argparse.ArgumentParser(prog="python -m codeupipe_lint.anonymous_steps",
description="Flag anonymous pipeline steps (packages/codeupipe-lint/README.md).")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,25 @@ def call(self, payload: Payload) -> Payload:
except OSError:
lines = []
cache[finding.path] = lines
places = _marker_lines(finding, lines, self.marker)
places = ApplySuppressionsFilter._marker_lines(finding, lines, self.marker)
used.update((finding.path, place) for place in places)
(suppressed if places else kept).append(finding)
return (payload.insert("findings", kept).insert("suppressed", suppressed)
.insert("used_markers", sorted(used)))

@staticmethod
def _line(lines, number):
return lines[number - 1] if number and 0 < number <= len(lines) else ""

def _line(lines, number):
return lines[number - 1] if number and 0 < number <= len(lines) else ""


def _marker_lines(finding, lines, marker=MARKER):
"""The lines of the comments that allow this finding. Empty when none does."""
places = [number for number in (finding.line, finding.call_line) if marker.search(_line(lines, number))]
above = _line(lines, finding.line - 1)
if _COMMENT_ONLY.match(above) and marker.search(above):
places.append(finding.line - 1)
return places
@staticmethod
def _marker_lines(finding, lines, marker=MARKER):
"""The lines of the comments that allow this finding. Empty when none does."""
places = [number for number in (finding.line, finding.call_line) if marker.search(ApplySuppressionsFilter._line(lines, number))]
above = ApplySuppressionsFilter._line(lines, finding.line - 1)
if _COMMENT_ONLY.match(above) and marker.search(above):
places.append(finding.line - 1)
return places


def _is_suppressed(finding, lines, marker=MARKER) -> bool:
return bool(_marker_lines(finding, lines, marker))
return bool(ApplySuppressionsFilter._marker_lines(finding, lines, marker))
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
from typing import NamedTuple, Optional


# What a report line has in front of the message, for each severity.
_PREFIX = {"warning": "warning: "}


class Finding(NamedTuple):
"""An anonymous step at ``path:line:col`` (1-based line and column).

Expand All @@ -23,5 +27,5 @@ class Finding(NamedTuple):
severity: Optional[str] = None

def format(self) -> str:
warning = "warning: " if self.severity == "warning" else ""
return "%s:%d:%d %s %s %s%s" % (self.path, self.line, self.col, self.rule, self.slug, warning, self.message)
prefix = _PREFIX.get(self.severity, "")
return "%s:%d:%d %s %s %s%s" % (self.path, self.line, self.col, self.rule, self.slug, prefix, self.message)
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ def lint_paths(paths: Iterable[str], exclude: Optional[Iterable[str]] = None,
"""Run the linter over ``paths``; the result Payload holds ``findings``,
``suppressed``, ``errors``, ``report``, ``error_report`` and ``exit_code``."""
data = {"paths": list(paths), "findings": [], "errors": []}
if exclude:
data["exclude"] = list(exclude)
if disable:
data["disable"] = list(disable)
data["exclude"] = list(exclude or ())
data["disable"] = list(disable or ())
return build_lint_pipeline().run(Payload(data))
Loading
Loading