Skip to content
Open
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
5 changes: 1 addition & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
/bazel-bin
/bazel-out
/bazel-rules_cue
/bazel-testlogs
/bazel-*

*~
158 changes: 158 additions & 0 deletions cue/cue.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -908,3 +908,161 @@ def cue_exported_instance(name, **kwargs):
],
**kwargs
)

def _cue_vet_test_impl(ctx):
instance = ctx.attr.instance[CUEInstanceInfo]
module = instance.module
cue_tool = ctx.toolchains[_cue_toolchain_type].cueinfo.tool

data_files = []
for t in ctx.attr.data:
data_files.extend(t.files.to_list())

module_file_rlocation = _runfile_path(ctx, module.module_file)

# Build module-relative instance path (same logic as _make_instance_consuming_action)
module_root_directory = _cue_module_root_directory_path(ctx, module)
relative_instance_path = paths.relativize(instance.directory_path, module_root_directory)
if relative_instance_path == instance.directory_path:
relative_instance_path = "."
else:
relative_instance_path = "./" + relative_instance_path

if instance.package_name:
instance_arg = relative_instance_path + ":" + instance.package_name
else:
instance_arg = relative_instance_path

# Build data file rlocation lines
data_rlocation_lines = "\n".join([
'data_files+=("$(rlocation {})")'.format(_runfile_path(ctx, f))
for f in data_files
])

# Build -d expression flags (single-quoted to avoid shell injection)
schema_expr_lines = "\n".join([
"vet_args+=('-d' '{}')".format(expr.replace("'", "'\\''"))
for expr in ctx.attr.schema_expression
])

concrete_flag = "'-c'" if ctx.attr.concrete else ""

# Choose invocation style based on should_fail (known at analysis time)
if ctx.attr.should_fail:
vet_invocation = """\
if "${{CUE}}" vet "${{vet_args[@]+"${{vet_args[@]}}}}" {instance_arg} "${{data_files[@]}}"; then
echo >&2 "cue vet succeeded but failure was expected"
exit 1
else
exit 0
fi"""
else:
vet_invocation = 'exec "${{CUE}}" vet "${{vet_args[@]+"${{vet_args[@]}}}}" {instance_arg} "${{data_files[@]}}"'

runfiles_boilerplate = """\
# Copy-pasted from the Bazel Bash runfiles library v2.
set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash
source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \\
source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \\
source "$0.runfiles/$f" 2>/dev/null || \\
source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \\
source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \\
{ echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e"""

script_content = """\
#!/usr/bin/env bash
{runfiles_boilerplate}

set -euo pipefail

# Set up a temporary CUE cache dir and clean it up on exit
cue_cache_dir="$(mktemp -d -t cue-vet-XXXXXX)"
trap "rm -rf '${{cue_cache_dir}}'" EXIT
export CUE_CACHE_DIR="${{cue_cache_dir}}"

# Resolve the CUE tool
CUE="$(rlocation {cue_tool_rlocation})"

# Resolve data files
data_files=()
{data_rlocation_lines}

# Resolve module file and cd to module root
module_file="$(rlocation {module_file_rlocation})"
module_root="$(dirname "$(dirname "${{module_file}}")")"
cd "${{module_root}}"

# Build vet args
vet_args=()
{concrete_line}
{schema_expr_lines}

{vet_invocation}
""".format(
runfiles_boilerplate = runfiles_boilerplate,
cue_tool_rlocation = _runfile_path(ctx, cue_tool),
data_rlocation_lines = data_rlocation_lines,
module_file_rlocation = module_file_rlocation,
concrete_line = 'vet_args+=({})'.format(concrete_flag) if concrete_flag else "# concrete flag not set",
schema_expr_lines = schema_expr_lines if schema_expr_lines else "# no schema expressions",
instance_arg = instance_arg,
vet_invocation = vet_invocation.format(instance_arg = instance_arg),
)

script = ctx.actions.declare_file(ctx.label.name + "_vet_test.sh")
ctx.actions.write(
output = script,
content = script_content,
is_executable = True,
)

bash_runfiles = ctx.attr._bash_runfiles[DefaultInfo].default_runfiles

return [
DefaultInfo(
executable = script,
runfiles = ctx.runfiles(
files = [cue_tool, script] + data_files,
transitive_files = instance.transitive_files,
).merge(bash_runfiles),
),
]

cue_vet_test = rule(
implementation = _cue_vet_test_impl,
test = True,
attrs = {
"instance": attr.label(
doc = """CUE instance to validate.

This value must refer either to a target using the cue_instance rule
or another rule that yields a CUEInstanceInfo provider.""",
providers = [CUEInstanceInfo],
mandatory = True,
),
"data": attr.label_list(
doc = "Data files (YAML, JSON) to validate against the CUE instance.",
mandatory = True,
allow_empty = False,
allow_files = [".yaml", ".yml", ".json"],
),
"schema_expression": attr.string_list(
doc = """CUE expressions selecting the schema(s) to validate against.

Each expression is passed as a "-d" flag to "cue vet".""",
default = [],
),
"concrete": attr.bool(
doc = "Require all values to be concrete (passes -c to cue vet).",
default = True,
),
"should_fail": attr.bool(
doc = "When True, the test passes if cue vet exits non-zero (i.e. validation fails as expected).",
default = False,
),
"_bash_runfiles": attr.label(
default = Label("@bazel_tools//tools/bash/runfiles"),
),
},
toolchains = [_cue_toolchain_type],
)
23 changes: 23 additions & 0 deletions test/testdata/vet/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
load("//cue:cue.bzl", "cue_instance", "cue_vet_test")

cue_instance(
name = "vet_test_instance",
srcs = ["schema.cue"],
package_name = "vet_test",
ancestor = "//test/testdata/vet/cue.mod:vet_test_module",
)

cue_vet_test(
name = "valid_config_test",
instance = ":vet_test_instance",
data = ["valid.yaml"],
schema_expression = ["#Config"],
)

cue_vet_test(
name = "invalid_config_test",
instance = ":vet_test_instance",
data = ["invalid.yaml"],
schema_expression = ["#Config"],
should_fail = True,
)
6 changes: 6 additions & 0 deletions test/testdata/vet/cue.mod/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
load("//cue:cue.bzl", "cue_module")

cue_module(
name = "vet_test_module",
visibility = ["//test/testdata/vet:__subpackages__"],
)
4 changes: 4 additions & 0 deletions test/testdata/vet/cue.mod/module.cue
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module: "example.com/vet_test"
language: {
version: "v0.9.0"
}
2 changes: 2 additions & 0 deletions test/testdata/vet/invalid.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
name: myservice
port: "not_a_number"
6 changes: 6 additions & 0 deletions test/testdata/vet/schema.cue
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package vet_test

#Config: {
name: string
port: int & >0 & <65536
}
2 changes: 2 additions & 0 deletions test/testdata/vet/valid.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
name: myservice
port: 8080