Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: C/C++ support (cc_build_error), introducing matcher, etc #1

Merged
merged 14 commits into from
Apr 7, 2024
Merged
1 change: 1 addition & 0 deletions .bazelignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.cache
2 changes: 2 additions & 0 deletions .bazeliskrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
BAZELISK_HOME=.cache/bazelisk
USE_BAZEL_VERSION=7.1.1
16 changes: 16 additions & 0 deletions .bazelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Set the cache location to local
startup --output_user_root=.cache/bazel

# Use bzlmod
common --enable_bzlmod

# For less error-prone testing
build --sandbox_default_allow_network=false
build --incompatible_strict_action_env
build --action_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1
build --incompatible_enable_cc_toolchain_resolution
test --test_verbose_timeout_warnings

# For testing convenience
common --heap_dump_on_oom
test --test_output=errors
2 changes: 2 additions & 0 deletions .cache/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!.gitignore
29 changes: 29 additions & 0 deletions .github/workflows/unit-tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
name: Run unit tests
on:
push:
branches:
- main
pull_request:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
- name: Install bazelisk
run: |
bazelisk_dir="$(realpath "$(mktemp -d -p .)")"
wget https://github.com/bazelbuild/bazelisk/releases/download/v1.19.0/bazelisk-linux-amd64 \
-O "${bazelisk_dir}/bazelisk"
chmod +x "${bazelisk_dir}/bazelisk"
echo "${bazelisk_dir}" >> "${GITHUB_PATH}"
- name: Run unit tests
run: ./execute_tests.sh
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Bazel
/bazel-*
MODULE.bazel.lock
Empty file added BUILD.bazel
Empty file.
16 changes: 16 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Module for `rules_build_error`.
"""

module(name = "rules_build_error", version = "0.1.0")

bazel_dep(name = "bazel_skylib", version = "1.5.0", dev_dependency = True)

bazel_dep(name = "toolchains_llvm", version = "1.0.0", dev_dependency = True)

llvm = use_extension("@toolchains_llvm//toolchain/extensions:llvm.bzl", "llvm")
llvm.toolchain(
llvm_version = "17.0.6",
)

use_repo(llvm, "llvm_toolchain")
register_toolchains("@llvm_toolchain//:all")
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# `rules_build_error`

Bazel implementations to test a build error.

## Description

There's a situation where a developer wants to test if particular code doesn't compile. However, when using ordinary testing rules, such as `cc_test`, `bazel test` results in an error if the test code doesn't compile.

`rules_build_error` is the repository to address such a problem. It provides some implementations to test the compilation error for each programming language. When the code written in a particular **does** compile, `bazel build` should fail for the associated target.

## Usage

### C/C++ usage

```bazel
load("@rules_build_error//lang/cc:defs.bzl", "cc_build_error")
load("@rules_build_error//matcher:defs.bzl", "matcher")

cc_build_error(
name = "cause_compile_error",
src = "cause_compile_error.cpp",
deps = [":library_to_successfully_link"], # `:library_to_successfully_link` must provide `CcInfo`, like `cc_library`
compile_stderr = matcher.has_substr("static assertion failed"),
)
```

## Language-specific implementations

The implementations to check the build error in a particular language is available.

### C/C++ implementation

Refer to [its readme](lang/cc/README.md)

## Matcher

In order to specify how to validate the error message, a struct `matcher` is available. Refer to [its readme](matcher/README.md) for more details.

## Development

### How to test

Execute [`execute_tests.sh`](execute_tests.sh) after installing [`bazelisk`](https://github.com/bazelbuild/bazelisk). It executes `bazelisk test` and `bazelisk build` commands under the hood.

When writing tests, in principle, use `tags = ["manual"]` if a test case target must fail with `bazelisk test`. In such a test case, confirm its failure in [`execute_tests.sh`](execute_tests.sh) one by one.
16 changes: 16 additions & 0 deletions build_script/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Scripts commonly used when validating build errors.
"""

