├── .cargo └── config.toml ├── .dockerignore ├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ └── pr.yml ├── .gitignore ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── README.md ├── assets ├── favicon.ico ├── report.css ├── report.js └── ui.css ├── build.rs ├── config.toml ├── docs ├── agent-http-api.md ├── agent-machine-setup-windows.md ├── agent-machine-setup.md ├── bot-usage.md ├── cli-usage.md ├── images │ └── pr-try-commit.png ├── legacy-workflow.md └── report-triage.md ├── find-bad-crates.py ├── local-crates ├── README.md ├── beta-faulty-deps │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── beta-fixed │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ └── main.rs ├── beta-regression │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ └── main.rs ├── broken-cargotoml │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── build-fail │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── build-pass │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── clippy-warn │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── docs-rs-features │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── error-code │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ └── lib.rs ├── faulty-deps │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── ice-regression │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ └── main.rs ├── memory-hungry │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ ├── allocate.rs │ │ └── main.rs ├── missing-examples │ ├── Cargo.toml │ ├── examples │ │ └── foo.rs │ ├── src │ │ └── main.rs │ └── tests │ │ └── test1.rs ├── network-access │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ ├── main.rs │ │ └── network.rs ├── outdated-lockfile │ ├── Cargo.lock │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── test-fail │ ├── Cargo.toml │ └── src │ │ └── main.rs └── yanked-deps │ ├── Cargo.toml │ └── src │ └── main.rs ├── rust-toolchain ├── rustfmt.toml ├── src ├── actions │ ├── experiments │ │ ├── create.rs │ │ ├── delete.rs │ │ ├── edit.rs │ │ └── mod.rs │ ├── lists │ │ ├── mod.rs │ │ └── update.rs │ └── mod.rs ├── agent │ ├── api.rs │ └── mod.rs ├── assets.rs ├── bin │ └── test-report.rs ├── cli.rs ├── config.rs ├── crates │ ├── lists.rs │ ├── mod.rs │ └── sources │ │ ├── github.rs │ │ ├── local.rs │ │ ├── mod.rs │ │ └── registry.rs ├── db │ ├── migrations.rs │ └── mod.rs ├── dirs.rs ├── experiments.rs ├── lib.rs ├── main.rs ├── prelude.rs ├── report │ ├── analyzer.rs │ ├── archives.rs │ ├── display.rs │ ├── html.rs │ ├── markdown.rs │ ├── mod.rs │ └── s3.rs ├── results │ ├── db.rs │ ├── dummy.rs │ └── mod.rs ├── runner │ ├── mod.rs │ ├── tasks.rs │ ├── test.rs │ ├── unstable_features.rs │ └── worker.rs ├── server │ ├── agents.rs │ ├── api_types.rs │ ├── auth.rs │ ├── cronjobs.rs │ ├── github.rs │ ├── messages.rs │ ├── metrics.rs │ ├── mod.rs │ ├── reports.rs │ ├── routes │ │ ├── agent.rs │ │ ├── metrics.rs │ │ ├── mod.rs │ │ ├── ui │ │ │ ├── agents.rs │ │ │ ├── experiments.rs │ │ │ └── mod.rs │ │ └── webhooks │ │ │ ├── args.rs │ │ │ ├── commands.rs │ │ │ └── mod.rs │ ├── tokens.rs │ └── try_builds.rs ├── toolchain.rs └── utils │ ├── disk_usage.rs │ ├── hex.rs │ ├── http.rs │ ├── macros.rs │ ├── mod.rs │ ├── path.rs │ ├── serialize.rs │ ├── size.rs │ └── string.rs ├── templates ├── macros.html ├── report │ ├── downloads.html │ ├── layout.html │ └── results.html └── ui │ ├── 404.html │ ├── 500.html │ ├── agents.html │ ├── experiment.html │ ├── layout.html │ └── queue.html ├── tests ├── check_config │ ├── bad-duplicate-crate.toml │ ├── bad-duplicate-repo.toml │ ├── bad-missing-crate.toml │ ├── bad-missing-repo.toml │ ├── good.toml │ └── mod.rs ├── common │ ├── cli_utils.rs │ └── mod.rs ├── integration.rs └── minicrater │ ├── README.md │ ├── blacklist │ ├── config.toml │ ├── downloads.html.context.expected.json │ ├── full.html.context.expected.json │ ├── index.html.context.expected.json │ ├── markdown.md.context.expected.json │ └── results.expected.json │ ├── clippy │ ├── config.toml │ ├── downloads.html.context.expected.json │ ├── full.html.context.expected.json │ ├── index.html.context.expected.json │ ├── markdown.md.context.expected.json │ └── results.expected.json │ ├── doc │ ├── config.toml │ ├── downloads.html.context.expected.json │ ├── full.html.context.expected.json │ ├── index.html.context.expected.json │ ├── markdown.md.context.expected.json │ └── results.expected.json │ ├── driver.rs │ ├── full │ ├── config.toml │ ├── downloads.html.context.expected.json │ ├── full.html.context.expected.json │ ├── index.html.context.expected.json │ ├── markdown.md.context.expected.json │ └── results.expected.json │ ├── ignore-blacklist │ ├── config.toml │ ├── downloads.html.context.expected.json │ ├── full.html.context.expected.json │ ├── index.html.context.expected.json │ ├── markdown.md.context.expected.json │ └── results.expected.json │ ├── missing-repo │ ├── config.toml │ ├── downloads.html.context.expected.json │ ├── full.html.context.expected.json │ ├── index.html.context.expected.json │ ├── markdown.md.context.expected.json │ └── results.expected.json │ ├── mod.rs │ ├── resource-exhaustion │ ├── config.toml │ ├── downloads.html.context.expected.json │ ├── full.html.context.expected.json │ ├── index.html.context.expected.json │ ├── markdown.md.context.expected.json │ └── results.expected.json │ └── small │ ├── config.toml │ ├── downloads.html.context.expected.json │ ├── full.html.context.expected.json │ ├── index.html.context.expected.json │ ├── markdown.md.context.expected.json │ └── results.expected.json ├── todo.md └── tokens.example.toml /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = [ 3 | "-Cforce-frame-pointers", 4 | "-Csymbol-mangling-version=v0" 5 | ] 6 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /work 2 | /target 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.rs text eol=lf 2 | *.lock text eol=lf 3 | *.txt text eol=lf 4 | *.toml text eol=lf 5 | *.md text eol=lf 6 | *.gitignore text eol=lf 7 | *.gitattributes text eol=lf 8 | *.json text eol=lf 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | merge_group: 4 | 5 | jobs: 6 | lint: 7 | name: Linting 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - name: Install Rust nightly 13 | run: rustup update nightly && rustup default nightly && rustup component add rustfmt clippy 14 | 15 | - name: Check the code formatting with rustfmt 16 | run: cargo fmt --all -- --check 17 | 18 | - name: Ensure there are no warnings with Clippy 19 | run: cargo clippy --all -- -Dwarnings 20 | 21 | - name: Check if the configuration is correct 22 | run: | 23 | cargo run -- create-lists 24 | cargo run -- check-config 25 | 26 | test: 27 | name: Testing 28 | strategy: 29 | matrix: 30 | os: [ubuntu-latest, windows-latest] 31 | channel: [nightly] 32 | runs-on: ${{ matrix.os }} 33 | steps: 34 | - uses: actions/checkout@v4 35 | 36 | - name: Install Rust ${{ matrix.channel }} 37 | shell: bash 38 | run: rustup update --no-self-update $CHANNEL && rustup default $CHANNEL 39 | env: 40 | CHANNEL: ${{ matrix.channel }} 41 | 42 | - name: Build Crater 43 | shell: bash 44 | run: cargo build 45 | 46 | - name: Run Crater tests 47 | shell: bash 48 | run: | 49 | cargo run -- create-lists 50 | cargo test 51 | 52 | minicrater: 53 | name: Minicrater 54 | strategy: 55 | matrix: 56 | os: [ubuntu-latest] 57 | runs-on: ${{ matrix.os }} 58 | steps: 59 | - uses: actions/checkout@v4 60 | 61 | - name: Install Rust nightly 62 | run: rustup update --no-self-update nightly && rustup default nightly 63 | 64 | - name: Run minicrater 65 | shell: bash 66 | run: | 67 | cargo run -- create-lists 68 | cargo test -- --ignored --nocapture --test-threads 1 69 | env: 70 | MINICRATER_FAST_WORKSPACE_INIT: 1 71 | MINICRATER_SHOW_OUTPUT: 1 72 | 73 | docker-build: 74 | name: Build the Docker image 75 | runs-on: ubuntu-latest 76 | steps: 77 | - uses: actions/checkout@v4 78 | 79 | - name: Build the Docker image 80 | run: docker build -t crater . 81 | 82 | - name: Prepare the Docker image to be uploaded 83 | run: | 84 | mkdir -p /tmp/docker-images 85 | docker save crater | gzip > /tmp/docker-images/crater.tar.gz 86 | 87 | - name: Upload the image to GitHub Actions artifacts 88 | uses: actions/upload-artifact@v4 89 | with: 90 | name: docker-images 91 | path: /tmp/docker-images 92 | 93 | docker-upload: 94 | name: Upload the Docker images to ECR 95 | runs-on: ubuntu-latest 96 | needs: 97 | - lint 98 | - test 99 | - minicrater 100 | - docker-build 101 | 102 | steps: 103 | - name: Download the image from GitHub Actions artifacts 104 | uses: actions/download-artifact@v4 105 | with: 106 | name: docker-images 107 | path: docker-images 108 | 109 | - name: Load the downloaded image 110 | run: cat docker-images/crater.tar.gz | gunzip | docker load 111 | 112 | - name: Upload the Docker image to ECR 113 | uses: rust-lang/simpleinfra/github-actions/upload-docker-image@master 114 | with: 115 | image: crater 116 | repository: crater 117 | region: us-west-1 118 | aws_access_key_id: "${{ secrets.aws_access_key_id }}" 119 | aws_secret_access_key: "${{ secrets.aws_secret_access_key }}" 120 | 121 | conclusion: 122 | needs: [docker-upload] 123 | # We need to ensure this job does *not* get skipped if its dependencies fail, 124 | # because a skipped job is considered a success by GitHub. So we have to 125 | # overwrite `if:`. We use `!cancelled()` to ensure the job does still not get run 126 | # when the workflow is canceled manually. 127 | # 128 | # ALL THE PREVIOUS JOBS NEED TO BE ADDED TO THE `needs` SECTION OF THIS JOB! 129 | if: ${{ !cancelled() }} 130 | runs-on: ubuntu-latest 131 | steps: 132 | # Manually check the status of all dependencies. `if: failure()` does not work. 133 | - name: Conclusion 134 | run: | 135 | # Print the dependent jobs to see them in the CI log 136 | jq -C <<< '${{ toJson(needs) }}' 137 | # Check if all jobs that we depend on (in the needs array) were successful. 138 | jq --exit-status 'all(.result == "success")' <<< '${{ toJson(needs) }}' 139 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR build 2 | on: [pull_request] 3 | 4 | jobs: 5 | lint: 6 | name: Linting 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | 11 | - name: Install Rust nightly 12 | run: rustup update nightly && rustup default nightly && rustup component add rustfmt clippy 13 | 14 | - name: Check the code formatting with rustfmt 15 | run: cargo fmt --all -- --check 16 | 17 | - name: Ensure there are no warnings with Clippy 18 | run: cargo clippy --all -- -Dwarnings 19 | 20 | - name: Check if the configuration is correct 21 | run: | 22 | cargo run -- create-lists 23 | cargo run -- check-config 24 | 25 | test: 26 | name: Linux testing 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | 31 | - name: Install Rust nightly 32 | run: rustup update nightly && rustup default nightly 33 | 34 | - name: Build Crater 35 | run: cargo build 36 | 37 | - name: Run Crater tests 38 | run: | 39 | cargo run -- create-lists 40 | cargo test 41 | 42 | # Note: this job is used so that there is a job named "conclusion" both in the PR and merge queue 43 | # CI. Otherwise, it would not be possible for PR CI to succeed. 44 | conclusion: 45 | needs: [lint, test] 46 | # We need to ensure this job does *not* get skipped if its dependencies fail, 47 | # because a skipped job is considered a success by GitHub. So we have to 48 | # overwrite `if:`. We use `!cancelled()` to ensure the job does still not get run 49 | # when the workflow is canceled manually. 50 | # 51 | # ALL THE PREVIOUS JOBS NEED TO BE ADDED TO THE `needs` SECTION OF THIS JOB! 52 | if: ${{ !cancelled() }} 53 | runs-on: ubuntu-latest 54 | steps: 55 | # Manually check the status of all dependencies. `if: failure()` does not work. 56 | - name: Conclusion 57 | run: | 58 | # Print the dependent jobs to see them in the CI log 59 | jq -C <<< '${{ toJson(needs) }}' 60 | # Check if all jobs that we depend on (in the needs array) were successful. 61 | jq --exit-status 'all(.result == "success")' <<< '${{ toJson(needs) }}' 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *~ 3 | work/ 4 | tokens.toml 5 | 6 | /tests/minicrater/*/*.actual.json 7 | /local-crates/*/target 8 | /local-crates/*/Cargo.lock 9 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crater" 3 | version = "0.1.0" 4 | edition = "2021" 5 | build = "build.rs" 6 | default-run = "crater" 7 | 8 | [profile.dev] 9 | opt-level = 0 10 | 11 | [profile.release] 12 | strip = false 13 | 14 | [dependencies] 15 | anyhow = "1.0.95" 16 | aws-config = { version = "1", features = ["behavior-version-latest"] } 17 | aws-sdk-s3 = "1.7" 18 | base64 = "0.21.5" 19 | bytes = "1" 20 | cargo_metadata = "0.18.1" 21 | cargo-util-schemas = "0.7.1" 22 | chrono = { version = "0.4", features = ["serde"] } 23 | clap = { version = "4", features = ["derive"] } 24 | crates-index = { version = "2.2.0", default-features = false, features = [ 25 | "git-performance", 26 | "git-https", 27 | "parallel", 28 | ] } 29 | crossbeam-channel = "0.5" 30 | csv = "1.0.2" 31 | ctrlc = "3.1.3" 32 | docsrs-metadata = { git = "https://github.com/rust-lang/docs.rs/" } 33 | dotenv = "0.15" 34 | env_logger = "0.10.0" 35 | flate2 = "1" 36 | hmac = "0.12" 37 | http = "0.2" 38 | hyper = "0.14" 39 | indexmap = { version = "2.0.2", features = ["serde"] } 40 | lazy_static = "1.0" 41 | log = "0.4.6" 42 | mime = "0.3.1" 43 | minifier = { version = "0.3", features = ["html"] } 44 | nix = { version = "0.27.1", features = ["mman", "resource"] } 45 | percent-encoding = "2.1.0" 46 | prometheus = "0.13.3" 47 | r2d2 = "0.8.2" 48 | rand = "0.8" 49 | rayon = "1.10" 50 | regex = "1.0" 51 | remove_dir_all = "0.7" 52 | reqwest = { version = "0.11", features = ["blocking", "json"] } 53 | rusqlite = { version = "0.32.1", features = ["chrono", "functions", "bundled"] } 54 | rust_team_data = { git = "https://github.com/rust-lang/team" } 55 | rustwide = { version = "0.19.1", features = [ 56 | "unstable", 57 | "unstable-toolchain-ci", 58 | ] } 59 | serde = "1.0" 60 | serde_derive = "1.0" 61 | serde_json = "1.0" 62 | serde_regex = "1.1.0" 63 | sha-1 = "0.10" 64 | systemstat = "0.1.11" 65 | tar = "0.4.36" 66 | tempfile = "3.0.0" 67 | tera = "1.19.1" 68 | thiserror = "1.0.38" 69 | tokio = "1.24" 70 | toml = "0.8.6" 71 | url = "2" 72 | walkdir = "2" 73 | warp = "0.3" 74 | zstd = "0.13.0" 75 | 76 | [dev-dependencies] 77 | assert_cmd = "2.0.4" 78 | difference = "2.0.0" 79 | predicates = "3.0.4" 80 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # This Dockerfile is composed of two steps: the first one builds the release 2 | # binary, and then the binary is copied inside another, empty image. 3 | 4 | ################# 5 | # Build image # 6 | ################# 7 | 8 | FROM ubuntu:24.04 AS build 9 | 10 | RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ 11 | ca-certificates \ 12 | curl \ 13 | build-essential \ 14 | git \ 15 | pkg-config \ 16 | cmake \ 17 | libsqlite3-dev \ 18 | libssl-dev 19 | 20 | # Install the currently pinned toolchain with rustup 21 | RUN curl https://static.rust-lang.org/rustup/dist/x86_64-unknown-linux-gnu/rustup-init >/tmp/rustup-init && \ 22 | chmod +x /tmp/rustup-init && \ 23 | /tmp/rustup-init -y --no-modify-path --default-toolchain nightly --profile minimal 24 | ENV PATH=/root/.cargo/bin:$PATH 25 | 26 | # Build the dependencies in a separate step to avoid rebuilding all of them 27 | # every time the source code changes. This takes advantage of Docker's layer 28 | # caching, and it works by copying the Cargo.{toml,lock} with dummy source code 29 | # and doing a full build with it. 30 | WORKDIR /source 31 | COPY Cargo.lock Cargo.toml /source/ 32 | RUN mkdir -p /source/src && \ 33 | echo "fn main() {}" > /source/src/main.rs && \ 34 | echo "fn main() {}" > /source/build.rs 35 | 36 | RUN cargo fetch 37 | RUN cargo build --release 38 | 39 | # Dependencies are now cached, copy the actual source code and do another full 40 | # build. The touch on all the .rs files is needed, otherwise cargo assumes the 41 | # source code didn't change thanks to mtime weirdness. 42 | RUN rm -rf /source/src /source/build.rs 43 | COPY src /source/src 44 | COPY build.rs /source/build.rs 45 | COPY assets /source/assets 46 | COPY templates /source/templates 47 | COPY .git /source/.git 48 | RUN find /source -name "*.rs" -exec touch {} \; && cargo build --release 49 | 50 | ################## 51 | # Output image # 52 | ################## 53 | 54 | FROM ubuntu:24.04 AS binary 55 | 56 | RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ 57 | docker.io \ 58 | build-essential \ 59 | pkg-config \ 60 | libssl-dev \ 61 | ca-certificates \ 62 | tini 63 | 64 | RUN mkdir /workspace 65 | ENV CRATER_WORK_DIR=/workspace 66 | ENV CRATER_INSIDE_DOCKER=1 67 | 68 | RUN mkdir /crater 69 | COPY config.toml /crater/config.toml 70 | WORKDIR /crater 71 | 72 | COPY --from=build /source/target/release/crater /usr/local/bin/ 73 | ENTRYPOINT ["tini", "--", "crater"] 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crater 2 | 3 | Crater is a tool to run experiments across parts of the Rust ecosystem. Its 4 | primary purpose is to detect regressions in the Rust compiler, and it does this 5 | by building a large number of crates, running their test suites and comparing the 6 | results between two versions of the Rust compiler. 7 | 8 | It can operate locally (with Docker as the only dependency) or distributed on 9 | the cloud. It only works on Linux at the moment, and it's licensed under both 10 | the MIT and Apache 2.0 licenses. 11 | 12 | The current features of Crater are: 13 | 14 | * Discover Rust codebases on crates.io and GitHub 15 | * Execute experiments on custom Rust toolchains 16 | * Run `cargo build` and `cargo test` over all the discovered codebases 17 | * Build and test without dependency updates or network access 18 | * Run arbitrary tests over all the discovered codebases 19 | * Generate HTML reports with results and logs 20 | * Isolate tests in Docker containers 21 | 22 | Crater is a successor to 23 | [taskcluster-crater](https://github.com/brson/taskcluster-crater). It was 24 | subsequently named cargobomb before resuming the Crater name. 25 | 26 | :warning: **DO NOT RUN CRATER IN AN UNSANDBOXED ENVIRONMENT** :warning: 27 | Crater executes malicious code that will destroy what you love. 28 | 29 | ## Documentation 30 | 31 | Want to contribute to Crater? Check out [the contribution 32 | guide](CONTRIBUTING.md). 33 | 34 | **User documentation:** 35 | 36 | * [Local/CLI usage](docs/cli-usage.md) 37 | * [GitHub bot usage](docs/bot-usage.md) 38 | * [Crater report triage procedure](docs/report-triage.md) 39 | 40 | **Operations documentation:** 41 | 42 | * [Legacy operational workflow](docs/legacy-workflow.md) 43 | * [Setting up a new Crater agent machine](docs/agent-machine-setup.md) 44 | 45 | **Technical documentation:** 46 | 47 | * [minicrater docs](tests/minicrater/README.md) 48 | * [Agent HTTP API specification](docs/agent-http-api.md) 49 | -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/crater/5827aec1dacdf8ef69df0e8606f9284a1f428f1a/assets/favicon.ico -------------------------------------------------------------------------------- /assets/report.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | font-family: sans-serif; 7 | background: #111; 8 | color: #eee; 9 | margin: 0; 10 | } 11 | 12 | a { 13 | color: #eee; 14 | } 15 | 16 | p { 17 | line-height: 1.4em; 18 | } 19 | 20 | .hidden { 21 | display: none; 22 | } 23 | 24 | .toggle { 25 | cursor: pointer; 26 | } 27 | 28 | header { 29 | padding: 1px; 30 | margin-bottom: 2em; 31 | background: #222; 32 | } 33 | 34 | header div.navbar { 35 | display: flex; 36 | padding: 1em; 37 | } 38 | 39 | header div.navbar h1 { 40 | flex: 1; 41 | 42 | margin: 0; 43 | font-size: 1em; 44 | font-weight: 400; 45 | } 46 | 47 | header div.navbar ul { 48 | flex: 1; 49 | text-align: center; 50 | margin: 0; 51 | padding: 0; 52 | } 53 | 54 | header div.navbar ul li { 55 | display: inline-block; 56 | padding: 0 0.5em; 57 | } 58 | 59 | header div.navbar ul li a { 60 | color: #999; 61 | font-weight: 400; 62 | text-decoration: none; 63 | } 64 | 65 | header div.navbar ul li a:hover { 66 | text-decoration: underline; 67 | } 68 | 69 | header div.navbar ul li a.active { 70 | color: #eee; 71 | text-decoration: none; 72 | } 73 | 74 | header div.navbar div.count { 75 | flex: 1; 76 | text-align: right; 77 | } 78 | 79 | header div.toolchains { 80 | display: flex; 81 | } 82 | 83 | header div.toolchains div.toolchain { 84 | display: flex; 85 | align-items: center; 86 | 87 | flex: 1; 88 | padding: 0 1em; 89 | } 90 | 91 | header div.toolchains div.toolchain.toolchain-start { 92 | background: #292929; 93 | } 94 | 95 | header div.toolchains div.toolchain.toolchain-start div { 96 | width: 100%; 97 | text-align: right; 98 | } 99 | 100 | header div.toolchains div.toolchain div.flags { 101 | margin-top: 0.2em; 102 | font-size: 0.9em; 103 | color: #999; 104 | } 105 | 106 | header div.toolchains div.toolchain div.flags span { 107 | display: inline-block; 108 | margin-left: 0.5em; 109 | } 110 | 111 | header div.toolchains div.toolchain div.flags span:first-child { 112 | margin-left: 0; 113 | } 114 | 115 | header div.toolchains div.toolchain div.flags code { 116 | color: #eee; 117 | } 118 | 119 | header div.toolchains div.arrow { 120 | flex: 0; 121 | height: 0; 122 | width: 0; 123 | 124 | border-top: 2em solid transparent; 125 | border-bottom: 2em solid transparent; 126 | border-left: 2em solid #292929; 127 | } 128 | 129 | div.nothing { 130 | text-align: center; 131 | } 132 | 133 | div.category, div.wrapper { 134 | width: 70em; 135 | margin: 1em auto; 136 | } 137 | 138 | div.category div.header { 139 | padding: 0.5em 0.8em; 140 | border-radius: 0.2em; 141 | box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.5); 142 | } 143 | 144 | .flex { 145 | display: flex; 146 | position: -webkit-sticky; 147 | position: sticky; 148 | top: 0.5em; 149 | } 150 | 151 | div.category div.subheader.header { 152 | border-radius: 0.2em 0 0 0.2em; 153 | } 154 | 155 | div.category div.subheader + div.header { 156 | flex-grow: 1; 157 | border-radius: 0 0.2em 0.2em 0; 158 | } 159 | 160 | div.category div.header.header-background { 161 | background: #292929; 162 | } 163 | 164 | div.category div.crate { 165 | display: flex; 166 | padding: 0.8em; 167 | border-top: 1px solid #333; 168 | } 169 | 170 | div.category div.crate:first-child { 171 | border-top: 0; 172 | } 173 | 174 | div.category div.crate:last-child { 175 | padding-bottom: 0; 176 | } 177 | 178 | div.category div.crate > a, div.category div.crate > span.title { 179 | flex: 1; 180 | text-decoration: none; 181 | } 182 | 183 | div.category div.crate > span { 184 | flex-basis: 12em; 185 | text-align: center; 186 | } 187 | 188 | div.category div.crate > span > b { 189 | display: inline-block; 190 | height: 0.5em; 191 | width: 0.5em; 192 | margin: 0 0.2em 0.1em 0; 193 | border-radius: 0.5em; 194 | } 195 | -------------------------------------------------------------------------------- /assets/report.js: -------------------------------------------------------------------------------- 1 | function setup_buttons() { 2 | let buttons = document.querySelectorAll(".toggle"); 3 | for (let i = 0; i < buttons.length; i++) { 4 | buttons[i].addEventListener("click", function(e) { 5 | e.preventDefault(); 6 | 7 | this.classList.toggle("selected"); 8 | 9 | let selector = this.getAttribute("data-toggle"); 10 | let elements = document.querySelectorAll(selector); 11 | for (let i = 0; i < elements.length; i++) { 12 | elements[i].classList.toggle("hidden"); 13 | } 14 | }.bind(buttons[i])); 15 | } 16 | } 17 | 18 | setup_buttons(); 19 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | fn cmd(args: &[&str]) -> Option { 4 | if let Ok(out) = Command::new(args[0]).args(&args[1..]).output() { 5 | if out.status.success() { 6 | return Some(String::from_utf8_lossy(&out.stdout).trim().to_string()); 7 | } 8 | } 9 | 10 | None 11 | } 12 | 13 | fn get_git_sha() -> Option { 14 | if let Some(sha) = cmd(&["git", "rev-parse", "--short", "HEAD"]) { 15 | let symbolic = cmd(&["git", "rev-parse", "--symbolic", "HEAD"]).unwrap(); 16 | let symbolic_full = cmd(&["git", "rev-parse", "--symbolic-full-name", "HEAD"]).unwrap(); 17 | 18 | println!("cargo:rerun-if-changed=.git/{symbolic}"); 19 | if symbolic != symbolic_full { 20 | println!("cargo:rerun-if-changed=.git/{symbolic_full}"); 21 | } 22 | 23 | Some(sha) 24 | } else { 25 | println!("cargo:warning=failed to get crater sha"); 26 | None 27 | } 28 | } 29 | 30 | fn main() { 31 | let sha = format!("{:?}", get_git_sha()); 32 | 33 | let output = std::env::var("OUT_DIR").unwrap(); 34 | ::std::fs::write(format!("{output}/sha"), sha.as_bytes()).unwrap(); 35 | } 36 | -------------------------------------------------------------------------------- /docs/agent-machine-setup.md: -------------------------------------------------------------------------------- 1 | # Setting up a new Crater agent machine 2 | 3 | This document contains the steps to configure a new Crater agent machine from 4 | scratch, from an Amazon Linux image. To get a new one ask in 5 | [t-infra](https://rust-lang.zulipchat.com/#narrow/stream/242791-t-infra). 6 | 7 | The machine type we're currently using is: 8 | 9 | * Instance: AWS `c5.2xlarge` 10 | * Storage: 2 Tb 11 | * OS: Amazon Linux 2 12 | 13 | Once they tell you the IP of the machine, ssh with the `ec2-user` user into it 14 | from the bastion server, and execute these commands: 15 | 16 | ``` 17 | curl https://sh.rustup.rs -sSf | sh 18 | source $HOME/.cargo/env 19 | sudo yum install git htop docker gcc cmake openssl-devel 20 | sudo yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm 21 | sudo yum install --enablerepo=epel byobu 22 | sudo systemctl start docker 23 | sudo systemctl enable docker 24 | sudo usermod -a -G docker ec2-user 25 | 26 | # Configure byobu to use Ctrl+Z instead of Ctrl+A 27 | mkdir ~/.byobu 28 | cat > ~/.byobu/keybindings.tmux << EOF 29 | unbind-key -n C-a 30 | unbind-key -n C-z 31 | set -g prefix ^Z 32 | set -g prefix2 ^Z 33 | bind z send-prefix 34 | EOF 35 | ``` 36 | 37 | Log out of the machine. From the bastion, copy the `~/.aws/credentials` file 38 | from an existing agent into the new one, and then log into it again and 39 | execute: 40 | 41 | ``` 42 | git clone https://github.com/rust-lang/crater 43 | cd crater 44 | cargo build --release 45 | 46 | # This is going to take a while to complete 47 | cargo run --release -- prepare-local 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/cli-usage.md: -------------------------------------------------------------------------------- 1 | # Basic local use 2 | 3 | These commands will run Crater, in local configuration, on the demo 4 | crate set. This should be safe to run locally because the set of crates 5 | tested is limited to the 'demo' set which you can control to crates you 6 | trust. Of course, the experiments are still run inside of Docker so there 7 | is _some_ form of sandboxing. Of course, this means you'll need Docker 8 | running locally. 9 | 10 | Today Crater expects to be run out of its own source directory so you'll 11 | want to clone it first. All of its output is put into the `./work` directory, 12 | where it maintains its own rustup installation, crate mirrors, a database 13 | of experiments, etc. 14 | 15 | To set up the local crater directory to run experiments first run the following: 16 | ```bash 17 | cargo run -- prepare-local 18 | ``` 19 | 20 | You can then define your own experiment like so: 21 | ```bash 22 | cargo run -- define-ex --crate-select=demo --cap-lints=forbid stable beta 23 | ``` 24 | 25 | This will create an experiment named "default", but you can give your experiment 26 | a more meaningful name using the `--ex` option. The configuration for which crates 27 | will be run in the experiment is definied the `config.toml` file found at the root 28 | of the repo. In this config file you'll find the `demo-crates` section which defines 29 | three sets of crates that combine to form the set of crates being tests. The `crates` 30 | section is the name of crates on crates.io, the `github-repos` section is a list of 31 | github repos, and the `local-crates` section is a list of creates located in the 32 | `local-crates` directory in this repo. 33 | 34 | To actually run the experiment do the following: 35 | ```bash 36 | cargo run -- run-graph --threads NUM_CPUS 37 | ``` 38 | 39 | Remember to pass the `--ex` option if you gave your experiment a distinct name. 40 | 41 | To see a report of the results, run the following: 42 | 43 | ```bash 44 | cargo run -- gen-report work/ex/default/ 45 | ``` 46 | 47 | This will output a report to `./work/ex/default/index.html`. 48 | 49 | If you want to clean things up you can use the following commands: 50 | ```bash 51 | # delete all the target directories 52 | cargo run -- delete-all-target-dirs 53 | # delete all results directories 54 | cargo run -- delete-all-results 55 | # remove a specific experiment 56 | cargo run -- delete-ex 57 | ``` 58 | 59 | Reminder: Each command except `prepare-local` optionally takes an `--ex` argument 60 | to identify the experiment being referred to. If not supplied, this 61 | defaults to `default`. Here's what each of the steps does: 62 | 63 | * `prepare-local` - sets up the stable toolchain for internal use, 64 | builds the docker container, builds lists of crates. This needs to 65 | be rerun periodically, but not between every experiment. 66 | 67 | * `define-ex` - defines a new experiment 68 | performing a build-test experiment on the 'demo' set of crates. 69 | 70 | * `run-graph` - executes the experiment. You can control the number of parallel 71 | tasks executed with the `--threads` flag. 72 | 73 | * `run` - runs tests on crates in the experiment, against both 74 | toolchains 75 | 76 | * `gen-report` - summarize the experiment results to 77 | work/ex/default/index.html 78 | 79 | * `delete-all-target-dirs`/`delete-all-results`/`delete-ex` - clean up 80 | everything relating to this experiment 81 | 82 | ## Custom toolchains 83 | 84 | Toolchains for rust PRs that have been built by asking bors to try a PR can 85 | be specified using `try#`. You will probably want to specify 86 | the comparison commit as `master#`. 87 | -------------------------------------------------------------------------------- /docs/images/pr-try-commit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/crater/5827aec1dacdf8ef69df0e8606f9284a1f428f1a/docs/images/pr-try-commit.png -------------------------------------------------------------------------------- /docs/report-triage.md: -------------------------------------------------------------------------------- 1 | # Crater report triage procedure 2 | 3 | > **Note:** beta runs should be triaged by the release team, and PR runs should 4 | > be triaged by the people involved in that PR. 5 | 6 | To start the triage you need to open the report generated by Crater. The report 7 | is a bit slow to load, so wait a few seconds until the buttons are populated 8 | with the numbers and then click "regressed". You don't need to find the cause 9 | of the regressions, but you should report them anyway (one issue per regression 10 | for beta runs or in a comment for PR runs). 11 | 12 | ## Reporting regressions 13 | 14 | You can follow whatever process you like for working through regressions, 15 | but a suggestion workflow is described below, per regression: 16 | 17 | 1. Open the regression log, in the "toolchain 2" column. 18 | 2. If the tests timed out, re-run the tests locally. 19 | 2. If the breakage is expected (for example a lint changing to deny by 20 | default), find the original PR and check it went through its own Crater run. 21 | Don't report it in this case. 22 | 3. If the regression is in a dependency, triage the dependency and then ignore 23 | all the fallout from it. 24 | 4. It this is a beta run and it's not the first one for the current cycle, 25 | search for the regressions already reported. If it was closed as "expected" 26 | skip reporting it, but if it was closed as "fixed" then reopen it because it 27 | regressed _again_. 28 | 5. Open a new issue about the regression, linking all the affected crates and 29 | cc-ing the crate authors. 30 | 31 | The template (for crates.io crates or git repos) is: 32 | 33 | ``` 34 | [CRATENAME-4.2](https://crates.io/crates/cratename) regressed from stable to beta ([build log](http://cargobomb-reports.../log.txt)). cc @AUTHOR 35 | [AUTHOR/REPO#SHORT_SHA](https://github.com/author/repo/tree/SHA) regressed from stable to beta ([build log](http://cargobomb-reports.../log.txt)). cc @AUTHOR 36 | ``` 37 | 38 | When in doubt about a regression, file an issue. It's best to force the Rust 39 | developers to aknowledge the regression that to let it slip through. 40 | 41 | ## Triaging regressions 42 | 43 | If you're interested in triaging the regressions once the issues are raised, 44 | you can follow these rough instructions: 45 | 46 | * Download the code locally. If you're downloading crates 47 | [cargo-clone][cargo-clone] is an useful tool to speed up the process. 48 | * Run `cargo +stable test` to ensure that the crate works on stable. If stable 49 | does not work run it a few times to see if it has a flaky test. Crates with 50 | flaky tests should then be added to the blacklist. 51 | * Run `cargo +TESTED_TOOLCHAIN test` to verify if it fails as reported by 52 | Crater. 53 | * Run `cargo +nightly test` to test if the issue is fixed on the latest 54 | nightly. If that's the case the regression fix can be easily backported. 55 | 56 | [cargo-clone]: https://github.com/JanLikar/cargo-clone 57 | 58 | ## Adding crates to the blacklist 59 | 60 | Crater includes a blacklist, which is used to skip parts of the experiment we 61 | already know can fail. The blacklist is stored in the `config.toml` file on 62 | this repository, in the `[crates]` and `[github-repos]` section. 63 | 64 | Each crate/repo has its own line, and can have multiple options. Check out the 65 | comment in the `config.toml` file to learn more about those. 66 | 67 | For example, if the crate `foo` has a flaky test suite you should add this line 68 | in the configuration file (possibly sorting it): 69 | 70 | ```toml 71 | flaky = { skip-tests = true } # flaky test suite 72 | ``` 73 | 74 | A comment should always be added (on the same line) to briefly explain why the 75 | crate was added to the blacklist. After you added all the crates you need to 76 | add to the blacklist, please send a PR against that file. 77 | -------------------------------------------------------------------------------- /find-bad-crates.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import glob 4 | 5 | RESULT_PATHS = 'result_paths.json' 6 | RESULTS = 'results.json' 7 | 8 | if not os.path.exists(RESULT_PATHS): 9 | # starts are: ex, commitish, reg/gh, crate 10 | result_paths = glob.glob('work/ex/*/res/*/*/*/results.txt') 11 | with open(RESULT_PATHS, 'w') as f: 12 | f.write(json.dumps(result_paths)) 13 | with open(RESULT_PATHS) as f: 14 | result_paths = json.load(f) 15 | 16 | if not os.path.exists(RESULTS): 17 | results = {} 18 | for path in result_paths: 19 | with open(path) as f: 20 | pathdir = path[:-(len('results.txt')+1)] 21 | results[pathdir] = f.read() 22 | with open(RESULTS, 'w') as f: 23 | f.write(json.dumps(results)) 24 | with open(RESULTS) as f: 25 | results = json.load(f) 26 | 27 | sames = {} 28 | for pathdir, result in results.iteritems(): 29 | assert result in ['build-fail', 'test-fail', 'test-pass', 'test-skipped'] 30 | head, crate = os.path.split(pathdir) 31 | _, gh_or_reg = os.path.split(head) 32 | if gh_or_reg == 'gh': 33 | # e.g. pepyakin.chipster.9cd0c41e16b8e1a58381b3fb18ed412c92237f4e 34 | name = crate.rsplit('.', 1)[0] 35 | elif gh_or_reg == 'reg': 36 | # e.g. effect-monad-0.3.1 37 | name = crate.rsplit('-', 1)[0] 38 | else: 39 | assert False, pathdir 40 | 41 | key = (gh_or_reg, name) 42 | cur = sames.get(key) 43 | if cur is None: 44 | sames[key] = (result, 1) 45 | else: 46 | cur_result, cur_count = cur 47 | if cur_result is None: 48 | # This one has already mismatched 49 | pass 50 | elif result != cur_result: 51 | sames[key] = (None, 0) 52 | else: 53 | sames[key] = (cur_result, cur_count + 1) 54 | 55 | num_exs = len(glob.glob('work/ex/*')) 56 | # Can be more than this, crater sometimes decides to test multiple versions of a crate 57 | all_fails = 2 * num_exs 58 | 59 | ghlines = [] 60 | reglines = [] 61 | for (gh_or_reg, name), (result, count) in sames.iteritems(): 62 | if result is None: 63 | continue 64 | if result in ['test-pass', 'test-skipped']: 65 | continue 66 | if count != all_fails: 67 | continue 68 | 69 | if result == 'build-fail': 70 | skipkind = 'skip' 71 | elif result == 'test-fail': 72 | skipkind = 'skip-tests' 73 | else: 74 | assert False, result 75 | if gh_or_reg == 'gh': 76 | if name.count('.') > 1: 77 | # Can't be bothered figuring this out - probably means the repo name has a dot 78 | continue 79 | org, repo = name.split('.') 80 | ghlines.append('"{}/{}" = {{ {} = true }}'.format(org, repo, skipkind)) 81 | else: 82 | if name.count('.') > 1: 83 | # Can't be bothered figuring this out - probably means the crate version had 84 | # a -beta1 or something 85 | continue 86 | reglines.append('{} = {{ {} = true }}'.format(name, skipkind)) 87 | #print '|'.join((gh_or_reg, name)), result, count 88 | ghlines.sort() 89 | reglines.sort() 90 | 91 | open('ghlines.toml', 'w').write('\n'.join(ghlines)) 92 | open('reglines.toml', 'w').write('\n'.join(reglines)) 93 | -------------------------------------------------------------------------------- /local-crates/README.md: -------------------------------------------------------------------------------- 1 | # Crater's local crates 2 | 3 | This directory contains Crater's local crates. Local crates are dummy crates 4 | used by the Crater test suite, and they're not meant to be published or used by 5 | other projects. 6 | 7 | To execute a Crater run with local crates in it use the `local` crate 8 | selection. 9 | -------------------------------------------------------------------------------- /local-crates/beta-faulty-deps/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "beta-faulty-deps" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | error_code = {git = "https://github.com/rust-lang/crater", rev = "f190933e896443e285e3bb6962fb87d7439b8d65"} 8 | 9 | -------------------------------------------------------------------------------- /local-crates/beta-faulty-deps/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /local-crates/beta-fixed/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "beta-fixed" 3 | version = "0.1.0" 4 | authors = ["Pietro Albini "] 5 | 6 | [dependencies] 7 | 8 | [build-dependencies] 9 | rustc_version = "0.2.3" 10 | -------------------------------------------------------------------------------- /local-crates/beta-fixed/build.rs: -------------------------------------------------------------------------------- 1 | extern crate rustc_version; 2 | 3 | use rustc_version::{version_meta, Channel}; 4 | 5 | fn main() { 6 | if let Channel::Beta = version_meta().unwrap().channel { 7 | println!("cargo:rustc-cfg=channel_beta"); 8 | } 9 | 10 | // Rebuild the crate only if the build.rs file changes 11 | println!("cargo:rebuild-if-changed=build.rs"); 12 | } 13 | -------------------------------------------------------------------------------- /local-crates/beta-fixed/src/main.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(channel_beta))] 2 | compile_error!("Non-beta regression \\o/"); 3 | 4 | fn main() { 5 | println!("Hello, world!"); 6 | } 7 | -------------------------------------------------------------------------------- /local-crates/beta-regression/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "beta-regression" 3 | version = "0.1.0" 4 | authors = ["Pietro Albini "] 5 | 6 | [dependencies] 7 | 8 | [build-dependencies] 9 | rustc_version = "0.2.3" 10 | -------------------------------------------------------------------------------- /local-crates/beta-regression/build.rs: -------------------------------------------------------------------------------- 1 | extern crate rustc_version; 2 | 3 | use rustc_version::{version_meta, Channel}; 4 | 5 | fn main() { 6 | if let Channel::Beta = version_meta().unwrap().channel { 7 | println!("cargo:rustc-cfg=channel_beta"); 8 | } 9 | 10 | // Rebuild the crate only if the build.rs file changes 11 | println!("cargo:rebuild-if-changed=build.rs"); 12 | } 13 | -------------------------------------------------------------------------------- /local-crates/beta-regression/src/main.rs: -------------------------------------------------------------------------------- 1 | #[cfg(channel_beta)] 2 | compile_error!("Beta regression \\o/"); 3 | 4 | fn main() { 5 | println!("Hello, world!"); 6 | } 7 | -------------------------------------------------------------------------------- /local-crates/broken-cargotoml/Cargo.toml: -------------------------------------------------------------------------------- 1 | wow! this isn't valid toml? :O 2 | -------------------------------------------------------------------------------- /local-crates/broken-cargotoml/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /local-crates/build-fail/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "build-fail" 3 | version = "0.1.0" 4 | authors = ["Pietro Albini "] 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /local-crates/build-fail/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | invalid syntax :( 3 | } 4 | -------------------------------------------------------------------------------- /local-crates/build-pass/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "build-pass" 3 | version = "0.1.0" 4 | authors = ["Pietro Albini "] 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /local-crates/build-pass/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /local-crates/clippy-warn/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "clippy-warn" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /local-crates/clippy-warn/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | // Trigger `clippy::print_with_newline` 3 | print!("Hello, world!\n"); 4 | } 5 | -------------------------------------------------------------------------------- /local-crates/docs-rs-features/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "docs-rs-features" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | 10 | [package.metadata.docs.rs] 11 | features = ["docs_rs_feature"] 12 | 13 | [features] 14 | docs_rs_feature = [] 15 | -------------------------------------------------------------------------------- /local-crates/docs-rs-features/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "docs_rs_feature")] 2 | compile_error!("oh no, a hidden regression!"); 3 | 4 | fn main() { 5 | println!("Hello, world!"); 6 | } 7 | -------------------------------------------------------------------------------- /local-crates/error-code/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "error_code" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | 8 | 9 | [build-dependencies] 10 | rustc_version = "0.2.3" 11 | -------------------------------------------------------------------------------- /local-crates/error-code/build.rs: -------------------------------------------------------------------------------- 1 | use rustc_version::{version_meta, Channel}; 2 | 3 | fn main() { 4 | if let Channel::Beta = version_meta().unwrap().channel { 5 | println!("cargo:rustc-cfg=channel_beta"); 6 | } 7 | 8 | // Rebuild the crate only if the build.rs file changes 9 | println!("cargo:rebuild-if-changed=build.rs"); 10 | } 11 | -------------------------------------------------------------------------------- /local-crates/error-code/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(channel_beta)] 2 | pub const STRING: String = String::from("invalid"); 3 | 4 | #[cfg(channel_beta)] 5 | static X: i32 = 42; 6 | #[cfg(channel_beta)] 7 | const Y: i32 = X; 8 | 9 | fn hello() { 10 | println!("Hello, world!"); 11 | } 12 | -------------------------------------------------------------------------------- /local-crates/faulty-deps/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "faulty-deps" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | error_code = {git = "https://github.com/rust-lang/crater", rev = "c3f462bdab37a93c24b2b172b90564749e892cbc"} 8 | lazy_static = "=0.1.0" 9 | -------------------------------------------------------------------------------- /local-crates/faulty-deps/src/main.rs: -------------------------------------------------------------------------------- 1 | use error_code::STRING; 2 | 3 | fn main() { 4 | println!("Hello, world!"); 5 | } 6 | -------------------------------------------------------------------------------- /local-crates/ice-regression/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "beta-regression" 3 | version = "0.1.0" 4 | authors = ["Pietro Albini "] 5 | 6 | [dependencies] 7 | 8 | [build-dependencies] 9 | rustc_version = "0.2.3" 10 | -------------------------------------------------------------------------------- /local-crates/ice-regression/build.rs: -------------------------------------------------------------------------------- 1 | extern crate rustc_version; 2 | 3 | use rustc_version::{version_meta, Channel}; 4 | 5 | fn main() { 6 | if let Channel::Beta = version_meta().unwrap().channel { 7 | println!("cargo:rustc-cfg=channel_beta"); 8 | } 9 | 10 | // Rebuild the crate only if the build.rs file changes 11 | println!("cargo:rebuild-if-changed=build.rs"); 12 | } 13 | -------------------------------------------------------------------------------- /local-crates/ice-regression/src/main.rs: -------------------------------------------------------------------------------- 1 | // This tests our ICE regression handling. 2 | 3 | #[cfg(channel_beta)] 4 | fn main() { 5 | break rust; 6 | } 7 | 8 | #[cfg(not(channel_beta))] 9 | fn main() { 10 | thisisabuildfailure; 11 | } 12 | -------------------------------------------------------------------------------- /local-crates/memory-hungry/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "memory-hungry" 3 | version = "0.1.0" 4 | authors = ["Pietro Albini "] 5 | 6 | [dependencies] 7 | 8 | [build-dependencies] 9 | rustc_version = "0.2.3" 10 | -------------------------------------------------------------------------------- /local-crates/memory-hungry/build.rs: -------------------------------------------------------------------------------- 1 | extern crate rustc_version; 2 | 3 | #[path = "src/allocate.rs"] 4 | mod allocate; 5 | 6 | use rustc_version::{version_meta, Channel}; 7 | 8 | fn main() { 9 | if let Channel::Beta = version_meta().unwrap().channel { 10 | // On the beta channel allocate in tests 11 | println!("cargo:rustc-cfg=channel_beta"); 12 | } else { 13 | // On the stable channel allocate in build.rs 14 | allocate::allocate(); 15 | } 16 | 17 | // Rebuild the crate only if the build.rs file changes 18 | println!("cargo:rebuild-if-changed=build.rs"); 19 | } 20 | -------------------------------------------------------------------------------- /local-crates/memory-hungry/src/allocate.rs: -------------------------------------------------------------------------------- 1 | const MAX_ALLOCATE: usize = 1_073_741_824; 2 | 3 | pub fn allocate() { 4 | let data = [0u8; 4096]; 5 | let mut allocated = 0; 6 | 7 | while allocated < MAX_ALLOCATE { 8 | Box::leak(Box::new(data)); 9 | allocated += data.len(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /local-crates/memory-hungry/src/main.rs: -------------------------------------------------------------------------------- 1 | #[cfg(channel_beta)] 2 | mod allocate; 3 | 4 | fn main() { 5 | println!("Hello world"); 6 | } 7 | 8 | #[test] 9 | #[cfg(channel_beta)] 10 | fn test_allocate() { 11 | allocate::allocate(); 12 | } 13 | -------------------------------------------------------------------------------- /local-crates/missing-examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "missing-examples" 3 | version = "0.1.0" 4 | authors = ["Giacomo Pasini"] 5 | 6 | [dependencies] 7 | 8 | [[example]] 9 | name = "foo" 10 | 11 | [[example]] 12 | name = "bar" 13 | path = "examples/ex.rs" 14 | 15 | [[example]] 16 | name = "missing" 17 | 18 | [[test]] 19 | name = "test1" 20 | 21 | [[test]] 22 | name = "test2" 23 | path = "test/t.rs" 24 | 25 | [[test]] 26 | name = "missing" 27 | 28 | -------------------------------------------------------------------------------- /local-crates/missing-examples/examples/foo.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello world"); 3 | } 4 | -------------------------------------------------------------------------------- /local-crates/missing-examples/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /local-crates/missing-examples/tests/test1.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/crater/5827aec1dacdf8ef69df0e8606f9284a1f428f1a/local-crates/missing-examples/tests/test1.rs -------------------------------------------------------------------------------- /local-crates/network-access/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "network-access" 3 | version = "0.1.0" 4 | authors = ["Pietro Albini "] 5 | 6 | [dependencies] 7 | 8 | [build-dependencies] 9 | rustc_version = "0.2.3" 10 | -------------------------------------------------------------------------------- /local-crates/network-access/build.rs: -------------------------------------------------------------------------------- 1 | extern crate rustc_version; 2 | 3 | #[path = "src/network.rs"] 4 | mod network; 5 | 6 | use rustc_version::{version_meta, Channel}; 7 | 8 | fn main() { 9 | if let Channel::Beta = version_meta().unwrap().channel { 10 | // On the beta channel connect to the network in tests 11 | println!("cargo:rustc-cfg=channel_beta"); 12 | } else { 13 | // On the stable channel connect to the network in build.rs 14 | network::call(); 15 | } 16 | 17 | // Rebuild the crate only if the build.rs file changes 18 | println!("cargo:rebuild-if-changed=build.rs"); 19 | } 20 | -------------------------------------------------------------------------------- /local-crates/network-access/src/main.rs: -------------------------------------------------------------------------------- 1 | #[cfg(channel_beta)] 2 | mod network; 3 | 4 | fn main() { 5 | println!("Hello, world!"); 6 | } 7 | 8 | #[cfg(channel_beta)] 9 | #[test] 10 | fn test_network_access() { 11 | network::call(); 12 | } 13 | -------------------------------------------------------------------------------- /local-crates/network-access/src/network.rs: -------------------------------------------------------------------------------- 1 | use std::net::TcpStream; 2 | 3 | pub fn call() { 4 | // Try to connect to www.rust-lang.org:80 5 | // If network access is disabled even this should fail 6 | let _ = TcpStream::connect("www.rust-lang.org:80").unwrap(); 7 | } 8 | -------------------------------------------------------------------------------- /local-crates/outdated-lockfile/Cargo.lock: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /local-crates/outdated-lockfile/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "outdated-lockfile" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /local-crates/outdated-lockfile/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /local-crates/test-fail/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-fail" 3 | version = "0.1.0" 4 | authors = ["Pietro Albini "] 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /local-crates/test-fail/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | 5 | #[test] 6 | fn bad_test() { 7 | panic!("this crate is just broken"); 8 | } 9 | -------------------------------------------------------------------------------- /local-crates/yanked-deps/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "yanked-deps" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | ring = "0.2" 8 | -------------------------------------------------------------------------------- /local-crates/yanked-deps/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | nightly 2 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | reorder_imports = true 2 | -------------------------------------------------------------------------------- /src/actions/experiments/delete.rs: -------------------------------------------------------------------------------- 1 | use crate::actions::{experiments::ExperimentError, Action, ActionsCtx}; 2 | use crate::db::QueryUtils; 3 | use crate::experiments::Experiment; 4 | use crate::prelude::*; 5 | 6 | pub struct DeleteExperiment { 7 | pub name: String, 8 | } 9 | 10 | impl Action for DeleteExperiment { 11 | fn apply(self, ctx: &ActionsCtx) -> Fallible<()> { 12 | if !Experiment::exists(ctx.db, &self.name)? { 13 | return Err(ExperimentError::NotFound(self.name).into()); 14 | } 15 | 16 | // This will also delete all the data related to this experiment, thanks to the foreign 17 | // keys in the SQLite database 18 | ctx.db 19 | .execute("DELETE FROM experiments WHERE name = ?1;", &[&self.name])?; 20 | 21 | Ok(()) 22 | } 23 | } 24 | 25 | #[cfg(test)] 26 | mod tests { 27 | use super::DeleteExperiment; 28 | use crate::actions::{Action, ActionsCtx, CreateExperiment, ExperimentError}; 29 | use crate::config::Config; 30 | use crate::db::Database; 31 | use crate::experiments::Experiment; 32 | 33 | #[test] 34 | fn test_delete_missing_experiment() { 35 | let db = Database::temp().unwrap(); 36 | let config = Config::default(); 37 | let ctx = ActionsCtx::new(&db, &config); 38 | 39 | let err = DeleteExperiment { 40 | name: "dummy".to_string(), 41 | } 42 | .apply(&ctx) 43 | .unwrap_err(); 44 | 45 | assert_eq!( 46 | err.downcast_ref(), 47 | Some(&ExperimentError::NotFound("dummy".into())) 48 | ); 49 | } 50 | 51 | #[test] 52 | fn test_delete_experiment() { 53 | let db = Database::temp().unwrap(); 54 | let config = Config::default(); 55 | let ctx = ActionsCtx::new(&db, &config); 56 | 57 | crate::crates::lists::setup_test_lists(&db, &config).unwrap(); 58 | 59 | // Create a dummy experiment and make sure it exists 60 | CreateExperiment::dummy("dummy").apply(&ctx).unwrap(); 61 | assert!(Experiment::exists(&db, "dummy").unwrap()); 62 | 63 | // Delete it and make sure it doesn't exist anymore 64 | DeleteExperiment { 65 | name: "dummy".to_string(), 66 | } 67 | .apply(&ctx) 68 | .unwrap(); 69 | assert!(!Experiment::exists(&db, "dummy").unwrap()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/actions/experiments/mod.rs: -------------------------------------------------------------------------------- 1 | mod create; 2 | mod delete; 3 | mod edit; 4 | 5 | pub use self::create::CreateExperiment; 6 | pub use self::delete::DeleteExperiment; 7 | pub use self::edit::EditExperiment; 8 | 9 | #[derive(Debug, thiserror::Error)] 10 | #[cfg_attr(test, derive(PartialEq, Eq))] 11 | pub enum ExperimentError { 12 | #[error("experiment '{0}' not found")] 13 | NotFound(String), 14 | #[error("experiment '{0}' already exists")] 15 | AlreadyExists(String), 16 | #[error("duplicate toolchains provided")] 17 | DuplicateToolchains, 18 | #[error("it's only possible to edit queued experiments")] 19 | CanOnlyEditQueuedExperiments, 20 | } 21 | -------------------------------------------------------------------------------- /src/actions/lists/mod.rs: -------------------------------------------------------------------------------- 1 | mod update; 2 | 3 | pub use self::update::UpdateLists; 4 | -------------------------------------------------------------------------------- /src/actions/lists/update.rs: -------------------------------------------------------------------------------- 1 | use crate::actions::{Action, ActionsCtx}; 2 | use crate::crates::lists::{GitHubList, List, LocalList, RegistryList}; 3 | use crate::prelude::*; 4 | 5 | pub struct UpdateLists { 6 | pub github: bool, 7 | pub registry: bool, 8 | pub local: bool, 9 | } 10 | 11 | impl Default for UpdateLists { 12 | fn default() -> Self { 13 | UpdateLists { 14 | github: true, 15 | registry: true, 16 | local: true, 17 | } 18 | } 19 | } 20 | 21 | impl Action for UpdateLists { 22 | fn apply(self, ctx: &ActionsCtx) -> Fallible<()> { 23 | if self.github { 24 | info!("updating GitHub repositories list"); 25 | GitHubList::default().update(ctx.db)?; 26 | } 27 | 28 | if self.registry { 29 | info!("updating crates.io crates list"); 30 | RegistryList.update(ctx.db)?; 31 | } 32 | 33 | if self.local { 34 | info!("updating local crates list"); 35 | LocalList::default().update(ctx.db)?; 36 | } 37 | 38 | Ok(()) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/actions/mod.rs: -------------------------------------------------------------------------------- 1 | mod experiments; 2 | mod lists; 3 | 4 | pub use self::experiments::*; 5 | pub use self::lists::*; 6 | 7 | use crate::config::Config; 8 | use crate::db::Database; 9 | use crate::prelude::*; 10 | 11 | pub trait Action { 12 | fn apply(self, ctx: &ActionsCtx) -> Fallible<()>; 13 | } 14 | 15 | pub struct ActionsCtx<'ctx> { 16 | db: &'ctx Database, 17 | config: &'ctx Config, 18 | } 19 | 20 | impl<'ctx> ActionsCtx<'ctx> { 21 | pub fn new(db: &'ctx Database, config: &'ctx Config) -> Self { 22 | ActionsCtx { db, config } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/assets.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use mime::Mime; 3 | use serde::Serialize; 4 | use std::borrow::Cow; 5 | use std::collections::HashMap; 6 | use std::path::PathBuf; 7 | use tera::Tera; 8 | 9 | #[cfg(not(debug_assertions))] 10 | lazy_static! { 11 | static ref TERA_CACHE: Tera = match build_tera_cache() { 12 | Ok(tera) => tera, 13 | Err(err) => { 14 | crate::utils::report_failure(&err); 15 | std::process::exit(1); 16 | } 17 | }; 18 | } 19 | 20 | macro_rules! load_files { 21 | (templates: [$($template:expr,)*], assets: [$($asset:expr => $mime:expr,)*],) => { 22 | lazy_static! { 23 | static ref ASSETS: HashMap<&'static str, Asset> = { 24 | let mut assets = HashMap::new(); 25 | $( 26 | let content = load_files!(_content concat!("assets/", $asset)); 27 | assets.insert($asset, Asset { 28 | content, 29 | mime: $mime, 30 | }); 31 | )* 32 | assets 33 | }; 34 | 35 | static ref TEMPLATES: HashMap<&'static str, FileContent> = { 36 | let mut templates = HashMap::new(); 37 | $(templates.insert($template, load_files!(_content concat!("templates/", $template)));)* 38 | templates 39 | }; 40 | } 41 | }; 42 | 43 | (_content $file:expr) => {{ 44 | #[cfg(debug_assertions)] 45 | { 46 | warn!("loaded dynamic asset (use release builds to statically bundle it): {}", $file); 47 | FileContent::Dynamic($file.into()) 48 | } 49 | 50 | #[cfg(not(debug_assertions))] 51 | { 52 | FileContent::Static(include_bytes!(concat!("../", $file))) 53 | } 54 | }}; 55 | } 56 | 57 | load_files! { 58 | templates: [ 59 | "macros.html", 60 | 61 | "ui/layout.html", 62 | 63 | "ui/agents.html", 64 | 65 | "ui/queue.html", 66 | "ui/experiment.html", 67 | 68 | "ui/404.html", 69 | "ui/500.html", 70 | 71 | "report/layout.html", 72 | "report/downloads.html", 73 | "report/results.html", 74 | ], 75 | assets: [ 76 | "ui.css" => mime::TEXT_CSS, 77 | 78 | "report.css" => mime::TEXT_CSS, 79 | "report.js" => mime::TEXT_JAVASCRIPT, 80 | 81 | "favicon.ico" => "image/x-icon".parse().unwrap(), 82 | ], 83 | } 84 | 85 | enum FileContent { 86 | #[cfg_attr(debug_assertions, allow(dead_code))] 87 | Static(&'static [u8]), 88 | #[cfg_attr(not(debug_assertions), allow(dead_code))] 89 | Dynamic(PathBuf), 90 | } 91 | 92 | impl FileContent { 93 | fn load(&self) -> Fallible> { 94 | Ok(match *self { 95 | FileContent::Static(content) => Cow::Borrowed(content), 96 | FileContent::Dynamic(ref path) => { 97 | Cow::Owned(::std::fs::read(path).with_context(|| { 98 | format!("failed to load dynamic asset: {}", path.to_string_lossy()) 99 | })?) 100 | } 101 | }) 102 | } 103 | } 104 | 105 | pub struct Asset { 106 | content: FileContent, 107 | mime: Mime, 108 | } 109 | 110 | impl Asset { 111 | pub fn content(&self) -> Fallible> { 112 | self.content.load() 113 | } 114 | 115 | pub fn mime(&self) -> &Mime { 116 | &self.mime 117 | } 118 | } 119 | 120 | pub fn load(name: &str) -> Fallible<&Asset> { 121 | if let Some(asset) = ASSETS.get(name) { 122 | Ok(asset) 123 | } else { 124 | bail!( 125 | "unknown static file (did you add it to src/assets.rs?): {}", 126 | name 127 | ); 128 | } 129 | } 130 | 131 | fn build_tera_cache() -> Fallible { 132 | let mut templates = Vec::new(); 133 | for (name, content) in TEMPLATES.iter() { 134 | templates.push((*name, String::from_utf8(content.load()?.into_owned())?)); 135 | } 136 | 137 | let to_add = templates 138 | .iter() 139 | .map(|(n, c)| (*n, c as &str)) 140 | .collect::>(); 141 | 142 | let mut tera = Tera::default(); 143 | tera.add_raw_templates(to_add) 144 | .map_err(|err| anyhow!("{err}"))?; 145 | Ok(tera) 146 | } 147 | 148 | #[allow(unused_variables)] 149 | pub fn render_template(name: &str, context: C) -> Fallible { 150 | // On debug builds the cache is rebuilt every time to pick up changed templates 151 | let tera_owned: Tera; 152 | let tera; 153 | 154 | #[cfg(debug_assertions)] 155 | { 156 | tera_owned = build_tera_cache()?; 157 | tera = &tera_owned; 158 | } 159 | 160 | #[cfg(not(debug_assertions))] 161 | { 162 | tera = &TERA_CACHE; 163 | } 164 | 165 | let tera_context = tera::Context::from_serialize(context)?; 166 | Ok(tera 167 | .render(name, &tera_context) 168 | .map_err(|e| anyhow!("{:?}", e))?) 169 | } 170 | -------------------------------------------------------------------------------- /src/bin/test-report.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use crater::experiments::ExperimentDBRecord; 3 | use crater::report::ReportWriter; 4 | use crater::results::EncodingType; 5 | use crater::{config::Config, db::QueryUtils}; 6 | use mime::{self, Mime}; 7 | use std::{borrow::Cow, fmt, path::Path}; 8 | 9 | #[cfg(not(unix))] 10 | fn main() { 11 | eprintln!("Not implemented!"); 12 | } 13 | 14 | #[cfg(unix)] 15 | fn main() { 16 | let mut env = env_logger::Builder::new(); 17 | env.filter_module("test_report", log::LevelFilter::Info); 18 | env.filter_module("crater", log::LevelFilter::Info); 19 | env.filter_module("rustwide", log::LevelFilter::Info); 20 | if let Ok(content) = std::env::var("RUST_LOG") { 21 | env.parse_filters(&content); 22 | } 23 | rustwide::logging::init_with(env.build()); 24 | let config: Config = toml::from_str(&std::fs::read_to_string("config.toml").unwrap()).unwrap(); 25 | let db = crater::db::Database::open_at(std::path::Path::new("crater.db")).unwrap(); 26 | let experiments = db 27 | .query("SELECT * FROM experiments;", [], |r| { 28 | ExperimentDBRecord::from_row(r) 29 | }) 30 | .unwrap(); 31 | let experiments: Vec<_> = experiments 32 | .into_iter() 33 | .map(|record| record.into_experiment()) 34 | .collect::>() 35 | .unwrap(); 36 | let ex = experiments.iter().find(|e| e.name == "pr-118920").unwrap(); 37 | let rdb = crater::results::DatabaseDB::new(&db); 38 | 39 | log::info!("Getting crates..."); 40 | 41 | let crates = ex.get_crates(&db).unwrap(); 42 | let writer = NullWriter; 43 | 44 | log::info!("Starting report generation..."); 45 | log::info!( 46 | "@ {:?}", 47 | nix::sys::resource::getrusage(nix::sys::resource::UsageWho::RUSAGE_SELF) 48 | .unwrap() 49 | .max_rss() 50 | ); 51 | crater::report::gen(&rdb, ex, &crates, &writer, &config, false).unwrap(); 52 | log::info!( 53 | "@ {:?}", 54 | nix::sys::resource::getrusage(nix::sys::resource::UsageWho::RUSAGE_SELF) 55 | .unwrap() 56 | .max_rss() 57 | ); 58 | } 59 | 60 | #[derive(Debug)] 61 | struct NullWriter; 62 | 63 | impl ReportWriter for NullWriter { 64 | fn write_bytes>( 65 | &self, 66 | _path: P, 67 | _b: &[u8], 68 | _mime: &Mime, 69 | _encoding_type: EncodingType, 70 | ) -> Result<()> { 71 | // no-op 72 | Ok(()) 73 | } 74 | fn write_string>(&self, _path: P, _s: Cow, _mime: &Mime) -> Result<()> { 75 | // no-op 76 | Ok(()) 77 | } 78 | } 79 | 80 | impl fmt::Display for NullWriter { 81 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 82 | write!(f, "{self:?}") 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/crates/sources/github.rs: -------------------------------------------------------------------------------- 1 | use crate::crates::{lists::List, Crate}; 2 | use crate::prelude::*; 3 | use std::borrow::Cow; 4 | use std::str::FromStr; 5 | 6 | static CACHED_LIST: &str = 7 | "https://raw.githubusercontent.com/rust-lang/rust-repos/master/data/github.csv"; 8 | const DUMMY_ORG: &str = "ghost"; 9 | const DUMMY_NAME: &str = "missing"; 10 | 11 | #[derive(Deserialize)] 12 | struct ListRepo { 13 | name: String, 14 | has_cargo_toml: bool, 15 | has_cargo_lock: bool, 16 | } 17 | 18 | pub(crate) struct GitHubList { 19 | source: Cow<'static, str>, 20 | } 21 | 22 | impl Default for GitHubList { 23 | fn default() -> Self { 24 | GitHubList { 25 | source: CACHED_LIST.into(), 26 | } 27 | } 28 | } 29 | 30 | impl List for GitHubList { 31 | const NAME: &'static str = "github-oss"; 32 | 33 | fn fetch(&self) -> Fallible> { 34 | info!("loading cached GitHub list from {}", self.source); 35 | 36 | let mut resp = crate::utils::http::get_sync(&self.source) 37 | .with_context(|| format!("failed to fetch GitHub crates list from {}", self.source))?; 38 | let mut reader = ::csv::Reader::from_reader(&mut resp); 39 | 40 | let mut list = Vec::new(); 41 | for line in reader.deserialize() { 42 | let line: ListRepo = line?; 43 | 44 | // Only import repos with a Cargo.toml or Cargo.lock 45 | if !line.has_cargo_toml || !line.has_cargo_lock { 46 | continue; 47 | } 48 | 49 | let mut name_parts = line.name.split('/'); 50 | let org = name_parts.next(); 51 | let name = name_parts.next(); 52 | let trailing = name_parts.next(); 53 | 54 | if let (Some(org), Some(name), None) = (org, name, trailing) { 55 | list.push(Crate::GitHub(GitHubRepo { 56 | org: org.to_string(), 57 | name: name.to_string(), 58 | sha: None, 59 | })); 60 | } else { 61 | warn!("skipping malformed repo name: {}", line.name); 62 | } 63 | } 64 | 65 | Ok(list) 66 | } 67 | } 68 | 69 | #[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, Clone)] 70 | pub struct GitHubRepo { 71 | pub org: String, 72 | pub name: String, 73 | pub sha: Option, 74 | } 75 | 76 | impl GitHubRepo { 77 | pub(crate) fn slug(&self) -> String { 78 | format!("{}/{}", self.org, self.name) 79 | } 80 | 81 | pub(crate) fn dummy() -> GitHubRepo { 82 | GitHubRepo { 83 | org: DUMMY_ORG.to_string(), 84 | name: DUMMY_NAME.to_string(), 85 | sha: None, 86 | } 87 | } 88 | } 89 | 90 | impl FromStr for GitHubRepo { 91 | type Err = ::anyhow::Error; 92 | 93 | fn from_str(input: &str) -> Fallible { 94 | let mut components = input 95 | .trim_start_matches("https://github.com/") 96 | .split('/') 97 | .rev() 98 | .collect::>(); 99 | let org = components.pop(); 100 | let name = components.pop(); 101 | let sha = components.pop(); 102 | 103 | match (org, name, sha) { 104 | (Some(org), Some(name), None) => Ok(GitHubRepo { 105 | org: org.to_string(), 106 | name: name.to_string(), 107 | sha: None, 108 | }), 109 | (Some(org), Some(name), Some(sha)) => Ok(GitHubRepo { 110 | org: org.to_string(), 111 | // remove additional queries if the sha is present 112 | // as the crate version is already uniquely determined 113 | name: name.split('?').next().unwrap().to_string(), 114 | sha: Some(sha.to_string()), 115 | }), 116 | _ => bail!("malformed repo url: {}", input), 117 | } 118 | } 119 | } 120 | 121 | #[cfg(test)] 122 | mod tests { 123 | use super::GitHubRepo; 124 | use std::str::FromStr; 125 | 126 | #[test] 127 | fn test_from_str() { 128 | assert_eq!( 129 | GitHubRepo::from_str("https://github.com/dummy_org/dummy/dummy_sha").unwrap(), 130 | GitHubRepo { 131 | org: "dummy_org".to_string(), 132 | name: "dummy".to_string(), 133 | sha: Some("dummy_sha".to_string()) 134 | } 135 | ); 136 | assert_eq!( 137 | GitHubRepo::from_str("https://github.com/dummy_org/dummy").unwrap(), 138 | GitHubRepo { 139 | org: "dummy_org".to_string(), 140 | name: "dummy".to_string(), 141 | sha: None 142 | } 143 | ); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/crates/sources/local.rs: -------------------------------------------------------------------------------- 1 | use crate::crates::{lists::List, Crate}; 2 | use crate::dirs::LOCAL_CRATES_DIR; 3 | use crate::prelude::*; 4 | use std::path::PathBuf; 5 | 6 | pub(crate) struct LocalList { 7 | source: PathBuf, 8 | } 9 | 10 | impl Default for LocalList { 11 | fn default() -> Self { 12 | LocalList { 13 | source: LOCAL_CRATES_DIR.clone(), 14 | } 15 | } 16 | } 17 | 18 | impl List for LocalList { 19 | const NAME: &'static str = "local"; 20 | 21 | fn fetch(&self) -> Fallible> { 22 | if !self.source.is_dir() { 23 | return Ok(Vec::new()); 24 | } 25 | 26 | let mut list = Vec::new(); 27 | for entry in ::std::fs::read_dir(&self.source)? { 28 | let entry = entry?; 29 | 30 | if entry.path().join("Cargo.toml").is_file() { 31 | let name = entry 32 | .file_name() 33 | .to_str() 34 | .ok_or_else(|| { 35 | anyhow!( 36 | "invalid UTF-8 in local crate name: {}", 37 | entry.file_name().to_string_lossy() 38 | ) 39 | })? 40 | .to_string(); 41 | 42 | list.push(Crate::Local(name)); 43 | } 44 | } 45 | 46 | Ok(list) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/crates/sources/mod.rs: -------------------------------------------------------------------------------- 1 | pub(in crate::crates) mod github; 2 | pub(in crate::crates) mod local; 3 | pub(in crate::crates) mod registry; 4 | -------------------------------------------------------------------------------- /src/crates/sources/registry.rs: -------------------------------------------------------------------------------- 1 | use crate::crates::{lists::List, Crate}; 2 | use crate::dirs::WORK_DIR; 3 | use crate::prelude::*; 4 | use crates_index::GitIndex; 5 | use rayon::iter::ParallelIterator; 6 | use std::collections::HashMap; 7 | use std::fs::{self}; 8 | use std::sync::Mutex; 9 | 10 | pub(crate) struct RegistryList; 11 | 12 | impl List for RegistryList { 13 | const NAME: &'static str = "registry"; 14 | 15 | fn fetch(&self) -> Fallible> { 16 | let counts = Mutex::new(HashMap::new()); 17 | 18 | debug!("updating git index"); 19 | fs::create_dir_all(&*WORK_DIR)?; 20 | let mut index = GitIndex::with_path( 21 | WORK_DIR.join("crates.io-index"), 22 | "https://github.com/rust-lang/crates.io-index", 23 | )?; 24 | index.update()?; 25 | debug!("collecting crate information"); 26 | 27 | let mut list: Vec<_> = index 28 | .crates_parallel() 29 | .filter_map(|krate| { 30 | let krate = krate.as_ref().unwrap(); 31 | // The versions() method returns the list of published versions starting from the 32 | // first one, so its output is reversed to check the latest first 33 | krate 34 | .versions() 35 | .iter() 36 | .rev() 37 | // Don't include yanked versions. If all versions are 38 | // yanked, then the crate is skipped. 39 | .filter(|version| !version.is_yanked()) 40 | .map(|version| { 41 | // Increment the counters of this crate's dependencies 42 | let mut counts = counts.lock().unwrap(); 43 | for dependency in version.dependencies() { 44 | let count = counts.entry(dependency.name().to_string()).or_insert(0); 45 | *count += 1; 46 | } 47 | Crate::Registry(RegistryCrate { 48 | name: krate.name().to_string(), 49 | version: version.version().to_string(), 50 | }) 51 | }) 52 | .next() 53 | }) 54 | .collect(); 55 | 56 | // Ensure the list is sorted by popularity 57 | let counts = counts.lock().unwrap(); 58 | list.sort_by(|a, b| { 59 | if let (Crate::Registry(ref a), Crate::Registry(ref b)) = (a, b) { 60 | let count_a = counts.get(&a.name).cloned().unwrap_or(0); 61 | let count_b = counts.get(&b.name).cloned().unwrap_or(0); 62 | count_b.cmp(&count_a) 63 | } else { 64 | panic!("non-registry crate produced in the registry list"); 65 | } 66 | }); 67 | 68 | Ok(list) 69 | } 70 | } 71 | 72 | #[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, Clone)] 73 | pub struct RegistryCrate { 74 | pub name: String, 75 | pub version: String, 76 | } 77 | -------------------------------------------------------------------------------- /src/dirs.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use std::env; 3 | use std::ffi::OsStr; 4 | use std::path::PathBuf; 5 | 6 | lazy_static! { 7 | pub static ref WORK_DIR: PathBuf = { 8 | env::var_os("CRATER_WORK_DIR") 9 | .unwrap_or_else(|| OsStr::new("work").to_os_string()) 10 | .into() 11 | }; 12 | pub static ref LOCAL_CRATES_DIR: PathBuf = "local-crates".into(); 13 | } 14 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "256"] 2 | #![allow( 3 | clippy::needless_pass_by_value, 4 | clippy::wrong_self_convention, 5 | clippy::new_ret_no_self, 6 | clippy::too_many_arguments, 7 | clippy::redundant_closure, 8 | clippy::unnecessary_wraps, 9 | clippy::needless_question_mark, 10 | clippy::vec_init_then_push, 11 | clippy::upper_case_acronyms, 12 | clippy::mutex_atomic 13 | )] 14 | 15 | pub mod actions; 16 | pub mod agent; 17 | mod assets; 18 | #[macro_use] 19 | pub mod utils; 20 | pub mod config; 21 | pub mod crates; 22 | pub mod db; 23 | pub mod dirs; 24 | pub mod experiments; 25 | mod prelude; 26 | pub mod report; 27 | pub mod results; 28 | pub mod runner; 29 | pub mod server; 30 | pub mod toolchain; 31 | 32 | pub(crate) static GIT_REVISION: Option<&str> = include!(concat!(env!("OUT_DIR"), "/sha")); 33 | pub(crate) static CRATER_REPO_URL: &str = "https://github.com/rust-lang/crater"; 34 | 35 | lazy_static::lazy_static! { 36 | pub static ref USER_AGENT: String = format!( 37 | "crater/{} ({})", 38 | crate::GIT_REVISION.unwrap_or("unknown"), 39 | crate::CRATER_REPO_URL 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::redundant_closure)] 2 | #![allow(clippy::needless_question_mark)] 3 | 4 | use log::info; 5 | mod cli; 6 | 7 | use clap::Parser; 8 | use crater::utils; 9 | use std::panic; 10 | use std::process; 11 | 12 | fn main() { 13 | // Ignore errors loading `.env` file. 14 | let _ = dotenv::dotenv(); 15 | 16 | // Ensure it's possible to close Crater with a Ctrl+C even inside Docker (as PID 1). 17 | ctrlc::set_handler(|| { 18 | std::process::exit(1); 19 | }) 20 | .unwrap(); 21 | 22 | // Initialize env_logger 23 | // This doesn't use from_default_env() because it doesn't allow to override filter_module() 24 | // with the RUST_LOG environment variable 25 | let mut env = env_logger::Builder::new(); 26 | env.filter_module("crater", log::LevelFilter::Info); 27 | env.filter_module("rustwide", log::LevelFilter::Info); 28 | if let Ok(content) = std::env::var("RUST_LOG") { 29 | env.parse_filters(&content); 30 | } 31 | rustwide::logging::init_with(env.build()); 32 | 33 | let success = match panic::catch_unwind(main_) { 34 | Ok(Ok(())) => true, 35 | Ok(Err(e)) => { 36 | utils::report_failure(&e); 37 | false 38 | } 39 | Err(e) => { 40 | utils::report_panic(&*e); 41 | false 42 | } 43 | }; 44 | info!( 45 | "{}", 46 | if success { 47 | "command succeeded" 48 | } else { 49 | "command failed" 50 | } 51 | ); 52 | process::exit(i32::from(!success)); 53 | } 54 | 55 | fn main_() -> anyhow::Result<()> { 56 | cli::Crater::parse().run() 57 | } 58 | -------------------------------------------------------------------------------- /src/prelude.rs: -------------------------------------------------------------------------------- 1 | pub use anyhow::Result as Fallible; 2 | pub use anyhow::{anyhow, bail, Context, Result}; 3 | 4 | pub use lazy_static::lazy_static; 5 | pub use log::{debug, error, info, trace, warn}; 6 | pub use serde_derive::{Deserialize, Serialize}; 7 | -------------------------------------------------------------------------------- /src/results/dummy.rs: -------------------------------------------------------------------------------- 1 | use crate::crates::Crate; 2 | use crate::experiments::Experiment; 3 | use crate::prelude::*; 4 | use crate::results::{EncodedLog, ReadResults, TestResult}; 5 | use crate::toolchain::Toolchain; 6 | use std::collections::HashMap; 7 | 8 | #[derive(Default)] 9 | struct DummyData { 10 | logs: HashMap<(Crate, Toolchain), EncodedLog>, 11 | results: HashMap<(Crate, Toolchain), TestResult>, 12 | } 13 | 14 | #[derive(Default)] 15 | pub struct DummyDB { 16 | experiments: HashMap, 17 | } 18 | 19 | impl DummyDB { 20 | fn get_data(&self, ex: &Experiment) -> Fallible<&DummyData> { 21 | Ok(self 22 | .experiments 23 | .get(&ex.name) 24 | .ok_or_else(|| anyhow!("missing experiment {}", ex.name))?) 25 | } 26 | 27 | pub fn add_dummy_log(&mut self, ex: &Experiment, krate: Crate, tc: Toolchain, log: EncodedLog) { 28 | self.experiments 29 | .entry(ex.name.to_string()) 30 | .or_default() 31 | .logs 32 | .insert((krate, tc), log); 33 | } 34 | 35 | pub fn add_dummy_result( 36 | &mut self, 37 | ex: &Experiment, 38 | krate: Crate, 39 | tc: Toolchain, 40 | res: TestResult, 41 | ) { 42 | self.experiments 43 | .entry(ex.name.to_string()) 44 | .or_default() 45 | .results 46 | .insert((krate, tc), res); 47 | } 48 | } 49 | 50 | impl ReadResults for DummyDB { 51 | fn load_log( 52 | &self, 53 | ex: &Experiment, 54 | toolchain: &Toolchain, 55 | krate: &Crate, 56 | ) -> Fallible> { 57 | Ok(self 58 | .get_data(ex)? 59 | .logs 60 | .get(&(krate.clone(), toolchain.clone())) 61 | .cloned()) 62 | } 63 | 64 | fn load_test_result( 65 | &self, 66 | ex: &Experiment, 67 | toolchain: &Toolchain, 68 | krate: &Crate, 69 | ) -> Fallible> { 70 | Ok(self 71 | .get_data(ex)? 72 | .results 73 | .get(&(krate.clone(), toolchain.clone())) 74 | .cloned()) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/runner/mod.rs: -------------------------------------------------------------------------------- 1 | mod tasks; 2 | mod test; 3 | mod unstable_features; 4 | mod worker; 5 | 6 | use crate::config::Config; 7 | use crate::crates::Crate; 8 | use crate::experiments::{Experiment, Mode}; 9 | use crate::prelude::*; 10 | use crate::results::TestResult; 11 | use crate::runner::worker::{DiskSpaceWatcher, Worker}; 12 | use rustwide::Workspace; 13 | use std::thread::scope; 14 | use std::time::Duration; 15 | pub use worker::RecordProgress; 16 | 17 | const DISK_SPACE_WATCHER_INTERVAL: Duration = Duration::from_secs(30); 18 | const DISK_SPACE_WATCHER_THRESHOLD: f32 = 0.80; 19 | 20 | #[derive(Debug, thiserror::Error)] 21 | #[error("overridden task result to {0}")] 22 | pub struct OverrideResult(TestResult); 23 | 24 | pub fn run_ex( 25 | ex: &Experiment, 26 | workspace: &Workspace, 27 | api: &dyn RecordProgress, 28 | threads_count: usize, 29 | config: &Config, 30 | next_crate: &(dyn Fn() -> Fallible> + Send + Sync), 31 | ) -> Fallible<()> { 32 | // Attempt to spin indefinitely until docker is up. Ideally, we would 33 | // decomission this agent until docker is up, instead of leaving the 34 | // assigned crates to 'hang' until we get our act together. In practice, we 35 | // expect workers to be around most of the time (just sometimes being 36 | // restarted etc.) and so the assigned crates shouldn't hang for long. 37 | // 38 | // If we return an Err(...) from this function, then currently that is 39 | // treated as a hard failure of the underlying experiment, but this error 40 | // has nothing to do with the experiment, so shouldn't be reported as such. 41 | // 42 | // In the future we'll want to *alert* on this error so that a human can 43 | // investigate, but the hope is that in practice docker is just being slow 44 | // or similar and this will fix itself, which currently makes the most sense 45 | // given low human resources. Additionally, it'll be indirectly alerted 46 | // through the worker being "down" according to our progress metrics, since 47 | // jobs won't be completed. 48 | let mut i = 0; 49 | while !rustwide::cmd::docker_running(workspace) { 50 | log::error!( 51 | "docker is not currently up, waiting for it to start (tried {} times)", 52 | i 53 | ); 54 | i += 1; 55 | } 56 | 57 | crate::agent::set_healthy(); 58 | 59 | info!("uninstalling toolchains..."); 60 | // Clean out all the toolchains currently installed. This minimizes the 61 | // amount of disk space used by the base system, letting the task execution 62 | // proceed slightly faster than it would otherwise. 63 | for tc in workspace.installed_toolchains()? { 64 | // But don't uninstall it if we're going to reinstall in a couple lines. 65 | // And don't uninstall stable, since that is mainly used for 66 | // installing tools. 67 | if !tc.is_needed_by_rustwide() && !ex.toolchains.iter().any(|t| tc == t.source) { 68 | tc.uninstall(workspace)?; 69 | } 70 | } 71 | 72 | info!("preparing the execution..."); 73 | for tc in &ex.toolchains { 74 | tc.install(workspace)?; 75 | if ex.mode == Mode::Clippy { 76 | tc.add_component(workspace, "clippy")?; 77 | } 78 | if let Some(requested_target) = &tc.target { 79 | tc.add_target(workspace, requested_target)?; 80 | } 81 | } 82 | 83 | info!("running tasks in {} threads...", threads_count); 84 | 85 | let workers = (0..threads_count) 86 | .map(|i| { 87 | Worker::new( 88 | format!("worker-{i}"), 89 | workspace, 90 | ex, 91 | config, 92 | api, 93 | next_crate, 94 | ) 95 | }) 96 | .collect::>(); 97 | 98 | let disk_watcher = DiskSpaceWatcher::new( 99 | DISK_SPACE_WATCHER_INTERVAL, 100 | DISK_SPACE_WATCHER_THRESHOLD, 101 | &workers, 102 | ); 103 | 104 | scope(|scope1| { 105 | std::thread::Builder::new() 106 | .name("disk-space-watcher".into()) 107 | .spawn_scoped(scope1, || { 108 | disk_watcher.run(); 109 | }) 110 | .unwrap(); 111 | 112 | scope(|scope| { 113 | for worker in &workers { 114 | std::thread::Builder::new() 115 | .name(worker.name().into()) 116 | .spawn_scoped(scope, move || -> Fallible<()> { 117 | match worker.run() { 118 | Ok(()) => Ok(()), 119 | Err(r) => { 120 | log::warn!("worker {} failed: {:?}", worker.name(), r); 121 | Err(r) 122 | } 123 | } 124 | }) 125 | .unwrap(); 126 | } 127 | }); 128 | 129 | disk_watcher.stop(); 130 | }); 131 | 132 | Ok(()) 133 | } 134 | -------------------------------------------------------------------------------- /src/runner/tasks.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use crate::crates::Crate; 3 | use crate::experiments::Experiment; 4 | use crate::prelude::*; 5 | use crate::results::TestResult; 6 | use crate::runner::test; 7 | use crate::toolchain::Toolchain; 8 | use rustwide::{Build, BuildDirectory}; 9 | use std::collections::HashMap; 10 | use std::sync::Mutex; 11 | 12 | use rustwide::logging::LogStorage; 13 | use std::fmt; 14 | 15 | pub(super) struct TaskCtx<'ctx> { 16 | pub(super) build_dir: &'ctx Mutex, 17 | pub(super) config: &'ctx Config, 18 | pub(super) experiment: &'ctx Experiment, 19 | pub(super) toolchain: &'ctx Toolchain, 20 | pub(super) krate: &'ctx Crate, 21 | pub(super) quiet: bool, 22 | } 23 | 24 | impl<'ctx> TaskCtx<'ctx> { 25 | fn new( 26 | build_dir: &'ctx Mutex, 27 | config: &'ctx Config, 28 | experiment: &'ctx Experiment, 29 | toolchain: &'ctx Toolchain, 30 | krate: &'ctx Crate, 31 | quiet: bool, 32 | ) -> Self { 33 | TaskCtx { 34 | build_dir, 35 | config, 36 | experiment, 37 | toolchain, 38 | krate, 39 | quiet, 40 | } 41 | } 42 | } 43 | 44 | pub(super) enum TaskStep { 45 | BuildAndTest { tc: Toolchain, quiet: bool }, 46 | BuildOnly { tc: Toolchain, quiet: bool }, 47 | CheckOnly { tc: Toolchain, quiet: bool }, 48 | Clippy { tc: Toolchain, quiet: bool }, 49 | Rustdoc { tc: Toolchain, quiet: bool }, 50 | UnstableFeatures { tc: Toolchain }, 51 | } 52 | 53 | impl fmt::Debug for TaskStep { 54 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 55 | let (name, quiet, tc) = match *self { 56 | TaskStep::BuildAndTest { ref tc, quiet } => ("build and test", quiet, Some(tc)), 57 | TaskStep::BuildOnly { ref tc, quiet } => ("build", quiet, Some(tc)), 58 | TaskStep::CheckOnly { ref tc, quiet } => ("check", quiet, Some(tc)), 59 | TaskStep::Clippy { ref tc, quiet } => ("clippy", quiet, Some(tc)), 60 | TaskStep::Rustdoc { ref tc, quiet } => ("doc", quiet, Some(tc)), 61 | TaskStep::UnstableFeatures { ref tc } => ("find unstable features on", false, Some(tc)), 62 | }; 63 | 64 | write!(f, "{name}")?; 65 | if let Some(tc) = tc { 66 | write!(f, " {tc}")?; 67 | } 68 | if quiet { 69 | write!(f, " (quiet)")?; 70 | } 71 | Ok(()) 72 | } 73 | } 74 | 75 | pub(super) struct Task { 76 | pub(super) krate: Crate, 77 | pub(super) step: TaskStep, 78 | } 79 | 80 | impl fmt::Debug for Task { 81 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 82 | write!(f, "{:?} of crate {}", self.step, self.krate) 83 | } 84 | } 85 | 86 | impl Task { 87 | pub(super) fn run<'ctx, 's: 'ctx>( 88 | &'s self, 89 | config: &'ctx Config, 90 | build_dir: &'ctx HashMap<&'ctx crate::toolchain::Toolchain, Mutex>, 91 | ex: &'ctx Experiment, 92 | logs: &LogStorage, 93 | ) -> Fallible { 94 | let (build_dir, action, test, toolchain, quiet): ( 95 | _, 96 | _, 97 | fn(&TaskCtx, &Build, &_) -> _, 98 | _, 99 | _, 100 | ) = match self.step { 101 | TaskStep::BuildAndTest { ref tc, quiet } => ( 102 | &build_dir[tc], 103 | "testing", 104 | test::test_build_and_test, 105 | tc, 106 | quiet, 107 | ), 108 | TaskStep::BuildOnly { ref tc, quiet } => { 109 | (&build_dir[tc], "building", test::test_build_only, tc, quiet) 110 | } 111 | TaskStep::CheckOnly { ref tc, quiet } => { 112 | (&build_dir[tc], "checking", test::test_check_only, tc, quiet) 113 | } 114 | TaskStep::Clippy { ref tc, quiet } => { 115 | (&build_dir[tc], "linting", test::test_clippy_only, tc, quiet) 116 | } 117 | TaskStep::Rustdoc { ref tc, quiet } => { 118 | (&build_dir[tc], "documenting", test::test_rustdoc, tc, quiet) 119 | } 120 | TaskStep::UnstableFeatures { ref tc } => ( 121 | &build_dir[tc], 122 | "checking unstable", 123 | crate::runner::unstable_features::find_unstable_features, 124 | tc, 125 | false, 126 | ), 127 | }; 128 | 129 | let ctx = TaskCtx::new(build_dir, config, ex, toolchain, &self.krate, quiet); 130 | test::run_test(action, &ctx, test, logs) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/runner/unstable_features.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use crate::results::TestResult; 3 | use crate::runner::tasks::TaskCtx; 4 | use cargo_metadata::Package; 5 | use rustwide::Build; 6 | use std::collections::HashSet; 7 | use std::path::Path; 8 | use walkdir::{DirEntry, WalkDir}; 9 | 10 | pub(super) fn find_unstable_features( 11 | _ctx: &TaskCtx, 12 | build: &Build, 13 | _local_packages_id: &[Package], 14 | ) -> Fallible { 15 | let mut features = HashSet::new(); 16 | 17 | for entry in WalkDir::new(build.host_source_dir()) 18 | .into_iter() 19 | .filter_entry(|e| !is_hidden(e)) 20 | { 21 | let entry = entry?; 22 | if !entry 23 | .file_name() 24 | .to_str() 25 | .map(|s| s.contains(".rs")) 26 | .unwrap_or(false) 27 | { 28 | continue; 29 | } 30 | if !entry.file_type().is_file() { 31 | continue; 32 | } 33 | 34 | let new_features = parse_features(entry.path())?; 35 | 36 | for feature in new_features { 37 | features.insert(feature); 38 | } 39 | } 40 | 41 | let mut features: Vec<_> = features.into_iter().collect(); 42 | features.sort(); 43 | for feature in features { 44 | info!("unstable-feature: {}", feature); 45 | } 46 | 47 | Ok(TestResult::TestPass) 48 | } 49 | 50 | fn parse_features(path: &Path) -> Fallible> { 51 | let mut features = Vec::new(); 52 | let contents = ::std::fs::read_to_string(path)?; 53 | for (hash_idx, _) in contents.match_indices('#') { 54 | let contents = &contents[hash_idx + 1..]; 55 | let contents = eat_token(Some(contents), "!").or(Some(contents)); 56 | let contents = eat_token(contents, "["); 57 | let contents = eat_token(contents, "feature"); 58 | let new_features = parse_list(contents, "(", ")"); 59 | features.extend_from_slice(&new_features); 60 | } 61 | 62 | Ok(features) 63 | } 64 | 65 | fn is_hidden(entry: &DirEntry) -> bool { 66 | entry 67 | .file_name() 68 | .to_str() 69 | .map(|s| s.starts_with('.')) 70 | .unwrap_or(false) 71 | } 72 | 73 | fn eat_token<'a>(s: Option<&'a str>, tok: &str) -> Option<&'a str> { 74 | eat_whitespace(s).and_then(|s| s.strip_prefix(tok)) 75 | } 76 | 77 | fn eat_whitespace(s: Option<&str>) -> Option<&str> { 78 | s.and_then(|s| { 79 | #[allow(clippy::manual_map)] 80 | if let Some(i) = s.find(|c: char| !c.is_whitespace()) { 81 | Some(&s[i..]) 82 | } else { 83 | None 84 | } 85 | }) 86 | } 87 | 88 | fn parse_list(s: Option<&str>, open: &str, close: &str) -> Vec { 89 | let s = eat_whitespace(s); 90 | let s = eat_token(s, open); 91 | if let Some(s) = s { 92 | if let Some(i) = s.find(close) { 93 | let s = &s[..i]; 94 | return s.split(',').map(|s| s.trim().to_string()).collect(); 95 | } 96 | } 97 | 98 | Vec::new() 99 | } 100 | -------------------------------------------------------------------------------- /src/server/api_types.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use crate::prelude::*; 3 | use http::header::{HeaderValue, CONTENT_TYPE}; 4 | use http::Response; 5 | use http::StatusCode; 6 | use hyper::Body; 7 | use serde::Serialize; 8 | use std::fmt; 9 | use std::fmt::Display; 10 | use std::str::FromStr; 11 | 12 | #[derive(Serialize, Deserialize)] 13 | #[serde(rename_all = "kebab-case")] 14 | pub struct AgentConfig { 15 | pub agent_name: String, 16 | pub crater_config: Config, 17 | } 18 | 19 | #[derive(Serialize, Deserialize)] 20 | #[serde(tag = "status", rename_all = "kebab-case")] 21 | pub enum ApiResponse { 22 | Success { result: T }, 23 | SlowDown, 24 | InternalError { error: String }, 25 | Unauthorized, 26 | NotFound, 27 | } 28 | 29 | impl ApiResponse<()> { 30 | pub(in crate::server) fn internal_error(error: String) -> ApiResponse<()> { 31 | ApiResponse::InternalError { error } 32 | } 33 | 34 | pub(in crate::server) fn unauthorized() -> ApiResponse<()> { 35 | ApiResponse::Unauthorized 36 | } 37 | 38 | pub(in crate::server) fn not_found() -> ApiResponse<()> { 39 | ApiResponse::NotFound 40 | } 41 | } 42 | 43 | impl ApiResponse { 44 | fn status_code(&self) -> StatusCode { 45 | match self { 46 | ApiResponse::Success { .. } => StatusCode::OK, 47 | ApiResponse::SlowDown => StatusCode::TOO_MANY_REQUESTS, 48 | ApiResponse::InternalError { .. } => StatusCode::INTERNAL_SERVER_ERROR, 49 | ApiResponse::Unauthorized => StatusCode::UNAUTHORIZED, 50 | ApiResponse::NotFound => StatusCode::NOT_FOUND, 51 | } 52 | } 53 | } 54 | 55 | impl ApiResponse { 56 | pub(in crate::server) fn into_response(self) -> Fallible> { 57 | let serialized = ::serde_json::to_vec(&self)?; 58 | 59 | let mut resp = Response::new(serialized.into()); 60 | resp.headers_mut() 61 | .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); 62 | *resp.status_mut() = self.status_code(); 63 | Ok(resp) 64 | } 65 | } 66 | 67 | #[derive(Debug, Clone)] 68 | pub struct CraterToken { 69 | pub token: String, 70 | } 71 | 72 | impl Display for CraterToken { 73 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 74 | write!(f, "CraterToken {}", self.token) 75 | } 76 | } 77 | 78 | impl FromStr for CraterToken { 79 | type Err = ::hyper::Error; 80 | 81 | fn from_str(s: &str) -> ::hyper::Result { 82 | Ok(CraterToken { 83 | token: s.to_owned(), 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/server/cronjobs.rs: -------------------------------------------------------------------------------- 1 | use crate::actions::{Action, ActionsCtx, UpdateLists}; 2 | use crate::prelude::*; 3 | use crate::server::Data; 4 | use crate::utils; 5 | use std::sync::Arc; 6 | use std::thread; 7 | use std::time::Duration; 8 | 9 | const DAY: Duration = Duration::from_secs(60 * 60 * 24); 10 | struct JobDescription { 11 | name: &'static str, 12 | interval: Duration, 13 | exec: fn(Arc) -> Fallible<()>, 14 | } 15 | 16 | static JOBS: &[JobDescription] = &[JobDescription { 17 | name: "crates lists update", 18 | interval: DAY, 19 | exec: update_crates as fn(Arc) -> Fallible<()>, 20 | }]; 21 | 22 | pub fn spawn(data: Data) { 23 | let data = Arc::new(data); 24 | for job in JOBS { 25 | // needed to make the borrowck happy 26 | let data = Arc::clone(&data); 27 | 28 | thread::spawn(move || loop { 29 | let result = (job.exec)(Arc::clone(&data)); 30 | if let Err(e) = result { 31 | utils::report_failure(&e); 32 | } 33 | 34 | info!( 35 | "the {} thread will be respawned in {}s", 36 | job.name, 37 | job.interval.as_secs() 38 | ); 39 | thread::sleep(job.interval); 40 | }); 41 | } 42 | } 43 | 44 | fn update_crates(data: Arc) -> Fallible<()> { 45 | let ctx = ActionsCtx::new(&data.db, &data.config); 46 | 47 | UpdateLists { 48 | github: true, 49 | registry: true, 50 | local: false, 51 | } 52 | .apply(&ctx) 53 | } 54 | -------------------------------------------------------------------------------- /src/server/messages.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use crate::server::github::GitHub; 3 | use crate::server::{Data, GithubData}; 4 | use std::fmt::Write; 5 | 6 | pub enum Label { 7 | ExperimentQueued, 8 | ExperimentCompleted, 9 | } 10 | 11 | struct Line { 12 | emoji: String, 13 | content: String, 14 | } 15 | 16 | pub struct Message { 17 | lines: Vec, 18 | notes: Vec, 19 | new_label: Option