From f02db60f5501b611b34167f2d6aab98cd3c1fba2 Mon Sep 17 00:00:00 2001 From: dcodes <101001810+dcodesdev@users.noreply.github.com> Date: Sat, 16 Nov 2024 18:14:43 +0300 Subject: [PATCH 1/3] Rustfinity Runner V2 (#43) --- .github/workflows/rustfinity-runner.yaml | 25 +-- Cargo.lock | 16 +- crates/rustfinity-runner/Cargo.toml | 4 +- crates/rustfinity-runner/Dockerfile | 37 ++-- crates/rustfinity-runner/Dockerfile.staging | 37 ---- crates/rustfinity-runner/README.md | 17 +- crates/rustfinity-runner/docker/Cargo.toml | 3 + crates/rustfinity-runner/docker/README.md | 1 + .../rustfinity-runner/example-playground.sh | 4 + crates/rustfinity-runner/example.sh | 6 - crates/rustfinity-runner/makefile | 4 +- crates/rustfinity-runner/src/cli.rs | 21 +- crates/rustfinity-runner/src/command.rs | 197 ------------------ crates/rustfinity-runner/src/commands/mod.rs | 2 + .../src/commands/playground.rs | 40 ++++ .../src/commands/run_tests.rs | 162 ++++++++++++++ crates/rustfinity-runner/src/constants.rs | 1 + crates/rustfinity-runner/src/main.rs | 37 ++-- crates/rustfinity-runner/src/utils.rs | 44 ++++ 19 files changed, 342 insertions(+), 316 deletions(-) delete mode 100644 crates/rustfinity-runner/Dockerfile.staging create mode 100644 crates/rustfinity-runner/docker/Cargo.toml create mode 100644 crates/rustfinity-runner/docker/README.md create mode 100755 crates/rustfinity-runner/example-playground.sh delete mode 100755 crates/rustfinity-runner/example.sh delete mode 100644 crates/rustfinity-runner/src/command.rs create mode 100644 crates/rustfinity-runner/src/commands/mod.rs create mode 100644 crates/rustfinity-runner/src/commands/playground.rs create mode 100644 crates/rustfinity-runner/src/commands/run_tests.rs create mode 100644 crates/rustfinity-runner/src/constants.rs create mode 100644 crates/rustfinity-runner/src/utils.rs diff --git a/.github/workflows/rustfinity-runner.yaml b/.github/workflows/rustfinity-runner.yaml index cbe73e7..6c07a45 100644 --- a/.github/workflows/rustfinity-runner.yaml +++ b/.github/workflows/rustfinity-runner.yaml @@ -2,14 +2,8 @@ name: Build Rustfinity Runner on: push: - branches: - - main - - staging - paths: - - ".github/workflows/rustfinity-runner.yaml" - - "crates/rustfinity-runner/**" - - "challenges/**" - workflow_dispatch: + tags: + - v* jobs: publish: @@ -30,21 +24,10 @@ jobs: - name: Login to GitHub Container Registry run: echo "${{ github.token }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin - - name: Determine tag and Dockerfile - id: vars - run: | - if [[ "${GITHUB_REF#refs/heads/}" == "main" ]]; then - echo "TAG_NAME=latest" >> $GITHUB_ENV - echo "DOCKERFILE_PATH=crates/rustfinity-runner/Dockerfile" >> $GITHUB_ENV - elif [[ "${GITHUB_REF#refs/heads/}" == "staging" ]]; then - echo "TAG_NAME=staging" >> $GITHUB_ENV - echo "DOCKERFILE_PATH=crates/rustfinity-runner/Dockerfile.staging" >> $GITHUB_ENV - fi - - name: Build and push uses: docker/build-push-action@v6 with: - file: ${{ env.DOCKERFILE_PATH }} + file: crates/rustfinity-runner/Dockerfile push: true tags: | - ghcr.io/${{ github.repository_owner }}/rustfinity-runner:${{ env.TAG_NAME }} + ghcr.io/${{ github.repository_owner }}/rustfinity-runner:${{ github.ref_name }} diff --git a/Cargo.lock b/Cargo.lock index 11db119..00dbced 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,9 +96,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" [[package]] name = "atomic-waker" @@ -220,9 +220,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.14" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c937d4061031a6d0c8da4b9a4f98a172fc2976dfb1c19213a9cf7d0d3c837e36" +checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" dependencies = [ "clap_builder", "clap_derive", @@ -230,9 +230,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.14" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85379ba512b21a328adf887e85f7742d12e96eb31f3ef077df4ffc26b506ffed" +checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" dependencies = [ "anstream", "anstyle", @@ -242,9 +242,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.13" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck", "proc-macro2", diff --git a/crates/rustfinity-runner/Cargo.toml b/crates/rustfinity-runner/Cargo.toml index 7b67102..22728e3 100644 --- a/crates/rustfinity-runner/Cargo.toml +++ b/crates/rustfinity-runner/Cargo.toml @@ -4,9 +4,9 @@ version = "0.1.0" edition = "2021" [dependencies] -anyhow = "1.0.86" +anyhow = "1.0.93" base64 = "0.22.1" -clap = { version = "4.5.11", features = ["derive"] } +clap = { version = "4.5.21", features = ["derive"] } dotenvy = "0.15.7" duct = "0.13.7" regex = "1.10.6" diff --git a/crates/rustfinity-runner/Dockerfile b/crates/rustfinity-runner/Dockerfile index c8a7bb5..f0b6564 100644 --- a/crates/rustfinity-runner/Dockerfile +++ b/crates/rustfinity-runner/Dockerfile @@ -1,37 +1,32 @@ -# Stage 1: Clone the repository -FROM alpine/git AS git +# --- Stage 1 --- +FROM rust:slim-buster AS builder 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 +COPY crates/rustfinity-runner . +RUN cargo build --release + +# --- Stage 2 --- + +FROM rust:slim-buster AS runner LABEL rustfinity-runner="true" # Install OpenSSL development packages and pkg-config RUN apt-get update && apt-get install -y \ - pkg-config \ - libssl-dev \ heaptrack \ && rm -rf /var/lib/apt/lists/* -# Build the Runner CLI -WORKDIR /app/runner -COPY . . -RUN cargo build --release +WORKDIR /app + +COPY crates/syntest crates/syntest +COPY crates/rustfinity-runner/docker/Cargo.toml Cargo.toml -# Move the Runner CLI executable -RUN mv target/release/rustfinity-runner /app/ +RUN cargo new challenges/playground && rm -rf challenges/playground/.git -# Build the rustfinity.com project -WORKDIR /app/rustfinity.com -COPY --from=git /app/rustfinity.com . RUN cargo build -# Create a new project for (rustfinity.com/playground) -WORKDIR /app -RUN cargo new playground +COPY --from=builder /app/target/release/rustfinity-runner /app/ # The final structure: # /app/rustfinity-runner (executable) -# /app/rustfinity.com/ (project directory) -# /app/playground/ (project directory) +# /app/challenges/playground (project) +# /app/crates/syntest (library) diff --git a/crates/rustfinity-runner/Dockerfile.staging b/crates/rustfinity-runner/Dockerfile.staging deleted file mode 100644 index c29fa8b..0000000 --- a/crates/rustfinity-runner/Dockerfile.staging +++ /dev/null @@ -1,37 +0,0 @@ -# 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 \ - heaptrack \ - && 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 - -# Create a new project for (rustfinity.com/playground) -WORKDIR /app -RUN cargo new playground - -# The final structure: -# /app/rustfinity-runner (executable) -# /app/rustfinity.com/ (project directory) -# /app/playground/ (project directory) diff --git a/crates/rustfinity-runner/README.md b/crates/rustfinity-runner/README.md index 178fc23..cc7d086 100644 --- a/crates/rustfinity-runner/README.md +++ b/crates/rustfinity-runner/README.md @@ -33,11 +33,18 @@ docker run -i \ --cpus=1 \ -m=500m \ rustfinity-runner \ - /bin/bash -c "/app/rustfinity-runner run --code 'cHViIGZuIGhlbGxvX3dvcmxkKCkgewogICAgcHJpbnRsbiEoIkdvb2Qgam9iLCB5b3UgZGVjb2RlZCBpdCA6RCIpCn0K' --challenge 'printing-hello-world'" + /bin/bash -c "/app/rustfinity-runner playground --code 'cHViIGZuIGhlbGxvX3dvcmxkKCkgewogICAgcHJpbnRsbiEoIkdvb2Qgam9iLCB5b3UgZGVjb2RlZCBpdCA6RCIpCn0K'" ``` -### Arguments +### Commands -- `--code`: The base64 encoded code to run. -- `--challenge`: The challenge name. This is used to identify the challenge in the logs. -- `--n-tests` or `n`: The number of tests to run (takes the minimum amount of time in `ms` and prints it). +- `test`: Runs a [rustfinity challenge](../../challenges/), requires a few arguments: + + - `--code`: Base64 encoded code (user submitted) + - `--tests`: Base64 encoded tests file + - `--cargo-toml`: Base64 encoded Cargo.toml file for that challenge + - `--n-tests` (optional): How many times the benchmarks should run (default = 1) + +- `playground`: Runs a provided snippet of code, used in [rustfinity.com/playground](https://www.rustfinity.com/playground), requires one argument: + + - `--code`: Base64 encoded code (user submitted) diff --git a/crates/rustfinity-runner/docker/Cargo.toml b/crates/rustfinity-runner/docker/Cargo.toml new file mode 100644 index 0000000..f7a2096 --- /dev/null +++ b/crates/rustfinity-runner/docker/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["challenges/*", "crates/*"] +resolver = "2" diff --git a/crates/rustfinity-runner/docker/README.md b/crates/rustfinity-runner/docker/README.md new file mode 100644 index 0000000..e53e35f --- /dev/null +++ b/crates/rustfinity-runner/docker/README.md @@ -0,0 +1 @@ +This content is only used in the Docker image. diff --git a/crates/rustfinity-runner/example-playground.sh b/crates/rustfinity-runner/example-playground.sh new file mode 100755 index 0000000..6a1028f --- /dev/null +++ b/crates/rustfinity-runner/example-playground.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +cargo -q run playground \ + --code Zm4gbWFpbigpIHsKICAgIHByaW50bG4hKCJIZWxsbywgV29ybGQhIik7Cn0K diff --git a/crates/rustfinity-runner/example.sh b/crates/rustfinity-runner/example.sh deleted file mode 100755 index 96c8b62..0000000 --- a/crates/rustfinity-runner/example.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -cargo -q run run \ - --code cHViIGZuIGhlbGxvX3dvcmxkKCkgewogICAgcHJpbnRsbiEoImhlbGxvIHdvcmxkIikKfQo= \ - --challenge printing-hello-world \ - -n 10 diff --git a/crates/rustfinity-runner/makefile b/crates/rustfinity-runner/makefile index bb36daa..35a859d 100644 --- a/crates/rustfinity-runner/makefile +++ b/crates/rustfinity-runner/makefile @@ -2,7 +2,7 @@ TAG ?= staging build: ifeq ($(TAG), staging) - docker build --no-cache -t rustfinity-runner:$(TAG) . -f Dockerfile.staging + docker build -t rustfinity-runner:$(TAG) ../../ -f Dockerfile else - docker build --no-cache -t rustfinity-runner:$(TAG) . -f Dockerfile + docker build -t rustfinity-runner:$(TAG) ../../ -f Dockerfile endif diff --git a/crates/rustfinity-runner/src/cli.rs b/crates/rustfinity-runner/src/cli.rs index a9e6d02..5de05a0 100644 --- a/crates/rustfinity-runner/src/cli.rs +++ b/crates/rustfinity-runner/src/cli.rs @@ -9,18 +9,29 @@ pub struct Cli { #[derive(Debug, Subcommand)] pub enum Commands { - #[clap(about = "Run and test the code based on the challenge and code provided")] - Run { - #[clap(long)] + #[clap(about = "Run the code based on the code, tests, and cargo toml file provided")] + Test { /// Code base64 encoded + #[clap(long)] code: String, + /// Tests base64 encoded #[clap(long)] - /// Challenge slug - challenge: Option, + tests: String, + + /// Cargo toml base64 encoded + #[clap(long)] + cargo_toml: String, #[clap(long = "n-tests", short)] /// number of tests to take the minimum time of n_tests: Option, }, + + #[clap(about = "Run and test the code based on the challenge and code provided")] + Playground { + /// Code base64 encoded + #[clap(long)] + code: String, + }, } diff --git a/crates/rustfinity-runner/src/command.rs b/crates/rustfinity-runner/src/command.rs deleted file mode 100644 index 6504f61..0000000 --- a/crates/rustfinity-runner/src/command.rs +++ /dev/null @@ -1,197 +0,0 @@ -use base64::prelude::*; -use duct::cmd; -use std::env; -use std::fs; -use std::path::Path; -use std::process::Command; -use std::time::Instant; - -use crate::regex::extract_unittest_path; - -pub struct RunCodeParams { - code_base64: String, - challenge: String, - n_tests: usize, -} - -impl RunCodeParams { - pub fn new(code_base64: String, challenge: Option, n_tests: Option) -> Self { - Self { - code_base64, - challenge: challenge.unwrap_or("playground".to_string()), - n_tests: n_tests.unwrap_or(1), - } - } -} - -pub async fn run_code(params: &RunCodeParams) -> anyhow::Result { - let RunCodeParams { - code_base64, - challenge, - n_tests, - } = params; - - let mut output = String::new(); - - let tests_output = run_tests(&code_base64, &challenge).await?; - output.push_str(&tests_output); - - let test_binary_path = extract_unittest_path(&output); - - if challenge.as_str() == "playground" { - return Ok(output); - } - - if let Some(test_binary_path) = test_binary_path { - let time_output = benchmark_time_min(&challenge, &test_binary_path, n_tests).await?; - let memory_output = memory_benchmark(&challenge, &test_binary_path).await?; - - output.push_str("\n"); - output.push_str("---"); - output.push_str("\n"); - output.push_str(time_output.as_str()); - output.push_str("\n"); - output.push_str(memory_output.as_str()); - } - - Ok(output) -} - -async fn benchmark_time(challenge: &str, test_binary_path: &str) -> anyhow::Result { - let challenges_path = get_challenges_path(); - let current_challenge_path = format!("{challenges_path}/{challenge}"); - - let start = Instant::now(); - - Command::new(&test_binary_path) - .current_dir(¤t_challenge_path) - .output()?; - - let elapsed = start.elapsed(); - let as_nanos = elapsed.as_nanos(); - - let as_ms = as_nanos as f64 / 1_000_000.0; - - Ok(as_ms) -} - -/// Runs the tests 10 times and gets the minimum time -async fn benchmark_time_min( - challenge: &str, - test_binary_path: &str, - n_tests: &usize, -) -> anyhow::Result { - let mut nums = Vec::with_capacity(10); - - for _ in 0..*n_tests { - let time = benchmark_time(&challenge, &test_binary_path).await?; - nums.push(time); - } - - // Take Average - let min = nums - .iter() - .min_by(|a, b| a.partial_cmp(b).unwrap()) - .unwrap(); - - let final_output = format!("Time: {:.8}ms", min); - - Ok(final_output) -} - -async fn memory_benchmark(challenge: &str, test_binary_path: &str) -> anyhow::Result { - let challenges_path = get_challenges_path(); - let current_challenge_path = format!("{challenges_path}/{challenge}"); - - let output = Command::new("heaptrack") - .arg(&test_binary_path) - .current_dir(¤t_challenge_path) - .output()?; - - let stdout = String::from_utf8(output.stdout)?; - - let output_path = stdout.split("\"").collect::>()[1]; - - // run heaptrack --analyze {output_path} - let output = Command::new("heaptrack") - .arg("--analyze") - .arg(output_path) - .output()?; - - let stdout = String::from_utf8(output.stdout)?; - - let details = stdout.split("\n\n").collect::>(); - let last_index = details.len() - 1; - let details = details[last_index]; - - let mut memory = String::new(); - - details.lines().for_each(|line| { - if line.contains("peak heap memory consumption:") { - memory = line.replace("peak", "Peak"); - } - }); - - Ok(memory) -} - -async fn run_tests(code_base64: &str, challenge: &str) -> anyhow::Result { - let code_utf8 = BASE64_STANDARD.decode(code_base64)?; - let code = String::from_utf8(code_utf8)?; - - if challenge == "playground" { - let cwd = "/app/playground"; - let main_path = Path::new(cwd).join("src/main.rs"); - - // Write src/main.rs - fs::write(&main_path, &code)?; - - // Run the code - let output = run_command_and_merge_output("cargo", &["run"], Some(cwd)).await?; - - return Ok(output); - } - - let challenges_path = get_challenges_path(); - - 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"], Some(cwd)).await?; - - Ok(output) -} - -fn get_challenges_path() -> String { - // If you run this code outside a container, use the env var - env::var("CHALLENGES_PATH") - // default value in container - .unwrap_or("/app/rustfinity.com/challenges".to_string()) -} - -pub async fn run_command_and_merge_output( - command: &str, - args: &[&str], - cwd: Option<&str>, -) -> anyhow::Result { - let cwd = cwd.unwrap_or("."); - - 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)?) -} diff --git a/crates/rustfinity-runner/src/commands/mod.rs b/crates/rustfinity-runner/src/commands/mod.rs new file mode 100644 index 0000000..9e5b2e6 --- /dev/null +++ b/crates/rustfinity-runner/src/commands/mod.rs @@ -0,0 +1,2 @@ +pub mod playground; +pub mod run_tests; diff --git a/crates/rustfinity-runner/src/commands/playground.rs b/crates/rustfinity-runner/src/commands/playground.rs new file mode 100644 index 0000000..c9a0df9 --- /dev/null +++ b/crates/rustfinity-runner/src/commands/playground.rs @@ -0,0 +1,40 @@ +use std::path::Path; + +use crate::{ + constants::PLAYGROUND_DIR, + utils::{run_command_and_merge_output, to_utf8, write_file}, +}; + +pub struct PlaygroundParams { + code_base64: String, +} + +impl PlaygroundParams { + pub fn new(code_base64: String) -> Self { + Self { code_base64 } + } +} + +pub async fn run_code_in_playground(params: &PlaygroundParams) -> anyhow::Result { + let PlaygroundParams { code_base64 } = params; + + let mut output = String::new(); + + let tests_output = execute_code(&code_base64).await?; + output.push_str(&tests_output); + + Ok(output) +} + +async fn execute_code(code_base64: &str) -> anyhow::Result { + let code = to_utf8(code_base64)?; + + let cwd = std::env::var("PROJECT_PATH").unwrap_or(PLAYGROUND_DIR.to_string()); + let main_path = Path::new(&cwd).join("src/main.rs"); + + // Write src/main.rs + write_file(&main_path, &code)?; + let output = run_command_and_merge_output("cargo", &["run"], Some(&cwd)).await?; + + Ok(output) +} diff --git a/crates/rustfinity-runner/src/commands/run_tests.rs b/crates/rustfinity-runner/src/commands/run_tests.rs new file mode 100644 index 0000000..67e1fe9 --- /dev/null +++ b/crates/rustfinity-runner/src/commands/run_tests.rs @@ -0,0 +1,162 @@ +use base64::prelude::*; +use std::path::Path; +use std::process::Command; +use std::time::Instant; + +use crate::constants::PLAYGROUND_DIR; +use crate::regex::extract_unittest_path; +use crate::utils::{run_command_and_merge_output, write_file}; + +pub struct RunTestsParams { + code_base64: String, + tests_base64: String, + cargo_toml_base64: String, + n_tests: usize, +} + +impl RunTestsParams { + pub fn new( + code_base64: String, + tests_base64: String, + cargo_toml_base64: String, + n_tests: Option, + ) -> Self { + Self { + code_base64, + n_tests: n_tests.unwrap_or(1), + tests_base64, + cargo_toml_base64, + } + } +} + +pub async fn run_tests(params: &RunTestsParams) -> anyhow::Result { + let RunTestsParams { + code_base64, + n_tests, + tests_base64, + cargo_toml_base64, + } = params; + + let mut output = String::new(); + + let tests_output = execute_code(&code_base64, &tests_base64, &cargo_toml_base64).await?; + output.push_str(&tests_output); + + let test_binary_path = extract_unittest_path(&output); + + if let Some(test_binary_path) = test_binary_path { + let time_output = benchmark_time_min(&test_binary_path, n_tests).await?; + let memory_output = memory_benchmark(&test_binary_path).await?; + + output.push_str("\n"); + output.push_str("---"); + output.push_str("\n"); + output.push_str(time_output.as_str()); + output.push_str("\n"); + output.push_str(memory_output.as_str()); + } + + Ok(output) +} + +async fn benchmark_time(test_binary_path: &str) -> anyhow::Result { + let cwd = std::env::var("PROJECT_PATH").unwrap_or(PLAYGROUND_DIR.to_string()); + + let start = Instant::now(); + + Command::new(&test_binary_path).current_dir(&cwd).output()?; + + let elapsed = start.elapsed(); + let as_nanos = elapsed.as_nanos(); + + let as_ms = as_nanos as f64 / 1_000_000.0; + + Ok(as_ms) +} + +/// Runs the tests 10 times and gets the minimum time +async fn benchmark_time_min(test_binary_path: &str, n_tests: &usize) -> anyhow::Result { + let mut nums = Vec::with_capacity(10); + + for _ in 0..*n_tests { + let time = benchmark_time(&test_binary_path).await?; + nums.push(time); + } + + // Take Average + let min = nums + .iter() + .min_by(|a, b| a.partial_cmp(b).unwrap()) + .unwrap(); + + let final_output = format!("Time: {:.8}ms", min); + + Ok(final_output) +} + +async fn memory_benchmark(test_binary_path: &str) -> anyhow::Result { + let cwd = std::env::var("PROJECT_PATH").unwrap_or(PLAYGROUND_DIR.to_string()); + + let output = Command::new("heaptrack") + .arg(&test_binary_path) + .current_dir(&cwd) + .output()?; + + let stdout = String::from_utf8(output.stdout)?; + + let output_path = stdout.split("\"").collect::>()[1]; + + // run heaptrack --analyze {output_path} + let output = Command::new("heaptrack") + .arg("--analyze") + .arg(output_path) + .output()?; + + let stdout = String::from_utf8(output.stdout)?; + + let details = stdout.split("\n\n").collect::>(); + let last_index = details.len() - 1; + let details = details[last_index]; + + let mut memory = String::new(); + + details.lines().for_each(|line| { + if line.contains("peak heap memory consumption:") { + memory = line.replace("peak", "Peak"); + } + }); + + Ok(memory) +} + +fn to_utf8(base64: &str) -> anyhow::Result { + let utf8 = BASE64_STANDARD.decode(base64)?; + Ok(String::from_utf8(utf8)?) +} + +async fn execute_code( + code_base64: &str, + tests_base64: &str, + config_toml_base64: &str, +) -> anyhow::Result { + let code = to_utf8(code_base64)?; + let tests = to_utf8(tests_base64)?; + let config_toml = to_utf8(config_toml_base64)?; + + let cwd = std::env::var("PROJECT_PATH").unwrap_or(PLAYGROUND_DIR.to_string()); + let tests_path = Path::new(&cwd).join("tests/tests.rs"); + let config_toml_path = Path::new(&cwd).join("Cargo.toml"); + let lib_path = Path::new(&cwd).join("src/lib.rs"); + + // Write src/lib.rs + write_file(&lib_path, &code)?; + // Write tests/tests.rs + write_file(&tests_path, &tests)?; + // Write Cargo.toml + write_file(&config_toml_path, &config_toml)?; + + let output = run_command_and_merge_output("cargo", &["test"], Some(&cwd)).await?; + + Ok(output) +} diff --git a/crates/rustfinity-runner/src/constants.rs b/crates/rustfinity-runner/src/constants.rs new file mode 100644 index 0000000..af16006 --- /dev/null +++ b/crates/rustfinity-runner/src/constants.rs @@ -0,0 +1 @@ +pub const PLAYGROUND_DIR: &str = "/app/challenges/playground"; diff --git a/crates/rustfinity-runner/src/main.rs b/crates/rustfinity-runner/src/main.rs index 8b6ef96..65dd52c 100644 --- a/crates/rustfinity-runner/src/main.rs +++ b/crates/rustfinity-runner/src/main.rs @@ -1,12 +1,16 @@ use clap::Parser; -use command::{run_code, RunCodeParams}; +use cli::{Cli, Commands}; +use commands::{ + playground::{run_code_in_playground, PlaygroundParams}, + run_tests::{run_tests, RunTestsParams}, +}; use dotenvy::dotenv; mod cli; -mod command; +mod commands; +mod constants; mod regex; - -use cli::{Cli, Commands}; +mod utils; #[tokio::main] async fn main() { @@ -14,18 +18,27 @@ async fn main() { let cli = Cli::parse(); - match cli.command { - Commands::Run { + let result = match cli.command { + Commands::Test { code: code_base64, - challenge, + tests: tests_base64, + cargo_toml: cargo_toml_base64, n_tests, } => { - let params = RunCodeParams::new(code_base64, challenge, n_tests); + let params = RunTestsParams::new(code_base64, tests_base64, cargo_toml_base64, n_tests); - match run_code(¶ms).await { - Ok(output) => println!("{}", output), - Err(e) => eprintln!("{}", e), - }; + run_tests(¶ms).await } + + Commands::Playground { code: code_base64 } => { + let params = PlaygroundParams::new(code_base64); + + run_code_in_playground(¶ms).await + } + }; + + match result { + Ok(output) => println!("{}", output), + Err(e) => eprintln!("{}", e), } } diff --git a/crates/rustfinity-runner/src/utils.rs b/crates/rustfinity-runner/src/utils.rs new file mode 100644 index 0000000..7a247b9 --- /dev/null +++ b/crates/rustfinity-runner/src/utils.rs @@ -0,0 +1,44 @@ +use std::{ + fs::{self, OpenOptions}, + io::Write, + path::Path, +}; + +use base64::{prelude::BASE64_STANDARD, Engine}; +use duct::cmd; + +pub fn to_utf8(base64: &str) -> anyhow::Result { + let utf8 = BASE64_STANDARD.decode(base64)?; + Ok(String::from_utf8(utf8)?) +} + +pub async fn run_command_and_merge_output( + command: &str, + args: &[&str], + cwd: Option<&str>, +) -> anyhow::Result { + let cwd = cwd.unwrap_or("."); + + 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)?) +} + +pub fn write_file(path: &Path, content: &str) -> std::io::Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; // Ensure parent directories exist + } + let mut file = OpenOptions::new() + .write(true) + .create(true) // Create the file if it doesn't exist + .truncate(true) // Replace the content if the file exists + .open(path)?; + file.write_all(content.as_bytes())?; + Ok(()) +} From 191357393bebbfe5e381230bfe1abf9dd6e12a20 Mon Sep 17 00:00:00 2001 From: dcodesdev <101001810+dcodesdev@users.noreply.github.com> Date: Sat, 16 Nov 2024 18:24:46 +0300 Subject: [PATCH 2/3] workflow trigger on PR only --- .github/workflows/ci.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d6cd0b0..3177f20 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,8 +1,9 @@ name: Test and publish crates on: - push: pull_request: + branches: + - main env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} From 62c4b67e6090dbc7374519904db8ea849faaf8ea Mon Sep 17 00:00:00 2001 From: dcodesdev <101001810+dcodesdev@users.noreply.github.com> Date: Sun, 17 Nov 2024 11:28:50 +0300 Subject: [PATCH 3/3] makefile updated --- crates/rustfinity-runner/makefile | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/rustfinity-runner/makefile b/crates/rustfinity-runner/makefile index 35a859d..873795e 100644 --- a/crates/rustfinity-runner/makefile +++ b/crates/rustfinity-runner/makefile @@ -1,8 +1,5 @@ TAG ?= staging +.PHONY: build build: -ifeq ($(TAG), staging) docker build -t rustfinity-runner:$(TAG) ../../ -f Dockerfile -else - docker build -t rustfinity-runner:$(TAG) ../../ -f Dockerfile -endif