package(
default_visibility = ["//lang:__subpackages__"],
)

filegroup(
name = "conditionally_execute",
srcs = ["conditionally_execute.bash"],
)

filegroup(
name = "check_emptiness",
srcs = ["check_emptiness.bash"],
)
75 changes: 75 additions & 0 deletions build_script/check_emptiness.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env bash

set -euo pipefail

usage() {
cat <<EOS >&2
Check if any of the files are empty.

Usage: $0 [OPTIONS]

OPTIONS
-f FILE_TO_CHECK
Successfully exit if the file is empty
-h
Show usage and exit
-m MESSAGE
Error message when no files are empty
-n NEW_FILE_PATH
If specified, create a new empty file
EOS
}

exit_if_empty_file() {
# Exit if the argument is a empty text file
#
# Args:
# $1: file path
local file_path
file_path=$1

if [[ ! -f "${file_path}" ]]; then
echo "ERROR: ${file_path} does not exist" >&2
exit 1
fi
if [[ ! -s "${file_path}" ]]; then
# File is empty: exit successfully
exit 0
fi
}

files_to_check=()
files_to_touch=()
error_message="ERROR: No files are empty"

while getopts "f:hm:n:" opt; do
case "${opt}" in
f)
files_to_check+=("${OPTARG}")
;;
h)
usage
exit 0
;;
m)
error_message="${OPTARG}"
;;
n)
files_to_touch+=("${OPTARG}")
;;
esac
done
shift $((OPTIND -1))

# Make sure the required files are touched before exiting
if [[ "${#files_to_touch[@]}" -gt 0 ]]; then
trap 'touch "${files_to_touch[@]}"' EXIT
fi

for file_to_check in "${files_to_check[@]}"; do
exit_if_empty_file "${file_to_check}"
done

# Exit with error if there's no empty file
echo "${error_message}" >&2
exit 1
103 changes: 103 additions & 0 deletions build_script/conditionally_execute.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/usr/bin/env bash

set -euo pipefail

usage() {
cat <<EOS >&2
Wrapper to conditionally execute the command.

With the option '-f', this script can receive arbitrary number of files.
If any of them is an empty file, this script exits successfully
without executing the commands.

Usage: $0 [OPTIONS] COMMAND

OPTIONS
-i
Ignore error when executing the command.
-f FILE_TO_CHECK
Successfully exit if the file is empty
-h
Show usage and exit
-e STDOUT_FILE
A file path to write stderr message
-o STDOUT_FILE
A file path to write stdout message
-m MESSAGE
Message when the command fails
-n NEW_FILE_PATH
If specified, create a new empty file before executing the command

COMMAND
Executed if there's no empty file given with '-f'.
EOS
}

exit_if_empty_file() {
# Exit if the argument is a empty text file
#
# Args:
# $1: file path
local file_path
file_path=$1

if [[ ! -f "${file_path}" ]]; then
echo "ERROR: ${file_path} does not exist" >&2
exit 1
fi
if [[ ! -s "${file_path}" ]]; then
# File is empty: exit successfully
exit 0
fi
}

files_to_check=()
files_to_touch=()
ignore_error="false"
error_message="ERROR: execution failed"

while getopts "e:f:him:n:o:" opt; do
case "${opt}" in
e)
stderr_file="${OPTARG}"
;;
f)
files_to_check+=("${OPTARG}")
;;
h)
usage
exit 0
;;
i)
ignore_error="true"
;;
m)
error_message="${OPTARG}"
;;
n)
files_to_touch+=("${OPTARG}")
;;
o)
stdout_file="${OPTARG}"
;;
esac
done
shift $((OPTIND -1))

# Make sure the required files are touched before exiting
if [[ "${#files_to_touch[@]}" -gt 0 ]]; then
trap 'touch "${files_to_touch[@]}"' EXIT
fi

