Skip to content

qryxip/cargo-compete

Repository files navigation

cargo-compete

CI codecov dependency status Crates.io Crates.io Join the chat at https://gitter.im/cargo-compete/community

日本語

A Cargo subcommand for competitive programming.

Supports AtCoder, Codeforces, and yukicoder. Other websites are available via online-judge-tools/api-client.

Features

  • Log in a website,
  • (Automatically) register in a contest,
  • Retrieves sample/system test cases, and save them as YAML files,
  • Test your code for the YAML files,
  • Submit your code,
  • Watch your submissions. (available for only AtCoder)
Registeration Sample Test Cases System Test Cases Submiting Watching Submissions Submission Details
AtCoder ✔️ ✔️ ✔️ ✔️
Codeforces ✔️ N/A ✔️
yukicoder N/A ✔️ ✔️ ✔️
Other websites Depends on online-judge-tools Depends on online-judge-tools Depends on online-judge-tools

Installation

From Crates.io

$ cargo install cargo-compete

If the build fails, adding --locked may help.

From master branch

$ cargo install --git https://github.com/qryxip/cargo-compete

From GitHub Releases

We provide the binaries in GitHub Releases.

Usages

cargo compete init

Generates some files for other commands.

Run this command first. It generates the following files.

  • compete.toml

    Required for other commands.

  • .cargo/config.toml

    Sets build/target-dir to share the target directory.

  • template-cargo-lock.toml

    A template of Cargo.lock for cargo compete new. Generated only if you answer 2 Yes to Do you use crates on AtCoder? question. If this file is generated, file path to it is added to new.template.lockfile in compete.toml.

Screenshot

cargo compete migrate cargo-atcoder

See the section in the Japanese readme.

cargo compete login

Logges in a website.

This is not a command for a package.

You don't have to run this command beforehand, because cargo-compete asks credentials if necessary.

cargo compete participate

Registeres in a contest.

This is not a command for a package.

You don't have to run this command beforehand, because cargo-compete registers in the contest if necessary.

cargo compete new

Retrieves test cases and creates a package for the contest.

Requires compete.toml. Generate it with cargo compete init first.

You can opens the pages in your browser with the --open option. And you can also open the source files and the test cases in your browser by testing open in compete.toml. If you forget to add --open, cd to the generated package and run cargo compete open.

Record

cargo compete add

Generates bin targets and retrieves the test cases for them.

Requires compete.toml. Generate it with cargo compete init first.

To use this function, configure add in the compete.toml like this.

# for yukicoder
[add]
url = '{% case args[0] %}{% when "contest" %}https://yukicoder.me/contests/{{ args[1] }}{% when "problem" %}https://yukicoder.me/problems/no/{{ args[1] }}{% endcase %}'
is-contest = ["bash", "-c", '[[ $(cut -d / -f 4) == "contests" ]]'] # optional
#target-kind = "bin" # ["bin", "example"]. default to "bin"
bin-name = '{% assign segments = url | split: "/" %}{{ segments[5] }}'
#bin-alias = '{% assign segments = url | split: "/" %}{{ segments[5] }}' # optional
#bin-src-path = './src/bin/{{ bin_alias }}.rs' # optional
cargo compete a contest 296
    Added `1358` (bin) for https://yukicoder.me/problems/no/1358
    Added `1359` (bin) for https://yukicoder.me/problems/no/1359
    Added `1360` (bin) for https://yukicoder.me/problems/no/1360
    Added `1361` (bin) for https://yukicoder.me/problems/no/1361
    Added `1362` (bin) for https://yukicoder.me/problems/no/1362
    Added `1363` (bin) for https://yukicoder.me/problems/no/1363
    Added `1364` (bin) for https://yukicoder.me/problems/no/1364
    Added `1365` (bin) for https://yukicoder.me/problems/no/1365
    Saved 1 test case to /home/ryo/src/competitive/yukicoder/testcases/1358.yml
    Saved 3 test cases to /home/ryo/src/competitive/yukicoder/testcases/1359.yml
    Saved 3 test cases to /home/ryo/src/competitive/yukicoder/testcases/1360.yml
    Saved 3 test cases to /home/ryo/src/competitive/yukicoder/testcases/1361.yml
    Saved 3 test cases to /home/ryo/src/competitive/yukicoder/testcases/1362.yml
    Saved 1 test case to /home/ryo/src/competitive/yukicoder/testcases/1363.yml
    Saved 3 test cases to /home/ryo/src/competitive/yukicoder/testcases/1364.yml
    Saved 3 test cases to /home/ryo/src/competitive/yukicoder/testcases/1365.ymlcargo compete a problem 9001
    Added `9001` (bin) for https://yukicoder.me/problems/no/9001
    Saved 1 test case to /home/ryo/src/competitive/yukicoder/testcases/9001.yml

