diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d4154172..f2b83e52 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -38,6 +38,9 @@ jobs: support-version: "0.3.0" assert-path: /usr/lib/bats/bats-assert assert-version: "2.1.0" - - name: Integration test + - name: "Integration test: example" working-directory: example run: bats ./test + - name: "Integration test: format" + working-directory: format + run: bats ./test diff --git a/example/src/hello.css b/example/src/hello.css new file mode 100644 index 00000000..d41f6f2c --- /dev/null +++ b/example/src/hello.css @@ -0,0 +1,2 @@ +.h1 { +} diff --git a/example/src/index.html b/example/src/index.html new file mode 100644 index 00000000..69e9da41 --- /dev/null +++ b/example/src/index.html @@ -0,0 +1,2 @@ + + diff --git a/format/private/format.sh b/format/private/format.sh index 4d40c3fc..d020a2f9 100755 --- a/format/private/format.sh +++ b/format/private/format.sh @@ -140,6 +140,13 @@ if [ -n "$files" ] && [ -n "$bin" ]; then echo "$files" | tr \\n \\0 | xargs -0 $bin $prettiermode fi +files=$(ls-files JSON $@) +bin=$(rlocation {{prettier}}) +if [ -n "$files" ] && [ -n "$bin" ]; then + echo "Formatting JSON with Prettier..." + echo "$files" | tr \\n \\0 | xargs -0 $bin $prettiermode +fi + files=$(ls-files JavaScript $@) bin=$(rlocation {{prettier}}) if [ -n "$files" ] && [ -n "$bin" ]; then @@ -178,14 +185,14 @@ fi files=$(ls-files SQL $@) bin=$(rlocation {{prettier-sql}}) if [ -n "$files" ] && [ -n "$bin" ]; then - echo "Running SQL with Prettier..." + echo "Formatting SQL with Prettier..." echo "$files" | tr \\n \\0 | xargs -0 $bin $prettiermode fi files=$(ls-files Python $@) bin=$(rlocation {{ruff}}) if [ -n "$files" ] && [ -n "$bin" ]; then - echo "Formatting Python with ruff..." + echo "Formatting Python with Ruff..." echo "$files" | tr \\n \\0 | xargs -0 $bin $ruffmode fi diff --git a/format/private/formatter_binary.bzl b/format/private/formatter_binary.bzl index e691c2fe..ab55c2ea 100644 --- a/format/private/formatter_binary.bzl +++ b/format/private/formatter_binary.bzl @@ -3,7 +3,7 @@ load("@aspect_bazel_lib//lib:paths.bzl", "BASH_RLOCATION_FUNCTION", "to_rlocation_path") # Per the formatter design, each language can only have a single formatter binary -_TOOLS = { +TOOLS = { "javascript": "prettier", "markdown": "prettier-md", "python": "ruff", @@ -22,17 +22,23 @@ _TOOLS = { } def _formatter_binary_impl(ctx): - # We need to fill in the rlocation paths in the shell script - substitutions = { - "{{BASH_RLOCATION_FUNCTION}}": BASH_RLOCATION_FUNCTION, - "{{fix_target}}": str(ctx.label), - } - tools = {v: getattr(ctx.attr, k) for k, v in _TOOLS.items()} + substitutions = {} + tools = {v: getattr(ctx.attr, k) for k, v in TOOLS.items()} for tool, attr in tools.items(): if attr: substitutions["{{%s}}" % tool] = to_rlocation_path(ctx, attr.files_to_run.executable) + if len(substitutions) == 0: + fail("multi_formatter_binary should have at least one language attribute set to a formatter tool") + + substitutions.update({ + # We need to fill in the rlocation paths in the shell script + "{{BASH_RLOCATION_FUNCTION}}": BASH_RLOCATION_FUNCTION, + # Support helpful error reporting + "{{fix_target}}": str(ctx.label), + }) - bin = ctx.actions.declare_file("format.sh") + # Uniquely named output file allowing more than one formatter in a package + bin = ctx.actions.declare_file("_{}.fmt.sh".format(ctx.label.name)) ctx.actions.expand_template( template = ctx.file._bin, output = bin, @@ -66,7 +72,7 @@ formatter_binary_lib = struct( implementation = _formatter_binary_impl, attrs = dict({ k: attr.label(doc = "a binary target that runs {} (or another tool with compatible CLI arguments)".format(v), executable = True, cfg = "exec", allow_files = True) - for k, v in _TOOLS.items() + for k, v in TOOLS.items() }, **{ "_bin": attr.label(default = "//format/private:format.sh", allow_single_file = True), "_runfiles_lib": attr.label(default = "@bazel_tools//tools/bash/runfiles", allow_single_file = True), diff --git a/format/test/BUILD.bazel b/format/test/BUILD.bazel new file mode 100644 index 00000000..ae47525d --- /dev/null +++ b/format/test/BUILD.bazel @@ -0,0 +1,104 @@ +load("@bazel_skylib//rules:write_file.bzl", "write_file") +load("//format:defs.bzl", "multi_formatter_binary") +load("//format/private:formatter_binary.bzl", "TOOLS") + +# Avoid depending on a bunch of actual tools in the root module. +# That's the job of the example/ submodule. +# Instead, just provide "recording mock" for each formatter we support. +[ + write_file( + name = "mock_{}_sh".format(t), + out = "mock_{}.sh".format(t), + content = [ + "#!/usr/bin/env bash", + "echo + {} $*".format(t), + ], + ) + for t in TOOLS.values() +] + +[ + sh_binary( + name = "mock_" + t, + srcs = ["mock_{}.sh".format(t)], + ) + for t in TOOLS.values() +] + +# Make a separate formatter binary to test each language in isolation. +# Users should NOT do it this way! +multi_formatter_binary( + name = "format_javascript", + javascript = ":mock_prettier.sh", +) + +multi_formatter_binary( + name = "format_starlark", + starlark = ":mock_buildifier.sh", +) + +multi_formatter_binary( + name = "format_markdown", + markdown = ":mock_prettier.sh", +) + +multi_formatter_binary( + name = "format_sql", + sql = ":mock_prettier.sh", +) + +multi_formatter_binary( + name = "format_python", + python = ":mock_ruff.sh", +) + +multi_formatter_binary( + name = "format_hcl", + # TODO: this attribute should be renamed to hcl + terraform = ":mock_terraform-fmt.sh", +) + +multi_formatter_binary( + name = "format_jsonnet", + jsonnet = ":mock_jsonnetfmt.sh", +) + +multi_formatter_binary( + name = "format_java", + java = ":mock_java-format.sh", +) + +multi_formatter_binary( + name = "format_kotlin", + kotlin = ":mock_ktfmt.sh", +) + +multi_formatter_binary( + name = "format_scala", + scala = ":mock_scalafmt.sh", +) + +multi_formatter_binary( + name = "format_go", + go = ":mock_gofmt.sh", +) + +multi_formatter_binary( + name = "format_cc", + cc = ":mock_clang-format.sh", +) + +multi_formatter_binary( + name = "format_sh", + sh = ":mock_shfmt.sh", +) + +multi_formatter_binary( + name = "format_swift", + swift = ":mock_swiftformat.sh", +) + +multi_formatter_binary( + name = "format_protobuf", + protobuf = ":mock_buf.sh", +) diff --git a/format/test/format_test.bats b/format/test/format_test.bats new file mode 100644 index 00000000..ba8a256f --- /dev/null +++ b/format/test/format_test.bats @@ -0,0 +1,149 @@ +# Simple test fixture that uses this "real" git repository. +# Ideally we would create self-contained "system under test" for each test case. +# That would let us test more scenarios with git, like deleted files. +bats_load_library "bats-support" +bats_load_library "bats-assert" + +# No arguments: will use git ls-files +@test "should run prettier on javascript using git ls-files" { + run bazel run //format/test:format_javascript + assert_success + + assert_output --partial "Formatting JavaScript with Prettier..." + assert_output --partial "+ prettier --write example/.eslintrc.cjs" + assert_output --partial "Formatting TypeScript with Prettier..." + assert_output --partial "+ prettier --write example/src/file.ts example/test/no_violations.ts" + assert_output --partial "Formatting TSX with Prettier..." + assert_output --partial "+ prettier --write example/src/hello.tsx" + assert_output --partial "Formatting JSON with Prettier..." + assert_output --partial "+ prettier --write renovate.json" + assert_output --partial "Formatting CSS with Prettier..." + assert_output --partial "+ prettier --write example/src/hello.css" + assert_output --partial "Formatting HTML with Prettier..." + assert_output --partial "+ prettier --write example/src/index.html" +} + +# File arguments: will filter with find +@test "should run prettier on javascript using find" { + run bazel run //format/test:format_javascript README.md example/.eslintrc.cjs + assert_success + + assert_output --partial "Formatting JavaScript with Prettier..." + refute_output --partial "Formatting TypeScript with Prettier..." +} + +@test "should run buildozer on starlark" { + run bazel run //format/test:format_starlark + assert_success + + assert_output --partial "Formatting Starlark with Buildifier..." + assert_output --partial "+ buildifier -mode=fix BUILD.bazel" + # FIXME(#122): this was broken by #105 + # assert_output --partial "format/private/BUILD.bazel" +} + +@test "should run prettier on Markdown" { + run bazel run //format/test:format_markdown + assert_success + + assert_output --partial "Formatting Markdown with Prettier..." + assert_output --partial "+ prettier --write CONTRIBUTING.md README.md" +} + +@test "should run prettier on SQL" { + run bazel run //format/test:format_sql + assert_success + + assert_output --partial "Formatting SQL with Prettier..." + assert_output --partial "+ prettier --write example/src/hello.sql" +} + +@test "should run ruff on Python" { + run bazel run //format/test:format_python + assert_success + + assert_output --partial "Formatting Python with Ruff..." + assert_output --partial "+ ruff format --force-exclude example/src/subdir/unused_import.py" +} + +@test "should run terraform fmt on HCL" { + run bazel run //format/test:format_hcl + assert_success + + assert_output --partial "Formatting Hashicorp Config Language with terraform fmt..." + assert_output --partial "+ terraform-fmt fmt example/src/hello.tf" +} + +@test "should run jsonnet-fmt on Jsonnet" { + run bazel run //format/test:format_jsonnet + assert_success + + assert_output --partial "Formatting Jsonnet with jsonnetfmt..." + assert_output --partial "+ jsonnetfmt --in-place example/src/hello.jsonnet example/src/hello.libsonnet" +} + +@test "should run java-format on Java" { + run bazel run //format/test:format_java + assert_success + + assert_output --partial "Formatting Java with java-format..." + assert_output --partial "+ java-format --replace example/src/Foo.java" +} + +@test "should run ktfmt on Kotlin" { + run bazel run //format/test:format_kotlin + assert_success + + assert_output --partial "Formatting Kotlin with ktfmt..." + assert_output --partial "+ ktfmt example/src/hello.kt" +} + +@test "should run scalafmt on Scala" { + run bazel run //format/test:format_scala + assert_success + + assert_output --partial "Formatting Scala with scalafmt..." + assert_output --partial "+ scalafmt example/src/hello.scala" +} + +@test "should run gofmt on Go" { + run bazel run //format/test:format_go + assert_success + + assert_output --partial "Formatting Go with gofmt..." + assert_output --partial "+ gofmt -w example/src/hello.go" +} + +@test "should run clang-format on C++" { + run bazel run //format/test:format_cc + assert_success + + assert_output --partial "Formatting C/C++ with clang-format..." + assert_output --partial "+ clang-format -style=file --fallback-style=none -i example/src/hello.cpp" +} + +@test "should run shfmt on Shell" { + run bazel run //format/test:format_sh + assert_success + + assert_output --partial "Formatting Shell with shfmt..." + assert_output --partial "+ shfmt -w .github/workflows/release_prep.sh" +} + +@test "should run swiftformat on Swift" { + run bazel run //format/test:format_swift + assert_success + + # The real swiftformat prints the "Formatting..." output so we don't + assert_output --partial "+ swiftformat example/src/hello.swift" +} + +@test "should run buf on Protobuf" { + run bazel run //format/test:format_protobuf + assert_success + + assert_output --partial "Formatting Protobuf with buf..." + # Buf only formats one file at a time + assert_output --partial "+ buf format -w example/src/file.proto" + assert_output --partial "+ buf format -w example/src/unused.proto" +}