for file_to_check in "${files_to_check[@]}"; do
exit_if_empty_file "${file_to_check}"
done

if [[ "${ignore_error}" == "true" ]]; then
"$@" >"${stdout_file:-"/dev/null"}" 2>"${stderr_file:-"/dev/null"}" || true
else
if ! "$@" >"${stdout_file:-"/dev/null"}" 2>"${stderr_file:-"/dev/null"}" ; then
echo "${error_message}"
exit 1
fi
fi
38 changes: 38 additions & 0 deletions execute_tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
#
# Execute all tests

set -euo pipefail

check_bazel_build_error() {
# Check bazel build error for a particular target
#
# Args:
# $1: label to check
local label
label=$1

# Before executing `bazelisk build`, check if the target exists with `bazelisk query`
bazelisk query "${label}"

# Check build error
if bazelisk build "${label}"; then
echo "Target '${label}' must fail to build, but succeeded" >&2
exit 1
else
echo "OK! It has failed as intended."
fi
}

echo "Executing the test cases which should pass straightforward 'bazelisk test'"
bazelisk test //...

echo "Executing the test cases which should fail at 'bazelisk build'"
check_bazel_build_error //tests/cc/cpp_successful_build:plain
check_bazel_build_error //tests/cc/cpp_successful_build:with_basic_regex_matcher
check_bazel_build_error //tests/cc/cpp_successful_build:with_extended_regex_matcher
check_bazel_build_error //tests/cc/cpp_successful_build:with_substr_matcher
check_bazel_build_error //tests/cc/cpp_successful_build_with_deps:plain
check_bazel_build_error //tests/cc/cpp_successful_build_with_deps:with_basic_regex_matcher
check_bazel_build_error //tests/cc/cpp_successful_build_with_deps:with_extended_regex_matcher
check_bazel_build_error //tests/cc/cpp_successful_build_with_deps:with_substr_matcher
Empty file added lang/BUILD.bazel
Empty file.
Empty file added lang/cc/BUILD.bazel
Empty file.
27 changes: 27 additions & 0 deletions lang/cc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# C/C++ build error

Defines some implementations to check build error in C/C++.

## `cc_build_error`

`cc_build_error` is a wrapper macro for rules, providing [`CcBuildErrorInfo`](#ccbuilderrorinfo).

In addition to the common rule attributes listed [here](https://bazel.build/reference/be/common-definitions#common-attributes), it can receive the following attributes (regarding the specific matcher, please refer to [its readme](../../matcher/README.md)):

| Attribute | Description | Type | Is this attribute required? | Other constraints |
| ------------------------ | ------------------------------------------------------------------ | ---------------- | --------------------------- | --------------------------------------------------- |
| name | Name of the target. | str | Yes | |
| src | C/C++ source file to check build | label | Yes | Must be a single file having an extension for C/C++ |
| additional_linker_inputs | Pass these files to the linker command | list of labels | No (defaults to `[]`) | |
| copts | C/C++ compilation options | list of str | No (defaults to `[]`) | |
| deps | The list of CcInfo libraries to be linked in to the binary target. | list of label | No (defaults to `[]`) | Each list element must provide `CcInfo` |
| linkopts | C/C++ linking options | list of str | No (defaults to `[]`) | |
| local_defines | Pre-processor macro definitions | list of str | No (defaults to `[]`) | |
| compile_stderr | Matcher for the stderr message while compiling | specific matcher | No (defaults to no-op) | |
| compile_stdout | Matcher for the stdout message while compiling | specific matcher | No (defaults to no-op) | |
| link_stderr | Matcher for the stderr message while compiling | specific matcher | No (defaults to no-op) | |
| link_stdout | Matcher for the stdout message while compiling | specific matcher | No (defaults to no-op) | |

## `CcBuildErrorInfo`

`CcBuildErrorInfo` is a provider describing the build error in C/C++. See its definition in [its bzl file](./build_error.bzl) for its details.
Loading