Skip to content

Commit

Permalink
Rustfinity test runner (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
dcodesdev authored Jul 30, 2024
1 parent 4daf530 commit e7fb5d7
Show file tree
Hide file tree
Showing 11 changed files with 340 additions and 8 deletions.
44 changes: 44 additions & 0 deletions .github/workflows/rustfinity-runner.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Build Rustfinity Runner

on:
push:
branches:
- main
- staging

paths:
- "crates/rustfinity-runner/**"
- "challenges/**"

jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to GitHub Container Registry
run: echo "${{ github.token }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin

- name: Determine tag
id: tag
run: |
if [[ "${GITHUB_REF#refs/heads/}" == "main" ]]; then
echo "::set-output name=tag::latest"
elif [[ "${GITHUB_REF#refs/heads/}" == "staging" ]]; then
echo "::set-output name=tag::staging"
fi
- name: Build and push
uses: docker/build-push-action@v6
with:
file: crates/rustfinity-runner/Dockerfile
push: true
tags: |
ghcr.io/${{ github.repository_owner }}/rustfinity-runner:${{ steps.tag.outputs.tag }}
88 changes: 80 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/rustfinity-runner/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target/**
12 changes: 12 additions & 0 deletions crates/rustfinity-runner/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "rustfinity-runner"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0.86"
base64 = "0.22.1"
clap = { version = "4.5.11", features = ["derive"] }
dotenvy = "0.15.7"
duct = "0.13.7"
tokio = { version = "1.39.2", features = ["full"] }
31 changes: 31 additions & 0 deletions crates/rustfinity-runner/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Stage 1: Clone the repository
FROM alpine/git AS git
WORKDIR /app
RUN git clone https://github.com/dcodesdev/rustfinity.com

# Stage 2: Build the Runner CLI and the rustfinity.com project
FROM rust:slim-buster
LABEL rustfinity-runner="true"

# Install OpenSSL development packages and pkg-config
RUN apt-get update && apt-get install -y \
pkg-config \
libssl-dev \
&& rm -rf /var/lib/apt/lists/*

# Build the Runner CLI
WORKDIR /app/runner
COPY . .
RUN cargo build --release

# Move the Runner CLI executable
RUN mv target/release/rustfinity-runner /app/

# Build the rustfinity.com project
WORKDIR /app/rustfinity.com
COPY --from=git /app/rustfinity.com .
RUN cargo build

# The final structure:
# /app/rustfinity-runner (executable)
# /app/rustfinity.com/ (project directory)
31 changes: 31 additions & 0 deletions crates/rustfinity-runner/Dockerfile.staging
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Stage 1: Clone the repository
FROM alpine/git AS git
WORKDIR /app
RUN git clone -b staging https://github.com/dcodesdev/rustfinity.com

# Stage 2: Build the Runner CLI and the rustfinity.com project
FROM rust:slim-buster
LABEL rustfinity-runner="true"

# Install OpenSSL development packages and pkg-config
RUN apt-get update && apt-get install -y \
pkg-config \
libssl-dev \
&& rm -rf /var/lib/apt/lists/*

# Build the Runner CLI
WORKDIR /app/runner
COPY . .
RUN cargo build --release

# Move the Runner CLI executable
RUN mv target/release/rustfinity-runner /app/

# Build the rustfinity.com project
WORKDIR /app/rustfinity.com
COPY --from=git /app/rustfinity.com .
RUN cargo build

# The final structure:
# /app/rustfinity-runner (executable)
# /app/rustfinity.com/ (project directory)
37 changes: 37 additions & 0 deletions crates/rustfinity-runner/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Rustfinity code runner

The official code runner used in [Rustfinity.com](https://www.rustfinity.com), a platform to learn Rust programming language.

## How to use

It's best to run this in a docker container to be completely isolated from the outside world.

1. Build the image locally

```sh
make build TAG=latest
```

2. Convert your code to **base64** format.

In JavaScript it would be something like this

```js
const code = `fn main() {
println!("Hello, world!");
}`;

const base64Code = Buffer.from(code).toString("base64");
```

3. Run the code using the CLI

```bash
docker run -i \
--rm \
--network=none \
--cpus=1 \
-m=500m \
rustfinity-runner \
/bin/bash -c "/app/rustfinity-runner run --code 'cHViIGZuIGhlbGxvX3dvcmxkKCkgewogICAgcHJpbnRsbiEoIkdvb2Qgam9iLCB5b3UgZGVjb2RlZCBpdCA6RCIpCn0K' --challenge 'printing-hello-world'"
```
8 changes: 8 additions & 0 deletions crates/rustfinity-runner/makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
TAG ?= staging

build:
ifeq ($(TAG), staging)
docker build --no-cache -t rustfinity-runner:$(TAG) . -f Dockerfile.staging
else
docker build --no-cache -t rustfinity-runner:$(TAG) . -f Dockerfile
endif
22 changes: 22 additions & 0 deletions crates/rustfinity-runner/src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use clap::{Parser, Subcommand};

#[derive(Debug, Parser)]
#[clap(about = "CLI for rust code runner", long_about = None)]
pub struct Cli {
#[clap(subcommand)]
pub command: Commands,
}

#[derive(Debug, Subcommand)]
pub enum Commands {
#[clap(about = "Run and test the code based on the challenge and code provided")]
Run {
#[clap(short, long)]
/// Code base64 encoded
code: String,

#[clap(short, long)]
/// Challenge slug
challenge: String,
},
}
47 changes: 47 additions & 0 deletions crates/rustfinity-runner/src/command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use base64::prelude::*;
use duct::cmd;
use std::env;
use std::fs;
use std::path::Path;

pub async fn run_code(code_base64: &str, challenge: &str) -> anyhow::Result<String> {
let challenges_path = env::var("CHALLENGES_PATH")
// default value in container
.unwrap_or("/app/rustfinity.com/challenges".to_string());

let code_utf8 = BASE64_STANDARD.decode(code_base64)?;

let code = String::from_utf8(code_utf8)?;

let repo_path = format!("{challenges_path}/{challenge}");
let repository_path = Path::new(&repo_path).canonicalize()?;

// write src/lib.rs
let lib_path = repository_path.join("src/lib.rs");

fs::write(&lib_path, &code)?;

let cwd = repository_path
.to_str()
.ok_or(anyhow::anyhow!("Invalid path"))?;

let output = run_command_and_merge_output("cargo", &["test"], cwd).await?;

Ok(output)
}

pub async fn run_command_and_merge_output(
command: &str,
args: &[&str],
cwd: &str,
) -> anyhow::Result<String> {
let output = cmd!(command, args.join(" "))
.stderr_to_stdout()
.stdout_capture()
.dir(cwd)
// don't care about exit code
.unchecked()
.run()?;

Ok(String::from_utf8(output.stdout)?)
}
Loading

0 comments on commit e7fb5d7

Please sign in to comment.