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
103 changes: 103 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json

name: Docker

on:
push:
branches:
- main
tags:
- v[0-9]+.*

jobs:
lint:
name: Lint Dockerfile
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Run Hadolint
uses: hadolint/hadolint-action@v3.3.0

build:
name: Build Docker image
permissions:
contents: read
packages: write
runs-on: ubuntu-latest
env:
REGISTRY: ghcr.io
IMAGE_NAME: ghcr.io/${{ github.repository }}
# Only build one base variant at a time so we can take advantage of the
# cache. I.e. only build one Alpine-based image and one Debian-based image
# at a time.
concurrency:
group: ${{ matrix.build_rust_tag }}
queue: max
steps:
- id: docker-metadata
name: Generate image names and tags
uses: docker/metadata-action@v6
with:
images: ${{ env.IMAGE_NAME }}
flavor: suffix=${{ matrix.tag_suffix }},onlatest=true
# - `type=semver,pattern={{version}}`,
# `type=semver,pattern={{major}}.{{minor}}` and
# `type=semver,pattern={{major}}`: Jobs that run on a semver-looking
# git tag will be tagged as `major.minor.patch`, `major.minor`,
# `major` and "latest". Eg. a job running on the git tag "v1.2.3"
# will be tagged as "1", "1.2", "1.2.3" and "latest". Semantic
# versions that indicate a pre-releases (eg. "v1.2.3-rc.4") will be
# tagged as the full version only (i.e. "1.2.3-rc.4").
# - `type=edge,branch=main`: Jobs that run on the "main" branch will
# be tagged with "edge".
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=edge,branch=main
- name: Login to GitHub container registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Build and push image
uses: docker/build-push-action@v7
with:
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.docker-metadata.outputs.tags }}
labels: ${{ steps.docker-metadata.outputs.labels }}
build-args: |
BUILD_RUST_TAG=${{ matrix.build_rust_tag }}
FINAL_RUST_TAG=${{ matrix.final_rust_tag }}
# Cache to and from the registry. Use image name like
# "ghcr.io/sourcefrog/cargo-mutants-cache".
cache-from: type=registry,ref=${{ env.IMAGE_NAME }}-cache
cache-to: type=registry,ref=${{ env.IMAGE_NAME }}-cache,mode=max
# - `build_rust_tag`: The Rust image tag to use for the build stage.
# - `final_rust_tag`: The Rust image tag to use for the final stage.
# - `tag_suffix`: The suffix to append to to our image tags. Eg. images that
# use "rust:latest" as the final stage will be tagged like ":1.2.3" and
# images that use "rust:slim" as the final stage will be tagged like
# ":1.2.3-slim".
# TODO: Don't tag as "latest-alpine", just tag as "alpine". I believe this
# is currently not possible using docker/metadata-action.
strategy:
matrix:
include:
- build_rust_tag: slim
final_rust_tag: latest
tag_suffix: ""
- build_rust_tag: slim
final_rust_tag: slim
tag_suffix: -slim
- build_rust_tag: alpine
final_rust_tag: alpine
tag_suffix: -alpine
93 changes: 93 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
ARG BUILD_RUST_TAG=latest
ARG FINAL_RUST_TAG=latest

# This stage sets up everything we need to cross-compile Rust programs in other
# stages.
FROM --platform=$BUILDPLATFORM "docker.io/library/rust:$BUILD_RUST_TAG" AS build

# Install platform-agnostic dependencies.
#
# Check for the existence of APK/APT to determine how to install dependencies.
# hadolint ignore=DL3008,DL3018 # We don't want to pin package versions.
RUN --mount=type=cache,target=/var/cache/apk \
--mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
( which apk && apk add clang ) || \
( \
which apt-get && \
apt-get update && \
apt-get --no-install-recommends --assume-yes install clang \
)

# Install cross-compilation helper scripts. See
# https://github.com/tonistiigi/xx#rust.
# hadolint ignore=DL3067 # We really do want to copy everything from this image.
COPY --from=docker.io/tonistiigi/xx:latest / /

# Everything from this point onwards is specific to the target platform.
ARG TARGETPLATFORM

# Install platform-specific dependencies.
#
# Check for the existence of APK/APT to determine how to install dependencies.
# hadolint ignore=DL3008,DL3018 # We don't want to pin package versions.
RUN --mount=type=cache,target=/var/cache/apk \
--mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
( which apk && xx-apk add xx-c-essentials ) || \
( \
which apt-get && \
xx-apt-get update && \
xx-apt-get --no-install-recommends --assume-yes install \
xx-c-essentials \
)

# Use the prepared build stage to build `cargo-mutants`.
FROM build AS build-mutants

# Build cargo-mutants.
#
# Cache `/var/cache/cargo` (dependencies are downloaded here) and `/tmp/target`
# (compilation artifacts are generated here). These downloads/artifacts can be
# cached between invocations of `docker build`.
#
# Use `xx-verify` to confirm that the installed binary was correctly
# cross-compiled for the target architecture.
RUN --mount=type=bind,source=src,target=src \
--mount=type=bind,source=mutants_attrs,target=mutants_attrs \
--mount=type=bind,source=Cargo.toml,target=Cargo.toml \
--mount=type=bind,source=Cargo.lock,target=Cargo.lock \
--mount=type=cache,target=/tmp/target \
--mount=type=cache,target=/var/cache/cargo \
CARGO_HOME=/var/cache/cargo \
xx-cargo install \
--path . \
--root / \
--target-dir /tmp/target \
--locked && \
xx-verify /bin/cargo-mutants