cargo compete retrieve testcases / cargo compete download

Retrieves test cases for an existing package.

This is a command for a package. cd to the package generated with cargo compete new.

Screenshot

With --open option, you can download system test cases instead of sample ones.

For AtCoder, we have to use Dropbox API. Generate an access token with these two permissions in some way,

  • files.metadata.read
  • sharing.read

and save a JSON file in the following format to {local data directory}/cargo-compete/tokens/dropbox.json. (I'm thinking of better way)

{
  "access_token": "<access token>"
}

asciicast

cargo compete retrieve submission-summaries

Retrieves your submissions, and outputs as JSON.

This is a command for a package. cd to the package generated with cargo compete new.

asciicast

For example, you can get "the URL for the latest submission" by adding | jq -r '.summaries[0].detail.

$ # for Linux
$ xdg-open "$(cargo compete r ss | jq -r '.summaries[0].detail')"

cargo compete open

Opens pages in your browser, and opens source and test cases in your editor.

This is a command for a package. cd to the package generated with cargo compete new.

cargo compete test

Runs tests.

This is a command for a package. cd to the package generated with cargo compete new.

You don't have to run this command beforehand, because the tests are run in the submit command.

cargo compete submit

Submits your code.

This is a command for a package. cd to the package generated with cargo compete new.

asciicast

You can convert code with a tool such as cargo-equip and cargo-executable-payload by setting submit in the compete.toml.

[submit]
kind = "command"
args = ["cargo", "+1.70.0", "equip", "--exclude-atcoder-202301-crates", "--remove", "docs", "--minify", "libs", "--bin", "{{ bin_name }}"]
language_id = "5054"
[submit]
kind = "command"
args = ["cargo", "executable-payload", "--bin", "{{ bin_name }}"]
language_id = "5054"

Configuration

Here is an example for compete.toml.

# Path to the test file (Liquid template)
#
# Variables:
#
# - `manifest_dir`: Package directory
# - `contest`:      Contest ID (e.g. "abc100")
# - `bin_name`:     Name of a `bin` target (e.g. "abc100-a")
# - `bin_alias`:    "Alias" for a `bin` target defined in `pacakge.metadata.cargo-compete` (e.g. "a")
# - `problem`:      Alias for `bin_alias` (deprecated)
#
# Additional filters:
#
# - `kebabcase`: Convert to kebab case (by using the `heck` crate)
test-suite = "{{ manifest_dir }}/testcases/{{ bin_alias }}.yml"

# Open files with the command (`jq` command that outputs `string[] | string[][]`)
#
# VSCode:
#open = '[["code", "-a", .manifest_dir], ["code"] + (.paths | map([.src, .test_suite]) | flatten)]'
# Emacs:
#open = '["emacsclient", "-n"] + (.paths | map([.src, .test_suite]) | flatten)'

[template]
src = '''
fn main() {
    todo!();
}
'''

[template.new]
# `edition` for `Cargo.toml`.
edition = "2018"
# `profile` for `Cargo.toml`.
#
# By setting this, you can run tests with `opt-level=3` while enabling `debug-assertions` and `overflow-checks`.
#profile = '''
#[dev]
#opt-level = 3
#'''
dependencies = '''
num = "=0.2.1"
num-bigint = "=0.2.6"
num-complex = "=0.2.4"
num-integer = "=0.1.42"
num-iter = "=0.1.40"
num-rational = "=0.2.4"
num-traits = "=0.2.11"
num-derive = "=0.3.0"
ndarray = "=0.13.0"
nalgebra = "=0.20.0"
alga = "=0.9.3"
libm = "=0.2.1"
rand = { version = "=0.7.3", features = ["small_rng"] }
getrandom = "=0.1.14"
rand_chacha = "=0.2.2"
rand_core = "=0.5.1"
rand_hc = "=0.2.0"
rand_pcg = "=0.2.1"
rand_distr = "=0.2.2"
petgraph = "=0.5.0"
indexmap = "=1.3.2"
regex = "=1.3.6"
lazy_static = "=1.4.0"
ordered-float = "=1.0.2"
ascii = "=1.0.0"
permutohedron = "=0.2.4"
superslice = "=1.0.0"
itertools = "=0.9.0"
itertools-num = "=0.1.3"
maplit = "=1.0.2"
either = "=1.5.3"
im-rc = "=14.3.0"
fixedbitset = "=0.2.0"
bitset-fixed = "=0.1.0"
proconio = { version = "=0.3.6", features = ["derive"] }
text_io = "=0.1.8"
whiteread = "=0.5.0"
rustc-hash = "=1.1.0"
smallvec = "=1.2.0"
'''
dev-dependencies = '''
#atcoder-202004-lock = { git = "https://github.com/qryxip/atcoder-202004-lock" }
'''

[template.new.copy-files]
"./template-cargo-lock.toml" = "Cargo.lock"

[new]
kind = "cargo-compete"
# Platform
#
# - atcoder
# - codeforces
# - yukicoder
platform = "atcoder"
# Path (Liquid template)
#
# Variables:
#
# - `contest`:      Contest ID. **May be nil**
# - `package_name`: Package name
path = "./{{ contest }}"

#[new]
#kind = "oj-api"
#url = "https://atcoder.jp/contests/{{ id }}"
#path = "./{{ contest }}"

# for Library-Checker
#[add]
#url = "https://judge.yosupo.jp/problem/{{ args[0] }}"
##is-contest = ["false"] # optional
##target-kind = "bin" # ["bin", "example"]. default to "bin"
#bin-name = '{{ args[0] }}'
##bin-alias = '{{ args[0] }}' # optional
##bin-src-path = './src/bin/{{ bin_alias }}.rs' # optional

# for yukicoder
#[add]
#url = '{% case args[0] %}{% when "contest" %}https://yukicoder.me/contests/{{ args[1] }}{% when "problem" %}https://yukicoder.me/problems/no/{{ args[1] }}{% endcase %}'
#is-contest = ["bash", "-c", '[[ $(cut -d / -f 4) == "contests" ]]'] # optional
##target-kind = "bin" # ["bin", "example"]. default to "bin"
#bin-name = '{% assign segments = url | split: "/" %}{{ segments[5] }}'
##bin-alias = '{% assign segments = url | split: "/" %}{{ segments[5] }}' # optional
##bin-src-path = './src/bin/{{ bin_alias }}.rs' # optional

[test]
# Toolchain for the test. (optional)
toolchain = "1.42.0"
# Profile for `cargo build`. ("dev" | "release")
#
# Defaults to `"dev"`.
#profile = "dev"

[submit]
kind = "file"
path = "{{ src_path }}"
language_id = "5054"
#[submit]
#kind = "command"
#args = ["cargo", "+1.70.0", "equip", "--exclude-atcoder-202301-crates", "--remove", "docs", "--minify", "libs", "--bin", "{{ bin_name }}"]
#language_id = "5054"

And here is an example for package.metadata in Cargo.toml.

[package]
name = "practice"
version = "0.1.0"
authors = ["Ryo Yamashita <qryxip@gmail.com>"]
edition = "2018"

[package.metadata.cargo-compete.bin]
practice-a = { alias = "a", problem = "https://atcoder.jp/contests/practice/tasks/practice_1" }
practice-b = { alias = "b", problem = "https://atcoder.jp/contests/practice/tasks/practice_2" }

#[package.metadata.cargo-compete.example]

[[bin]]
name = "practice-a"
path = "src/bin/a.rs"

[[bin]]
name = "practice-b"
path = "src/bin/b.rs"

[dependencies]
num = "=0.2.1"
num-bigint = "=0.2.6"
num-complex = "=0.2.4"
num-integer = "=0.1.42"
num-iter = "=0.1.40"
num-rational = "=0.2.4"
num-traits = "=0.2.11"
num-derive = "=0.3.0"
ndarray = "=0.13.0"
nalgebra = "=0.20.0"
alga = "=0.9.3"
libm = "=0.2.1"
rand = { version = "=0.7.3", features = ["small_rng"] }
getrandom = "=0.1.14"
rand_chacha = "=0.2.2"
rand_core = "=0.5.1"
rand_hc = "=0.2.0"
rand_pcg = "=0.2.1"
rand_distr = "=0.2.2"
petgraph = "=0.5.0"
indexmap = "=1.3.2"
regex = "=1.3.6"
lazy_static = "=1.4.0"
ordered-float = "=1.0.2"
ascii = "=1.0.0"
permutohedron = "=0.2.4"
superslice = "=1.0.0"
itertools = "=0.9.0"
itertools-num = "=0.1.3"
maplit = "=1.0.2"
either = "=1.5.3"
im-rc = "=14.3.0"
fixedbitset = "=0.2.0"
bitset-fixed = "=0.1.0"
proconio = { version = "=0.3.6", features = ["derive"] }
text_io = "=0.1.8"
whiteread = "=0.5.0"
rustc-hash = "=1.1.0"
smallvec = "=1.2.0"

[dev-dependencies]

Test suite

Test cases are saved as YAML files.

# https://atcoder.jp/contests/practice/tasks/practice_1
---
type: Batch
timelimit: 2s
match: Lines

cases:
  - name: sample1
    in: |
      1
      2 3
      test
    out: |
      6 test
  - name: sample2
    in: |
      72
      128 256
      myonmyon
    out: |
      456 myonmyon

extend:
  - type: Text
    path: "./a"
    in: /in/*.txt
    out: /out/*.txt
# https://atcoder.jp/contests/ddcc2019-final/tasks/ddcc2019_final_a
---
type: Batch
timelimit: 2s
match:
  Float:
    relative_error: 1e-8
    absolute_error: 1e-8

cases:
  - name: sample1
    in: |
      5
      -->--
    out: |
      3.83333333333333
  - name: sample2
    in: |
      7
      -------
    out: |
      6.5
  - name: sample3
    in: |
      10
      -->>>-->--
    out: |
      6.78333333333333

extend:
  - type: Text
    path: "./a"
    in: /in/*.txt
    out: /out/*.txt
# https://judge.yosupo.jp/problem/sqrt_mod
---
type: Batch
timelimit: 10s
match:
  Checker:
    cmd: ~/.cache/online-judge-tools/library-checker-problems/math/sqrt_mod/checker "$INPUT" "$ACTUAL_OUTPUT" "$EXPECTED_OUTPUT"
    shell: Bash

cases: []

extend:
  - type: SystemTestCases

The format is TestSuite in the following schemas.

TestSuite

An internally tagged ADT.

TestSuite::Batch

A test suite for a normal problem.

Field Type Default Description
timelimit Duration | null ~ Time limit
match Match Judging method
cases Case[] [] Sets of input and output
extend Extend[] [] Additional sets of input and output

Duration

A string that can parsed with humantime::format_duration.

Match

An untagged ADT.

Match::Exact = "Exact"

Compares whole strings.

Match::SplitWhiteSpace = "SplitWhitespace"

Compares words splitted by whitespace.

Match::Lines = "Lines"

Compares lines.

Match::Float

Compares words splitted by whitespace.

absolute_error and relative_error are applied for pairs of words that can parsed as floating point numbers.

Field Type Default Description
relative_error PositiveFiniteFloat64 | null ~ Relative error
absolute_error PositiveFiniteFloat64 | null ~ Absolute error

PositiveFiniteFloat64

A 64-bit floating point number that is positive and is not inf.

Match::Checker

Checks with a shell script.

The following environment variables are given for the script.

  • INPUT
  • ACTUAL_OUTPUT
  • EXPECTED_OUTPUT (only if the Case.out is present)
Field Type Default Description
cmd str Command
shell Shell Shell

Shell

An untagged ADT.

Shell::Bash = "Bash"

Bash.

Case

Field Type Default Description
name str "" Name
in str Input
out str | null ~ Output
timelimit Duration | null ~ Overrides timelimit
match Match | null ~ Overrides match

Extend

An internally tagged ADT.

Extend::Text

Field Type Default Description
path str Directory
in Glob Text files for input
out Glob Text files for output
timelimit Duration | null ~ Overrides timelimit
match Match | null ~ Overrides match

Glob

A glob.

Extend::SystemTestCases

System test cases.

System test cases are stored under { cache directory }/cargo-compete/system-test-cases. They are automatically downloaded if missing when testing code.

Field Type Default Description
problem Url | null ~ URL of the problem

Url

A URL.

TestSuite::Interactive

A test suite for an interactive problem.

Field Type Default Description
timelimit Duration | null ~ Time limit

TestSuite::Unsubmittable

A dummy test suite for dummy problems such as ones in APG4b.

Field Type Default Description

Cookies and tokens

The cookies and tokens are saved under { local data directory }/cargo-compete.

.
├── cookies.jsonl
└── tokens
    ├── codeforces.json
    ├── dropbox.json
    └── yukicoder.json

Environment variables

cargo-compete reads these environment variables if they exist, and use them.

  • $DROPBOX_ACCESS_TOKEN
  • $YUKICODER_API_KEY
  • $CODEFORCES_API_KEY
  • $CODEFORCES_API_SECRET

For unsupported websites, oj-api(.exe) in the $PATH is used when downloading and submitting.

[package]
name = "library-checker"
version = "0.0.0"
edition = "2018"
publish = false

[package.metadata.cargo-compete.bin]
aplusb = { problem = "https://judge.yosupo.jp/problem/aplusb" }

Video

Compared with cargo-atcoder

See the section in the Japanese readme.

License

Dual-licensed under MIT or Apache-2.0.