diff --git a/anneal/v1/playground/rust-anneal-playground/.github/ISSUE_TEMPLATE/crate-addition-or-update.md b/anneal/v1/playground/rust-anneal-playground/.github/ISSUE_TEMPLATE/crate-addition-or-update.md new file mode 100644 index 0000000000..02e817c949 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/.github/ISSUE_TEMPLATE/crate-addition-or-update.md @@ -0,0 +1,12 @@ +--- +name: Crate addition or update +about: Suggest a new or updated crate for the playground +title: Please add/update [cratename] +labels: wontfix +assignees: '' + +--- + +Please read [our crate policy][policy]. Any issue opened for a new or updated crate version will be closed with a reference to the same policy. + +[policy]: https://github.com/rust-lang/rust-playground/blob/master/CRATE_POLICY.md diff --git a/anneal/v1/playground/rust-anneal-playground/.github/actions/build-and-push-image/action.yml b/anneal/v1/playground/rust-anneal-playground/.github/actions/build-and-push-image/action.yml new file mode 100644 index 0000000000..0a54b3055e --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/.github/actions/build-and-push-image/action.yml @@ -0,0 +1,26 @@ +name: "Build and push image" +description: "Build and push image" + +inputs: + channel: + description: "The Rust channel to build" + required: true + + tags: + description: "The newline-separated tags to create" + required: true + +runs: + using: "composite" + + steps: + - uses: docker/build-push-action@v6 + with: + context: compiler/base/ + file: compiler/base/Dockerfile + build-args: |- + channel=${{ inputs.channel }} + push: true + tags: ${{ inputs.tags }} + cache-from: type=gha,scope=${{ inputs.channel }} + cache-to: type=gha,scope=${{ inputs.channel }},mode=max diff --git a/anneal/v1/playground/rust-anneal-playground/.github/workflows/ci.yml b/anneal/v1/playground/rust-anneal-playground/.github/workflows/ci.yml new file mode 100644 index 0000000000..8523256bf2 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/.github/workflows/ci.yml @@ -0,0 +1,265 @@ +# This file was generated by ci/generate and should not be modified by hand +--- +name: Validate everything +'on': + push: + pull_request: + types: + - labeled + branches: + - main +env: + DOCKER_HUB_USERNAME: shepmaster + GH_CONTAINER_REGISTRY_USERNAME: shepmaster + AWS_ACCESS_KEY_ID: AKIAWESVHZ3JQAY5NM5K +jobs: + build_compiler_images: + name: Build provisional ${{ matrix.channel }} image + runs-on: ubuntu-latest + strategy: + matrix: + channel: + - stable + - beta + - nightly + if: 'github.event_name == ''push'' || contains(github.event.pull_request.labels.*.name, ''CI: approved'')' + env: + GHCR_IMAGE_NAME: ghcr.io/integer32llc/rust-playground-ci-rust-${{ matrix.channel }} + DOCKER_HUB_IMAGE_NAME: shepmaster/rust-${{ matrix.channel }} + steps: + - name: Checkout code + uses: actions/checkout@v6 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: "${{ env.GH_CONTAINER_REGISTRY_USERNAME }}" + password: "${{ secrets.GH_CONTAINER_REGISTRY_TOKEN }}" + - name: Build provisional ${{ matrix.channel }} image + uses: "./.github/actions/build-and-push-image" + with: + channel: "${{ matrix.channel }}" + tags: "${{ env.GHCR_IMAGE_NAME }}:${{ github.run_id }}" + build_backend: + name: Build backend + runs-on: ubuntu-latest + if: 'github.event_name == ''push'' || contains(github.event.pull_request.labels.*.name, ''CI: approved'')' + steps: + - name: Checkout code + uses: actions/checkout@v6 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - name: Format server + run: cargo fmt --manifest-path ui/Cargo.toml --all --check + - name: Format top-crates + run: cargo fmt --manifest-path top-crates/Cargo.toml --check + - name: Format orchestrator + run: cargo fmt --manifest-path compiler/base/orchestrator/Cargo.toml --check + - name: Build backend + run: "./ci/build-backend.sh" + - name: Save backend artifact + uses: actions/upload-artifact@v6 + with: + name: backend + path: | + docker-output/ui + docker-output/unit_tests_ui + docker-output/unit_tests_orchestrator + build_frontend: + name: Build frontend + runs-on: ubuntu-latest + if: 'github.event_name == ''push'' || contains(github.event.pull_request.labels.*.name, ''CI: approved'')' + steps: + - name: Checkout code + uses: actions/checkout@v6 + - name: Configure node + uses: actions/setup-node@v6 + with: + node-version: 24.12 + - name: Get pnpm version from package.json + id: pnpm-version + run: 'echo "pnpm_version=$(node -p ''require(`./ui/frontend/package.json`).engines.pnpm'')" >> $GITHUB_OUTPUT + + ' + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: "${{ steps.pnpm-version.outputs.pnpm_version }}" + - name: Get pnpm store directory + id: pnpm-cache-dir-path + run: 'echo "dir=$(pnpm store path --silent)" >> $GITHUB_OUTPUT + + ' + - name: Cache pnpm intermediate products + uses: actions/cache@v5 + with: + path: "${{ steps.pnpm-cache-dir-path.outputs.dir }}" + key: "${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}" + restore-keys: "${{ runner.os }}-pnpm-\n" + - name: Install dependencies + run: pnpm --dir ui/frontend/ install + - name: Run tests + run: pnpm --dir ui/frontend/ run test + - name: Lint + run: pnpm --dir ui/frontend/ run test:lint + - name: Style + run: pnpm --dir ui/frontend/ run test:style + - name: Build frontend + run: pnpm --dir ui/frontend/ run build:production + - name: Save frontend artifact + uses: actions/upload-artifact@v6 + with: + name: frontend + path: ui/frontend/build + run_integration_tests: + name: Running integration tests + runs-on: ubuntu-latest + if: 'github.event_name == ''push'' || contains(github.event.pull_request.labels.*.name, ''CI: approved'')' + needs: + - build_compiler_images + - build_backend + - build_frontend + defaults: + run: + working-directory: tests + steps: + - name: Checkout code + uses: actions/checkout@v6 + - name: Recover disk space + run: sudo rm -rf /usr/local/lib/android + - name: Configure Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.3.6 + - name: Cache bundler intermediate products + uses: actions/cache@v5 + with: + path: tests/vendor/bundle + key: "${{ runner.os }}-gems-${{ hashFiles('tests/**/Gemfile.lock') }}" + restore-keys: "${{ runner.os }}-gems-" + - name: Install gems + run: |- + gem install bundler + bundle config path vendor/bundle + bundle install --jobs 4 --retry 3 + - name: Pull images + run: echo ghcr.io/integer32llc/rust-playground-ci-rust-{stable,beta,nightly}:${{ github.run_id }} | xargs -n1 docker pull --platform linux/amd64 + - name: Rename images + run: |- + for c in stable beta nightly; do + docker tag ghcr.io/integer32llc/rust-playground-ci-rust-$c:${{ github.run_id }} rust-$c + done + - name: Download backend + uses: actions/download-artifact@v7 + with: + name: backend + path: tests/server/ + - name: Download frontend + uses: actions/download-artifact@v7 + with: + name: frontend + path: tests/server/build/ + - name: Run orchestrator unit tests + env: + TESTS_MAX_CONCURRENCY: 3 + TESTS_TIMEOUT_MS: 30000 + run: chmod +x ./server/unit_tests_orchestrator && ./server/unit_tests_orchestrator + - name: Run ui unit tests + run: chmod +x ./server/unit_tests_ui && ./server/unit_tests_ui + - name: Run tests + env: + PLAYGROUND_UI_ROOT: server/build/ + PLAYGROUND_CORS_ENABLED: true + PLAYGROUND_GITHUB_TOKEN: "${{ secrets.PLAYGROUND_GITHUB_TOKEN }}" + run: |- + killall -q ui || true + chmod +x ./server/ui && ./server/ui & + bundle exec rspec + - name: Preserve screenshots + if: "${{ failure() }}" + uses: actions/upload-artifact@v6 + with: + name: test-failures + path: tests/test-failures + release_artifacts: + name: Release artifacts + runs-on: ubuntu-latest + needs: + - run_integration_tests + if: github.event_name == 'push' && github.event.ref == 'refs/heads/main' + permissions: + contents: read + id-token: write + steps: + - name: Download backend + uses: actions/download-artifact@v7 + with: + name: backend + path: server/ + - name: Download frontend + uses: actions/download-artifact@v7 + with: + name: frontend + path: server/build/ + - name: Configure AWS credentials (i32) + uses: aws-actions/configure-aws-credentials@v6 + with: + aws-access-key-id: "${{ env.AWS_ACCESS_KEY_ID }}" + aws-secret-access-key: "${{ secrets.AWS_SECRET_ACCESS_KEY }}" + aws-region: us-east-2 + - name: Push backend (i32) + run: aws s3 cp server/ui s3://playground-artifacts-i32 + - name: Push frontend (i32) + run: aws s3 sync server/build/ s3://playground-artifacts-i32/build + - name: Configure AWS credentials (rust-lang) + uses: aws-actions/configure-aws-credentials@v6 + with: + role-skip-session-tagging: true + role-to-assume: arn:aws:iam::890664054962:role/upload-playground-artifacts + aws-region: us-west-1 + - name: Push backend (rust-lang) + run: aws s3 cp server/ui s3://rust-playground-artifacts + - name: Push frontend (rust-lang) + run: aws s3 sync server/build/ s3://rust-playground-artifacts/build + release_docker_artifacts: + name: Release ${{ matrix.channel }} image + runs-on: ubuntu-latest + strategy: + matrix: + channel: + - stable + - beta + - nightly + needs: + - run_integration_tests + env: + GHCR_IMAGE_NAME: ghcr.io/integer32llc/rust-playground-ci-rust-${{ matrix.channel }} + DOCKER_HUB_IMAGE_NAME: shepmaster/rust-${{ matrix.channel }} + if: github.event_name == 'push' && github.event.ref == 'refs/heads/main' + steps: + - name: Checkout code + uses: actions/checkout@v6 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: "${{ env.GH_CONTAINER_REGISTRY_USERNAME }}" + password: "${{ secrets.GH_CONTAINER_REGISTRY_TOKEN }}" + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: "${{ env.DOCKER_HUB_USERNAME }}" + password: "${{ secrets.DOCKER_HUB_TOKEN }}" + - name: Release ${{ matrix.channel }} image + uses: "./.github/actions/build-and-push-image" + with: + channel: "${{ matrix.channel }}" + tags: |- + ${{ env.GHCR_IMAGE_NAME }} + ${{ env.DOCKER_HUB_IMAGE_NAME }} diff --git a/anneal/v1/playground/rust-anneal-playground/.github/workflows/cron.yml b/anneal/v1/playground/rust-anneal-playground/.github/workflows/cron.yml new file mode 100644 index 0000000000..a684f98ff7 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/.github/workflows/cron.yml @@ -0,0 +1,48 @@ +# This file was generated by ci/generate and should not be modified by hand +--- +name: Scheduled rebuild +'on': + workflow_dispatch: + schedule: + - cron: 7 2 * * * +env: + DOCKER_HUB_USERNAME: shepmaster + GH_CONTAINER_REGISTRY_USERNAME: shepmaster + AWS_ACCESS_KEY_ID: AKIAWESVHZ3JQAY5NM5K +jobs: + build_compiler_images: + name: Release ${{ matrix.channel }} image + runs-on: ubuntu-latest + strategy: + matrix: + channel: + - stable + - beta + - nightly + env: + GHCR_IMAGE_NAME: ghcr.io/integer32llc/rust-playground-ci-rust-${{ matrix.channel }} + DOCKER_HUB_IMAGE_NAME: shepmaster/rust-${{ matrix.channel }} + continue-on-error: true + steps: + - name: Checkout code + uses: actions/checkout@v6 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: "${{ env.GH_CONTAINER_REGISTRY_USERNAME }}" + password: "${{ secrets.GH_CONTAINER_REGISTRY_TOKEN }}" + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: "${{ env.DOCKER_HUB_USERNAME }}" + password: "${{ secrets.DOCKER_HUB_TOKEN }}" + - name: Release ${{ matrix.channel }} image + uses: "./.github/actions/build-and-push-image" + with: + channel: "${{ matrix.channel }}" + tags: |- + ${{ env.GHCR_IMAGE_NAME }} + ${{ env.DOCKER_HUB_IMAGE_NAME }} diff --git a/anneal/v1/playground/rust-anneal-playground/ci/Dockerfile b/anneal/v1/playground/rust-anneal-playground/ci/Dockerfile new file mode 100644 index 0000000000..6fedf8d260 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ci/Dockerfile @@ -0,0 +1,42 @@ +FROM rust:alpine + +RUN apk add musl-dev openssl-dev openssl-libs-static + +# Adding -C relocation-model=static due to +# https://github.com/rust-lang/rust/issues/95926 + +# Adding this to find the statically-built version +ENV OPENSSL_NO_PKG_CONFIG=1 OPENSSL_STATIC=1 OPENSSL_DIR=/usr/ + +RUN mkdir /output + +COPY compiler/base/asm-cleanup /compiler/base/asm-cleanup +COPY compiler/base/orchestrator /compiler/base/orchestrator +COPY compiler/base/modify-cargo-toml /compiler/base/modify-cargo-toml +COPY ui /ui + +WORKDIR /compiler/base/orchestrator + +RUN \ + cargo rustc --profile test --lib --locked -- --cfg force_docker -C relocation-model=static; \ + test_bin=$(find target/debug/deps/ -name "orchestrator*" -type f -perm -a=x); \ + mv "${test_bin}" /output/unit_tests_orchestrator; + +WORKDIR /ui + +RUN \ + cargo rustc --tests --locked -- -C relocation-model=static; \ + test_bin=$(find target/debug/deps/ -name "ui*" -type f -perm -a=x); \ + mv "${test_bin}" /output/unit_tests_ui; + +# Bake in debug information to debug deadlocks in production +RUN mkdir .cargo +RUN cat <.cargo/config.toml +[profile.release] +debug = "full" +split-debuginfo = "off" +EOF + +RUN \ + cargo rustc --locked --release -- -C relocation-model=static; \ + mv target/release/ui /output/ui; diff --git a/anneal/v1/playground/rust-anneal-playground/ci/Dockerfile.dockerignore b/anneal/v1/playground/rust-anneal-playground/ci/Dockerfile.dockerignore new file mode 100644 index 0000000000..ce1fa91b61 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ci/Dockerfile.dockerignore @@ -0,0 +1,10 @@ +* +!compiler/base/asm-cleanup/** +compiler/base/asm-cleanup/target +!compiler/base/orchestrator/** +compiler/base/orchestrator/target +!compiler/base/modify-cargo-toml/** +compiler/base/modify-cargo-toml/target +!ui/** +ui/frontend +ui/target diff --git a/anneal/v1/playground/rust-anneal-playground/ci/Gemfile b/anneal/v1/playground/rust-anneal-playground/ci/Gemfile new file mode 100644 index 0000000000..a41e731d6d --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ci/Gemfile @@ -0,0 +1,3 @@ +# frozen_string_literal: true +source "https://rubygems.org" +ruby "3.3.6" diff --git a/anneal/v1/playground/rust-anneal-playground/ci/build-backend.sh b/anneal/v1/playground/rust-anneal-playground/ci/build-backend.sh new file mode 100755 index 0000000000..53671d5cdc --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ci/build-backend.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -eu + +IMAGE_NAME=backend-build +OUTPUT_DIR=docker-output + +docker build -t "${IMAGE_NAME}" -f ci/Dockerfile . + +mkdir -p "${OUTPUT_DIR}" + +container_id=$(docker create "${IMAGE_NAME}") +for f in unit_tests_orchestrator unit_tests_ui ui; do + docker cp "${container_id}:/output/${f}" "${OUTPUT_DIR}" +done +docker rm -f "${container_id}" diff --git a/anneal/v1/playground/rust-anneal-playground/ci/generate b/anneal/v1/playground/rust-anneal-playground/ci/generate new file mode 100755 index 0000000000..7db945f92a --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ci/generate @@ -0,0 +1,23 @@ +#!/usr/bin/env ruby + +require 'json' +require 'pathname' +require 'psych' + +workflows_path = Pathname.new(__dir__).join('..', '.github', 'workflows') + +workflows = Psych.load_file("workflows.yml", aliases: true) +workflows['workflows'].each do |k, v| + # Roundtrip through JSON to perform a deep clone of the value. + # If we don't, then anchors are preserved from the input file + # and GitHub disallows anchors. + v = JSON.parse(JSON.generate(v)) + + workflow_path = workflows_path.join(k).sub_ext('.yml') + File.open(workflow_path, 'w') do |file| + file.write <<~EOF + # This file was generated by ci/generate and should not be modified by hand + EOF + file.write(Psych.dump(v, line_width: -1)) + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/ci/workflows.yml b/anneal/v1/playground/rust-anneal-playground/ci/workflows.yml new file mode 100644 index 0000000000..4cffab7aa6 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ci/workflows.yml @@ -0,0 +1,375 @@ +components: + - global_env: &global_env + env: + DOCKER_HUB_USERNAME: shepmaster + GH_CONTAINER_REGISTRY_USERNAME: shepmaster + AWS_ACCESS_KEY_ID: AKIAWESVHZ3JQAY5NM5K + + - checkout: &checkout + name: "Checkout code" + uses: actions/checkout@v6 + + - checkout_pr: &checkout_pr + name: "Checkout code" + uses: actions/checkout@v6 + + - recover_disk_space: &recover_disk_space + name: Recover disk space + run: |- + sudo rm -rf /usr/local/lib/android + + - docker_buildx: &docker_buildx + name: "Set up Docker Buildx" + uses: docker/setup-buildx-action@v3 + + - login_ghcr: &login_ghcr + name: "Login to GitHub Container Registry" + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ env.GH_CONTAINER_REGISTRY_USERNAME }} + password: ${{ secrets.GH_CONTAINER_REGISTRY_TOKEN }} + + - login_docker_hub: &login_docker_hub + name: "Login to Docker Hub" + uses: docker/login-action@v3 + with: + username: ${{ env.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - each_channel_matrix_job: &each_channel_matrix_job + runs-on: ubuntu-latest + + strategy: + matrix: + channel: [stable, beta, nightly] + + - image_names_env: &image_names_env + GHCR_IMAGE_NAME: ghcr.io/integer32llc/rust-playground-ci-rust-${{ matrix.channel }} + DOCKER_HUB_IMAGE_NAME: shepmaster/rust-${{ matrix.channel }} + +workflows: + ci: + name: "Validate everything" + + 'on': + push: + pull_request: + types: [labeled] + branches: + - main + + <<: *global_env + + jobs: + build_compiler_images: + name: "Build provisional ${{ matrix.channel }} image" + <<: *each_channel_matrix_job + if: "github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'CI: approved')" + env: + <<: *image_names_env + + steps: + - *checkout_pr + - *docker_buildx + - *login_ghcr + + - name: "Build provisional ${{ matrix.channel }} image" + uses: ./.github/actions/build-and-push-image + with: + channel: ${{ matrix.channel }} + tags: |- + ${{ env.GHCR_IMAGE_NAME }}:${{ github.run_id }} + + build_backend: + name: "Build backend" + runs-on: ubuntu-latest + if: "github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'CI: approved')" + + steps: + - *checkout_pr + + - name: "Install Rust" + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + + - name: "Format server" + run: cargo fmt --manifest-path ui/Cargo.toml --all --check + + - name: "Format top-crates" + run: cargo fmt --manifest-path top-crates/Cargo.toml --check + + - name: "Format orchestrator" + run: cargo fmt --manifest-path compiler/base/orchestrator/Cargo.toml --check + + - name: "Build backend" + run: ./ci/build-backend.sh + + - name: "Save backend artifact" + uses: actions/upload-artifact@v6 + with: + name: backend + path: | + docker-output/ui + docker-output/unit_tests_ui + docker-output/unit_tests_orchestrator + + build_frontend: + name: "Build frontend" + runs-on: ubuntu-latest + if: "github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'CI: approved')" + + steps: + - *checkout_pr + + - name: "Configure node" + uses: actions/setup-node@v6 + with: + node-version: 24.12 + + - name: "Get pnpm version from package.json" + id: pnpm-version + run: | + echo "pnpm_version=$(node -p 'require(`./ui/frontend/package.json`).engines.pnpm')" >> $GITHUB_OUTPUT + + - name: "Install pnpm" + uses: pnpm/action-setup@v4 + with: + version: ${{ steps.pnpm-version.outputs.pnpm_version }} + + - name: "Get pnpm store directory" + id: pnpm-cache-dir-path + run: | + echo "dir=$(pnpm store path --silent)" >> $GITHUB_OUTPUT + + - name: "Cache pnpm intermediate products" + uses: actions/cache@v5 + with: + path: ${{ steps.pnpm-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm- + + - name: "Install dependencies" + run: >- + pnpm --dir ui/frontend/ install + + - name: "Run tests" + run: >- + pnpm --dir ui/frontend/ run test + + - name: "Lint" + run: >- + pnpm --dir ui/frontend/ run test:lint + + - name: "Style" + run: >- + pnpm --dir ui/frontend/ run test:style + + - name: "Build frontend" + run: >- + pnpm --dir ui/frontend/ run build:production + + - name: "Save frontend artifact" + uses: actions/upload-artifact@v6 + with: + name: frontend + path: ui/frontend/build + + run_integration_tests: + name: "Running integration tests" + runs-on: ubuntu-latest + if: "github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'CI: approved')" + needs: + - build_compiler_images + - build_backend + - build_frontend + + defaults: + run: + working-directory: tests + + steps: + - *checkout_pr + + - *recover_disk_space + + - name: "Configure Ruby" + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3.6' + + - name: "Cache bundler intermediate products" + uses: actions/cache@v5 + with: + path: tests/vendor/bundle + key: ${{ runner.os }}-gems-${{ hashFiles('tests/**/Gemfile.lock') }} + restore-keys: |- + ${{ runner.os }}-gems- + + - name: "Install gems" + run: |- + gem install bundler + bundle config path vendor/bundle + bundle install --jobs 4 --retry 3 + + - name: "Pull images" + run: |- + echo ghcr.io/integer32llc/rust-playground-ci-rust-{stable,beta,nightly}:${{ github.run_id }} | xargs -n1 docker pull --platform linux/amd64 + + - name: "Rename images" + run: |- + for c in stable beta nightly; do + docker tag ghcr.io/integer32llc/rust-playground-ci-rust-$c:${{ github.run_id }} rust-$c + done + + - name: "Download backend" + uses: actions/download-artifact@v7 + with: + name: backend + path: tests/server/ + + - name: "Download frontend" + uses: actions/download-artifact@v7 + with: + name: frontend + path: tests/server/build/ + + - name: "Run orchestrator unit tests" + env: + TESTS_MAX_CONCURRENCY: 3 + TESTS_TIMEOUT_MS: 30000 + run: |- + chmod +x ./server/unit_tests_orchestrator && ./server/unit_tests_orchestrator + + - name: "Run ui unit tests" + run: |- + chmod +x ./server/unit_tests_ui && ./server/unit_tests_ui + + - name: "Run tests" + env: + PLAYGROUND_UI_ROOT: server/build/ + PLAYGROUND_CORS_ENABLED: true + PLAYGROUND_GITHUB_TOKEN: ${{ secrets.PLAYGROUND_GITHUB_TOKEN }} + run: |- + killall -q ui || true + chmod +x ./server/ui && ./server/ui & + bundle exec rspec + + - name: "Preserve screenshots" + if: ${{ failure() }} + uses: actions/upload-artifact@v6 + with: + name: test-failures + path: tests/test-failures + + release_artifacts: + name: "Release artifacts" + runs-on: ubuntu-latest + needs: + - run_integration_tests + if: github.event_name == 'push' && github.event.ref == 'refs/heads/main' + permissions: + contents: read + id-token: write + + steps: + - name: "Download backend" + uses: actions/download-artifact@v7 + with: + name: backend + path: server/ + + - name: "Download frontend" + uses: actions/download-artifact@v7 + with: + name: frontend + path: server/build/ + + - name: "Configure AWS credentials (i32)" + uses: aws-actions/configure-aws-credentials@v6 + with: + aws-access-key-id: ${{ env.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-2 + + - name: "Push backend (i32)" + run: |- + aws s3 cp server/ui s3://playground-artifacts-i32 + + - name: "Push frontend (i32)" + run: |- + aws s3 sync server/build/ s3://playground-artifacts-i32/build + + - name: "Configure AWS credentials (rust-lang)" + uses: aws-actions/configure-aws-credentials@v6 + with: + role-skip-session-tagging: true + role-to-assume: arn:aws:iam::890664054962:role/upload-playground-artifacts + aws-region: us-west-1 + + - name: "Push backend (rust-lang)" + run: |- + aws s3 cp server/ui s3://rust-playground-artifacts + + - name: "Push frontend (rust-lang)" + run: |- + aws s3 sync server/build/ s3://rust-playground-artifacts/build + + release_docker_artifacts: + name: "Release ${{ matrix.channel }} image" + <<: *each_channel_matrix_job + needs: + - run_integration_tests + env: + <<: *image_names_env + if: github.event_name == 'push' && github.event.ref == 'refs/heads/main' + + steps: + - *checkout_pr + - *docker_buildx + - *login_ghcr + - *login_docker_hub + + - name: "Release ${{ matrix.channel }} image" + uses: ./.github/actions/build-and-push-image + with: + channel: ${{ matrix.channel }} + tags: |- + ${{ env.GHCR_IMAGE_NAME }} + ${{ env.DOCKER_HUB_IMAGE_NAME }} + + cron: + name: "Scheduled rebuild" + + 'on': + workflow_dispatch: + schedule: + # Nightlies are available ~1am UTC, should definitely be out by 2am + - cron: '7 2 * * *' + + <<: *global_env + + jobs: + build_compiler_images: + name: "Release ${{ matrix.channel }} image" + <<: *each_channel_matrix_job + env: + <<: *image_names_env + + continue-on-error: true + + steps: + - *checkout + - *docker_buildx + - *login_ghcr + - *login_docker_hub + + - name: "Release ${{ matrix.channel }} image" + uses: ./.github/actions/build-and-push-image + with: + channel: ${{ matrix.channel }} + tags: |- + ${{ env.GHCR_IMAGE_NAME }} + ${{ env.DOCKER_HUB_IMAGE_NAME }} diff --git a/anneal/v1/playground/rust-anneal-playground/deployment/crontab b/anneal/v1/playground/rust-anneal-playground/deployment/crontab new file mode 100644 index 0000000000..cc44d3a046 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/deployment/crontab @@ -0,0 +1,12 @@ +AWS_ACCESS_KEY_ID=THE_KEY_ID +AWS_SECRET_ACCESS_KEY=THE_SECRET +MAILTO=admin@example.com + +# ┌───────────── minute (0 - 59) +# │ ┌───────────── hour (0 - 23) +# │ │ ┌───────────── day of month (1 - 31) +# │ │ │ ┌───────────── month (1 - 12) +# │ │ │ │ ┌───────────── day of week (0 - 6) (Sunday to Saturday; 7 is also Sunday on some systems) +# │ │ │ │ │ + + 23 * * * * /home/ec2-user/update.sh diff --git a/anneal/v1/playground/rust-anneal-playground/deployment/playground-reverse-proxy b/anneal/v1/playground/rust-anneal-playground/deployment/playground-reverse-proxy new file mode 100644 index 0000000000..a11f4e3410 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/deployment/playground-reverse-proxy @@ -0,0 +1,18 @@ +server { + listen 80 default_server; + listen [::]:80 default_server; + + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + + server_name play.integer32.com; + + ssl_certificate /etc/nginx/ssl/play.integer32.com/cert.pem; + ssl_certificate_key /etc/nginx/ssl/play.integer32.com/key.pem; + + location / { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_pass http://localhost:8080; + } +} diff --git a/anneal/v1/playground/rust-anneal-playground/deployment/playground.service b/anneal/v1/playground/rust-anneal-playground/deployment/playground.service new file mode 100644 index 0000000000..113fe52868 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/deployment/playground.service @@ -0,0 +1,14 @@ +[Unit] +Description=The Rust Playground + +[Service] +Environment=TMPDIR=/mnt/playground +Environment=RUST_LOG=info +Environment=PLAYGROUND_UI_ADDRESS=0.0.0.0 +Environment=PLAYGROUND_UI_PORT=8080 +Environment=PLAYGROUND_UI_ROOT=/home/ubuntu/playground-artifacts/build +Environment=PLAYGROUND_CORS_ENABLED=1 + +WorkingDirectory=/home/ubuntu/playground-artifacts + +ExecStart=/home/ubuntu/playground-artifacts/ui diff --git a/anneal/v1/playground/rust-anneal-playground/deployment/ubuntu.md b/anneal/v1/playground/rust-anneal-playground/deployment/ubuntu.md new file mode 100644 index 0000000000..82c879834f --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/deployment/ubuntu.md @@ -0,0 +1,90 @@ +### Amazon EC2 (Ubuntu) + +Here's an example session. This could definitely be improved and +automated. + +#### Dependencies (as root) + +``` +apt-get update +apt-get upgrade -y +apt-get install git awscli nginx + +# Install Docker CE +# Configure apt-get per instructions +# https://docs.docker.com/engine/installation/linux/docker-ce/ubuntu/ +apt-get install docker-ce + +# Use a production-quality storage driver that doesn't leak disk space +cat >>/etc/docker/daemon.json <>/etc/fstab < 3.35' +gem 'rspec', '~> 3.10' +gem 'capybara-screenshot', '~> 1.0' +gem 'selenium-webdriver', '~> 4.0' diff --git a/anneal/v1/playground/rust-anneal-playground/tests/Gemfile.lock b/anneal/v1/playground/rust-anneal-playground/tests/Gemfile.lock new file mode 100644 index 0000000000..35fc0162a1 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/Gemfile.lock @@ -0,0 +1,77 @@ +GEM + remote: https://rubygems.org/ + specs: + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + base64 (0.3.0) + capybara (3.40.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + capybara-screenshot (1.0.27) + capybara (>= 1.0, < 4) + launchy + childprocess (5.1.0) + logger (~> 1.5) + diff-lcs (1.6.2) + launchy (3.1.1) + addressable (~> 2.8) + childprocess (~> 5.0) + logger (~> 1.6) + logger (1.7.0) + matrix (0.4.3) + mini_mime (1.1.5) + mini_portile2 (2.8.9) + nokogiri (1.19.4) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + public_suffix (7.0.5) + racc (1.8.1) + rack (3.2.6) + rack-test (2.2.0) + rack (>= 1.3) + regexp_parser (2.12.0) + rexml (3.4.4) + rspec (3.13.2) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.8) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.7) + rubyzip (3.3.1) + selenium-webdriver (4.44.0) + base64 (~> 0.2) + logger (~> 1.4) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 4.0) + websocket (~> 1.0) + websocket (1.2.11) + xpath (3.2.0) + nokogiri (~> 1.8) + +PLATFORMS + ruby + +DEPENDENCIES + capybara (~> 3.35) + capybara-screenshot (~> 1.0) + rspec (~> 3.10) + selenium-webdriver (~> 4.0) + +RUBY VERSION + ruby 3.3.6p108 + +BUNDLED WITH + 2.5.22 diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/features/assistance_spec.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/assistance_spec.rb new file mode 100644 index 0000000000..fe8a051cbb --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/assistance_spec.rb @@ -0,0 +1,76 @@ +require 'spec_helper' +require 'support/editor' +require 'support/notifications' +require 'support/playground_actions' + +RSpec.feature "Editor assistance for common code modifications", type: :feature, js: true do + include PlaygroundActions + + before do + visit '/' + Notifications.new(page).close_all + end + + scenario "building code without a main method offers adding one" do + editor.set <<~EOF + fn example() {} + EOF + click_on("Build") + + within(:output, :warning) do + click_on("add a main function") + end + + expect(editor).to have_line 'println!("Hello, world!")' + end + + scenario "using an unstable feature offers adding the feature flag" do + in_channel_menu { click_on("Nightly") } + editor.set <<~EOF + extern "avr-interrupt" fn dummy() {} + EOF + click_on("Build") + + within(:output, :stderr) do + click_on("add `#![feature(abi_avr_interrupt)]`") + end + + expect(editor).to have_line '#![feature(abi_avr_interrupt)]' + end + + scenario "using a type that hasn't been imported offers importing it" do + editor.set <<~EOF + fn example(_: NonZeroU128) {} + EOF + click_on("Build") + + within(:output, :stderr) do + click_on("use std::num::NonZeroU128;") + end + + expect(editor).to have_line 'use std::num::NonZeroU128;' + end + + scenario "triggering a panic offers enabling backtraces" do + editor.set <<~EOF + fn main() { + panic!("Oops"); + } + EOF + click_on("Run") + + within(:output, :stderr) do + click_on("run with `RUST_BACKTRACE=1` environment variable to display a backtrace") + end + + within(:output, :stderr) do + expect(page).to have_content("stack backtrace:") + end + end + + private + + def editor + Editor.new(page) + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/features/automatic_primary_action_spec.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/automatic_primary_action_spec.rb new file mode 100644 index 0000000000..0ae8f9cecf --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/automatic_primary_action_spec.rb @@ -0,0 +1,149 @@ +require 'spec_helper' +require 'support/editor' + +RSpec.feature "Automatically selecting the primary action", type: :feature, js: true do + before { visit '/' } + + scenario "when the crate is a binary" do + editor.set <<~EOF + #![crate_type="bin"] + fn main() { + println!("Hello, world"); + } + EOF + click_on("Run") + + within(:output, :stdout) do + expect(page).to have_content 'Hello, world' + end + end + + scenario "when the crate is a library" do + editor.set <<~EOF + #![crate_type="lib"] + fn main() { + println!("Hello, world"); + } + EOF + click_on("Build") + + within(:output, :stderr) do + expect(page).to have_content 'function `main` is never used' + end + end + + scenario "when the crate is a procedural macro" do + editor.set <<~EOF + #![crate_type = "proc-macro"] + + use proc_macro::TokenStream; + + /// Example: + /// ``` + /// playground::demo!(); + /// type T = aaa; + /// ``` + #[proc_macro] + pub fn demo(_input: TokenStream) -> TokenStream { + eprintln!("wow wow"); + "struct Aaa;".parse().unwrap() + } + + /* + #[test]*/ + EOF + click_on("Test") + + within(:output) do + expect(page).to_not have_selector(:loader, wait: 15) + end + within(:output, :stdout) do + expect(page).to have_content 'a struct with a similar name exists' + end + end + + scenario "when the crate is a library with tests" do + editor.set <<~EOF + #![crate_type="lib"] + + #[test] + fn test() { + assert_eq!(1 + 2, 3); + } + + fn main() { + println!("Hello, world"); + } + EOF + click_on("Build") + + within(:output, :stderr) do + expect(page).to have_content 'function `main` is never used' + end + end + + scenario "when tests are present" do + editor.set <<~EOF + #[test] + fn awesome() {} + EOF + click_on("Test") + + within(:output, :stdout) do + expect(page).to have_content 'running 1 test' + expect(page).to have_content 'test awesome ... ok' + expect(page).to have_content 'test result: ok' + expect(page).to have_content '1 passed' + expect(page).to have_content '0 failed' + end + end + + scenario "when tests and a main method are present" do + editor.set <<~EOF + #[test] + fn awesome() {} + + fn main() { + println!("Running in main"); + } + EOF + click_on("Test") + + within(:output, :stdout) do + expect(page).to have_content 'running 1 test' + expect(page).to_not have_content 'Running in main' + end + end + + scenario "when a main method is present" do + editor.set <<~EOF + fn main() { + println!("Running in main"); + } + EOF + click_on("Run") + + within(:output, :stdout) do + expect(page).to have_content 'Running in main' + end + end + + scenario "when neither tests nor a main method are present" do + editor.set <<~EOF + fn arbitrary_code() { + println!("I am code"); + } + EOF + click_on("Build") + + within(:output, :stderr) do + expect(page).to have_content 'function `arbitrary_code` is never used' + end + end + + private + + def editor + Editor.new(page) + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/features/backtrace_spec.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/backtrace_spec.rb new file mode 100644 index 0000000000..b2909d5fd7 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/backtrace_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' +require 'support/editor' +require 'support/playground_actions' + +RSpec.feature "A backtrace is shown for certain errors", type: :feature, js: true do + include PlaygroundActions + + before do + visit '/' + editor.set(code) + end + + context "backtraces are enabled" do + before do + in_advanced_options_menu { within(:config_option, 'Backtrace') { choose 'On' } } + within(:header) { click_on("Run") } + end + + scenario "a backtrace is shown" do + within(:output, :stderr) do + expect(page).to have_content 'stack backtrace:' + expect(page).to have_content 'rust_begin_unwind' + end + end + + scenario "filenames link to that line of code" do + within(:output, :stderr) do + expect(page).to have_link('main.rs:2') + expect(page).to have_link('main.rs:6') + end + end + end + + context "backtraces are disabled" do + before do + within(:header) { click_on("Run") } + end + + scenario "the backtrace suggestion is a link" do + within(:output, :stderr) do + expect(page).to have_link(text: /Run with .* a backtrace/i) + end + end + end + + def editor + Editor.new(page) + end + + def code + <<~EOF + fn trigger_the_problem() { + None::.unwrap(); + } + + fn main() { + trigger_the_problem() + } + EOF + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/features/compilation_modes_spec.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/compilation_modes_spec.rb new file mode 100644 index 0000000000..bb2058cefb --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/compilation_modes_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' +require 'support/editor' +require 'support/playground_actions' + +RSpec.feature "Compiling in different modes", type: :feature, js: true do + include PlaygroundActions + + before do + visit '/' + editor.set(compilation_mode_code) + end + + scenario "compiling in debug mode" do + in_mode_menu { click_on("Debug") } + click_on("Run") + + within(:output, :stdout) do + expect(page).to have_content 'Compiling in debug mode' + expect(page).to_not have_content 'Compiling in release mode' + end + end + + scenario "compiling in release mode" do + in_mode_menu { click_on("Release") } + click_on("Run") + + within(:output, :stdout) do + expect(page).to_not have_content 'Compiling in debug mode' + expect(page).to have_content 'Compiling in release mode' + end + end + + def editor + Editor.new(page) + end + + def compilation_mode_code + <<~EOF + #[cfg(debug_assertions)] + fn main() { + println!("Compiling in debug mode"); + } + + #[cfg(not(debug_assertions))] + fn main() { + println!("Compiling in release mode"); + } + EOF + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/features/compilation_targets_spec.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/compilation_targets_spec.rb new file mode 100644 index 0000000000..4c4f588d7d --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/compilation_targets_spec.rb @@ -0,0 +1,158 @@ +require 'spec_helper' +require 'support/editor' +require 'support/playground_actions' + +RSpec.feature "Compiling to different formats", type: :feature, js: true do + include PlaygroundActions + + before do + visit '/' + editor.set(code) + end + + context "ignoring automatic primary actions" do + before do + code = <<~EOF + #[test] + fn a_test() { assert!(false); } + fn main() { println!("Hello, world!"); } + EOF + editor.set(code) + end + + scenario "choosing to run the code" do + in_build_menu { click_on(build_button: "Run") } + within(:output, :stdout) do + expect(page).to have_content 'Hello, world!' + end + end + + scenario "choosing to build the code" do + in_build_menu { click_on(build_button: "Build") } + within(:output, :stderr) do + expect(page).to have_content 'function `main` is never used' + end + end + + scenario "choosing to test the code" do + in_build_menu { click_on(build_button: "Test") } + within(:output, :stdout) do + expect(page).to have_content "assertion failed: false" + end + end + end + + context "when AT&T syntax is selected", :assembly do + before do + in_config_menu { choose("AT&T") } + end + + scenario "compiling to assembly" do + in_build_menu { click_on("assembly") } + + within(:output, :code) do + # We demangle the symbols + expect(page).to have_content 'playground::main:' + + expect(page).to have_content 'addq $40, %rsp' + end + end + end + + context "when Intel syntax is selected", :assembly do + before do + in_config_menu { choose("Intel") } + end + + scenario "compiling to assembly" do + in_build_menu { click_on("assembly") } + + within(:output, :code) do + # We demangle the symbols + expect(page).to have_content 'playground::main:' + + expect(page).to have_content 'add rsp, 40' + end + end + end + + scenario "compiling to LLVM IR" do + in_build_menu { click_on("LLVM IR") } + + within(:output, :code) do + expect(page).to have_content 'ModuleID' + expect(page).to have_content 'target datalayout' + expect(page).to have_content 'target triple' + end + end + + scenario "compiling to MIR" do + in_build_menu { click_on("MIR") } + + within(:output, :result) do + expect(page).to have_content 'bb0: {' + end + end + + scenario "compiling to HIR" do + editor.set <<~EOF + fn demo() -> impl std::fmt::Display { 42 } + EOF + + in_build_menu { click_on("HIR") } + + within(:output, :result) do + expect(page).to have_content 'fn demo() -> /*impl Trait*/ { 42 }' + end + end + + scenario "compiling to WebAssembly" do + editor.set ['#![crate_type = "bin"]', code].join("\n") + + in_build_menu { click_on("Wasm") } + + within(:output, :code) do + expect(page).to have_content '(module' + expect(page).to have_content 'block ;;' + end + end + + scenario "compiling a library to WebAssembly" do + editor.set <<~EOF + #[unsafe(no_mangle)] + pub fn calculator(a: u8) -> u8 { a + 42 } + EOF + + in_build_menu { click_on("Wasm") } + + within(:output, :code) do + expect(page).to have_content '(func $calculator' + end + + expect(editor).to have_line('#![crate_type = "cdylib"]') + end + + context "when the code doesn't compile" do + before { editor.set("fn main() {") } + + scenario "it shows the compilation error" do + in_build_menu { click_on("MIR") } + + within(:output, :stderr) do + expect(page).to have_content 'an unclosed delimiter' + end + end + end + + def editor + Editor.new(page) + end + + def code + <<~EOF + fn main() { + println!("Hello, world!"); + } + EOF + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/features/editions_spec.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/editions_spec.rb new file mode 100644 index 0000000000..d9521f2f9e --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/editions_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' +require 'support/editor' +require 'support/playground_actions' + +RSpec.feature "Multiple Rust editions", type: :feature, js: true do + include PlaygroundActions + + before do + visit '/' + editor.set(rust_edition_code) + end + + scenario "using the 2015 edition" do + in_advanced_options_menu { select '2015' } + click_on("Run") + + within(:output, :stderr) do + expect(page).to have_content 'cannot find struct, variant or union type `async` in this scope' + end + end + + scenario "using the 2018 edition" do + in_advanced_options_menu { select '2018' } + click_on("Run") + + within(:output, :stderr) do + expect(page).to have_content %r{thread 'main' \(\d+\) panicked at src/main.rs:3:5} + end + end + + scenario "using the 2021 edition" do + in_advanced_options_menu { select '2021' } + click_on("Run") + + within(:output, :stderr) do + expect(page).to have_content 'format argument must be a string literal', wait: 10 + end + end + + scenario "using the 2024 edition" do + editor.set <<-EOF + fn main() { + let gen = 1; + } + EOF + + in_advanced_options_menu { select '2024' } + click_on("Run") + + within(:output, :stderr) do + expect(page).to have_content 'found reserved keyword `gen`' + end + end + + def editor + Editor.new(page) + end + + def rust_edition_code + <<~EOF + #![allow(non_fmt_panics)] + fn main() { + panic!(async {}) + } + EOF + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/features/editor_types_spec.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/editor_types_spec.rb new file mode 100644 index 0000000000..2a8a5e9c9a --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/editor_types_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' +require 'support/playground_actions' + +RSpec.feature "Editing in different editors", type: :feature, js: true do + include PlaygroundActions + + before { visit '/' } + + scenario "using the simple editor" do + in_config_menu { select("simple") } + + fill_in('editor-simple', with: simple_editor_code) + + click_on("Run") + + within(:output, :stdout) do + expect(page).to have_content 'simple editor' + end + end + + def simple_editor_code + <<~EOF + fn main() { + println!("Using the simple editor"); + } + EOF + end + + scenario "using the Monaco editor" do + in_config_menu { select("monaco") } + + editor = page.find('.monaco-editor') + + # Click on the last line as that will replace the entire content + editor.find('.view-line:last-child').click + t = editor.find('textarea', visible: false) + t.set(monaco_editor_code, clear: :backspace) + + click_on("Run") + + within(:output, :stdout) do + expect(page).to have_content 'Monaco editor' + end + end + + # Missing indentation and closing curly braces as those are auto-inserted + def monaco_editor_code + <<~EOF + fn main() { + println!("Using the Monaco editor"); + EOF + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/features/excessive_execution_spec.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/excessive_execution_spec.rb new file mode 100644 index 0000000000..d9814e1b90 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/excessive_execution_spec.rb @@ -0,0 +1,64 @@ +require 'json' + +require 'spec_helper' +require 'support/editor' +require 'support/playground_actions' + +RSpec.feature "Excessive executions", type: :feature, js: true do + include PlaygroundActions + + before do + visit "/?#{config_overrides}" + editor.set(code) + end + + scenario "a notification is shown" do + within(:header) { click_on("Run") } + within(:notification, text: 'will be automatically killed') do + expect(page).to have_button 'Kill the process now' + expect(page).to have_button 'Allow the process to continue' + end + end + + scenario "the process is automatically killed if nothing is done" do + within(:header) { click_on("Run") } + expect(page).to have_selector(:notification, text: 'will be automatically killed', wait: 2) + expect(page).to_not have_selector(:notification, text: 'will be automatically killed', wait: 4) + expect(page).to have_content("Exited with signal 9") + end + + scenario "the process can continue running" do + within(:header) { click_on("Run") } + within(:notification, text: 'will be automatically killed') do + click_on 'Allow the process to continue' + end + within(:output, :stdout) do + expect(page).to have_content("Exited normally") + end + end + + def editor + Editor.new(page) + end + + def code + <<~EOF + use std::time::{Duration, Instant}; + + fn main() { + let start = Instant::now(); + while start.elapsed() < Duration::from_secs(5) {} + println!("Exited normally"); + } + EOF + end + + def config_overrides + config = { + killGracePeriodS: 3.0, + excessiveExecutionTimeS: 0.5, + } + + "whte_rbt.obj=#{config.to_json}" + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/features/highlighting_error_output_spec.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/highlighting_error_output_spec.rb new file mode 100644 index 0000000000..20887aff52 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/highlighting_error_output_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' +require 'support/editor' + +RSpec.feature "Highlighting the output", type: :feature, js: true do + before do + visit '/' + editor.set(code) + within(:header) { click_on("Run") } + end + + scenario "errors are highlighted" do + within(:output, :stderr) do + expect(page).to have_css '.error', text: '1 generic argument but 2 generic arguments were supplied' + expect(page).to have_css '.error', text: 'due to 2 previous errors' + expect(page).to have_css '.error', text: /Could not compile `playground`/i + end + end + + scenario "error locations are links" do + within(:output, :stderr) do + expect(page).to have_link('src/main.rs') + end + end + + scenario "github see-issues are links" do + within(:output, :stderr) do + expect(page).to have_link('see issue #31844 ', href: 'https://github.com/rust-lang/rust/issues/31844') + end + end + + scenario "error codes link to the error page" do + within(:output, :stderr) do + expect(page).to have_link('E0107', href: %r{/error_codes/E0107.html}) + end + end + + def editor + Editor.new(page) + end + + def code + <<~EOF + fn main() { + drop::(1); + } + + trait X { fn foo(&self); } + impl X for () { default fn foo(&self) {} } + EOF + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/features/highlighting_mir_output_spec.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/highlighting_mir_output_spec.rb new file mode 100644 index 0000000000..2661e02253 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/highlighting_mir_output_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' +require 'support/editor' +require 'support/playground_actions' + +RSpec.feature "Highlighting MIR output", type: :feature, js: true do + include PlaygroundActions + + before do + visit '/' + editor.set(code) + in_build_menu { click_on("MIR") } + end + + scenario "error locations are links" do + pending "This feature was removed in Rust 1.72, enabling it again is nightly-only" + + within(:output, :mir) do + click_link('src/main.rs:4:14: 4:19', match: :first) + end + expect(editor).to have_highlighted_text('a + b') + end + + def editor + Editor.new(page) + end + + def code + <<~EOF + fn main() { + let a = 1; + let b = 2; + let _c = a + b; + } + EOF + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/features/multiple_channels_spec.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/multiple_channels_spec.rb new file mode 100644 index 0000000000..a77a0c1825 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/multiple_channels_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' +require 'support/editor' +require 'support/playground_actions' + +RSpec.feature "Multiple Rust versions", type: :feature, js: true do + include PlaygroundActions + + before do + visit '/' + editor.set(version_code) + end + + scenario "using stable Rust" do + in_channel_menu { click_on("Stable") } + click_on("Run") + + within(:output, :stdout) do + expect(page).to have_content 'rustc' + expect(page).to_not have_content 'beta' + expect(page).to_not have_content 'nightly' + end + end + + scenario "using beta Rust" do + in_channel_menu { click_on("Beta") } + click_on("Run") + + within(:output, :stdout) do + expect(page).to have_content 'rustc' + expect(page).to have_content 'beta' + expect(page).to_not have_content 'nightly' + end + end + + scenario "using nightly Rust" do + in_channel_menu { click_on("Nightly") } + click_on("Run") + + within(:output, :stdout) do + expect(page).to have_content 'rustc' + expect(page).to_not have_content 'beta' + expect(page).to have_content 'nightly' + end + end + + def editor + Editor.new(page) + end + + def version_code + <<~EOF + use std::process::Command; + + fn main() { + let output = Command::new("rustc").arg("--version").output().unwrap(); + let output = String::from_utf8(output.stdout).unwrap(); + println!("{}", output); + } + EOF + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/features/navigation_spec.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/navigation_spec.rb new file mode 100644 index 0000000000..c7280b6396 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/navigation_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' +require 'support/editor' +require 'support/matchers/be_at_url' +require 'support/playground_actions' + +RSpec.feature "Navigating between pages", type: :feature, js: true do + include PlaygroundActions + + # This is kind of a test of the router library too, so if that ever + # gets extracted, a chunk of these tests can be removed. + + scenario "Mode and channel buttons update the URL" do + visit '/' + expect(page).to have_content('RUN') + + editor.set("dummy") + expect(page).to be_at_url('/') + + in_mode_menu { click_on('Release') } + expect(page).to be_at_url('/', version: 'stable', mode: 'release') + + go_back + expect(page).to be_at_url('/') + + in_mode_menu { click_on('Release') } + in_channel_menu { click_on('Beta') } + expect(page).to be_at_url('/', version: 'beta', mode: 'release') + + go_back + expect(page).to be_at_url('/') + end + + scenario "Navigating to help changes the URL" do + visit '/' + expect(page).to have_content('RUN') + + click_on 'View help' + expect(page).to be_at_url('/help') + expect(page).to have_content('The Rust Playground') + + go_back + expect(page).to be_at_url('/') + expect(page).to have_content('RUN') + + go_forward + expect(page).to be_at_url('/help') + expect(page).to have_content('The Rust Playground') + + click_on "Return to the playground" + expect(page).to be_at_url('/') + expect(page).to have_content('RUN') + end + + scenario "Navigating from help changes the URL" do + visit '/help' + expect(page).to have_content('The Rust Playground') + + click_on "Return to the playground" + expect(page).to be_at_url('/') + expect(page).to have_content('RUN') + end + + def editor + Editor.new(page) + end + + # It probably would be worth figuring out why + # `page.go_{back,forward}` doesn't work... :-( + # Maybe https://github.com/teampoltergeist/poltergeist/issues/624 + def go_back + page.execute_script('window.history.go(-1);') + end + + def go_forward + page.execute_script('window.history.go(1);') + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/features/reset_configuration_spec.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/reset_configuration_spec.rb new file mode 100644 index 0000000000..8d047b7085 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/reset_configuration_spec.rb @@ -0,0 +1,76 @@ +require 'json' + +require 'spec_helper' +require 'support/editor' +require 'support/playground_actions' + +RSpec.feature "Resetting the configuration to defaults", type: :feature, js: true do + include PlaygroundActions + + describe "after not visiting in a while" do + before do + visit "/" + editor.set(code) + + sleep(0.002) + visit "/?#{config_overrides}" + end + + scenario "the default values are restored" do + within(:notification) { click_on 'Reset all code and configuration' } + + expect(editor).to_not have_line(code) + expect(editor).to have_line(some_default_code) + end + + scenario "the current values are kept" do + within(:notification) { click_on 'Keep the current code and configuration' } + + expect(editor).to have_line(code) + expect(editor).to_not have_line(some_default_code) + end + + def config_overrides + config = { + oldConfigurationThresholdS: 0.001, + } + + "whte_rbt.obj=#{config.to_json}" + end + end + + describe "manually" do + before do + visit "/" + editor.set(code) + end + + scenario "the default values are restored" do + in_config_menu { click_on 'Reset all code and configuration to default values' } + within(:notification) { click_on 'Reset all code and configuration' } + + expect(editor).to_not have_line(code) + expect(editor).to have_line(some_default_code) + end + + scenario "the current values are kept" do + in_config_menu { click_on 'Reset all code and configuration to default values' } + within(:notification) { click_on 'Keep the current code and configuration' } + + expect(editor).to have_line(code) + expect(editor).to_not have_line(some_default_code) + end + end + + def editor + Editor.new(page) + end + + def code + 'This is my old code' + end + + def some_default_code + 'Hello, world!' + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/features/security_spec.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/security_spec.rb new file mode 100644 index 0000000000..bbfacaed68 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/security_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' +require 'support/editor' +require 'support/playground_actions' + +RSpec.feature "Security concerns", type: :feature, js: true do + include PlaygroundActions + + before do + visit '/' + editor.set(code) + end + + scenario "a notice is present for filesystem snoopers" do + within(:header) { click_on("Run") } + within(:output, :stdout) do + expect(page).to have_content 'www.rust-lang.org/policies/security' + end + end + + def editor + Editor.new(page) + end + + def code + <<~EOF + fn main() { + println!("{}", std::fs::read_to_string("/etc/passwd").unwrap()); + } + EOF + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/features/segfault_spec.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/segfault_spec.rb new file mode 100644 index 0000000000..c65486da31 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/segfault_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' +require 'support/editor' +require 'support/playground_actions' + +RSpec.feature "When the code triggers a signal", type: :feature, js: true do + include PlaygroundActions + + before { visit '/' } + + scenario "when a crate type is provided" do + code = <<~EOF + fn main() { + unsafe { *(0xDECAF_000 as *mut usize) = 1 }; + } + EOF + editor.set(code) + + click_on("Run") + within(:output, :error) do + expect(page).to have_content "signal 11" + expect(page).to have_content "SIGSEGV" + expect(page).to have_content "segmentation violation" + end + end + + def editor + Editor.new(page) + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/features/sharing_with_others_spec.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/sharing_with_others_spec.rb new file mode 100644 index 0000000000..80fe0d7f59 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/sharing_with_others_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' +require 'support/editor' +require 'support/matchers/be_at_url' +require 'support/playground_actions' + +RSpec.feature "Sharing the code with others", :external, type: :feature, js: true do + include PlaygroundActions + + before { visit '/' } + + # This test does more than one thing so we can avoid sending too + # many requests to GitHub + scenario "saving to a Gist" do + editor.set(code) + + in_channel_menu { click_on("Nightly") } + in_mode_menu { click_on("Release") } + in_advanced_options_menu { select("2018") } + + within(:header) { click_on 'Share' } + + # Save the links before we navigate away + perma_link = find_link("Permalink to the playground")[:href] + direct_link = find_link("Direct link to the gist")[:href] + urlo_link = find_link("Open a new thread in the Rust user forum")[:href] + + # Have we automatically added the gist to our URL? + gist_id = Addressable::URI.parse(perma_link).query_values['gist'] + expect(page).to be_at_url('/', gist: gist_id) + + # Navigate away so we can tell that we go back to the same page + visit 'about:blank' + + visit perma_link + expect(page).to have_link("Permalink to the playground") + expect(editor).to have_line 'automated test' + expect(perma_link).to match(/mode=release/) + expect(perma_link).to match(/version=nightly/) + expect(perma_link).to match(/edition=2018/) + + visit direct_link + expect(page).to have_content 'All gists' + expect(page).to have_content 'GitHub, Inc.' + + # Need to be logged in to URLO for this link to work + expect(urlo_link).to match(%r{https://users.rust-lang.org/new-topic}) + expect(urlo_link).to include('automated+test') + end + + def editor + Editor.new(page) + end + + def code + <<~EOF + // This code was saved by an automated test for the Rust Playground + EOF + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/features/streaming_spec.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/streaming_spec.rb new file mode 100644 index 0000000000..6485b69bf3 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/streaming_spec.rb @@ -0,0 +1,137 @@ +require 'spec_helper' +require 'support/editor' +require 'support/playground_actions' + +RSpec.feature "Streaming interaction using WebSockets", type: :feature, js: true do + include PlaygroundActions + + before do + visit '/?features=true' + end + + scenario "output comes when it is available" do + editor.set <<~EOF + use std::time::Duration; + + fn main() { + println!("First"); + std::thread::sleep(Duration::from_millis(750)); + println!("Second"); + std::thread::sleep(Duration::from_millis(750)); + println!("Third"); + } + EOF + + click_on("Run") + + within(:output, :stdout) do + expect(page).to have_content 'First' + expect(page).to_not have_content 'Second' + + expect(page).to have_content 'Second', wait: 0.8 + expect(page).to_not have_content 'Third' + + expect(page).to have_content 'Third', wait: 0.8 + end + end + + scenario "input can be supplied" do + editor.set <<~EOF + fn main() { + println!("Enter some text!"); + let mut input = String::new(); + std::io::stdin().read_line(&mut input).expect("Unable to read input"); + println!("You entered >>>{input:?}<<<"); + } + EOF + + click_on("Run") + + within(:output, :stdout) do + expect(page).to have_content 'Enter some text' + expect(page).to_not have_content 'You entered' + end + + within(:stdin) do + fill_in 'content', with: 'An automated test' + click_on 'Send' + end + + within(:output, :stdout) do + expect(page).to have_content 'Enter some text' + expect(page).to have_content 'You entered >>>"An automated test\n"<<<' + end + end + + scenario "input can be closed" do + editor.set <<~EOF + fn main() { + let mut input = String::new(); + while std::io::stdin().read_line(&mut input).unwrap() != 0 { + println!("You entered >>>{input:?}<<<"); + input.clear(); + } + println!("All done"); + } + EOF + + click_on("Run") + + within(:stdin) do + click_on 'Execution control' + click_on 'Close stdin' + end + + within(:output, :stdout) do + expect(page).to have_content 'All done' + end + end + + scenario "The process can be killed" do + editor.set <<~EOF + fn main() { + loop { + std::thread::sleep(std::time::Duration::from_secs(1)); + } + } + EOF + + click_on("Run") + + within(:stdin) do + click_on 'Execution control' + click_on 'Kill process' + end + + within(:output, :error) do + expect(page).to have_content 'SIGKILL' + end + end + + scenario "The process can be killed after stdin is closed" do + editor.set <<~EOF + fn main() { + loop { + std::thread::sleep(std::time::Duration::from_secs(1)); + } + } + EOF + + click_on("Run") + + within(:stdin) do + click_on 'Execution control' + click_on 'Close stdin' + click_on 'Execution control' + click_on 'Kill process' + end + + within(:output, :error) do + expect(page).to have_content 'SIGKILL' + end + end + + def editor + Editor.new(page) + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/features/test_action_spec.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/test_action_spec.rb new file mode 100644 index 0000000000..5aada75ff0 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/test_action_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' +require 'support/editor' +require 'support/playground_actions' + +RSpec.feature "Testing the code", type: :feature, js: true do + include PlaygroundActions + + before { visit '/' } + + scenario "when a crate type is provided" do + code = <<~EOF + #![crate_type = "proc-macro"] + + use proc_macro::TokenStream; + + #[proc_macro] + pub fn demo(input: TokenStream) -> TokenStream { + input + } + EOF + editor.set(code) + + in_build_menu { click_on(build_button: "Test") } + within(:output, :stdout) do + expect(page).to have_content "running 0 tests" + end + end + + def editor + Editor.new(page) + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/features/tools_spec.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/tools_spec.rb new file mode 100644 index 0000000000..8644a4aefe --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/tools_spec.rb @@ -0,0 +1,117 @@ +# coding: utf-8 + +require 'spec_helper' +require 'support/editor' +require 'support/playground_actions' + +RSpec.feature "Using third-party Rust tools", type: :feature, js: true do + include PlaygroundActions + + before { visit '/' } + + scenario "formatting code" do + editor.set 'fn main() { [1,2,3,4]; }' + in_tools_menu { click_on("Rustfmt") } + + expect(editor).to have_line '[1, 2, 3, 4];' + end + + scenario "linting code with Clippy" do + editor.set code_with_lint_warnings + in_tools_menu { click_on("Clippy") } + + within(:output, :stderr) do + expect(page).to have_content 'deny(clippy::eq_op)' + expect(page).to have_content 'warn(clippy::zero_divided_by_zero)' + end + end + + def code_with_lint_warnings + <<~EOF + use itertools::Itertools; + + fn example() { + let a = 0.0 / 0.0; + println!("NaN is {}", a); + } + EOF + end + + scenario "sanitize code with Miri" do + editor.set code_with_undefined_behavior + in_tools_menu { click_on("Miri") } + + within(:output, :stderr) do + expect(page).to have_content %r{reading memory at alloc.*, but memory is uninitialized at .*, and this operation requires initialized memory}, wait: 10 + end + end + + def code_with_undefined_behavior + <<~EOF + fn main() { + unsafe { core::mem::MaybeUninit::::uninit().assume_init() }; + } + EOF + end + + scenario "configure Miri for tree borrows" do + editor.set code_valid_under_tree_borrows_but_not_stacked_borrows + in_advanced_options_menu { choose("tree") } + in_tools_menu { click_on("Miri") } + + within(:output, :stdout) do + expect(page).to have_content %r{[1, 2]}, wait: 10 + end + + within(:output, :stderr) do + expect(page).to_not have_content %r{Undefined Behavior} + end + end + + def code_valid_under_tree_borrows_but_not_stacked_borrows + <<~EOF + fn main() { + let val = [1u8, 2]; + let ptr = &val[0] as *const u8; + let _val = unsafe { *ptr.add(1) }; + } + EOF + end + + scenario "expand macros with the nightly compiler" do + editor.set code_that_uses_macros + in_tools_menu { click_on("Expand macros") } + + within(:output, :stdout) do + # First-party + expect(page).to have_content('::std::io::_print') + + # Third-party procedural macro + expect(page).to have_content('block_on(body)') + + # User-specified declarative macro + expect(page).to have_content('fn created_by_macro() -> i32 { 42 }') + end + end + + def code_that_uses_macros + <<~EOF + macro_rules! demo { + ($name:ident) => { + fn $name() -> i32 { 42 } + } + } + + demo!(created_by_macro); + + #[tokio::main] + async fn example() { + println!("a value: {}", created_by_macro()); + } + EOF + end + + def editor + Editor.new(page) + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/features/url_parameters_spec.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/url_parameters_spec.rb new file mode 100644 index 0000000000..b70c2ccb2a --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/features/url_parameters_spec.rb @@ -0,0 +1,113 @@ +# coding: utf-8 + +require 'spec_helper' +require 'support/editor' +require 'support/playground_actions' + +RSpec.feature "Configuration by URL parameters", type: :feature, js: true do + include PlaygroundActions + + RSpec::Matchers.define :have_mode do |expected| + match do |actual| + within actual.find_button("Mode — Choose the optimization level") do |page| + expect(page).to have_text(expected) + end + end + end + + RSpec::Matchers.define :have_channel do |expected| + match do |actual| + within actual.find_button("Channel — Choose the Rust version") do |page| + expect(page).to have_text(expected) + end + end + end + + RSpec::Matchers.define :have_edition do |expected| + match do |actual| + in_advanced_options_menu do + expect(page).to have_select(selected: expected) + end + end + end + + scenario "loading from a Gist" do + visit '/?gist=20fb1e0475f890d0fdb7864e3ad0820c' + + expect(editor).to have_line 'This source code came from a Gist' + end + + scenario "loading from a Gist preserves the links" do + visit '/?gist=20fb1e0475f890d0fdb7864e3ad0820c' + + within(:output) { click_on 'Share' } + expect(page).to have_link("Permalink to the playground", href: /gist=20fb1e0475f890d0fdb7864e3ad0820c/) + end + + scenario "loading from a Gist with a channel preserves the channel" do + visit '/?gist=20fb1e0475f890d0fdb7864e3ad0820c&version=beta' + + expect(page).to have_channel('Beta') + expect(page).to have_link("Permalink to the playground", href: /version=beta/) + end + + scenario "loading from a Gist with a mode preserves the mode" do + visit '/?gist=20fb1e0475f890d0fdb7864e3ad0820c&mode=release' + + expect(page).to have_mode('Release') + expect(page).to have_link("Permalink to the playground", href: /mode=release/) + end + + scenario "loading from a Gist with an edition preserves the edition" do + visit '/?gist=20fb1e0475f890d0fdb7864e3ad0820c&edition=2018' + + expect(page).to have_edition('2018') + expect(page).to have_link("Permalink to the playground", href: /edition=2018/) + end + + scenario "loading from a Gist without an edition selects Rust 2015" do + visit '/?gist=20fb1e0475f890d0fdb7864e3ad0820c' + + expect(page).to have_edition('2015') + expect(page).to have_link("Permalink to the playground", href: /edition=2015/) + end + + scenario "loading code directly from a parameter" do + visit '/?code=fn%20main()%20%7B%0A%20%20%20%20println!(%22Hello%2C%20world!%22)%3B%0A%7D' + + expect(editor).to have_line 'println!("Hello, world!")' + end + + scenario "loading code directly from a parameter without an edition selects Rust 2015" do + visit '/?code=fn%20main()%20%7B%0A%20%20%20%20println!(%22Hello%2C%20world!%22)%3B%0A%7D' + + expect(page).to have_edition('2015') + end + + scenario "loading with a channel" do + visit '/?version=nightly' + + expect(page).to have_channel('Nightly') + end + + scenario "loading with a mode" do + visit '/?mode=release' + + expect(page).to have_mode('Release') + end + + scenario "loading with an edition" do + visit '/?edition=2018' + + expect(page).to have_edition('2018') + end + + scenario "loading without code or an edition" do + visit '/' + expect(page).to have_edition('2024') + end + + def editor + Editor.new(page) + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/requests/caching_spec.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/requests/caching_spec.rb new file mode 100644 index 0000000000..9bb24991c8 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/requests/caching_spec.rb @@ -0,0 +1,38 @@ +require 'net/http' +require 'spec_helper' +require_relative './connection' + +RSpec.feature "Caching headers are provided for assets ", type: :request do + let(:index_uri) { URI(Capybara.app_host) } + + describe "the index page" do + let(:one_day_s) { 24 * 60 * 60 } + + it "is cached for one day" do + start_http(index_uri) do |http| + request = Net::HTTP::Get.new(index_uri) + response = http.request(request) + + expect(response['cache-control']).to match(/public.*max-age.*=.*#{one_day_s}/) + expect(response['last-modified']).to_not be_nil + end + end + end + + describe "an asset" do + let(:index_body) { Net::HTTP.get(index_uri) } + let(:index_page) { Capybara.string(index_body) } + let(:asset_path) { index_page.first('head script', visible: false)[:src] } + let(:asset_uri) { URI.join(index_uri, asset_path) } + let(:one_year_s) { 365 * 24 * 60 * 60 } + + it 'is cached for one year' do + start_http(asset_uri) do |http| + request = Net::HTTP::Get.new(asset_uri) + response = http.request(request) + expect(response['cache-control']).to match(/public.*max-age.*=.*#{one_year_s}/) + expect(response['last-modified']).to_not be_nil + end + end + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/requests/connection.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/requests/connection.rb new file mode 100644 index 0000000000..bc4bbd4bbb --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/requests/connection.rb @@ -0,0 +1,6 @@ +def start_http(uri, &block) + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = uri.scheme == 'https' + + http.start(&block) +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/requests/cors_spec.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/requests/cors_spec.rb new file mode 100644 index 0000000000..1bb88ea668 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/requests/cors_spec.rb @@ -0,0 +1,24 @@ +require 'net/http' +require 'spec_helper' +require_relative './connection' + +RSpec.feature "Cross-origin requests", :cors, type: :request do + let(:evaluate_json_uri) { URI.join(Capybara.app_host, '/evaluate.json') } + + it "allows preflight requests for POSTing to evaluate.json" do + start_http(evaluate_json_uri) do |http| + request = Net::HTTP::Options.new(evaluate_json_uri) + request['origin'] = 'https://rust-lang.org' + request['access-control-request-headers'] = 'content-type' + request['access-control-request-method'] = 'POST' + + response = http.request(request) + + expect(response['access-control-allow-headers']).to match(/content-type/i) + expect(response['access-control-allow-methods'].split(',').map(&:strip)).to match_array([/GET/i, /POST/i]) + expect(response['access-control-allow-origin']).to eq('*').or eq('https://rust-lang.org') + expect(response['access-control-max-age']).to eq('3600') + expect(response['vary'].split(',').map(&:strip)).to match_array([/origin/i, /access-control-request-method/i, /access-control-request-headers/i]) + end + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/requests/evaluate_spec.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/requests/evaluate_spec.rb new file mode 100644 index 0000000000..82f230fbde --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/requests/evaluate_spec.rb @@ -0,0 +1,61 @@ +# coding: utf-8 + +require 'net/http' +require 'spec_helper' +require_relative './connection' + +RSpec.feature "evaluate.json endpoint", type: :request do + let(:evaluate_json_uri) { URI.join(Capybara.app_host, '/evaluate.json') } + + let(:code) { + <<~EOF + fn main() { + let greetings = ["Hello", "Hola", "Bonjour", + "Ciao", "こんにちは", "안녕하세요", + "Cześć", "Olá", "Здравствуйте", + "Chào bạn", "您好", "Hallo", + "Hej", "Ahoj", "سلام","สวัสดี"]; + + for (num, greeting) in greetings.iter().enumerate() { + print!("{} : ", greeting); + match num { + 0 => println!("This code is editable and runnable!"), + 1 => println!("¡Este código es editable y ejecutable!"), + 2 => println!("Ce code est modifiable et exécutable !"), + 3 => println!("Questo codice è modificabile ed eseguibile!"), + 4 => println!("このコードは編集して実行出来ます!"), + 5 => println!("여기에서 코드를 수정하고 실행할 수 있습니다!"), + 6 => println!("Ten kod można edytować oraz uruchomić!"), + 7 => println!("Este código é editável e executável!"), + 8 => println!("Этот код можно отредактировать и запустить!"), + 9 => println!("Bạn có thể edit và run code trực tiếp!"), + 10 => println!("这段代码是可以编辑并且能够运行的!"), + 11 => println!("Dieser Code kann bearbeitet und ausgeführt werden!"), + 12 => println!("Den här koden kan redigeras och köras!"), + 13 => println!("Tento kód můžete upravit a spustit"), + 14 => println!("این کد قابلیت ویرایش و اجرا دارد!"), + 15 => println!("โค้ดนี้สามารถแก้ไขได้และรันได้"), + _ => {}, + } + } + } + EOF + } + let(:request) {{ 'version': 'beta', 'optimize': '0', 'code': code } } + let(:body) { JSON.generate(request) } + + it "allows evaluating compilation requests from the Rust home page" do + start_http(evaluate_json_uri) do |http| + request = Net::HTTP::Post.new(evaluate_json_uri) + request.body = body + request['Content-Type'] = 'application/json' + + response = http.request(request) + + body = response.body.force_encoding('UTF-8') + expect(body).to match('Hello : This code is editable and runnable') + expect(body).to match('こんにちは : このコードは編集して実行出来ます') + expect(body).to match('สวัสดี : โค้ดนี้สามารถแก้ไขได้และรันได้') + end + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/requests/malformed_json_spec.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/requests/malformed_json_spec.rb new file mode 100644 index 0000000000..d813040a7e --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/requests/malformed_json_spec.rb @@ -0,0 +1,46 @@ +# coding: utf-8 + +require 'json' +require 'net/http' +require 'spec_helper' +require_relative './connection' + +RSpec.feature "JSON argument deserialization", type: :request do + let(:evaluate_json_uri) { URI.join(Capybara.app_host, '/evaluate.json') } + + context "when the JSON data is malformed" do + let(:body) { JSON.generate({}) } + + it "responds with a JSON error" do + start_http(evaluate_json_uri) do |http| + request = Net::HTTP::Post.new(evaluate_json_uri) + request.body = body + request['Content-Type'] = 'application/json' + + response = http.request(request) + + expect(response['Content-Type']).to eq('application/json') + body = JSON.parse(response.body) + expect(body['error']).to match('Unable to deserialize request') + end + end + end + + context "when the data is not JSON" do + let(:body) { 'lolhello' } + + it "responds with a JSON error" do + start_http(evaluate_json_uri) do |http| + request = Net::HTTP::Post.new(evaluate_json_uri) + request.body = body + request['Content-Type'] = 'text/plain' + + response = http.request(request) + + expect(response['Content-Type']).to eq('application/json') + body = JSON.parse(response.body) + expect(body['error']).to match('Unable to deserialize request') + end + end + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/spec_helper.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/spec_helper.rb new file mode 100644 index 0000000000..431f8033e2 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/spec_helper.rb @@ -0,0 +1,111 @@ +require 'tmpdir' + +RSpec.configure do |config| + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + + config.disable_monkey_patching! + + # We aren't really testing Ruby code, so let's assume the Ruby code + # of the tests is good enough + config.warnings = false + + if config.files_to_run.one? + config.default_formatter = 'doc' + end + + config.example_status_persistence_file_path = "#{Dir.tmpdir()}/playground-test-failures" + + config.order = :random + + Kernel.srand config.seed +end + +require 'capybara/rspec' +require 'capybara-screenshot/rspec' + +PROTOCOL = ENV.fetch('PLAYGROUND_UI_PROTOCOL', 'http') +ADDRESS = ENV.fetch('PLAYGROUND_UI_ADDRESS', '127.0.0.1') +PORT = ENV.fetch('PLAYGROUND_UI_PORT', '5000') + +Capybara.register_driver :firefox do |app| + Capybara::Selenium::Driver.load_selenium + + capture_js_log = ENV.fetch('CAPTURE_JS_LOG', 'false').casecmp?('true') + Selenium::WebDriver.logger.level = :debug if capture_js_log + Selenium::WebDriver.logger.ignore(:clear_local_storage, :clear_session_storage) + + browser_options = ::Selenium::WebDriver::Firefox::Options.new + browser_options.add_argument('-headless') if ENV.fetch('HEADLESS', 'true').casecmp?('true') + browser_options.add_preference('devtools.console.stdout.content', true) if capture_js_log + + Capybara::Selenium::Driver.new( + app, + browser: :firefox, + options: browser_options, + clear_local_storage: true, + clear_session_storage: true, + ) +end + +Capybara.default_driver = Capybara.javascript_driver = :firefox +Capybara.app_host = "#{PROTOCOL}://#{ADDRESS}:#{PORT}" +Capybara.run_server = false +Capybara.default_max_wait_time = ENV.fetch('CAPYBARA_WAIT', 5).to_f +Capybara.automatic_label_click = true + +Capybara::Screenshot.register_driver(:firefox) do |driver, path| + driver.browser.save_screenshot(path) +end +Capybara.save_path = "./test-failures" + +Capybara.modify_selector(:link_or_button) do + expression_filter(:build_button) {|xpath, name| xpath[XPath.css('[data-test-id="button-menu-item__name"]').contains(name)] } +end + +Capybara.add_selector(:header) do + css { |_id| "[data-test-id = 'header']" } +end + +Capybara.add_selector(:output) do + css do |id| + id_s = 'output' + id_s += "-#{id}" if id + "[data-test-id = '#{id_s}']" + end +end + +Capybara.add_selector(:stdin) do + css { '[data-test-id = "stdin"]' } +end + +Capybara.add_selector(:loader) do + css { '[data-test-id = "loader"]' } +end + +Capybara.add_selector(:notification) do + css { '[data-test-id = "notification"]' } +end + +Capybara.add_selector(:config_option) do + xpath do |label| + ".//div[span[contains(., '#{label}')]]" + end +end + +RSpec.configure do |config| + config.after(:example, :js) do + page.execute_script <<~JS + (() => { + if (window.rustPlayground) { + window.rustPlayground.disableSyncChangesToStorage(); + } + })() + JS + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/support/editor.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/support/editor.rb new file mode 100644 index 0000000000..c3e7ab3641 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/support/editor.rb @@ -0,0 +1,31 @@ +class Editor + attr_reader :page + def initialize(page) + @page = page + end + + def set(text) + page.within('.ace_text-input', visible: :any) do + page.execute_script <<~JS + window.rustPlayground.setCode(#{text.to_json}); + JS + end + end + + def has_line?(text) + page.has_css? '.ace_line', text: text + end + + def has_highlighted_text?(text) + page.within('.ace_text-input', visible: :any) do + selected = page.evaluate_script <<~JS + (() => { + const editor = document.querySelector('.ace_editor').env.editor; + return editor.getSelectedText(); + })() + JS + + selected == text + end + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/support/matchers/be_at_url.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/support/matchers/be_at_url.rb new file mode 100644 index 0000000000..2d08fca6a2 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/support/matchers/be_at_url.rb @@ -0,0 +1,22 @@ +class NotAtUrlError < StandardError; end + +RSpec::Matchers.define :be_at_url do |path, query = {}| + match do |page| + page.document.synchronize(nil, errors: [NotAtUrlError] ) do + uri = URI::parse(page.current_url) + raise NotAtUrlError unless uri.path == path + + query = query.map { |k, v| [k.to_s, Array(v).map(&:to_s)] }.to_h + query_hash = CGI::parse(uri.query || '') + raise NotAtUrlError unless query <= query_hash + + true + end + rescue NotAtUrlError + false + end + + failure_message do |page| + "expected that #{page.current_url} would be #{path} with the query parameters #{query}" + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/support/notifications.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/support/notifications.rb new file mode 100644 index 0000000000..6b2ce183f0 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/support/notifications.rb @@ -0,0 +1,14 @@ +class Notifications + attr_reader :page + def initialize(page) + @page = page + end + + def close_all + page.all(:notification).each do |notification| + page.within(notification) do + page.click_on('dismiss notification') + end + end + end +end diff --git a/anneal/v1/playground/rust-anneal-playground/tests/spec/support/playground_actions.rb b/anneal/v1/playground/rust-anneal-playground/tests/spec/support/playground_actions.rb new file mode 100644 index 0000000000..8e6a05f581 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/tests/spec/support/playground_actions.rb @@ -0,0 +1,36 @@ +# coding: utf-8 + +module PlaygroundActions + def in_build_menu(&block) + in_menu("Select what to build", &block) + end + + def in_mode_menu(&block) + in_menu("Mode — Choose the optimization level", &block) + end + + def in_channel_menu(&block) + in_menu("Channel — Choose the Rust version", &block) + end + + def in_advanced_options_menu(&block) + in_menu("Advanced compilation flags", &block) + end + + def in_tools_menu(&block) + in_menu("Tools", &block) + end + + def in_config_menu(&block) + in_menu("Config", close: true, &block) + end + + private + + def in_menu(button_locator, close: false) + click_on(button_locator) + yield + ensure + click_on(button_locator) if close + end +end