# Use the prepared build stage to build `cargo-nextest`.
FROM build AS build-nextest

# Build `cargo-nextest` in a similar fashion to `cargo-mutants` above.
RUN --mount=type=cache,target=/tmp/target \
--mount=type=cache,target=/var/cache/cargo \
CARGO_HOME=/var/cache/cargo \
xx-cargo install \
--root / \
--target-dir /tmp/target \
--locked \
cargo-nextest && \
xx-verify /bin/cargo-nextest

# Create the final image by adding `cargo-mutants` and `cargo-nextest` to the
# Rust image.
FROM "docker.io/library/rust:$FINAL_RUST_TAG" AS final

COPY --from=build-mutants /bin/cargo-mutants /usr/local/cargo/bin/
COPY --from=build-nextest /bin/cargo-nextest /usr/local/cargo/bin/

WORKDIR /app

ENTRYPOINT [ "cargo", "mutants" ]
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

- New: `#[mutants::exclude_re("pattern")]` attribute to exclude specific mutations by regex, without disabling all mutations on the function. The attribute can be placed on functions, `impl` blocks, `trait` blocks, modules, files, and on expressions that can carry an attribute (such as `match`, struct literals, call expressions, method calls, and unary expressions). Multiple patterns can be applied. Also supported within `cfg_attr`. Requires the [mutants](https://crates.io/crates/mutants) crate version `0.0.5` or later.
- Fixed: `#[mutants::skip]` (and `#[cfg_attr(..., mutants::skip)]`) is now honoured when placed on `const` and `static` items, including associated constants in `impl` and `trait` blocks. Previously the attribute was silently ignored on these items and operator mutants inside the initializer expression were still generated ([#508](https://github.com/sourcefrog/cargo-mutants/issues/508)).
- New: Build a cargo-mutants Docker image (`ghcr.io/sourcefrog/cargo-mutants`).
- Docs: Add GitLab CI/CD section, which uses the new cargo-mutants Docker image.

## 27.1.0

Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,28 @@ To generate mutants in only one file:
cargo mutants -f src/something.rs
```

## Docker

The cargo-mutants Docker image is just the official Rust Docker image with cargo-mutants and cargo-nextest preinstalled.

From within a Rust source directory, just run

```sh
docker run --rm -v .:/app ghcr.io/sourcefrog/cargo-mutants:latest
```

Available Docker tags are

* `<major>.<minor>.<patch>`, `<major>.<minor>`, `<major>`: Tracks a specific cargo-mutants release.
`latest`: Tracks the latest cargo-mutants release.
* `edge`: Tracks the main branch in the cargo-mutants repository.

A suffix can be added to each tag to specify which Rust Docker image the cargo-mutants Docker image is based on.

* No suffix: Built on the `rust:latest` Docker image. Eg. `0.27.1`, `latest`, `edge`.
* `-slim` suffix: Built on the `rust:slim` Docker image. Eg. `0.27.1-slim`, `latest-slim`, `edge-slim`.
* `-alpine` suffix: Built on the `rust:alpine` Docker image. Eg. `0.27.1-alpine`, `latest-alpine`, `edge-alpine`.

## Integration with CI

The [manual includes instructions and examples for automatically testing mutants in CI](https://mutants.rs/ci.html), including incremental testing of pull requests and full testing of the development branch.
Expand Down
31 changes: 28 additions & 3 deletions book/src/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ There are at least two complementary ways to use cargo-mutants in CI:

* Use the [`--in-place`](in-place.md) option to avoid copying the tree.

## Installing into CI
## GitHub Actions

### Installing into CI

The recommended way to install cargo-mutants is using [install-action](https://github.com/taiki-e/install-action), which will fetch a binary from cargo-mutants most recent GitHub release, which is faster than building from source. You could alternatively use [baptiste0928/cargo-install](https://github.com/baptiste0928/cargo-install) which will build it from source in your worker and cache the result.

## Example workflow
### Example workflow

Here is an example of a GitHub Actions workflow that runs mutation tests and uploads the results as an artifact. This will fail if it finds any uncaught mutants.

Expand All @@ -29,8 +31,31 @@ The recommended way to install cargo-mutants is using [install-action](https://g
The workflow used by cargo-mutants on itself can be seen at
<https://github.com/sourcefrog/cargo-mutants/blob/main/.github/workflows/tests.yml>, but this is different from what you will typically want to use, because it runs cargo-mutants from HEAD.

## Annotations
### Annotations

cargo-mutants will emit GitHub Actions structured annotations when it detects that it's running within an action. (Specifically, when `$GITHUB_ACTION` is set.)

This behavior can be forced on with the `--annotations=github` option, or off with `--annotations=none`.

## GitLab CI/CD

In GitLab CI/CD, the cargo-mutants Docker image may be used. Here is an example job.

```yaml
cargo-mutants:
image:
# Use the cargo-mutants Docker image.
name: ghcr.io/sourcefrog/cargo-mutants:latest
# The default entrypoint of this image is ["cargo", "mutants"]. However, the
# GitLab runner needs a shell so it can execute commands, so we must
# override the entrypoint. See
# https://docs.gitlab.com/ci/docker/using_docker_images/#override-the-entrypoint-of-an-image
entrypoint: [""]
script:
# Run cargo-mutants.
#
# - Specify "--in-place" to avoid copying the source tree.
# - Specify "--caught" and "--unviable" so we can see every mutant in the CI
# log, not just the mutants we failed to catch.
- cargo mutants --in-place --caught --unviable
```