Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: Test

on:
push:
branches: [main]
pull_request:
branches: [main]

env:
CARGO_TERM_COLOR: always

jobs:
test:
name: Run Tests
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
rust: [stable]

steps:
- uses: actions/checkout@v4

- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
components: clippy, rustfmt

- name: Set up cargo cache
uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.os }}-test

- name: Check formatting
run: cargo fmt --all -- --check

- name: Run clippy
run: cargo clippy --all-targets --all-features -- -D warnings

- name: Build
run: cargo build --verbose

- name: Run tests
run: cargo test --verbose

coverage:
name: Code Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable

- name: Install cargo-tarpaulin
run: cargo install cargo-tarpaulin

- name: Run coverage
run: cargo tarpaulin --out Xml --verbose

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: cobertura.xml
fail_ci_if_error: false
92 changes: 92 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# CLAUDE.md

## Contributing

### Development Setup

```bash
# Build
cargo build

# Run tests
cargo test

# Format code (required before commits)
cargo fmt --all

# Lint (must pass with no warnings)
cargo clippy --all-targets --all-features -- -D warnings
```

### CI Requirements

All PRs must pass:
- `cargo fmt --all -- --check` - Code formatting
- `cargo clippy -- -D warnings` - No clippy warnings allowed
- `cargo test` - All tests pass
- Builds on Linux, macOS, and Windows

## Architecture Overview

Popcorn CLI is a command-line tool for submitting GPU kernel optimization solutions to [gpumode.com](https://gpumode.com) competitions.

### Directory Structure

```
src/
├── main.rs # Entry point, sets POPCORN_API_URL
├── cmd/ # Command handling
│ ├── mod.rs # CLI argument parsing (clap), config loading
│ ├── auth.rs # OAuth authentication (Discord/GitHub)
│ └── submit.rs # Submission logic, TUI app state machine
├── service/
│ └── mod.rs # HTTP client, API calls, SSE streaming
├── models/
│ └── mod.rs # Data structures (LeaderboardItem, GpuItem, AppState)
├── utils/
│ └── mod.rs # Directive parsing, text wrapping, ASCII art
└── views/
├── loading_page.rs # TUI loading screen with progress bar
└── result_page.rs # TUI results display with scrolling
```

### Core Flow

1. **Authentication** (`cmd/auth.rs`): User registers via Discord/GitHub OAuth. CLI ID stored in `~/.popcorn.yaml`.

2. **Submission** (`cmd/submit.rs`):
- TUI mode: Interactive selection of leaderboard → GPU → mode
- Plain mode (`--no-tui`): Direct submission with CLI flags
- Reads solution file with optional `#!POPCORN` directives for defaults

3. **API Communication** (`service/mod.rs`):
- Fetches available leaderboards and GPUs
- Submits solutions via multipart form POST
- Handles SSE (Server-Sent Events) streaming for real-time results
- Supports modes: `test`, `benchmark`, `leaderboard`, `profile`

### File Directives

Users can embed defaults in their solution files:

```python
#!POPCORN leaderboard amd-fp8-mm
#!POPCORN gpu MI300

def solution():
...
```

Or C++ style:
```cpp
//!POPCORN leaderboard nvidia-matmul
//!POPCORN gpu H100
```

### Key Dependencies

- `clap` - CLI argument parsing
- `ratatui` + `crossterm` - Terminal UI
- `reqwest` - HTTP client with SSE streaming
- `tokio` - Async runtime
- `serde` / `serde_yaml` / `serde_json` - Serialization
1 change: 1 addition & 0 deletions Cargo.lock

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

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ urlencoding = "2.1.3"
bytes = "1.10.1"
futures-util = "0.3.31"


[dev-dependencies]
tempfile = "3.10"
15 changes: 12 additions & 3 deletions src/cmd/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,10 @@ pub async fn handle_admin(action: AdminAction) -> Result<()> {
if !skipped.is_empty() {
println!("\nSkipped {} leaderboard(s):", skipped.len());
for item in skipped {
let name = item.get("name").and_then(|n| n.as_str()).unwrap_or("unknown");
let name = item
.get("name")
.and_then(|n| n.as_str())
.unwrap_or("unknown");
let reason = item
.get("reason")
.and_then(|r| r.as_str())
Expand All @@ -166,8 +169,14 @@ pub async fn handle_admin(action: AdminAction) -> Result<()> {
if !errors.is_empty() {
println!("\nErrors ({}):", errors.len());
for item in errors {
let name = item.get("name").and_then(|n| n.as_str()).unwrap_or("unknown");
let error = item.get("error").and_then(|e| e.as_str()).unwrap_or("unknown");
let name = item
.get("name")
.and_then(|n| n.as_str())
.unwrap_or("unknown");
let error = item
.get("error")
.and_then(|e| e.as_str())
.unwrap_or("unknown");
println!(" ! {}: {}", name, error);
}
}
Expand Down
4 changes: 0 additions & 4 deletions src/cmd/auth.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
use anyhow::{anyhow, Result};
use base64_url;
use dirs;
use serde::{Deserialize, Serialize};
use serde_yaml;
use std::fs::{File, OpenOptions};
use std::path::PathBuf;
use webbrowser;

use crate::service; // Assuming service::create_client is needed

Expand Down
10 changes: 4 additions & 6 deletions src/cmd/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use anyhow::{anyhow, Result};
use clap::{Parser, Subcommand};
use dirs;
use serde::{Deserialize, Serialize};
use serde_yaml;
use std::fs::File;
use std::path::PathBuf;

Expand Down Expand Up @@ -173,9 +171,7 @@ pub async fn execute(cli: Cli) -> Result<()> {
.await
}
}
Some(Commands::Admin { action }) => {
admin::handle_admin(action).await
}
Some(Commands::Admin { action }) => admin::handle_admin(action).await,
None => {
// Check if any of the submission-related flags were used at the top level
if cli.gpu.is_some() || cli.leaderboard.is_some() || cli.mode.is_some() {
Expand Down Expand Up @@ -207,7 +203,9 @@ pub async fn execute(cli: Cli) -> Result<()> {
)
.await
} else {
Err(anyhow!("No command or submission file specified. Use --help for usage."))
Err(anyhow!(
"No command or submission file specified. Use --help for usage."
))
}
}
}
Expand Down
1 change: 0 additions & 1 deletion src/cmd/submit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,6 @@ impl App {
"Error starting GPU fetch: {}",
e
));
return;
}
}
} else {
Expand Down
5 changes: 4 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ use std::process;
async fn main() {
// Set the API URL FIRST - before anything else
if env::var("POPCORN_API_URL").is_err() {
env::set_var("POPCORN_API_URL", "https://discord-cluster-manager-1f6c4782e60a.herokuapp.com");
env::set_var(
"POPCORN_API_URL",
"https://discord-cluster-manager-1f6c4782e60a.herokuapp.com",
);
}
// Parse command line arguments
let cli = Cli::parse();
Expand Down
5 changes: 0 additions & 5 deletions src/models/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug)]
pub struct LeaderboardItem {
pub title_text: String,
Expand Down Expand Up @@ -51,6 +49,3 @@ pub enum AppState {
SubmissionModeSelection,
WaitingForResult,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct SubmissionResultMsg(pub String);
Loading