Skip to content

Commit

Permalink
Add support for fmt cue files. (pantsbuild#18106)
Browse files Browse the repository at this point in the history
  • Loading branch information
kaos authored Jan 27, 2023
1 parent d87f9b6 commit 557c47d
Show file tree
Hide file tree
Showing 7 changed files with 283 additions and 88 deletions.
66 changes: 15 additions & 51 deletions src/python/pants/backend/cue/goals/fix.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,72 +2,36 @@
# Licensed under the Apache License, Version 2.0 (see LICENSE).
from __future__ import annotations

from dataclasses import dataclass
from typing import Any

from pants.backend.cue.rules import _run_cue
from pants.backend.cue.subsystem import Cue
from pants.backend.cue.target_types import CuePackageSourcesField
from pants.core.goals.lint import LintResult, LintTargetsRequest
from pants.core.util_rules.external_tool import DownloadedExternalTool, ExternalToolRequest
from pants.backend.cue.target_types import CueFieldSet
from pants.core.goals.fmt import FmtResult, FmtTargetsRequest
from pants.core.util_rules.partitions import PartitionerType
from pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest
from pants.engine.fs import Digest, MergeDigests
from pants.engine.platform import Platform
from pants.engine.process import FallibleProcessResult, Process
from pants.engine.rules import Get, MultiGet, collect_rules, rule
from pants.engine.target import FieldSet
from pants.engine.rules import collect_rules, rule
from pants.util.logging import LogLevel
from pants.util.strutil import pluralize


@dataclass(frozen=True)
class CueFieldSet(FieldSet):
required_fields = (CuePackageSourcesField,)

sources: CuePackageSourcesField


class CueLintRequest(LintTargetsRequest):
class CueFmtRequest(FmtTargetsRequest):
field_set_type = CueFieldSet
tool_subsystem = Cue
partitioner_type = PartitionerType.DEFAULT_SINGLE_PARTITION


def generate_argv(*args: str, sources: SourceFiles, cue: Cue) -> tuple[str, ...]:
return args + cue.args + sources.snapshot.files


@rule(desc="Lint CUE files", level=LogLevel.DEBUG)
async def run_cue_vet(
request: CueLintRequest.Batch[CueFieldSet, Any],
cue: Cue,
platform: Platform,
) -> LintResult:
downloaded_cue, sources = await MultiGet(
Get(DownloadedExternalTool, ExternalToolRequest, cue.get_request(platform)),
Get(
SourceFiles,
SourceFilesRequest(
sources_fields=[field_set.sources for field_set in request.elements]
),
),
@rule(desc="Format with cue", level=LogLevel.DEBUG)
async def run_cue_fmt(request: CueFmtRequest.Batch, cue: Cue, platform: Platform) -> FmtResult:
process_result = await _run_cue(
"fmt",
cue=cue,
snapshot=request.snapshot,
platform=platform,
output_files=request.snapshot.files,
)
input_digest = await Get(Digest, MergeDigests((downloaded_cue.digest, sources.snapshot.digest)))
process_result = await Get(
FallibleProcessResult,
Process(
argv=[downloaded_cue.exe, *generate_argv("vet", sources=sources, cue=cue)],
input_digest=input_digest,
description=f"Run `cue vet` on {pluralize(len(sources.snapshot.files), 'file')}.",
level=LogLevel.DEBUG,
),
)

return LintResult.create(request, process_result)
return await FmtResult.create(request, process_result)


def rules():
return [
*collect_rules(),
*CueLintRequest.rules(),
*CueFmtRequest.rules(),
]
91 changes: 62 additions & 29 deletions src/python/pants/backend/cue/goals/fix_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,41 @@

from collections import namedtuple
from textwrap import dedent
from typing import Any, Iterable
from typing import Iterable

import pytest

from pants.backend.cue.goals.fix import CueFieldSet, CueLintRequest
from pants.backend.cue.rules import rules as cue_rules
from pants.backend.cue.target_types import CuePackageTarget
from pants.core.goals.lint import LintResult, Partitions
from pants.backend.cue.goals.fix import CueFmtRequest, rules
from pants.backend.cue.target_types import CueFieldSet, CuePackageTarget
from pants.core.goals.fmt import FmtResult, Partitions
from pants.core.util_rules import external_tool, source_files
from pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest
from pants.engine.addresses import AddressInput
from pants.engine.fs import DigestContents
from pants.testutil.rule_runner import QueryRule, RuleRunner


@pytest.fixture
def rule_runner() -> RuleRunner:
return RuleRunner(
rules=[
*rules(),
*external_tool.rules(),
*cue_rules(),
*source_files.rules(),
QueryRule(Partitions, [CueLintRequest.PartitionRequest]),
QueryRule(LintResult, [CueLintRequest.Batch]),
QueryRule(Partitions, [CueFmtRequest.PartitionRequest]),
QueryRule(FmtResult, [CueFmtRequest.Batch]),
QueryRule(SourceFiles, (SourceFilesRequest,)),
],
target_types=[CuePackageTarget],
)


def run_cue(
rule_runner: RuleRunner, addresses: Iterable[str], *, extra_args: list[str] | None = None
) -> tuple[LintResult, ...]:
rule_runner: RuleRunner,
addresses: Iterable[str],
*,
extra_args: list[str] | None = None,
) -> tuple[FmtResult, ...]:
targets = [
rule_runner.get_target(
AddressInput.parse(address, description_of_origin="cue tests").file_to_address()
Expand All @@ -42,34 +47,50 @@ def run_cue(
]
rule_runner.set_options(
extra_args or (),
# env_inherit={"PATH"},
)
field_sets = [CueFieldSet.create(tgt) for tgt in targets]
kwargs = {}
partitions = rule_runner.request(
Partitions[CueFieldSet, Any],
[CueLintRequest.PartitionRequest(tuple(CueFieldSet.create(tgt) for tgt in targets))],
Partitions, [CueFmtRequest.PartitionRequest(tuple(field_sets))]
)
input_sources = rule_runner.request(
SourceFiles, [SourceFilesRequest(field_set.sources for field_set in field_sets)]
)
kwargs["snapshot"] = input_sources.snapshot
assert len(partitions) == 1
results = []
for partition in partitions:
result = rule_runner.request(
LintResult,
[CueLintRequest.Batch("", partition.elements, partition.metadata)],
FmtResult,
[CueFmtRequest.Batch("cue", partition.elements, partition.metadata, **kwargs)],
)
assert result.tool_name == "cue"
results.append(result)
return tuple(results)


Result = namedtuple("Result", "exit_code, stdout, stderr", defaults=("", ""))
ExpectedResult = namedtuple("ExpectedResult", "stdout, stderr, files", defaults=("", "", ()))


def assert_results(results: tuple[LintResult, ...], *expected_results: Result) -> None:
def assert_results(
rule_runner: RuleRunner,
results: tuple[FmtResult, ...],
*expected_results: ExpectedResult,
) -> None:
assert len(results) == len(expected_results)
for result, expected in zip(results, expected_results):
assert result.exit_code == expected.exit_code
assert result.stdout == expected.stdout
assert result.stderr == expected.stderr
for filename, contents in expected.files:
fc = next(
fc
for fc in rule_runner.request(DigestContents, [result.output.digest])
if fc.path == filename
)
assert fc.content.decode() == contents


def test_simple_cue_vet(rule_runner: RuleRunner) -> None:
def test_simple_cue_fmt(rule_runner: RuleRunner) -> None:
rule_runner.write_files(
{
"src/BUILD": "cue_package()",
Expand All @@ -83,33 +104,45 @@ def test_simple_cue_vet(rule_runner: RuleRunner) -> None:
}
)
assert_results(
rule_runner,
run_cue(rule_runner, ["src/example.cue"]),
Result(0),
ExpectedResult(),
)


def test_simple_cue_vet_issue(rule_runner: RuleRunner) -> None:
def test_simple_cue_fmt_issue(rule_runner: RuleRunner) -> None:
rule_runner.write_files(
{
"src/BUILD": "cue_package()",
"src/example.cue": dedent(
"""\
package example
config: "value"
config:{
config: 42
}
"""
),
}
)
assert_results(
rule_runner,
run_cue(rule_runner, ["src/example.cue"]),
Result(
1,
stderr=(
'config: conflicting values "value" and 42 (mismatched types string and int):\n'
" ./src/example.cue:3:9\n"
" ./src/example.cue:4:9\n"
),
# `cue fmt` does not output anything.. so we have only the formatted files to go on. :/
ExpectedResult(
files=[
(
"src/example.cue",
dedent(
"""\
package example
config: {
\tconfig: 42
}
"""
),
)
]
),
)
42 changes: 42 additions & 0 deletions src/python/pants/backend/cue/goals/lint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Copyright 2023 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).
from __future__ import annotations

from typing import Any

from pants.backend.cue.rules import _run_cue
from pants.backend.cue.subsystem import Cue
from pants.backend.cue.target_types import CueFieldSet
from pants.core.goals.lint import LintResult, LintTargetsRequest
from pants.core.util_rules.partitions import PartitionerType
from pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest
from pants.engine.platform import Platform
from pants.engine.rules import Get, collect_rules, rule
from pants.util.logging import LogLevel


class CueLintRequest(LintTargetsRequest):
field_set_type = CueFieldSet
tool_subsystem = Cue
partitioner_type = PartitionerType.DEFAULT_SINGLE_PARTITION


@rule(desc="Lint with cue", level=LogLevel.DEBUG)
async def run_cue_vet(
request: CueLintRequest.Batch[CueFieldSet, Any],
cue: Cue,
platform: Platform,
) -> LintResult:
sources = await Get(
SourceFiles,
SourceFilesRequest(sources_fields=[field_set.sources for field_set in request.elements]),
)
process_result = await _run_cue("vet", cue=cue, snapshot=sources.snapshot, platform=platform)
return LintResult.create(request, process_result)


def rules():
return [
*collect_rules(),
*CueLintRequest.rules(),
]
Loading

0 comments on commit 557c47d

Please sign in to comment.