mirror of
https://github.com/rust-lang/rustlings.git
synced 2025-12-28 14:59:18 +00:00
Compare commits
386 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7850a73d95 | ||
|
|
1ebb4d25a6 | ||
|
|
b5d440fdc3 | ||
|
|
4700e8a12c | ||
|
|
8753dd6b2e | ||
|
|
f80fbca12e | ||
|
|
d8f4b06c91 | ||
|
|
1955313362 | ||
|
|
95a597eb82 | ||
|
|
2af9e89ba5 | ||
|
|
6ec2e194ae | ||
|
|
295ad2e4bd | ||
|
|
628ef55337 | ||
|
|
b6b94e3e96 | ||
|
|
6765a0b61a | ||
|
|
436c95f4cc | ||
|
|
c6888685e6 | ||
|
|
208a593216 | ||
|
|
2d1d531550 | ||
|
|
a712e484d0 | ||
|
|
4f9f0907c3 | ||
|
|
3a2fe2c394 | ||
|
|
f24861957a | ||
|
|
1a633e2757 | ||
|
|
9fecdba101 | ||
|
|
7af38e684d | ||
|
|
e8da6869f8 | ||
|
|
57b3727b3e | ||
|
|
278edc0b96 | ||
|
|
cb60c8887c | ||
|
|
46814d397a | ||
|
|
734fc482eb | ||
|
|
520dfdc464 | ||
|
|
2267f99684 | ||
|
|
bf74a3d0a7 | ||
|
|
adf3ddd968 | ||
|
|
f80c2edc3d | ||
|
|
04520ae7ad | ||
|
|
e36dd7a120 | ||
|
|
edc8528dde | ||
|
|
47e490a997 | ||
|
|
596e7f36cc | ||
|
|
512ded81c4 | ||
|
|
69a9e9cafc | ||
|
|
54a74fd638 | ||
|
|
a51d6f1309 | ||
|
|
f6a657a0c3 | ||
|
|
8c24763259 | ||
|
|
dc468882cc | ||
|
|
5fc787f4e4 | ||
|
|
8fa598ae7e | ||
|
|
2f700991f3 | ||
|
|
b4a6b87e24 | ||
|
|
984e9fea7c | ||
|
|
8339007633 | ||
|
|
23b9aa3a15 | ||
|
|
69fe9626da | ||
|
|
f387f4c1d9 | ||
|
|
40fe3aa741 | ||
|
|
b30973afa1 | ||
|
|
3d8bef4bc3 | ||
|
|
2673177b17 | ||
|
|
6d5369d4d0 | ||
|
|
b9d1e636a4 | ||
|
|
7e26418952 | ||
|
|
61c17cb349 | ||
|
|
fda18e8895 | ||
|
|
7ec6986965 | ||
|
|
74ab9924b4 | ||
|
|
a28000acc4 | ||
|
|
08548abcc2 | ||
|
|
5927a781a3 | ||
|
|
e73fff3bd4 | ||
|
|
8dff0df266 | ||
|
|
5ee7dfb5c2 | ||
|
|
9a3586878d | ||
|
|
a99433c62d | ||
|
|
e76ca5e2b9 | ||
|
|
48bab77609 | ||
|
|
a063bcfb4c | ||
|
|
c5f49cfa48 | ||
|
|
9bcd4198c5 | ||
|
|
29dc8ea9fa | ||
|
|
fa91814aa9 | ||
|
|
0b91db2195 | ||
|
|
7b2d42b0f0 | ||
|
|
bd3bdd620b | ||
|
|
8b4562e102 | ||
|
|
63d8986f2a | ||
|
|
ecaecc2f76 | ||
|
|
78194b4441 | ||
|
|
44699e9b1b | ||
|
|
9978c17d5f | ||
|
|
3cc7e0377c | ||
|
|
d2abc359cc | ||
|
|
7c0d269279 | ||
|
|
8db85946af | ||
|
|
7019f4d178 | ||
|
|
fcd77a83cc | ||
|
|
ae444eb3da | ||
|
|
425c9821e0 | ||
|
|
46c6fb2c82 | ||
|
|
374c3874af | ||
|
|
1eb6c1e469 | ||
|
|
06af3ffc99 | ||
|
|
65dc019fa6 | ||
|
|
a56ccb6f4f | ||
|
|
d9872f2615 | ||
|
|
298be671b9 | ||
|
|
fbfd4f25e7 | ||
|
|
d12735a573 | ||
|
|
1aec7c1152 | ||
|
|
0b55809bb9 | ||
|
|
bde6f7470c | ||
|
|
53ec59ed95 | ||
|
|
ed1ee38923 | ||
|
|
26cf4989a2 | ||
|
|
6e60f441e9 | ||
|
|
d07de879a7 | ||
|
|
dd0634c483 | ||
|
|
fc0cd8f0f8 | ||
|
|
d5cae8ff59 | ||
|
|
38016cb2d6 | ||
|
|
e6cb104294 | ||
|
|
410eb69d25 | ||
|
|
243cf5f261 | ||
|
|
eff2ce8a23 | ||
|
|
fd33c29b26 | ||
|
|
f49164e69b | ||
|
|
9bc7bbe4b4 | ||
|
|
46ad25f925 | ||
|
|
2a725fb137 | ||
|
|
449858655d | ||
|
|
e8c2a79516 | ||
|
|
ea85c1b46e | ||
|
|
6bec6f92c4 | ||
|
|
930a0ea73b | ||
|
|
7e2f56f41a | ||
|
|
e90f5f03f3 | ||
|
|
0e090ae112 | ||
|
|
99496706c5 | ||
|
|
f146553dea | ||
|
|
0432e07864 | ||
|
|
f33ba139b4 | ||
|
|
990a722852 | ||
|
|
a675cb5754 | ||
|
|
baeeff389c | ||
|
|
932bc25d88 | ||
|
|
bdc6dad8de | ||
|
|
ea73af9ba3 | ||
|
|
fc5fc0920f | ||
|
|
9705c161b4 | ||
|
|
8cac21511c | ||
|
|
396ee4d618 | ||
|
|
326169a7fa | ||
|
|
685e069c58 | ||
|
|
84a42a2b24 | ||
|
|
ac6e1b7ce5 | ||
|
|
f516da4138 | ||
|
|
e852e60416 | ||
|
|
bf7d171915 | ||
|
|
d3f819f86f | ||
|
|
aa83fd6bc4 | ||
|
|
e2f7734f37 | ||
|
|
5c17abd1bf | ||
|
|
c52867eb8b | ||
|
|
26fd97a209 | ||
|
|
f0a2cdeb18 | ||
|
|
0c79f2ea3e | ||
|
|
0e9eb9e87e | ||
|
|
0d258b9e96 | ||
|
|
d4fa61e435 | ||
|
|
554301b8e9 | ||
|
|
e3ec0abca4 | ||
|
|
a55e848359 | ||
|
|
2653c3c4d4 | ||
|
|
4e4b65711a | ||
|
|
89c40ba256 | ||
|
|
e56ae6d651 | ||
|
|
64b2f18d92 | ||
|
|
2894f3c45c | ||
|
|
1bae2dcb00 | ||
|
|
b540c6df25 | ||
|
|
8b476e678a | ||
|
|
47f8a0cbe5 | ||
|
|
9459eef032 | ||
|
|
5aaa8924a6 | ||
|
|
4ffce1c297 | ||
|
|
0513660b05 | ||
|
|
3947c4de28 | ||
|
|
664228ef8b | ||
|
|
234a61a3ee | ||
|
|
83d1275d72 | ||
|
|
45abd7d59e | ||
|
|
88e10a9e54 | ||
|
|
1f624d4c2a | ||
|
|
9a25309c1c | ||
|
|
2b7caf6fcb | ||
|
|
938500fd2f | ||
|
|
2d26358602 | ||
|
|
9faa5d3aa4 | ||
|
|
bcc2a136c8 | ||
|
|
dcad002057 | ||
|
|
51b8d2ab25 | ||
|
|
aa3eda70e5 | ||
|
|
2d0860fe1b | ||
|
|
17877366b7 | ||
|
|
5eb3dee59c | ||
|
|
247bd19f93 | ||
|
|
e5ed115288 | ||
|
|
03baa471d9 | ||
|
|
da8b3d143a | ||
|
|
20616ff954 | ||
|
|
f463cf8662 | ||
|
|
e9879eac91 | ||
|
|
47148e78a3 | ||
|
|
fea917c8f2 | ||
|
|
948e16e3c7 | ||
|
|
1e7fc46406 | ||
|
|
71494264ca | ||
|
|
3125561474 | ||
|
|
abf1228a0a | ||
|
|
547a9d947b | ||
|
|
f696d98270 | ||
|
|
44ab7f995d | ||
|
|
92a1214dcd | ||
|
|
388f8da97f | ||
|
|
e96623588c | ||
|
|
e1e316b931 | ||
|
|
c4fd29541b | ||
|
|
a8b13f5a82 | ||
|
|
86fc573d7a | ||
|
|
f82e47f2af | ||
|
|
75a38fa38b | ||
|
|
ac62a3713c | ||
|
|
ea52c99560 | ||
|
|
7d4100ed8a | ||
|
|
c8d1d9c51f | ||
|
|
ab2eb3442e | ||
|
|
dbbeb7d4ed | ||
|
|
bfa00ffbdc | ||
|
|
10eb1a3aee | ||
|
|
fd2bf9f6f6 | ||
|
|
fc1f9f0124 | ||
|
|
789492d1a9 | ||
|
|
afc320bed4 | ||
|
|
cba4a6f9c8 | ||
|
|
5556d42b46 | ||
|
|
7d2bc1c7a4 | ||
|
|
c209c874a9 | ||
|
|
dd52e9cd72 | ||
|
|
0f71a150ff | ||
|
|
74388d4bf4 | ||
|
|
e811dd15b5 | ||
|
|
f22700a4ec | ||
|
|
ee25a7d458 | ||
|
|
594e212b8a | ||
|
|
5c355468c1 | ||
|
|
d1571d18f9 | ||
|
|
cb86b44dea | ||
|
|
833e6e0c92 | ||
|
|
159273e532 | ||
|
|
631f2db1a3 | ||
|
|
a1f0eaab54 | ||
|
|
b1898f6d8b | ||
|
|
d29e9e7e07 | ||
|
|
360605e284 | ||
|
|
64772544fa | ||
|
|
5f4875e2ba | ||
|
|
fd2a8c01cb | ||
|
|
b6129ad081 | ||
|
|
28d0b0a21e | ||
|
|
b779c43126 | ||
|
|
4e12725616 | ||
|
|
570bc9f32d | ||
|
|
47976caa69 | ||
|
|
f1abd8577c | ||
|
|
423b50b068 | ||
|
|
bedf0789f2 | ||
|
|
a2d1cb3b22 | ||
|
|
e7ba88f905 | ||
|
|
50f6e5232e | ||
|
|
8854f0a5ed | ||
|
|
13cc3acdfd | ||
|
|
5b7368c46d | ||
|
|
27999f2d26 | ||
|
|
e74f2a4274 | ||
|
|
d141a73493 | ||
|
|
631f44331e | ||
|
|
b01fddef8b | ||
|
|
78a8553f1c | ||
|
|
b70c1abd7c | ||
|
|
71f31d74bc | ||
|
|
72e557b3a9 | ||
|
|
3eaccbb61a | ||
|
|
b678bd8ed2 | ||
|
|
2baa140615 | ||
|
|
e760f07767 | ||
|
|
ca5d5f0a49 | ||
|
|
69b4fd49fc | ||
|
|
36f315c344 | ||
|
|
8016f5ca2d | ||
|
|
8ef2ff1257 | ||
|
|
6ce31defb6 | ||
|
|
0b3ad9141b | ||
|
|
c903db5c53 | ||
|
|
8a038b946c | ||
|
|
ed9740b72c | ||
|
|
ce3dcc9856 | ||
|
|
4472d50eba | ||
|
|
a1d5702ba0 | ||
|
|
52a231ce2f | ||
|
|
16af981772 | ||
|
|
fc141b8dfc | ||
|
|
82ebd29ff6 | ||
|
|
f5737b5a49 | ||
|
|
55e68d2c63 | ||
|
|
479f45da9b | ||
|
|
140c4e4812 | ||
|
|
337460d299 | ||
|
|
e41c3a7c92 | ||
|
|
1b9faa4d61 | ||
|
|
9f9a754a64 | ||
|
|
f7b0cfe8d1 | ||
|
|
4ce8667b9d | ||
|
|
0785b24192 | ||
|
|
34f02cf83d | ||
|
|
8b43d79257 | ||
|
|
dc086c6bf1 | ||
|
|
dc0ffbe16e | ||
|
|
8df66f7991 | ||
|
|
39580381fa | ||
|
|
06a0f278e5 | ||
|
|
fd97470f35 | ||
|
|
11fc3f1e56 | ||
|
|
693bb708b2 | ||
|
|
97719fe8da | ||
|
|
4933ace50b | ||
|
|
81bf0a6430 | ||
|
|
24aed1b14e | ||
|
|
09c3ac02f8 | ||
|
|
45a39585b3 | ||
|
|
286a455fa9 | ||
|
|
bdf4960b6a | ||
|
|
13124aafe3 | ||
|
|
2128be8b28 | ||
|
|
175294fa5d | ||
|
|
5016c7cf7c | ||
|
|
1468206052 | ||
|
|
d1ff4b5cf0 | ||
|
|
700a065abd | ||
|
|
3fc462f90f | ||
|
|
65a8f6bb4b | ||
|
|
e0f0944bff | ||
|
|
c7590dd752 | ||
|
|
33a5680328 | ||
|
|
455d87cadd | ||
|
|
e65ae09789 | ||
|
|
dacdce1ea2 | ||
|
|
766f3c50ec | ||
|
|
802b97b2ed | ||
|
|
2ad408f2b8 | ||
|
|
c8fddd8f62 | ||
|
|
74fab994e2 | ||
|
|
3a99542f73 | ||
|
|
2ae9f3555b | ||
|
|
1937b4bf66 | ||
|
|
8beb290842 | ||
|
|
8fec5155c7 | ||
|
|
3f49decce9 | ||
|
|
e2492f65a0 | ||
|
|
5116a812fb | ||
|
|
82409c060f | ||
|
|
183ed3f88f | ||
|
|
447ac3c40b | ||
|
|
96f96927da | ||
|
|
2c79e29483 | ||
|
|
362473dde0 | ||
|
|
8339682112 | ||
|
|
3f06d767b5 | ||
|
|
2854dc9ab3 | ||
|
|
516fcf9168 | ||
|
|
12d1971b0d | ||
|
|
3e09e509d6 | ||
|
|
99fb11cc72 | ||
|
|
d176ddd27e |
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[alias]
|
||||||
|
dev = ["run", "--", "dev"]
|
||||||
@ -1,7 +0,0 @@
|
|||||||
root = true
|
|
||||||
|
|
||||||
[*.rs]
|
|
||||||
end_of_line = lf
|
|
||||||
insert_final_newline = true
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 4
|
|
||||||
28
.github/workflows/rust.yml
vendored
28
.github/workflows/rust.yml
vendored
@ -1,10 +1,16 @@
|
|||||||
name: Rustlings Tests
|
name: Check
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
paths-ignore:
|
||||||
|
- website
|
||||||
|
- '*.md'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
paths-ignore:
|
||||||
|
- website
|
||||||
|
- '*.md'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
@ -14,30 +20,28 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- run: cargo clippy -- --deny warnings
|
- name: Clippy
|
||||||
|
run: cargo clippy -- --deny warnings
|
||||||
fmt:
|
fmt:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: DavidAnson/markdownlint-cli2-action@v16
|
- name: rustfmt
|
||||||
with:
|
run: cargo fmt --all --check
|
||||||
globs: "exercises/**/*.md"
|
|
||||||
- name: Run cargo fmt
|
|
||||||
run: cargo fmt --all -- --check
|
|
||||||
test:
|
test:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: swatinem/rust-cache@v2
|
- uses: swatinem/rust-cache@v2
|
||||||
- name: Run cargo test
|
- name: cargo test
|
||||||
run: cargo test
|
run: cargo test --workspace
|
||||||
dev-check:
|
dev-check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: swatinem/rust-cache@v2
|
- uses: swatinem/rust-cache@v2
|
||||||
- name: Run rustlings dev check
|
- name: rustlings dev check
|
||||||
run: cargo run -- dev check --require-solutions
|
run: cargo dev check --require-solutions
|
||||||
|
|||||||
87
.github/workflows/web.yml
vendored
87
.github/workflows/web.yml
vendored
@ -1,87 +0,0 @@
|
|||||||
# Workflow to build your docs with oranda (and mdbook)
|
|
||||||
# and deploy them to Github Pages
|
|
||||||
name: Web
|
|
||||||
|
|
||||||
# We're going to push to the gh-pages branch, so we need that permission
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
# What situations do we want to build docs in?
|
|
||||||
# All of these work independently and can be removed / commented out
|
|
||||||
# if you don't want oranda/mdbook running in that situation
|
|
||||||
on:
|
|
||||||
# Check that a PR didn't break docs!
|
|
||||||
#
|
|
||||||
# Note that the "Deploy to Github Pages" step won't run in this mode,
|
|
||||||
# so this won't have any side-effects. But it will tell you if a PR
|
|
||||||
# completely broke oranda/mdbook. Sadly we don't provide previews (yet)!
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
# Whenever something gets pushed to main, update the docs!
|
|
||||||
# This is great for getting docs changes live without cutting a full release.
|
|
||||||
#
|
|
||||||
# Note that if you're using cargo-dist, this will "race" the Release workflow
|
|
||||||
# that actually builds the Github Release that oranda tries to read (and
|
|
||||||
# this will almost certainly complete first). As a result you will publish
|
|
||||||
# docs for the latest commit but the oranda landing page won't know about
|
|
||||||
# the latest release. The workflow_run trigger below will properly wait for
|
|
||||||
# cargo-dist, and so this half-published state will only last for ~10 minutes.
|
|
||||||
#
|
|
||||||
# If you only want docs to update with releases, disable this, or change it to
|
|
||||||
# a "release" branch. You can, of course, also manually trigger a workflow run
|
|
||||||
# when you want the docs to update.
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
# Whenever a workflow called "Release" completes, update the docs!
|
|
||||||
#
|
|
||||||
# If you're using cargo-dist, this is recommended, as it will ensure that
|
|
||||||
# oranda always sees the latest release right when it's available. Note
|
|
||||||
# however that Github's UI is wonky when you use workflow_run, and won't
|
|
||||||
# show this workflow as part of any commit. You have to go to the "actions"
|
|
||||||
# tab for your repo to see this one running (the gh-pages deploy will also
|
|
||||||
# only show up there).
|
|
||||||
workflow_run:
|
|
||||||
workflows: [ "Release" ]
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
|
|
||||||
# Alright, let's do it!
|
|
||||||
jobs:
|
|
||||||
web:
|
|
||||||
name: Build and deploy site and docs
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
# Setup
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- uses: swatinem/rust-cache@v2
|
|
||||||
|
|
||||||
# If you use any mdbook plugins, here's the place to install them!
|
|
||||||
|
|
||||||
# Install and run oranda (and mdbook)
|
|
||||||
# This will write all output to ./public/ (including copying mdbook's output to there)
|
|
||||||
- name: Install and run oranda
|
|
||||||
run: |
|
|
||||||
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/oranda/releases/download/v0.3.1/oranda-installer.sh | sh
|
|
||||||
oranda build
|
|
||||||
|
|
||||||
# Deploy to our gh-pages branch (creating it if it doesn't exist)
|
|
||||||
# the "public" dir that oranda made above will become the root dir
|
|
||||||
# of this branch.
|
|
||||||
#
|
|
||||||
# Note that once the gh-pages branch exists, you must
|
|
||||||
# go into repo's settings > pages and set "deploy from branch: gh-pages"
|
|
||||||
# the other defaults work fine.
|
|
||||||
- name: Deploy to Github Pages
|
|
||||||
uses: JamesIves/github-pages-deploy-action@v4.4.1
|
|
||||||
# ONLY if we're on main (so no PRs or feature branches allowed!)
|
|
||||||
if: ${{ github.ref == 'refs/heads/main' }}
|
|
||||||
with:
|
|
||||||
branch: gh-pages
|
|
||||||
# Gotta tell the action where to find oranda's output
|
|
||||||
folder: public
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
single-commit: true
|
|
||||||
43
.github/workflows/website.yml
vendored
Normal file
43
.github/workflows/website.yml
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
name: Website
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths: [website]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: website
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install TailwindCSS
|
||||||
|
run: npm install
|
||||||
|
- name: Build CSS
|
||||||
|
run: npx @tailwindcss/cli -m -i input.css -o static/main.css
|
||||||
|
- name: Download Zola
|
||||||
|
run: curl -fsSL https://github.com/getzola/zola/releases/download/v0.20.0/zola-v0.20.0-x86_64-unknown-linux-gnu.tar.gz | tar xz
|
||||||
|
- name: Build site
|
||||||
|
run: ./zola build
|
||||||
|
- name: Upload static files as artifact
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: website/public/
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: build
|
||||||
|
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
|
||||||
|
permissions:
|
||||||
|
pages: write # to deploy to Pages
|
||||||
|
id-token: write # to verify the deployment originates from an appropriate source
|
||||||
|
# Deploy to the github-pages environment
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -6,10 +6,6 @@ Cargo.lock
|
|||||||
# State file
|
# State file
|
||||||
.rustlings-state.txt
|
.rustlings-state.txt
|
||||||
|
|
||||||
# oranda
|
|
||||||
public/
|
|
||||||
.netlify
|
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.direnv/
|
.direnv/
|
||||||
|
|||||||
@ -1,2 +0,0 @@
|
|||||||
# MD013/line-length Line length, Expected: 80
|
|
||||||
MD013: false
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
|
[default.extend-words]
|
||||||
|
"earch" = "earch" # Because of <s>earch in the list footer
|
||||||
|
|
||||||
[files]
|
[files]
|
||||||
extend-exclude = [
|
extend-exclude = [
|
||||||
"CHANGELOG.md",
|
"CHANGELOG.md",
|
||||||
]
|
]
|
||||||
|
|
||||||
[default.extend-words]
|
|
||||||
"ratatui" = "ratatui"
|
|
||||||
|
|||||||
203
CHANGELOG.md
203
CHANGELOG.md
@ -1,10 +1,117 @@
|
|||||||
<a name="6.1.0"></a>
|
## Unreleased
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `vecs2`: Removed the use of `map` and `collect`, which are only taught later.
|
||||||
|
|
||||||
|
## 6.5.0 (2025-08-21)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Check that Clippy is installed before initialization
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgrade to Rust edition 2024
|
||||||
|
- Raise the minimum supported Rust version to `1.88`
|
||||||
|
- Don't follow symlinks in the file watcher
|
||||||
|
- `dev new`: Don't add `.rustlings-state.txt` to `.gitignore`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix file links in VS Code
|
||||||
|
- Fix error printing when the progress bar is shown
|
||||||
|
- `dev check`: Don't check formatting if there are no solution files
|
||||||
|
|
||||||
|
## 6.4.0 (2024-11-11)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- The list of exercises is now searchable by pressing `s` or `/` 🔍️ (thanks to [@frroossst](https://github.com/frroossst))
|
||||||
|
- New option `c` in the prompt to manually check all exercises ✅ (thanks to [@Nahor](https://github.com/Nahor))
|
||||||
|
- New command `check-all` to manually check all exercises ✅ (thanks to [@Nahor](https://github.com/Nahor))
|
||||||
|
- Addictive animation for showing the progress of checking all exercises. A nice showcase of parallelism in Rust ✨
|
||||||
|
- New option `x` in the prompt to reset the file of the current exercise 🔄
|
||||||
|
- Allow `dead_code` for all exercises and solutions ⚰️ (thanks to [@huss4in](https://github.com/huss4in))
|
||||||
|
- Pause input while running an exercise to avoid unexpected prompt interactions ⏸️
|
||||||
|
- Limit the maximum number of exercises to 999. Any community exercises willing to reach that limit? 🔝
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `enums3`: Remove redundant enum definition task (thanks to [@senekor](https://github.com/senekor))
|
||||||
|
- `if2`: Make the exercise less confusing by avoiding "fizz", "fuzz", "foo", "bar" and "baz" (thanks to [@senekor](https://github.com/senekor))
|
||||||
|
- `hashmap3`: Use the method `Entry::or_default`.
|
||||||
|
- Update the state of all exercises when checking all of them (thanks to [@Nahor](https://github.com/Nahor))
|
||||||
|
- The main prompt doesn't need a confirmation with ENTER on Unix-like systems anymore.
|
||||||
|
- No more jumping back to a previous exercise when its file is changed. Use the list to jump between exercises.
|
||||||
|
- Dump the solution file after an exercise is done even if the solution's directory doesn't exist.
|
||||||
|
- Rework the footer in the list.
|
||||||
|
- Optimize the file watcher.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix bad contrast in the list on terminals with a light theme.
|
||||||
|
|
||||||
|
## 6.3.0 (2024-08-29)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add the following exercise lints:
|
||||||
|
- `forbid(unsafe_code)`: You shouldn't write unsafe code in Rustlings.
|
||||||
|
- `forbid(unstable_features)`: You don't need unstable features in Rustlings and shouldn't rely on them while learning Rust.
|
||||||
|
- `forbid(todo)`: You forgot a `todo!()`.
|
||||||
|
- `forbid(empty_loop)`: This can only happen by mistake in Rustlings.
|
||||||
|
- `deny(infinite_loop)`: No infinite loops are needed in Rustlings.
|
||||||
|
- `deny(mem_forget)`: You shouldn't leak memory while still learning Rust.
|
||||||
|
- Show a link to every exercise file in the list.
|
||||||
|
- Add scroll padding in the list.
|
||||||
|
- Break the help footer of the list into two lines when the terminal width isn't big enough.
|
||||||
|
- Enable scrolling with the mouse in the list.
|
||||||
|
- `dev check`: Show the progress of checks.
|
||||||
|
- `dev check`: Check that the length of all exercise names is lower than 32.
|
||||||
|
- `dev check`: Check if exercise contains no tests and isn't marked with `test = false`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The compilation time when installing Rustlings is reduced.
|
||||||
|
- Pressing `c` in the list for "continue on" now quits the list after setting the selected exercise as the current one.
|
||||||
|
- Better highlighting of the solution file after an exercise is done.
|
||||||
|
- Don't show the output of successful tests anymore. Instead, show the pretty output for tests.
|
||||||
|
- Be explicit about `q` only quitting the list and not the whole program in the list.
|
||||||
|
- Be explicit about `r` only resetting one exercise (the selected one) in the list.
|
||||||
|
- Ignore the standard output of `git init`.
|
||||||
|
- `threads3`: Remove the queue length and improve tests.
|
||||||
|
- `errors4`: Use match instead of a comparison chain in the solution.
|
||||||
|
- `functions3`: Only take `u8` to avoid using a too high number of iterations by mistake.
|
||||||
|
- `dev check`: Always check with strict Clippy (warnings to errors) when checking the solutions.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix the error on some systems about too many open files during the final check of all exercises.
|
||||||
|
- Fix the list when the terminal height is too low.
|
||||||
|
- Restore the terminal after an error in the list.
|
||||||
|
|
||||||
|
## 6.2.0 (2024-08-09)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Show a message before checking and running an exercise. This gives the user instant feedback and avoids confusion if the checks take too long.
|
||||||
|
- Show a helpful error message when trying to install Rustlings with a Rust version lower than the minimum one that Rustlings supports.
|
||||||
|
- Add a `README.md` file to the `solutions/` directory.
|
||||||
|
- Allow initializing Rustlings in a Cargo workspace.
|
||||||
|
- `dev check`: Check that all solutions are formatted with `rustfmt`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Remove the state file and the solutions directory from the generated `.gitignore` file.
|
||||||
|
- Run the final check of all exercises in parallel.
|
||||||
|
- Small exercise improvements.
|
||||||
|
|
||||||
## 6.1.0 (2024-07-10)
|
## 6.1.0 (2024-07-10)
|
||||||
|
|
||||||
#### Added
|
#### Added
|
||||||
|
|
||||||
- `dev check`: Check that all exercises (including third-party ones) include at least one `TODO` comment.
|
- `dev check`: Check that all exercises (including community ones) include at least one `TODO` comment.
|
||||||
- `dev check`: Check that all exercises actually fail to run (not already solved).
|
- `dev check`: Check that all exercises actually fail to run (not already solved).
|
||||||
|
|
||||||
#### Changed
|
#### Changed
|
||||||
@ -17,15 +124,11 @@
|
|||||||
- Exit with a helpful error message on missing/unsupported terminal/TTY.
|
- Exit with a helpful error message on missing/unsupported terminal/TTY.
|
||||||
- Mark the last exercise as done.
|
- Mark the last exercise as done.
|
||||||
|
|
||||||
<a name="6.0.1"></a>
|
|
||||||
|
|
||||||
## 6.0.1 (2024-07-04)
|
## 6.0.1 (2024-07-04)
|
||||||
|
|
||||||
Small exercise improvements and fixes.
|
Small exercise improvements and fixes.
|
||||||
Most importantly, fixed that the exercise `clippy1` was already solved 😅
|
Most importantly, fixed that the exercise `clippy1` was already solved 😅
|
||||||
|
|
||||||
<a name="6.0.0"></a>
|
|
||||||
|
|
||||||
## 6.0.0 (2024-07-03)
|
## 6.0.0 (2024-07-03)
|
||||||
|
|
||||||
This release is the result of a complete rewrite to deliver a ton of new features and improvements ✨
|
This release is the result of a complete rewrite to deliver a ton of new features and improvements ✨
|
||||||
@ -54,7 +157,7 @@ You can read about the motivations of this change in [this issue](https://github
|
|||||||
|
|
||||||
### List mode
|
### List mode
|
||||||
|
|
||||||
A list mode was added using [Ratatui](https://ratatui.rs).
|
A new list mode was added!
|
||||||
You can enter it by entering `l` in the watch mode.
|
You can enter it by entering `l` in the watch mode.
|
||||||
It offers the following features:
|
It offers the following features:
|
||||||
|
|
||||||
@ -83,15 +186,13 @@ This should avoid issues related to the language server or to running exercises,
|
|||||||
Clippy lints are now shown on all exercises, not only the Clippy exercises 📎
|
Clippy lints are now shown on all exercises, not only the Clippy exercises 📎
|
||||||
Make Clippy your friend from early on 🥰
|
Make Clippy your friend from early on 🥰
|
||||||
|
|
||||||
### Third-party exercises
|
### Community Exercises
|
||||||
|
|
||||||
Rustlings now supports third-party exercises!
|
Rustlings now supports community exercises!
|
||||||
|
|
||||||
Do you want to create your own set of Rustlings exercises to focus on some specific topic?
|
Do you want to create your own set of Rustlings exercises to focus on some specific topic?
|
||||||
Or do you want to translate the original Rustlings exercises?
|
Or do you want to translate the original Rustlings exercises?
|
||||||
Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXERCISES.md)!
|
Then follow the link to the guide about [community exercises](https://rustlings.rust-lang.org/community-exercises)!
|
||||||
|
|
||||||
<a name="5.6.1"></a>
|
|
||||||
|
|
||||||
## 5.6.1 (2023-09-18)
|
## 5.6.1 (2023-09-18)
|
||||||
|
|
||||||
@ -108,8 +209,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- `as_ref_mut`: Fixed a typo in a test function name.
|
- `as_ref_mut`: Fixed a typo in a test function name.
|
||||||
- `enums3`: Fixed formatting with `rustfmt`.
|
- `enums3`: Fixed formatting with `rustfmt`.
|
||||||
|
|
||||||
<a name="5.6.0"></a>
|
|
||||||
|
|
||||||
## 5.6.0 (2023-09-04)
|
## 5.6.0 (2023-09-04)
|
||||||
|
|
||||||
#### Added
|
#### Added
|
||||||
@ -149,16 +248,12 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- Lots of Nix housekeeping that I don't feel qualified to write about!
|
- Lots of Nix housekeeping that I don't feel qualified to write about!
|
||||||
- Improved CI workflows, we're now testing on multiple platforms at once.
|
- Improved CI workflows, we're now testing on multiple platforms at once.
|
||||||
|
|
||||||
<a name="5.5.1"></a>
|
|
||||||
|
|
||||||
## 5.5.1 (2023-05-17)
|
## 5.5.1 (2023-05-17)
|
||||||
|
|
||||||
#### Fixed
|
#### Fixed
|
||||||
|
|
||||||
- Reverted `rust-project.json` path generation due to an upstream `rust-analyzer` fix.
|
- Reverted `rust-project.json` path generation due to an upstream `rust-analyzer` fix.
|
||||||
|
|
||||||
<a name="5.5.0"></a>
|
|
||||||
|
|
||||||
## 5.5.0 (2023-05-17)
|
## 5.5.0 (2023-05-17)
|
||||||
|
|
||||||
#### Added
|
#### Added
|
||||||
@ -193,8 +288,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- Added a markdown linter to run on GitHub actions
|
- Added a markdown linter to run on GitHub actions
|
||||||
- Split quick installation section into two code blocks
|
- Split quick installation section into two code blocks
|
||||||
|
|
||||||
<a name="5.4.1"></a>
|
|
||||||
|
|
||||||
## 5.4.1 (2023-03-10)
|
## 5.4.1 (2023-03-10)
|
||||||
|
|
||||||
#### Changed
|
#### Changed
|
||||||
@ -210,8 +303,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- `macros4`: Prevented auto-fix by adding `#[rustfmt::skip]`
|
- `macros4`: Prevented auto-fix by adding `#[rustfmt::skip]`
|
||||||
- `cli`: Actually show correct progress percentages
|
- `cli`: Actually show correct progress percentages
|
||||||
|
|
||||||
<a name="5.4.0"></a>
|
|
||||||
|
|
||||||
## 5.4.0 (2023-02-12)
|
## 5.4.0 (2023-02-12)
|
||||||
|
|
||||||
#### Changed
|
#### Changed
|
||||||
@ -240,8 +331,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
|
|
||||||
- Bumped min Rust version to 1.58 in installation script
|
- Bumped min Rust version to 1.58 in installation script
|
||||||
|
|
||||||
<a name="5.3.0"></a>
|
|
||||||
|
|
||||||
## 5.3.0 (2022-12-23)
|
## 5.3.0 (2022-12-23)
|
||||||
|
|
||||||
#### Added
|
#### Added
|
||||||
@ -274,8 +363,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- Applied some Clippy and rustfmt formatting
|
- Applied some Clippy and rustfmt formatting
|
||||||
- Added a note on Windows PowerShell and other shell compatibility
|
- Added a note on Windows PowerShell and other shell compatibility
|
||||||
|
|
||||||
<a name="5.2.1"></a>
|
|
||||||
|
|
||||||
## 5.2.1 (2022-09-06)
|
## 5.2.1 (2022-09-06)
|
||||||
|
|
||||||
#### Fixed
|
#### Fixed
|
||||||
@ -289,8 +376,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
|
|
||||||
- Fixed a typo in README.md
|
- Fixed a typo in README.md
|
||||||
|
|
||||||
<a name="5.2.0"></a>
|
|
||||||
|
|
||||||
## 5.2.0 (2022-08-27)
|
## 5.2.0 (2022-08-27)
|
||||||
|
|
||||||
#### Added
|
#### Added
|
||||||
@ -307,16 +392,12 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- **quiz1**: Adjusted the explanations to be consistent with
|
- **quiz1**: Adjusted the explanations to be consistent with
|
||||||
the tests
|
the tests
|
||||||
|
|
||||||
<a name="5.1.1"></a>
|
|
||||||
|
|
||||||
## 5.1.1 (2022-08-17)
|
## 5.1.1 (2022-08-17)
|
||||||
|
|
||||||
#### Bug Fixes
|
#### Bug Fixes
|
||||||
|
|
||||||
- Fixed an incorrect assertion in options1
|
- Fixed an incorrect assertion in options1
|
||||||
|
|
||||||
<a name="5.1.0"></a>
|
|
||||||
|
|
||||||
## 5.1.0 (2022-08-16)
|
## 5.1.0 (2022-08-16)
|
||||||
|
|
||||||
#### Features
|
#### Features
|
||||||
@ -351,8 +432,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- Clarified manual installation instructions using `cargo install --path .`
|
- Clarified manual installation instructions using `cargo install --path .`
|
||||||
- Added a link to our Zulip in the readme file
|
- Added a link to our Zulip in the readme file
|
||||||
|
|
||||||
<a name="5.0.0"></a>
|
|
||||||
|
|
||||||
## 5.0.0 (2022-07-16)
|
## 5.0.0 (2022-07-16)
|
||||||
|
|
||||||
#### Features
|
#### Features
|
||||||
@ -425,8 +504,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- Updated spacing in Cargo.toml.
|
- Updated spacing in Cargo.toml.
|
||||||
- Added a GitHub actions config so that tests run on every PR/commit.
|
- Added a GitHub actions config so that tests run on every PR/commit.
|
||||||
|
|
||||||
<a name="4.8.0"></a>
|
|
||||||
|
|
||||||
## 4.8.0 (2022-07-01)
|
## 4.8.0 (2022-07-01)
|
||||||
|
|
||||||
#### Features
|
#### Features
|
||||||
@ -447,8 +524,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- Replaced the git.io URL with the fully qualified URL because of git.io's sunsetting.
|
- Replaced the git.io URL with the fully qualified URL because of git.io's sunsetting.
|
||||||
- Removed the deprecated Rust GitPod extension.
|
- Removed the deprecated Rust GitPod extension.
|
||||||
|
|
||||||
<a name="4.7.1"></a>
|
|
||||||
|
|
||||||
## 4.7.1 (2022-04-20)
|
## 4.7.1 (2022-04-20)
|
||||||
|
|
||||||
#### Features
|
#### Features
|
||||||
@ -469,8 +544,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- The changelog will now be manually written instead of being automatically generated by the
|
- The changelog will now be manually written instead of being automatically generated by the
|
||||||
Git log.
|
Git log.
|
||||||
|
|
||||||
<a name="4.7.0"></a>
|
|
||||||
|
|
||||||
## 4.7.0 (2022-04-14)
|
## 4.7.0 (2022-04-14)
|
||||||
|
|
||||||
#### Features
|
#### Features
|
||||||
@ -511,8 +584,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- Add hints on how to get GCC installed (#741) ([bc56861](https://github.com/rust-lang/rustlings/commit/bc5686174463ad6f4f6b824b0e9b97c3039d4886))
|
- Add hints on how to get GCC installed (#741) ([bc56861](https://github.com/rust-lang/rustlings/commit/bc5686174463ad6f4f6b824b0e9b97c3039d4886))
|
||||||
- Fix some code blocks that were not highlighted ([17f9d74](https://github.com/rust-lang/rustlings/commit/17f9d7429ccd133a72e815fb5618e0ce79560929))
|
- Fix some code blocks that were not highlighted ([17f9d74](https://github.com/rust-lang/rustlings/commit/17f9d7429ccd133a72e815fb5618e0ce79560929))
|
||||||
|
|
||||||
<a name="4.6.0"></a>
|
|
||||||
|
|
||||||
## 4.6.0 (2021-09-25)
|
## 4.6.0 (2021-09-25)
|
||||||
|
|
||||||
#### Features
|
#### Features
|
||||||
@ -535,8 +606,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- Clarify instructions ([df25684c](https://github.com/rust-lang/rustlings/commit/df25684cb79f8413915e00b5efef29369849cef1))
|
- Clarify instructions ([df25684c](https://github.com/rust-lang/rustlings/commit/df25684cb79f8413915e00b5efef29369849cef1))
|
||||||
- **quiz1:** Fix inconsistent wording (#826) ([03131a3d](https://github.com/rust-lang/rustlings/commit/03131a3d35d9842598150f9da817f7cc26e2669a))
|
- **quiz1:** Fix inconsistent wording (#826) ([03131a3d](https://github.com/rust-lang/rustlings/commit/03131a3d35d9842598150f9da817f7cc26e2669a))
|
||||||
|
|
||||||
<a name="4.5.0"></a>
|
|
||||||
|
|
||||||
## 4.5.0 (2021-07-07)
|
## 4.5.0 (2021-07-07)
|
||||||
|
|
||||||
#### Features
|
#### Features
|
||||||
@ -557,8 +626,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- **try_from_into, from_str:** hints for dyn Error ([11d2cf0d](https://github.com/rust-lang/rustlings/commit/11d2cf0d604dee3f5023c17802d69438e69fa50e))
|
- **try_from_into, from_str:** hints for dyn Error ([11d2cf0d](https://github.com/rust-lang/rustlings/commit/11d2cf0d604dee3f5023c17802d69438e69fa50e))
|
||||||
- **variables5:** confine the answer further ([48ffcbd2](https://github.com/rust-lang/rustlings/commit/48ffcbd2c4cc4d936c2c7480019190f179813cc5))
|
- **variables5:** confine the answer further ([48ffcbd2](https://github.com/rust-lang/rustlings/commit/48ffcbd2c4cc4d936c2c7480019190f179813cc5))
|
||||||
|
|
||||||
<a name="4.4.0"></a>
|
|
||||||
|
|
||||||
## 4.4.0 (2021-04-24)
|
## 4.4.0 (2021-04-24)
|
||||||
|
|
||||||
#### Bug Fixes
|
#### Bug Fixes
|
||||||
@ -600,8 +667,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- updated progress percentage ([1c6f7e4b](https://github.com/rust-lang/rustlings/commit/1c6f7e4b7b9b3bd36f4da2bb2b69c549cc8bd913))
|
- updated progress percentage ([1c6f7e4b](https://github.com/rust-lang/rustlings/commit/1c6f7e4b7b9b3bd36f4da2bb2b69c549cc8bd913))
|
||||||
- added progress info ([c0e3daac](https://github.com/rust-lang/rustlings/commit/c0e3daacaf6850811df5bc57fa43e0f249d5cfa4))
|
- added progress info ([c0e3daac](https://github.com/rust-lang/rustlings/commit/c0e3daacaf6850811df5bc57fa43e0f249d5cfa4))
|
||||||
|
|
||||||
<a name="4.3.0"></a>
|
|
||||||
|
|
||||||
## 4.3.0 (2020-12-29)
|
## 4.3.0 (2020-12-29)
|
||||||
|
|
||||||
#### Features
|
#### Features
|
||||||
@ -624,8 +689,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- Update description (#584) ([96347df9](https://github.com/rust-lang/rustlings/commit/96347df9df294f01153b29d9ad4ba361f665c755))
|
- Update description (#584) ([96347df9](https://github.com/rust-lang/rustlings/commit/96347df9df294f01153b29d9ad4ba361f665c755))
|
||||||
- **vec1:** Have test compare every element in a and v ([9b6c6293](https://github.com/rust-lang/rustlings/commit/9b6c629397b24b944f484f5b2bbd8144266b5695))
|
- **vec1:** Have test compare every element in a and v ([9b6c6293](https://github.com/rust-lang/rustlings/commit/9b6c629397b24b944f484f5b2bbd8144266b5695))
|
||||||
|
|
||||||
<a name="4.2.0"></a>
|
|
||||||
|
|
||||||
## 4.2.0 (2020-11-07)
|
## 4.2.0 (2020-11-07)
|
||||||
|
|
||||||
#### Features
|
#### Features
|
||||||
@ -646,8 +709,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- missing comma in test ([4fb230da](https://github.com/rust-lang/rustlings/commit/4fb230daf1251444fcf29e085cee222a91f8a37e))
|
- missing comma in test ([4fb230da](https://github.com/rust-lang/rustlings/commit/4fb230daf1251444fcf29e085cee222a91f8a37e))
|
||||||
- **quiz3:** Second test is for odd numbers, not even. (#553) ([18e0bfef](https://github.com/rust-lang/rustlings/commit/18e0bfef1de53071e353ba1ec5837002ff7290e6))
|
- **quiz3:** Second test is for odd numbers, not even. (#553) ([18e0bfef](https://github.com/rust-lang/rustlings/commit/18e0bfef1de53071e353ba1ec5837002ff7290e6))
|
||||||
|
|
||||||
<a name="4.1.0"></a>
|
|
||||||
|
|
||||||
## 4.1.0 (2020-10-05)
|
## 4.1.0 (2020-10-05)
|
||||||
|
|
||||||
#### Bug Fixes
|
#### Bug Fixes
|
||||||
@ -670,8 +731,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- **cli:** Added 'cls' command to 'watch' mode (#474) ([4f2468e1](https://github.com/rust-lang/rustlings/commit/4f2468e14f574a93a2e9b688367b5752ed96ae7b))
|
- **cli:** Added 'cls' command to 'watch' mode (#474) ([4f2468e1](https://github.com/rust-lang/rustlings/commit/4f2468e14f574a93a2e9b688367b5752ed96ae7b))
|
||||||
- **try_from_into:** Add insufficient length test (#469) ([523d18b8](https://github.com/rust-lang/rustlings/commit/523d18b873a319f7c09262f44bd40e2fab1830e5))
|
- **try_from_into:** Add insufficient length test (#469) ([523d18b8](https://github.com/rust-lang/rustlings/commit/523d18b873a319f7c09262f44bd40e2fab1830e5))
|
||||||
|
|
||||||
<a name="4.0.0"></a>
|
|
||||||
|
|
||||||
## 4.0.0 (2020-07-08)
|
## 4.0.0 (2020-07-08)
|
||||||
|
|
||||||
#### Breaking Changes
|
#### Breaking Changes
|
||||||
@ -713,8 +772,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- **test2:** name of type String and &str (#394) ([d6c0a688](https://github.com/rust-lang/rustlings/commit/d6c0a688e6a96f93ad60d540d4b326f342fc0d45))
|
- **test2:** name of type String and &str (#394) ([d6c0a688](https://github.com/rust-lang/rustlings/commit/d6c0a688e6a96f93ad60d540d4b326f342fc0d45))
|
||||||
- **variables6:** minor typo (#419) ([524e17df](https://github.com/rust-lang/rustlings/commit/524e17df10db95f7b90a0f75cc8997182a8a4094))
|
- **variables6:** minor typo (#419) ([524e17df](https://github.com/rust-lang/rustlings/commit/524e17df10db95f7b90a0f75cc8997182a8a4094))
|
||||||
|
|
||||||
<a name="3.0.0"></a>
|
|
||||||
|
|
||||||
## 3.0.0 (2020-04-11)
|
## 3.0.0 (2020-04-11)
|
||||||
|
|
||||||
#### Breaking Changes
|
#### Breaking Changes
|
||||||
@ -737,8 +794,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- add new exercises for generics (#280) ([76be5e4e](https://github.com/rust-lang/rustlings/commit/76be5e4e991160f5fd9093f03ee2ba260e8f7229))
|
- add new exercises for generics (#280) ([76be5e4e](https://github.com/rust-lang/rustlings/commit/76be5e4e991160f5fd9093f03ee2ba260e8f7229))
|
||||||
- **ci:** add buildkite config ([b049fa2c](https://github.com/rust-lang/rustlings/commit/b049fa2c84dba0f0c8906ac44e28fd45fba51a71))
|
- **ci:** add buildkite config ([b049fa2c](https://github.com/rust-lang/rustlings/commit/b049fa2c84dba0f0c8906ac44e28fd45fba51a71))
|
||||||
|
|
||||||
<a name="2.2.1"></a>
|
|
||||||
|
|
||||||
### 2.2.1 (2020-02-27)
|
### 2.2.1 (2020-02-27)
|
||||||
|
|
||||||
#### Bug Fixes
|
#### Bug Fixes
|
||||||
@ -749,13 +804,11 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
|
|
||||||
- Add clippy lints (#269) ([1e2fd9c9](https://github.com/rust-lang/rustlings/commit/1e2fd9c92f8cd6e389525ca1a999fca4c90b5921))
|
- Add clippy lints (#269) ([1e2fd9c9](https://github.com/rust-lang/rustlings/commit/1e2fd9c92f8cd6e389525ca1a999fca4c90b5921))
|
||||||
|
|
||||||
<a name="2.2.0"></a>
|
|
||||||
|
|
||||||
## 2.2.0 (2020-02-25)
|
## 2.2.0 (2020-02-25)
|
||||||
|
|
||||||
#### Bug Fixes
|
#### Bug Fixes
|
||||||
|
|
||||||
- Update deps to version compatable with aarch64-pc-windows (#263) ([19a93428](https://github.com/rust-lang/rustlings/commit/19a93428b3c73d994292671f829bdc8e5b7b3401))
|
- Update deps to version compatible with aarch64-pc-windows (#263) ([19a93428](https://github.com/rust-lang/rustlings/commit/19a93428b3c73d994292671f829bdc8e5b7b3401))
|
||||||
- **docs:**
|
- **docs:**
|
||||||
- Added a necessary step to Windows installation process (#242) ([3906efcd](https://github.com/rust-lang/rustlings/commit/3906efcd52a004047b460ed548037093de3f523f))
|
- Added a necessary step to Windows installation process (#242) ([3906efcd](https://github.com/rust-lang/rustlings/commit/3906efcd52a004047b460ed548037093de3f523f))
|
||||||
- Fixed mangled sentence from book; edited for clarity (#266) ([ade52ff](https://github.com/rust-lang/rustlings/commit/ade52ffb739987287ddd5705944c8777705faed9))
|
- Fixed mangled sentence from book; edited for clarity (#266) ([ade52ff](https://github.com/rust-lang/rustlings/commit/ade52ffb739987287ddd5705944c8777705faed9))
|
||||||
@ -778,8 +831,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- Added traits exercises (#274 but specifically #216, which originally added
|
- Added traits exercises (#274 but specifically #216, which originally added
|
||||||
this :heart:) ([b559cdd](https://github.com/rust-lang/rustlings/commit/b559cdd73f32c0d0cfc1feda39f82b3e3583df17))
|
this :heart:) ([b559cdd](https://github.com/rust-lang/rustlings/commit/b559cdd73f32c0d0cfc1feda39f82b3e3583df17))
|
||||||
|
|
||||||
<a name="2.1.0"></a>
|
|
||||||
|
|
||||||
## 2.1.0 (2019-11-27)
|
## 2.1.0 (2019-11-27)
|
||||||
|
|
||||||
#### Bug Fixes
|
#### Bug Fixes
|
||||||
@ -797,8 +848,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
|
|
||||||
- **watch:** show hint while watching ([8143d57b](https://github.com/rust-lang/rustlings/commit/8143d57b4e88c51341dd4a18a14c536042cc009c))
|
- **watch:** show hint while watching ([8143d57b](https://github.com/rust-lang/rustlings/commit/8143d57b4e88c51341dd4a18a14c536042cc009c))
|
||||||
|
|
||||||
<a name="2.0.0"></a>
|
|
||||||
|
|
||||||
## 2.0.0 (2019-11-12)
|
## 2.0.0 (2019-11-12)
|
||||||
|
|
||||||
#### Bug Fixes
|
#### Bug Fixes
|
||||||
@ -819,8 +868,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- **cli:** check for rustc before doing anything ([36a033b8](https://github.com/rust-lang/rustlings/commit/36a033b87a6549c1e5639c908bf7381c84f4f425))
|
- **cli:** check for rustc before doing anything ([36a033b8](https://github.com/rust-lang/rustlings/commit/36a033b87a6549c1e5639c908bf7381c84f4f425))
|
||||||
- **hint:** Add test for hint ([ce9fa6eb](https://github.com/rust-lang/rustlings/commit/ce9fa6ebbfdc3e7585d488d9409797285708316f))
|
- **hint:** Add test for hint ([ce9fa6eb](https://github.com/rust-lang/rustlings/commit/ce9fa6ebbfdc3e7585d488d9409797285708316f))
|
||||||
|
|
||||||
<a name="1.5.1"></a>
|
|
||||||
|
|
||||||
### 1.5.1 (2019-11-11)
|
### 1.5.1 (2019-11-11)
|
||||||
|
|
||||||
#### Bug Fixes
|
#### Bug Fixes
|
||||||
@ -832,8 +879,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- **threads:** Move Threads behind SLT ([fbe91a67](https://github.com/rust-lang/rustlings/commit/fbe91a67a482bfe64cbcdd58d06ba830a0f39da3), closes [#205](https://github.com/rust-lang/rustlings/issues/205))
|
- **threads:** Move Threads behind SLT ([fbe91a67](https://github.com/rust-lang/rustlings/commit/fbe91a67a482bfe64cbcdd58d06ba830a0f39da3), closes [#205](https://github.com/rust-lang/rustlings/issues/205))
|
||||||
- **watch:** clear screen before each `verify()` ([3aff590](https://github.com/rust-lang/rustlings/commit/3aff59085586c24196a547c2693adbdcf4432648))
|
- **watch:** clear screen before each `verify()` ([3aff590](https://github.com/rust-lang/rustlings/commit/3aff59085586c24196a547c2693adbdcf4432648))
|
||||||
|
|
||||||
<a name="1.5.0"></a>
|
|
||||||
|
|
||||||
## 1.5.0 (2019-11-09)
|
## 1.5.0 (2019-11-09)
|
||||||
|
|
||||||
#### Bug Fixes
|
#### Bug Fixes
|
||||||
@ -858,8 +903,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- Added exercise for struct update syntax ([1c4c8764](https://github.com/rust-lang/rustlings/commit/1c4c8764ed118740cd4cee73272ddc6cceb9d959))
|
- Added exercise for struct update syntax ([1c4c8764](https://github.com/rust-lang/rustlings/commit/1c4c8764ed118740cd4cee73272ddc6cceb9d959))
|
||||||
- **iterators2:** adds iterators2 exercise including config ([9288fccf](https://github.com/rust-lang/rustlings/commit/9288fccf07a2c5043b76d0fd6491e4cf72d76031))
|
- **iterators2:** adds iterators2 exercise including config ([9288fccf](https://github.com/rust-lang/rustlings/commit/9288fccf07a2c5043b76d0fd6491e4cf72d76031))
|
||||||
|
|
||||||
<a name="1.4.1"></a>
|
|
||||||
|
|
||||||
### 1.4.1 (2019-08-13)
|
### 1.4.1 (2019-08-13)
|
||||||
|
|
||||||
#### Bug Fixes
|
#### Bug Fixes
|
||||||
@ -868,8 +911,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- **option1:** Add test for prematurely passing exercise ([a750e4a1](https://github.com/rust-lang/rustlings/commit/a750e4a1a3006227292bb17d57d78ce84da6bfc6))
|
- **option1:** Add test for prematurely passing exercise ([a750e4a1](https://github.com/rust-lang/rustlings/commit/a750e4a1a3006227292bb17d57d78ce84da6bfc6))
|
||||||
- **test1:** Swap assertion parameter order ([4086d463](https://github.com/rust-lang/rustlings/commit/4086d463a981e81d97781851d17db2ced290f446))
|
- **test1:** Swap assertion parameter order ([4086d463](https://github.com/rust-lang/rustlings/commit/4086d463a981e81d97781851d17db2ced290f446))
|
||||||
|
|
||||||
<a name="1.4.0"></a>
|
|
||||||
|
|
||||||
## 1.4.0 (2019-07-13)
|
## 1.4.0 (2019-07-13)
|
||||||
|
|
||||||
#### Bug Fixes
|
#### Bug Fixes
|
||||||
@ -886,8 +927,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- **changelog:** Use clog for changelogs ([34e31232](https://github.com/rust-lang/rustlings/commit/34e31232dfddde284a341c9609b33cd27d9d5724))
|
- **changelog:** Use clog for changelogs ([34e31232](https://github.com/rust-lang/rustlings/commit/34e31232dfddde284a341c9609b33cd27d9d5724))
|
||||||
- **iterators2:** adds iterators2 exercise including config ([9288fccf](https://github.com/rust-lang/rustlings/commit/9288fccf07a2c5043b76d0fd6491e4cf72d76031))
|
- **iterators2:** adds iterators2 exercise including config ([9288fccf](https://github.com/rust-lang/rustlings/commit/9288fccf07a2c5043b76d0fd6491e4cf72d76031))
|
||||||
|
|
||||||
<a name="1.3.0"></a>
|
|
||||||
|
|
||||||
### 1.3.0 (2019-06-05)
|
### 1.3.0 (2019-06-05)
|
||||||
|
|
||||||
#### Features
|
#### Features
|
||||||
@ -903,16 +942,12 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- Fix broken link (#164, @HanKruiger)
|
- Fix broken link (#164, @HanKruiger)
|
||||||
- Remove highlighting and syntect (#167, @komaeda)
|
- Remove highlighting and syntect (#167, @komaeda)
|
||||||
|
|
||||||
<a name="1.2.2"></a>
|
|
||||||
|
|
||||||
### 1.2.2 (2019-05-07)
|
### 1.2.2 (2019-05-07)
|
||||||
|
|
||||||
#### Bug Fixes
|
#### Bug Fixes
|
||||||
|
|
||||||
- Reverted `--nocapture` flag since it was causing tests to pass unconditionally
|
- Reverted `--nocapture` flag since it was causing tests to pass unconditionally
|
||||||
|
|
||||||
<a name="1.2.1"></a>
|
|
||||||
|
|
||||||
### 1.2.1 (2019-04-22)
|
### 1.2.1 (2019-04-22)
|
||||||
|
|
||||||
#### Bug Fixes
|
#### Bug Fixes
|
||||||
@ -920,8 +955,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- Fix the `--nocapture` feature (@komaeda)
|
- Fix the `--nocapture` feature (@komaeda)
|
||||||
- Provide a nicer error message for when you're in the wrong directory
|
- Provide a nicer error message for when you're in the wrong directory
|
||||||
|
|
||||||
<a name="1.2.0"></a>
|
|
||||||
|
|
||||||
### 1.2.0 (2019-04-22)
|
### 1.2.0 (2019-04-22)
|
||||||
|
|
||||||
#### Features
|
#### Features
|
||||||
@ -929,8 +962,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- Add errors to exercises that compile without user changes (@yvan-sraka)
|
- Add errors to exercises that compile without user changes (@yvan-sraka)
|
||||||
- Use --nocapture when testing, enabling `println!` when running (@komaeda)
|
- Use --nocapture when testing, enabling `println!` when running (@komaeda)
|
||||||
|
|
||||||
<a name="1.1.1"></a>
|
|
||||||
|
|
||||||
### 1.1.1 (2019-04-14)
|
### 1.1.1 (2019-04-14)
|
||||||
|
|
||||||
#### Bug fixes
|
#### Bug fixes
|
||||||
@ -943,8 +974,6 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- Fix links by deleting book version (@diodfr, #142)
|
- Fix links by deleting book version (@diodfr, #142)
|
||||||
- Canonicalize paths to fix path matching (@cjpearce, #143)
|
- Canonicalize paths to fix path matching (@cjpearce, #143)
|
||||||
|
|
||||||
<a name="1.1.0"></a>
|
|
||||||
|
|
||||||
### 1.1.0 (2019-03-20)
|
### 1.1.0 (2019-03-20)
|
||||||
|
|
||||||
- errors2.rs: update link to Rust book (#124)
|
- errors2.rs: update link to Rust book (#124)
|
||||||
@ -954,16 +983,12 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
|||||||
- Give a warning when Rustlings isn't run from the right directory (#123)
|
- Give a warning when Rustlings isn't run from the right directory (#123)
|
||||||
- Verify that rust version is recent enough to install Rustlings (#131)
|
- Verify that rust version is recent enough to install Rustlings (#131)
|
||||||
|
|
||||||
<a name="1.0.1"></a>
|
|
||||||
|
|
||||||
### 1.0.1 (2019-03-06)
|
### 1.0.1 (2019-03-06)
|
||||||
|
|
||||||
- Adds a way to install Rustlings in one command (`curl -L https://git.io/rustlings | bash`)
|
- Adds a way to install Rustlings in one command (`curl -L https://git.io/rustlings | bash`)
|
||||||
- Makes `rustlings watch` react to create file events (@shaunbennett, #117)
|
- Makes `rustlings watch` react to create file events (@shaunbennett, #117)
|
||||||
- Reworks the exercise management to use an external TOML file instead of just listing them in the code
|
- Reworks the exercise management to use an external TOML file instead of just listing them in the code
|
||||||
|
|
||||||
<a name="1.0.0"></a>
|
|
||||||
|
|
||||||
### 1.0.0 (2019-03-06)
|
### 1.0.0 (2019-03-06)
|
||||||
|
|
||||||
Initial release.
|
Initial release.
|
||||||
|
|||||||
803
Cargo.lock
generated
803
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
61
Cargo.toml
61
Cargo.toml
@ -1,27 +1,25 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
|
||||||
exclude = [
|
exclude = [
|
||||||
"tests/fixture/failure",
|
"tests/test_exercises",
|
||||||
"tests/fixture/state",
|
|
||||||
"tests/fixture/success",
|
|
||||||
"dev",
|
"dev",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "6.1.0"
|
version = "6.5.0"
|
||||||
authors = [
|
authors = [
|
||||||
"Liv <mokou@fastmail.com>",
|
"Mo Bitar <mo8it@proton.me>", # https://github.com/mo8it
|
||||||
"Mo Bitar <mo8it@proton.me>",
|
"Liv <mokou@fastmail.com>", # https://github.com/shadows-withal
|
||||||
# Alumni
|
# Alumni
|
||||||
"Carol (Nichols || Goulding) <carol.nichols@gmail.com>",
|
"Carol (Nichols || Goulding) <carol.nichols@gmail.com>", # https://github.com/carols10cents
|
||||||
]
|
]
|
||||||
repository = "https://github.com/rust-lang/rustlings"
|
repository = "https://github.com/rust-lang/rustlings"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
edition = "2021"
|
edition = "2024" # On Update: Update the edition of `rustfmt` in `dev check` and `CARGO_TOML` in `dev new`.
|
||||||
|
rust-version = "1.88"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
serde = { version = "1.0.204", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
toml_edit = { version = "0.22.15", default-features = false, features = ["parse", "serde"] }
|
toml = { version = "0.9", default-features = false, features = ["std", "parse", "serde"] }
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "rustlings"
|
name = "rustlings"
|
||||||
@ -31,6 +29,7 @@ authors.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
keywords = [
|
keywords = [
|
||||||
"exercise",
|
"exercise",
|
||||||
"learning",
|
"learning",
|
||||||
@ -46,21 +45,20 @@ include = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.86"
|
anyhow = "1.0"
|
||||||
clap = { version = "4.5.9", features = ["derive"] }
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
crossterm = "0.27.0"
|
crossterm = { version = "0.29", default-features = false, features = ["windows", "events"] }
|
||||||
hashbrown = "0.14.5"
|
notify = "8.0"
|
||||||
notify-debouncer-mini = { version = "0.4.1", default-features = false }
|
rustlings-macros = { path = "rustlings-macros", version = "=6.5.0" }
|
||||||
os_pipe = "1.2.0"
|
serde_json = "1.0"
|
||||||
ratatui = { version = "0.27.0", default-features = false, features = ["crossterm"] }
|
|
||||||
rustlings-macros = { path = "rustlings-macros", version = "=6.1.0" }
|
|
||||||
serde_json = "1.0.120"
|
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
toml_edit.workspace = true
|
toml.workspace = true
|
||||||
|
|
||||||
|
[target.'cfg(not(windows))'.dependencies]
|
||||||
|
rustix = { version = "1.0", default-features = false, features = ["std", "stdio", "termios"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_cmd = "2.0.14"
|
tempfile = "3.21"
|
||||||
predicates = "3.1.0"
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
@ -70,3 +68,20 @@ panic = "abort"
|
|||||||
|
|
||||||
[package.metadata.release]
|
[package.metadata.release]
|
||||||
pre-release-hook = ["./release-hook.sh"]
|
pre-release-hook = ["./release-hook.sh"]
|
||||||
|
pre-release-commit-message = "Release 🎉"
|
||||||
|
|
||||||
|
[workspace.lints.rust]
|
||||||
|
unsafe_code = "forbid"
|
||||||
|
unstable_features = "forbid"
|
||||||
|
|
||||||
|
[workspace.lints.clippy]
|
||||||
|
empty_loop = "forbid"
|
||||||
|
disallowed-types = "deny"
|
||||||
|
disallowed-methods = "deny"
|
||||||
|
infinite_loop = "deny"
|
||||||
|
mem_forget = "deny"
|
||||||
|
dbg_macro = "warn"
|
||||||
|
todo = "warn"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|||||||
144
README.md
144
README.md
@ -1,143 +1,7 @@
|
|||||||
<div class="oranda-hide">
|
# [Rustlings](https://rustlings.rust-lang.org) 🦀
|
||||||
|
|
||||||
# Rustlings 🦀❤️
|
Small exercises to get you used to reading and writing [Rust](https://www.rust-lang.org) code - _Recommended in parallel to reading [the official Rust book](https://doc.rust-lang.org/book) 📚️_
|
||||||
|
|
||||||
</div>
|
Visit the **website** for a demo, info about setup and more:
|
||||||
|
|
||||||
Greetings and welcome to Rustlings.
|
## ➡️ [rustlings.rust-lang.org](https://rustlings.rust-lang.org) ⬅️
|
||||||
This project contains small exercises to get you used to reading and writing Rust code.
|
|
||||||
This includes reading and responding to compiler messages!
|
|
||||||
|
|
||||||
It is recommended to do the Rustlings exercises in parallel to reading [the official Rust book](https://doc.rust-lang.org/book/), the most comprehensive resource for learning Rust 📚️
|
|
||||||
|
|
||||||
[Rust By Example](https://doc.rust-lang.org/rust-by-example/) is another recommended resource that you might find helpful.
|
|
||||||
It contains code examples and exercises similar to Rustlings, but online.
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
### Installing Rust
|
|
||||||
|
|
||||||
Before installing Rustlings, you need to have _Rust installed_.
|
|
||||||
Visit [www.rust-lang.org/tools/install](https://www.rust-lang.org/tools/install) for further instructions on installing Rust.
|
|
||||||
This will also install _Cargo_, Rust's package/project manager.
|
|
||||||
|
|
||||||
> 🐧 If you're on Linux, make sure you've installed `gcc` (for a linker).
|
|
||||||
>
|
|
||||||
> Deb: `sudo apt install gcc`.
|
|
||||||
> Dnf: `sudo dnf install gcc`.
|
|
||||||
|
|
||||||
> 🍎 If you're on MacOS, make sure you've installed Xcode and its developer tools by running `xcode-select --install`.
|
|
||||||
|
|
||||||
### Installing Rustlings
|
|
||||||
|
|
||||||
The following command will download and compile Rustlings:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo install rustlings
|
|
||||||
```
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>If the installation fails…</strong> (<em>click to expand</em>)</summary>
|
|
||||||
|
|
||||||
- Make sure you have the latest Rust version by running `rustup update`
|
|
||||||
- Try adding the `--locked` flag: `cargo install rustlings --locked`
|
|
||||||
- Otherwise, please [report the issue](https://github.com/rust-lang/rustlings/issues/new)
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
### Initialization
|
|
||||||
|
|
||||||
After installing Rustlings, run the following command to initialize the `rustlings/` directory:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rustlings init
|
|
||||||
```
|
|
||||||
|
|
||||||
Now, go into the newly initialized directory and launch Rustlings for further instructions on getting started with the exercises:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd rustlings/
|
|
||||||
rustlings
|
|
||||||
```
|
|
||||||
|
|
||||||
## Working environment
|
|
||||||
|
|
||||||
### Editor
|
|
||||||
|
|
||||||
Our general recommendation is [VS Code](https://code.visualstudio.com/) with the [rust-analyzer plugin](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer).
|
|
||||||
But any editor that supports [rust-analyzer](https://rust-analyzer.github.io/) should be enough for working on the exercises.
|
|
||||||
|
|
||||||
### Terminal
|
|
||||||
|
|
||||||
While working with Rustlings, please use a modern terminal for the best user experience.
|
|
||||||
The default terminal on Linux and Mac should be sufficient.
|
|
||||||
On Windows, we recommend the [Windows Terminal](https://aka.ms/terminal).
|
|
||||||
|
|
||||||
If you use VS Code, the builtin terminal should also be fine.
|
|
||||||
|
|
||||||
## Doing exercises
|
|
||||||
|
|
||||||
The exercises are sorted by topic and can be found in the subdirectory `exercises/<topic>`.
|
|
||||||
For every topic, there is an additional `README.md` file with some resources to get you started on the topic.
|
|
||||||
We highly recommend that you have a look at them before you start 📚️
|
|
||||||
|
|
||||||
Most exercises contain an error that keeps them from compiling, and it's up to you to fix it!
|
|
||||||
Some exercises contain tests that need to pass for the exercise to be done ✅
|
|
||||||
|
|
||||||
Search for `TODO` and `todo!()` to find out what you need to change.
|
|
||||||
Ask for hints by entering `h` in the _watch mode_ 💡
|
|
||||||
|
|
||||||
### Watch Mode
|
|
||||||
|
|
||||||
After [initialization](#initialization), Rustlings can be launched by simply running the command `rustlings`.
|
|
||||||
|
|
||||||
This will start the _watch mode_ which walks you through the exercises in a predefined order (what we think is best for newcomers).
|
|
||||||
It will rerun the current exercise automatically every time you change the exercise's file in the `exercises/` directory.
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>If detecting file changes in the <code>exercises/</code> directory fails…</strong> (<em>click to expand</em>)</summary>
|
|
||||||
|
|
||||||
> You can add the **`--manual-run`** flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` in the watch mode.
|
|
||||||
>
|
|
||||||
> Please [report the issue](https://github.com/rust-lang/rustlings/issues/new) with some information about your operating system and whether you run Rustlings in a container or virtual machine (e.g. WSL).
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
### Exercise List
|
|
||||||
|
|
||||||
In the [watch mode](#watch-mode) (after launching `rustlings`), you can enter `l` to open the interactive exercise list.
|
|
||||||
|
|
||||||
The list allows you to…
|
|
||||||
|
|
||||||
- See the status of all exercises (done or pending)
|
|
||||||
- `c`: Continue at another exercise (temporarily skip some exercises or go back to a previous one)
|
|
||||||
- `r`: Reset status and file of an exercise (you need to _reload/reopen_ its file in your editor afterwards)
|
|
||||||
|
|
||||||
See the footer of the list for all possible keys.
|
|
||||||
|
|
||||||
## Continuing On
|
|
||||||
|
|
||||||
Once you've completed Rustlings, put your new knowledge to good use!
|
|
||||||
Continue practicing your Rust skills by building your own projects, contributing to Rustlings, or finding other open-source projects to contribute to.
|
|
||||||
|
|
||||||
## Third-Party Exercises
|
|
||||||
|
|
||||||
Do you want to create your own set of Rustlings exercises to focus on some specific topic?
|
|
||||||
Or do you want to translate the original Rustlings exercises?
|
|
||||||
Then follow the link to the guide about [third-party exercises](https://github.com/rust-lang/rustlings/blob/main/THIRD_PARTY_EXERCISES.md)!
|
|
||||||
|
|
||||||
## Uninstalling Rustlings
|
|
||||||
|
|
||||||
If you want to remove Rustlings from your system, run the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo uninstall rustlings
|
|
||||||
```
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
See [CONTRIBUTING.md](https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md) 🔗
|
|
||||||
|
|
||||||
## Contributors ✨
|
|
||||||
|
|
||||||
Thanks to [all the wonderful contributors](https://github.com/rust-lang/rustlings/graphs/contributors) 🎉
|
|
||||||
|
|||||||
@ -1,53 +0,0 @@
|
|||||||
# Third-Party Exercises
|
|
||||||
|
|
||||||
The support of Rustlings for third-party exercises allows you to create your own set of Rustlings exercises to focus on some specific topic.
|
|
||||||
You could also offer a translation of the original Rustlings exercises as third-party exercises.
|
|
||||||
|
|
||||||
## Getting started
|
|
||||||
|
|
||||||
To create third-party exercises, install Rustlings and run `rustlings dev new PROJECT_NAME`.
|
|
||||||
This command will, similar to `cargo new PROJECT_NAME`, create a template directory called `PROJECT_NAME` with all what you need to get started.
|
|
||||||
|
|
||||||
Read the comments in the generated `info.toml` file to understand its format.
|
|
||||||
It allows you to set a custom welcome and final message and specify the metadata of every exercise.
|
|
||||||
|
|
||||||
## Create an exercise
|
|
||||||
|
|
||||||
Here is an example of the metadata of one file:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[[exercises]]
|
|
||||||
name = "intro1"
|
|
||||||
hint = """
|
|
||||||
To finish this exercise, you need to …
|
|
||||||
This link might help you …"""
|
|
||||||
```
|
|
||||||
|
|
||||||
After entering this in `info.toml`, create the file `intro1.rs` in the `exercises/` directory.
|
|
||||||
The exercise needs to contain a `main` function, but it can be empty.
|
|
||||||
Adding tests is recommended.
|
|
||||||
Look at the official Rustlings exercises for inspiration.
|
|
||||||
|
|
||||||
You can optionally add a solution file `intro1.rs` to the `solutions/` directory.
|
|
||||||
|
|
||||||
Now, run `rustlings dev check`.
|
|
||||||
It will tell you about any issues with your exercises.
|
|
||||||
For example, it will tell you to run `rustlings dev update` to update the `Cargo.toml` file to include the new exercise `intro1`.
|
|
||||||
|
|
||||||
`rustlings dev check` will also run your solutions (if you have any) to make sure that they run successfully.
|
|
||||||
|
|
||||||
That's it!
|
|
||||||
You finished your first exercise 🎉
|
|
||||||
|
|
||||||
## Publish
|
|
||||||
|
|
||||||
Now, add more exercises and publish them as a Git repository.
|
|
||||||
|
|
||||||
Users just have to clone that repository and run `rustlings` in it to start working on your set of exercises just like the official ones.
|
|
||||||
|
|
||||||
One difference to the official exercises is that the solution files will not be hidden until the user finishes an exercise.
|
|
||||||
But you can trust the users to not look at the solution too early 😉
|
|
||||||
|
|
||||||
## Share
|
|
||||||
|
|
||||||
After publishing your set of exercises, open an issue or a pull request in the official Rustlings repository to link to your project in the README 😃
|
|
||||||
5
build.rs
Normal file
5
build.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
fn main() {
|
||||||
|
// Fix building from source on Windows because it can't handle file links.
|
||||||
|
#[cfg(windows)]
|
||||||
|
let _ = std::fs::copy("dev/Cargo.toml", "dev-Cargo.toml");
|
||||||
|
}
|
||||||
11
clippy.toml
Normal file
11
clippy.toml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
disallowed-types = [
|
||||||
|
{ path = "crossterm::style::Stylize", reason = "inefficient, use `.queue(…)` instead" },
|
||||||
|
{ path = "crossterm::style::styled_content::StyledContent", reason = "inefficient, use `.queue(…)` instead" },
|
||||||
|
]
|
||||||
|
|
||||||
|
disallowed-methods = [
|
||||||
|
{ path = "crossterm::style::style", reason = "inefficient, use `.queue(…)` instead" },
|
||||||
|
{ path = "std::thread::spawn", replacement = "std::thread::Builder::spawn", reason = "handle the error" },
|
||||||
|
{ path = "std::thread::Scope::spawn", replacement = "std::thread::Builder::spawn", reason = "handle the error" },
|
||||||
|
{ path = "std::process::exit", replacement = "std::process::ExitCode" },
|
||||||
|
]
|
||||||
@ -1,4 +1,4 @@
|
|||||||
# Don't edit the `bin` list manually! It is updated by `cargo run -- dev update`. This comment line will be stripped in `rustlings init`.
|
# Don't edit the `bin` list manually! It is updated by `cargo dev update`. This comment line will be stripped in `rustlings init`.
|
||||||
bin = [
|
bin = [
|
||||||
{ name = "intro1", path = "../exercises/00_intro/intro1.rs" },
|
{ name = "intro1", path = "../exercises/00_intro/intro1.rs" },
|
||||||
{ name = "intro1_sol", path = "../solutions/00_intro/intro1.rs" },
|
{ name = "intro1_sol", path = "../solutions/00_intro/intro1.rs" },
|
||||||
@ -192,6 +192,32 @@ bin = [
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "exercises"
|
name = "exercises"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
# Don't publish the exercises on crates.io!
|
# Don't publish the exercises on crates.io!
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
panic = "abort"
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
panic = "abort"
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
# You shouldn't write unsafe code in Rustlings!
|
||||||
|
unsafe_code = "forbid"
|
||||||
|
# You don't need unstable features in Rustlings and shouldn't rely on them while learning Rust.
|
||||||
|
unstable_features = "forbid"
|
||||||
|
# Dead code warnings can't be avoided in some exercises and might distract while learning.
|
||||||
|
dead_code = "allow"
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
# You forgot a `todo!()`!
|
||||||
|
todo = "forbid"
|
||||||
|
# This can only happen by mistake in Rustlings.
|
||||||
|
empty_loop = "forbid"
|
||||||
|
# No infinite loops are needed in Rustlings.
|
||||||
|
infinite_loop = "deny"
|
||||||
|
# You shouldn't leak memory while still learning Rust!
|
||||||
|
mem_forget = "deny"
|
||||||
|
# Currently, there are no disallowed methods. This line avoids problems when developing Rustlings.
|
||||||
|
disallowed_methods = "allow"
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// TODO: We sometimes encourage you to keep trying things on a given exercise,
|
// TODO: We sometimes encourage you to keep trying things on a given exercise
|
||||||
// even after you already figured it out. If you got everything working and feel
|
// even after you already figured it out. If you got everything working and feel
|
||||||
// ready for the next exercise, enter `n` in the terminal.
|
// ready for the next exercise, enter `n` in the terminal.
|
||||||
//
|
//
|
||||||
@ -6,8 +6,7 @@
|
|||||||
// Try adding a new `println!` and check the updated output in the terminal.
|
// Try adding a new `println!` and check the updated output in the terminal.
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("Hello and");
|
println!(r#" Welcome to... "#);
|
||||||
println!(r#" welcome to... "#);
|
|
||||||
println!(r#" _ _ _ "#);
|
println!(r#" _ _ _ "#);
|
||||||
println!(r#" _ __ _ _ ___| |_| (_)_ __ __ _ ___ "#);
|
println!(r#" _ __ _ _ ___| |_| (_)_ __ __ _ ___ "#);
|
||||||
println!(r#" | '__| | | / __| __| | | '_ \ / _` / __| "#);
|
println!(r#" | '__| | | / __| __| | | '_ \ / _` / __| "#);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# Variables
|
# Variables
|
||||||
|
|
||||||
In Rust, variables are immutable by default.
|
In Rust, variables are immutable by default.
|
||||||
When a variable is immutable, once a value is bound to a name, you can’t change that value.
|
When a variable is immutable, once a value is bound to a name, you can't change that value.
|
||||||
You can make them mutable by adding `mut` in front of the variable name.
|
You can make them mutable by adding `mut` in front of the variable name.
|
||||||
|
|
||||||
## Further information
|
## Further information
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
// TODO: Add missing keyword.
|
// TODO: Add the missing keyword.
|
||||||
x = 5;
|
x = 5;
|
||||||
|
|
||||||
println!("x has the value {x}");
|
println!("x has the value {x}");
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
let number = "T-H-R-E-E"; // Don't change this line
|
let number = "T-H-R-E-E"; // Don't change this line
|
||||||
println!("Spell a number: {}", number);
|
println!("Spell a number: {number}");
|
||||||
|
|
||||||
// TODO: Fix the compiler error by changing the line below without renaming the variable.
|
// TODO: Fix the compiler error by changing the line below without renaming the variable.
|
||||||
number = 3;
|
number = 3;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
fn call_me(num: u32) {
|
fn call_me(num: u8) {
|
||||||
for i in 0..num {
|
for i in 0..num {
|
||||||
println!("Ring! Call number {}", i + 1);
|
println!("Ring! Call number {}", i + 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// TODO: Fix the compiler error on this function.
|
// TODO: Fix the compiler error on this function.
|
||||||
fn foo_if_fizz(fizzish: &str) -> &str {
|
fn picky_eater(food: &str) -> &str {
|
||||||
if fizzish == "fizz" {
|
if food == "strawberry" {
|
||||||
"foo"
|
"Yummy!"
|
||||||
} else {
|
} else {
|
||||||
1
|
1
|
||||||
}
|
}
|
||||||
@ -18,18 +18,20 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn foo_for_fizz() {
|
fn yummy_food() {
|
||||||
// This means that calling `foo_if_fizz` with the argument "fizz" should return "foo".
|
// This means that calling `picky_eater` with the argument "strawberry" should return "Yummy!".
|
||||||
assert_eq!(foo_if_fizz("fizz"), "foo");
|
assert_eq!(picky_eater("strawberry"), "Yummy!");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bar_for_fuzz() {
|
fn neutral_food() {
|
||||||
assert_eq!(foo_if_fizz("fuzz"), "bar");
|
assert_eq!(picky_eater("potato"), "I guess I can eat that.");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn default_to_baz() {
|
fn default_disliked_food() {
|
||||||
assert_eq!(foo_if_fizz("literally anything"), "baz");
|
assert_eq!(picky_eater("broccoli"), "No thanks!");
|
||||||
|
assert_eq!(picky_eater("gummy bears"), "No thanks!");
|
||||||
|
assert_eq!(picky_eater("literally anything"), "No thanks!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,26 +9,6 @@ fn vec_loop(input: &[i32]) -> Vec<i32> {
|
|||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
fn vec_map_example(input: &[i32]) -> Vec<i32> {
|
|
||||||
// An example of collecting a vector after mapping.
|
|
||||||
// We map each element of the `input` slice to its value plus 1.
|
|
||||||
// If the input is `[1, 2, 3]`, the output is `[2, 3, 4]`.
|
|
||||||
input.iter().map(|element| element + 1).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn vec_map(input: &[i32]) -> Vec<i32> {
|
|
||||||
// TODO: Here, we also want to multiply each element in the `input` slice
|
|
||||||
// by 2, but with iterator mapping instead of manually pushing into an empty
|
|
||||||
// vector.
|
|
||||||
// See the example in the function `vec_map_example` above.
|
|
||||||
input
|
|
||||||
.iter()
|
|
||||||
.map(|element| {
|
|
||||||
// ???
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// You can optionally experiment here.
|
// You can optionally experiment here.
|
||||||
}
|
}
|
||||||
@ -43,18 +23,4 @@ mod tests {
|
|||||||
let ans = vec_loop(&input);
|
let ans = vec_loop(&input);
|
||||||
assert_eq!(ans, [4, 8, 12, 16, 20]);
|
assert_eq!(ans, [4, 8, 12, 16, 20]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_vec_map_example() {
|
|
||||||
let input = [1, 2, 3];
|
|
||||||
let ans = vec_map_example(&input);
|
|
||||||
assert_eq!(ans, [2, 3, 4]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_vec_map() {
|
|
||||||
let input = [2, 4, 6, 8, 10];
|
|
||||||
let ans = vec_map(&input);
|
|
||||||
assert_eq!(ans, [4, 8, 12, 16, 20]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
# Enums
|
# Enums
|
||||||
|
|
||||||
Rust allows you to define types called "enums" which enumerate possible values.
|
Rust allows you to define types called "enums" which enumerate possible values.
|
||||||
Enums are a feature in many languages, but their capabilities differ in each language. Rust’s enums are most similar to algebraic data types in functional languages, such as F#, OCaml, and Haskell.
|
Enums are a feature in many languages, but their capabilities differ in each language. Rust's enums are most similar to algebraic data types in functional languages, such as F#, OCaml, and Haskell.
|
||||||
Useful in combination with enums is Rust's "pattern matching" facility, which makes it easy to run different code for different values of an enumeration.
|
Useful in combination with enums is Rust's "pattern matching" facility, which makes it easy to run different code for different values of an enumeration.
|
||||||
|
|
||||||
## Further information
|
## Further information
|
||||||
|
|
||||||
- [Enums](https://doc.rust-lang.org/book/ch06-00-enums.html)
|
- [Enums](https://doc.rust-lang.org/book/ch06-00-enums.html)
|
||||||
- [Pattern syntax](https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html)
|
- [Pattern syntax](https://doc.rust-lang.org/book/ch19-03-pattern-syntax.html)
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Point {
|
struct Point {
|
||||||
x: u64,
|
x: u64,
|
||||||
|
|||||||
@ -4,7 +4,11 @@ struct Point {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum Message {
|
enum Message {
|
||||||
// TODO: Implement the message variant types based on their usage below.
|
Resize { width: u64, height: u64 },
|
||||||
|
Move(Point),
|
||||||
|
Echo(String),
|
||||||
|
ChangeColor(u8, u8, u8),
|
||||||
|
Quit,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct State {
|
struct State {
|
||||||
|
|||||||
@ -23,6 +23,7 @@ mod tests {
|
|||||||
assert_eq!(trim_me("Hello! "), "Hello!");
|
assert_eq!(trim_me("Hello! "), "Hello!");
|
||||||
assert_eq!(trim_me(" What's up!"), "What's up!");
|
assert_eq!(trim_me(" What's up!"), "What's up!");
|
||||||
assert_eq!(trim_me(" Hola! "), "Hola!");
|
assert_eq!(trim_me(" Hola! "), "Hola!");
|
||||||
|
assert_eq!(trim_me("Hi!"), "Hi!");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
// You can bring module paths into scopes and provide new names for them with
|
// You can bring module paths into scopes and provide new names for them with
|
||||||
// the `use` and `as` keywords.
|
// the `use` and `as` keywords.
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
mod delicious_snacks {
|
mod delicious_snacks {
|
||||||
// TODO: Add the following two `use` statements after fixing them.
|
// TODO: Add the following two `use` statements after fixing them.
|
||||||
// use self::fruits::PEAR as ???;
|
// use self::fruits::PEAR as ???;
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
// must add fruit to the basket so that there is at least one of each kind and
|
// must add fruit to the basket so that there is at least one of each kind and
|
||||||
// more than 11 in total - we have a lot of mouths to feed. You are not allowed
|
// more than 11 in total - we have a lot of mouths to feed. You are not allowed
|
||||||
// to insert any more of the fruits that are already in the basket (Apple,
|
// to insert any more of the fruits that are already in the basket (Apple,
|
||||||
// Mango, and Lyche).
|
// Mango, and Lychee).
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
|||||||
@ -10,14 +10,14 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
// A structure to store the goal details of a team.
|
// A structure to store the goal details of a team.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct Team {
|
struct TeamScores {
|
||||||
goals_scored: u8,
|
goals_scored: u8,
|
||||||
goals_conceded: u8,
|
goals_conceded: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_scores_table(results: &str) -> HashMap<&str, Team> {
|
fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
|
||||||
// The name of the team is the key and its associated struct is the value.
|
// The name of the team is the key and its associated struct is the value.
|
||||||
let mut scores = HashMap::new();
|
let mut scores = HashMap::<&str, TeamScores>::new();
|
||||||
|
|
||||||
for line in results.lines() {
|
for line in results.lines() {
|
||||||
let mut split_iterator = line.split(',');
|
let mut split_iterator = line.split(',');
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
// This function returns how much icecream there is left in the fridge.
|
// This function returns how much ice cream there is left in the fridge.
|
||||||
// If it's before 22:00 (24-hour system), then 5 scoops are left. At 22:00,
|
// If it's before 22:00 (24-hour system), then 5 scoops are left. At 22:00,
|
||||||
// someone eats it all, so no icecream is left (value 0). Return `None` if
|
// someone eats it all, so no ice cream is left (value 0). Return `None` if
|
||||||
// `hour_of_day` is higher than 23.
|
// `hour_of_day` is higher than 23.
|
||||||
fn maybe_icecream(hour_of_day: u16) -> Option<u16> {
|
fn maybe_ice_cream(hour_of_day: u16) -> Option<u16> {
|
||||||
// TODO: Complete the function body.
|
// TODO: Complete the function body.
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,19 +18,19 @@ mod tests {
|
|||||||
fn raw_value() {
|
fn raw_value() {
|
||||||
// TODO: Fix this test. How do you get the value contained in the
|
// TODO: Fix this test. How do you get the value contained in the
|
||||||
// Option?
|
// Option?
|
||||||
let icecreams = maybe_icecream(12);
|
let ice_creams = maybe_ice_cream(12);
|
||||||
|
|
||||||
assert_eq!(icecreams, 5); // Don't change this line.
|
assert_eq!(ice_creams, 5); // Don't change this line.
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_icecream() {
|
fn check_ice_cream() {
|
||||||
assert_eq!(maybe_icecream(0), Some(5));
|
assert_eq!(maybe_ice_cream(0), Some(5));
|
||||||
assert_eq!(maybe_icecream(9), Some(5));
|
assert_eq!(maybe_ice_cream(9), Some(5));
|
||||||
assert_eq!(maybe_icecream(18), Some(5));
|
assert_eq!(maybe_ice_cream(18), Some(5));
|
||||||
assert_eq!(maybe_icecream(22), Some(0));
|
assert_eq!(maybe_ice_cream(22), Some(0));
|
||||||
assert_eq!(maybe_icecream(23), Some(0));
|
assert_eq!(maybe_ice_cream(23), Some(0));
|
||||||
assert_eq!(maybe_icecream(24), None);
|
assert_eq!(maybe_ice_cream(24), None);
|
||||||
assert_eq!(maybe_icecream(25), None);
|
assert_eq!(maybe_ice_cream(25), None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ fn main() {
|
|||||||
|
|
||||||
// TODO: Fix the compiler error by adding something to this match statement.
|
// TODO: Fix the compiler error by adding something to this match statement.
|
||||||
match optional_point {
|
match optional_point {
|
||||||
Some(p) => println!("Co-ordinates are {},{}", p.x, p.y),
|
Some(p) => println!("Coordinates are {},{}", p.x, p.y),
|
||||||
_ => panic!("No match!"),
|
_ => panic!("No match!"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
# Error handling
|
# Error handling
|
||||||
|
|
||||||
Most errors aren’t serious enough to require the program to stop entirely.
|
Most errors aren't serious enough to require the program to stop entirely.
|
||||||
Sometimes, when a function fails, it’s for a reason that you can easily interpret and respond to.
|
Sometimes, when a function fails, it's for a reason that you can easily interpret and respond to.
|
||||||
For example, if you try to open a file and that operation fails because the file doesn’t exist, you might want to create the file instead of terminating the process.
|
For example, if you try to open a file and that operation fails because the file doesn't exist, you might want to create the file instead of terminating the process.
|
||||||
|
|
||||||
## Further information
|
## Further information
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
// of `Option<String>`.
|
// of `Option<String>`.
|
||||||
fn generate_nametag_text(name: String) -> Option<String> {
|
fn generate_nametag_text(name: String) -> Option<String> {
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
// Empty names aren't allowed.
|
// Empty names aren't allowed
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(format!("Hi! My name is {name}"))
|
Some(format!("Hi! My name is {name}"))
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
#![allow(clippy::comparison_chain)]
|
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
enum CreationError {
|
enum CreationError {
|
||||||
Negative,
|
Negative,
|
||||||
@ -12,6 +10,7 @@ struct PositiveNonzeroInteger(u64);
|
|||||||
impl PositiveNonzeroInteger {
|
impl PositiveNonzeroInteger {
|
||||||
fn new(value: i64) -> Result<Self, CreationError> {
|
fn new(value: i64) -> Result<Self, CreationError> {
|
||||||
// TODO: This function shouldn't always return an `Ok`.
|
// TODO: This function shouldn't always return an `Ok`.
|
||||||
|
// Read the tests below to clarify what should be returned.
|
||||||
Ok(Self(value as u64))
|
Ok(Self(value as u64))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
//
|
//
|
||||||
// In short, this particular use case for boxes is for when you want to own a
|
// In short, this particular use case for boxes is for when you want to own a
|
||||||
// value and you care only that it is a type which implements a particular
|
// value and you care only that it is a type which implements a particular
|
||||||
// trait. To do so, The `Box` is declared as of type `Box<dyn Trait>` where
|
// trait. To do so, the `Box` is declared as of type `Box<dyn Trait>` where
|
||||||
// `Trait` is the trait the compiler looks for on any value used in that
|
// `Trait` is the trait the compiler looks for on any value used in that
|
||||||
// context. For this exercise, that context is the potential errors which
|
// context. For this exercise, that context is the potential errors which
|
||||||
// can be returned in a `Result`.
|
// can be returned in a `Result`.
|
||||||
|
|||||||
@ -25,7 +25,7 @@ impl ParsePosNonzeroError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add another error conversion function here.
|
// TODO: Add another error conversion function here.
|
||||||
// fn from_parseint(???) -> Self { ??? }
|
// fn from_parse_int(???) -> Self { ??? }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
trait Licensed {
|
trait Licensed {
|
||||||
// TODO: Add a default implementation for `licensing_info` so that
|
// TODO: Add a default implementation for `licensing_info` so that
|
||||||
// implementors like the two structs below can share that default behavior
|
// implementors like the two structs below can share that default behavior
|
||||||
|
|||||||
@ -9,7 +9,7 @@ impl Rectangle {
|
|||||||
if width <= 0 || height <= 0 {
|
if width <= 0 || height <= 0 {
|
||||||
// Returning a `Result` would be better here. But we want to learn
|
// Returning a `Result` would be better here. But we want to learn
|
||||||
// how to test functions that can panic.
|
// how to test functions that can panic.
|
||||||
panic!("Rectangle width and height can't be negative");
|
panic!("Rectangle width and height must be positive");
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle { width, height }
|
Rectangle { width, height }
|
||||||
|
|||||||
@ -39,6 +39,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_success() {
|
fn test_success() {
|
||||||
assert_eq!(divide(81, 9), Ok(9));
|
assert_eq!(divide(81, 9), Ok(9));
|
||||||
|
assert_eq!(divide(81, -1), Ok(-81));
|
||||||
|
assert_eq!(divide(i64::MIN, i64::MIN), Ok(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -8,7 +8,6 @@ use std::rc::Rc;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Sun;
|
struct Sun;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum Planet {
|
enum Planet {
|
||||||
Mercury(Rc<Sun>),
|
Mercury(Rc<Sun>),
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// This program spawns multiple threads that each run for at least 250ms, and
|
// This program spawns multiple threads that each runs for at least 250ms, and
|
||||||
// each thread returns how much time they took to complete. The program should
|
// each thread returns how much time it took to complete. The program should
|
||||||
// wait until all the spawned threads have finished and should collect their
|
// wait until all the spawned threads have finished and should collect their
|
||||||
// return values into a vector.
|
// return values into a vector.
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
use std::{sync::mpsc, thread, time::Duration};
|
use std::{sync::mpsc, thread, time::Duration};
|
||||||
|
|
||||||
struct Queue {
|
struct Queue {
|
||||||
length: u32,
|
|
||||||
first_half: Vec<u32>,
|
first_half: Vec<u32>,
|
||||||
second_half: Vec<u32>,
|
second_half: Vec<u32>,
|
||||||
}
|
}
|
||||||
@ -9,7 +8,6 @@ struct Queue {
|
|||||||
impl Queue {
|
impl Queue {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
length: 10,
|
|
||||||
first_half: vec![1, 2, 3, 4, 5],
|
first_half: vec![1, 2, 3, 4, 5],
|
||||||
second_half: vec![6, 7, 8, 9, 10],
|
second_half: vec![6, 7, 8, 9, 10],
|
||||||
}
|
}
|
||||||
@ -48,17 +46,15 @@ mod tests {
|
|||||||
fn threads3() {
|
fn threads3() {
|
||||||
let (tx, rx) = mpsc::channel();
|
let (tx, rx) = mpsc::channel();
|
||||||
let queue = Queue::new();
|
let queue = Queue::new();
|
||||||
let queue_length = queue.length;
|
|
||||||
|
|
||||||
send_tx(queue, tx);
|
send_tx(queue, tx);
|
||||||
|
|
||||||
let mut total_received: u32 = 0;
|
let mut received = Vec::with_capacity(10);
|
||||||
for received in rx {
|
for value in rx {
|
||||||
println!("Got: {received}");
|
received.push(value);
|
||||||
total_received += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Number of received values: {total_received}");
|
received.sort();
|
||||||
assert_eq!(total_received, queue_length);
|
assert_eq!(received, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,5 +10,6 @@ of exercises to Rustlings, but is all about learning to write Macros.
|
|||||||
|
|
||||||
## Further information
|
## Further information
|
||||||
|
|
||||||
- [Macros](https://doc.rust-lang.org/book/ch19-06-macros.html)
|
- [The Rust Book - Macros](https://doc.rust-lang.org/book/ch20-05-macros.html)
|
||||||
- [The Little Book of Rust Macros](https://veykril.github.io/tlborm/)
|
- [The Little Book of Rust Macros](https://veykril.github.io/tlborm/)
|
||||||
|
- [Rust by Example - macro_rules!](https://doc.rust-lang.org/rust-by-example/macros.html)
|
||||||
|
|||||||
@ -1,22 +1,25 @@
|
|||||||
// Here are some more easy Clippy fixes so you can see its utility 📎
|
// Here are some more easy Clippy fixes so you can see its utility.
|
||||||
// TODO: Fix all the Clippy lints.
|
// TODO: Fix all the Clippy lints.
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[allow(unused_variables, unused_assignments)]
|
#[allow(unused_variables, unused_assignments)]
|
||||||
fn main() {
|
fn main() {
|
||||||
let my_option: Option<()> = None;
|
let my_option: Option<&str> = None;
|
||||||
|
// Assume that you don't know the value of `my_option`.
|
||||||
|
// In the case of `Some`, we want to print its value.
|
||||||
if my_option.is_none() {
|
if my_option.is_none() {
|
||||||
println!("{:?}", my_option.unwrap());
|
println!("{}", my_option.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
let my_arr = &[
|
let my_arr = &[
|
||||||
-1, -2, -3
|
-1, -2, -3
|
||||||
-4, -5, -6
|
-4, -5, -6
|
||||||
];
|
];
|
||||||
println!("My array! Here it is: {my_arr:?}");
|
println!("My array! Here it is: {my_arr:?}");
|
||||||
|
|
||||||
let my_empty_vec = vec![1, 2, 3, 4, 5].resize(0, 5);
|
let mut my_vec = vec![1, 2, 3, 4, 5];
|
||||||
println!("This Vec is empty, see? {my_empty_vec:?}");
|
my_vec.resize(0, 5);
|
||||||
|
println!("This Vec is empty, see? {my_vec:?}");
|
||||||
|
|
||||||
let mut value_a = 45;
|
let mut value_a = 45;
|
||||||
let mut value_b = 66;
|
let mut value_b = 66;
|
||||||
|
|||||||
@ -2,10 +2,11 @@
|
|||||||
// about them at https://doc.rust-lang.org/std/convert/trait.AsRef.html and
|
// about them at https://doc.rust-lang.org/std/convert/trait.AsRef.html and
|
||||||
// https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively.
|
// https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively.
|
||||||
|
|
||||||
// Obtain the number of bytes (not characters) in the given argument.
|
// Obtain the number of bytes (not characters) in the given argument
|
||||||
|
// (`.len()` returns the number of bytes in a string).
|
||||||
// TODO: Add the `AsRef` trait appropriately as a trait bound.
|
// TODO: Add the `AsRef` trait appropriately as a trait bound.
|
||||||
fn byte_counter<T>(arg: T) -> usize {
|
fn byte_counter<T>(arg: T) -> usize {
|
||||||
arg.as_ref().as_bytes().len()
|
arg.as_ref().len()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obtain the number of characters (not bytes) in the given argument.
|
// Obtain the number of characters (not bytes) in the given argument.
|
||||||
|
|||||||
@ -25,7 +25,7 @@ enum ParsePersonError {
|
|||||||
ParseInt(ParseIntError),
|
ParseInt(ParseIntError),
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Complete this `From` implementation to be able to parse a `Person`
|
// TODO: Complete this `FromStr` implementation to be able to parse a `Person`
|
||||||
// out of a string in the form of "Mark,20".
|
// out of a string in the form of "Mark,20".
|
||||||
// Note that you'll need to parse the age component into a `u8` with something
|
// Note that you'll need to parse the age component into a `u8` with something
|
||||||
// like `"4".parse::<u8>()`.
|
// like `"4".parse::<u8>()`.
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
| vecs | §8.1 |
|
| vecs | §8.1 |
|
||||||
| move_semantics | §4.1-2 |
|
| move_semantics | §4.1-2 |
|
||||||
| structs | §5.1, §5.3 |
|
| structs | §5.1, §5.3 |
|
||||||
| enums | §6, §18.3 |
|
| enums | §6, §19.3 |
|
||||||
| strings | §8.2 |
|
| strings | §8.2 |
|
||||||
| modules | §7 |
|
| modules | §7 |
|
||||||
| hashmaps | §8.3 |
|
| hashmaps | §8.3 |
|
||||||
@ -22,6 +22,6 @@
|
|||||||
| iterators | §13.2-4 |
|
| iterators | §13.2-4 |
|
||||||
| smart_pointers | §15, §16.3 |
|
| smart_pointers | §15, §16.3 |
|
||||||
| threads | §16.1-3 |
|
| threads | §16.1-3 |
|
||||||
| macros | §19.5 |
|
| macros | §20.5 |
|
||||||
| clippy | §21.4 |
|
| clippy | Appendix D |
|
||||||
| conversions | n/a |
|
| conversions | n/a |
|
||||||
|
|||||||
@ -26,7 +26,7 @@ enum Command {
|
|||||||
mod my_module {
|
mod my_module {
|
||||||
use super::Command;
|
use super::Command;
|
||||||
|
|
||||||
// TODO: Complete the function.
|
// TODO: Complete the function as described above.
|
||||||
// pub fn transformer(input: ???) -> ??? { ??? }
|
// pub fn transformer(input: ???) -> ??? { ??? }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
oranda.json
13
oranda.json
@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"project": {
|
|
||||||
"homepage": "https://rustlings.cool",
|
|
||||||
"repository": "https://github.com/rust-lang/rustlings"
|
|
||||||
},
|
|
||||||
"marketing": {
|
|
||||||
"analytics": {
|
|
||||||
"plausible": {
|
|
||||||
"domain": "rustlings.cool"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -3,7 +3,14 @@
|
|||||||
# Error out if any command fails
|
# Error out if any command fails
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
cargo run -- dev check
|
|
||||||
typos
|
typos
|
||||||
cargo outdated -w --exit-code 1
|
cargo upgrades
|
||||||
cargo test --workspace --all-targets
|
|
||||||
|
# Similar to CI
|
||||||
|
cargo clippy -- --deny warnings
|
||||||
|
cargo fmt --all --check
|
||||||
|
cargo test --workspace
|
||||||
|
cargo dev check --require-solutions
|
||||||
|
|
||||||
|
# MSRV
|
||||||
|
cargo +1.88 dev check --require-solutions
|
||||||
|
|||||||
@ -6,6 +6,7 @@ authors.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
include = [
|
include = [
|
||||||
"/src/",
|
"/src/",
|
||||||
"/info.toml",
|
"/info.toml",
|
||||||
@ -15,6 +16,9 @@ include = [
|
|||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
quote = "1.0.36"
|
quote = "1.0"
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
toml_edit.workspace = true
|
toml.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
format_version = 1
|
format_version = 1
|
||||||
|
|
||||||
welcome_message = """Is this your first time? Don't worry, Rustlings is made for beginners!
|
welcome_message = """
|
||||||
|
Is this your first time? Don't worry, Rustlings is made for beginners!
|
||||||
We are going to teach you a lot of things about Rust, but before we can
|
We are going to teach you a lot of things about Rust, but before we can
|
||||||
get started, here are some notes about how Rustlings operates:
|
get started, here are some notes about how Rustlings operates:
|
||||||
|
|
||||||
@ -10,15 +11,16 @@ get started, here are some notes about how Rustlings operates:
|
|||||||
and fix them!
|
and fix them!
|
||||||
2. Make sure to have your editor open in the `rustlings/` directory. Rustlings
|
2. Make sure to have your editor open in the `rustlings/` directory. Rustlings
|
||||||
will show you the path of the current exercise under the progress bar. Open
|
will show you the path of the current exercise under the progress bar. Open
|
||||||
the exercise file in your editor, fix errors and save the file. Rustlings will
|
the exercise file in your editor, fix errors and save the file. Rustlings
|
||||||
automatically detect the file change and rerun the exercise. If all errors are
|
will automatically detect the file change and rerun the exercise. If all
|
||||||
fixed, Rustlings will ask you to move on to the next exercise.
|
errors are fixed, Rustlings will ask you to move on to the next exercise.
|
||||||
3. If you're stuck on an exercise, enter `h` to show a hint.
|
3. If you're stuck on an exercise, enter `h` to show a hint.
|
||||||
4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub!
|
4. If an exercise doesn't make sense to you, feel free to open an issue on
|
||||||
(https://github.com/rust-lang/rustlings). We look at every issue, and sometimes,
|
GitHub! (https://github.com/rust-lang/rustlings). We look at every issue, and
|
||||||
other learners do too so you can help each other out!"""
|
sometimes, other learners do too so you can help each other out!"""
|
||||||
|
|
||||||
final_message = """We hope you enjoyed learning about the various aspects of Rust!
|
final_message = """
|
||||||
|
We hope you enjoyed learning about the various aspects of Rust!
|
||||||
If you noticed any issues, don't hesitate to report them on Github.
|
If you noticed any issues, don't hesitate to report them on Github.
|
||||||
You can also contribute your own exercises to help the greater community!
|
You can also contribute your own exercises to help the greater community!
|
||||||
|
|
||||||
@ -120,10 +122,10 @@ dir = "01_variables"
|
|||||||
test = false
|
test = false
|
||||||
hint = """
|
hint = """
|
||||||
We know about variables and mutability, but there is another important type of
|
We know about variables and mutability, but there is another important type of
|
||||||
variables available: constants.
|
variable available: constants.
|
||||||
|
|
||||||
Constants are always immutable. They are declared with the keyword `const` instead
|
Constants are always immutable. They are declared with the keyword `const`
|
||||||
of `let`.
|
instead of `let`.
|
||||||
|
|
||||||
The type of Constants must always be annotated.
|
The type of Constants must always be annotated.
|
||||||
|
|
||||||
@ -253,7 +255,7 @@ require you to type in 100 items (but you certainly can if you want!).
|
|||||||
|
|
||||||
For example, you can do:
|
For example, you can do:
|
||||||
```
|
```
|
||||||
let array = ["Are we there yet?"; 10];
|
let array = ["Are we there yet?"; 100];
|
||||||
```
|
```
|
||||||
|
|
||||||
Bonus: what are some other things you could have that would return `true`
|
Bonus: what are some other things you could have that would return `true`
|
||||||
@ -316,15 +318,7 @@ of the Rust book to learn more."""
|
|||||||
name = "vecs2"
|
name = "vecs2"
|
||||||
dir = "05_vecs"
|
dir = "05_vecs"
|
||||||
hint = """
|
hint = """
|
||||||
In the first function, we create an empty vector and want to push new elements
|
Use the `.push()` method on the vector to push new elements to it."""
|
||||||
to it.
|
|
||||||
|
|
||||||
In the second function, we map the values of the input and collect them into a vector.
|
|
||||||
|
|
||||||
After you've completed both functions, decide for yourself which approach you
|
|
||||||
like better.
|
|
||||||
|
|
||||||
What do you think is the more commonly used pattern under Rust developers?"""
|
|
||||||
|
|
||||||
# MOVE SEMANTICS
|
# MOVE SEMANTICS
|
||||||
|
|
||||||
@ -332,8 +326,8 @@ What do you think is the more commonly used pattern under Rust developers?"""
|
|||||||
name = "move_semantics1"
|
name = "move_semantics1"
|
||||||
dir = "06_move_semantics"
|
dir = "06_move_semantics"
|
||||||
hint = """
|
hint = """
|
||||||
So you've got the "cannot borrow `vec` as mutable, as it is not declared as mutable"
|
So you've got the "cannot borrow `vec` as mutable, as it is not declared as
|
||||||
error on the line where we push an element to the vector, right?
|
mutable" error on the line where we push an element to the vector, right?
|
||||||
|
|
||||||
The fix for this is going to be adding one keyword, and the addition is NOT on
|
The fix for this is going to be adding one keyword, and the addition is NOT on
|
||||||
the line where we push to the vector (where the error is).
|
the line where we push to the vector (where the error is).
|
||||||
@ -369,7 +363,8 @@ hint = """
|
|||||||
Carefully reason about the range in which each mutable reference is in
|
Carefully reason about the range in which each mutable reference is in
|
||||||
scope. Does it help to update the value of `x` immediately after
|
scope. Does it help to update the value of `x` immediately after
|
||||||
the mutable reference is taken?
|
the mutable reference is taken?
|
||||||
Read more about 'Mutable References' in the book's section 'References and Borrowing':
|
Read more about 'Mutable References' in the book's section 'References and
|
||||||
|
Borrowing':
|
||||||
https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-references."""
|
https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-references."""
|
||||||
|
|
||||||
[[exercises]]
|
[[exercises]]
|
||||||
@ -498,14 +493,18 @@ some of them:
|
|||||||
https://doc.rust-lang.org/std/string/struct.String.html#method.trim
|
https://doc.rust-lang.org/std/string/struct.String.html#method.trim
|
||||||
|
|
||||||
For the `compose_me` method: You can either use the `format!` macro, or convert
|
For the `compose_me` method: You can either use the `format!` macro, or convert
|
||||||
the string slice into an owned string, which you can then freely extend."""
|
the string slice into an owned string, which you can then freely extend.
|
||||||
|
|
||||||
|
For the `replace_me` method, you can check out the `replace` method:
|
||||||
|
https://doc.rust-lang.org/std/string/struct.String.html#method.replace"""
|
||||||
|
|
||||||
[[exercises]]
|
[[exercises]]
|
||||||
name = "strings4"
|
name = "strings4"
|
||||||
dir = "09_strings"
|
dir = "09_strings"
|
||||||
test = false
|
test = false
|
||||||
hint = """
|
hint = """
|
||||||
Replace `placeholder` with either `string` or `string_slice` in the `main` function.
|
Replace `placeholder` with either `string` or `string_slice` in the `main`
|
||||||
|
function.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
`placeholder("blue");`
|
`placeholder("blue");`
|
||||||
@ -567,12 +566,8 @@ https://doc.rust-lang.org/book/ch08-03-hash-maps.html#only-inserting-a-value-if-
|
|||||||
name = "hashmaps3"
|
name = "hashmaps3"
|
||||||
dir = "11_hashmaps"
|
dir = "11_hashmaps"
|
||||||
hint = """
|
hint = """
|
||||||
Hint 1: Use the `entry()` and `or_insert()` (or `or_insert_with()`) methods of
|
Hint 1: Use the `entry()` and `or_default()` methods of `HashMap` to insert the
|
||||||
`HashMap` to insert the default value of `Team` if a team doesn't
|
default value of `TeamScores` if a team doesn't exist in the table yet.
|
||||||
exist in the table yet.
|
|
||||||
|
|
||||||
Learn more in The Book:
|
|
||||||
https://doc.rust-lang.org/book/ch08-03-hash-maps.html#only-inserting-a-value-if-the-key-has-no-value
|
|
||||||
|
|
||||||
Hint 2: If there is already an entry for a given key, the value returned by
|
Hint 2: If there is already an entry for a given key, the value returned by
|
||||||
`entry()` can be updated based on the existing value.
|
`entry()` can be updated based on the existing value.
|
||||||
@ -760,7 +755,7 @@ Notice how the trait takes ownership of `self` and returns `Self`.
|
|||||||
|
|
||||||
Although the signature of `append_bar` in the trait takes `self` as argument,
|
Although the signature of `append_bar` in the trait takes `self` as argument,
|
||||||
the implementation can take `mut self` instead. This is possible because the
|
the implementation can take `mut self` instead. This is possible because the
|
||||||
the value is owned anyway."""
|
value is owned anyway."""
|
||||||
|
|
||||||
[[exercises]]
|
[[exercises]]
|
||||||
name = "traits3"
|
name = "traits3"
|
||||||
@ -1136,7 +1131,7 @@ constants, but clippy recognizes those imprecise mathematical constants as a
|
|||||||
source of potential error.
|
source of potential error.
|
||||||
|
|
||||||
See the suggestions of the Clippy warning in the compile output and use the
|
See the suggestions of the Clippy warning in the compile output and use the
|
||||||
appropriate replacement constant from `std::f32::consts`..."""
|
appropriate replacement constant from `std::f32::consts`."""
|
||||||
|
|
||||||
[[exercises]]
|
[[exercises]]
|
||||||
name = "clippy2"
|
name = "clippy2"
|
||||||
@ -1197,7 +1192,8 @@ hint = """
|
|||||||
Is there an implementation of `TryFrom` in the standard library that can both do
|
Is there an implementation of `TryFrom` in the standard library that can both do
|
||||||
the required integer conversion and check the range of the input?
|
the required integer conversion and check the range of the input?
|
||||||
|
|
||||||
Challenge: Can you make the `TryFrom` implementations generic over many integer types?"""
|
Challenge: Can you make the `TryFrom` implementations generic over many integer
|
||||||
|
types?"""
|
||||||
|
|
||||||
[[exercises]]
|
[[exercises]]
|
||||||
name = "as_ref_mut"
|
name = "as_ref_mut"
|
||||||
|
|||||||
@ -16,7 +16,7 @@ struct InfoFile {
|
|||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
pub fn include_files(_: TokenStream) -> TokenStream {
|
pub fn include_files(_: TokenStream) -> TokenStream {
|
||||||
let info_file = include_str!("../info.toml");
|
let info_file = include_str!("../info.toml");
|
||||||
let exercises = toml_edit::de::from_str::<InfoFile>(info_file)
|
let exercises = toml::de::from_str::<InfoFile>(info_file)
|
||||||
.expect("Failed to parse `info.toml`")
|
.expect("Failed to parse `info.toml`")
|
||||||
.exercises;
|
.exercises;
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
let number = "T-H-R-E-E";
|
let number = "T-H-R-E-E";
|
||||||
println!("Spell a number: {}", number);
|
println!("Spell a number: {number}");
|
||||||
|
|
||||||
// Using variable shadowing
|
// Using variable shadowing
|
||||||
// https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#shadowing
|
// https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#shadowing
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
fn call_me(num: u32) {
|
fn call_me(num: u8) {
|
||||||
for i in 0..num {
|
for i in 0..num {
|
||||||
println!("Ring! Call number {}", i + 1);
|
println!("Ring! Call number {}", i + 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,5 @@
|
|||||||
fn bigger(a: i32, b: i32) -> i32 {
|
fn bigger(a: i32, b: i32) -> i32 {
|
||||||
if a > b {
|
if a > b { a } else { b }
|
||||||
a
|
|
||||||
} else {
|
|
||||||
b
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
fn foo_if_fizz(fizzish: &str) -> &str {
|
fn picky_eater(food: &str) -> &str {
|
||||||
if fizzish == "fizz" {
|
if food == "strawberry" {
|
||||||
"foo"
|
"Yummy!"
|
||||||
} else if fizzish == "fuzz" {
|
} else if food == "potato" {
|
||||||
"bar"
|
"I guess I can eat that."
|
||||||
} else {
|
} else {
|
||||||
"baz"
|
"No thanks!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,17 +17,19 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn foo_for_fizz() {
|
fn yummy_food() {
|
||||||
assert_eq!(foo_if_fizz("fizz"), "foo");
|
assert_eq!(picky_eater("strawberry"), "Yummy!");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bar_for_fuzz() {
|
fn neutral_food() {
|
||||||
assert_eq!(foo_if_fizz("fuzz"), "bar");
|
assert_eq!(picky_eater("potato"), "I guess I can eat that.");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn default_to_baz() {
|
fn default_disliked_food() {
|
||||||
assert_eq!(foo_if_fizz("literally anything"), "baz");
|
assert_eq!(picky_eater("broccoli"), "No thanks!");
|
||||||
|
assert_eq!(picky_eater("gummy bears"), "No thanks!");
|
||||||
|
assert_eq!(picky_eater("literally anything"), "No thanks!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,22 +8,6 @@ fn vec_loop(input: &[i32]) -> Vec<i32> {
|
|||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
fn vec_map_example(input: &[i32]) -> Vec<i32> {
|
|
||||||
// An example of collecting a vector after mapping.
|
|
||||||
// We map each element of the `input` slice to its value plus 1.
|
|
||||||
// If the input is `[1, 2, 3]`, the output is `[2, 3, 4]`.
|
|
||||||
input.iter().map(|element| element + 1).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn vec_map(input: &[i32]) -> Vec<i32> {
|
|
||||||
// We will dive deeper into iterators, but for now, this is all what you
|
|
||||||
// had to do!
|
|
||||||
// Advanced note: This method is more efficient because it automatically
|
|
||||||
// preallocates enough capacity. This can be done manually in `vec_loop`
|
|
||||||
// using `Vec::with_capacity(input.len())` instead of `Vec::new()`.
|
|
||||||
input.iter().map(|element| 2 * element).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// You can optionally experiment here.
|
// You can optionally experiment here.
|
||||||
}
|
}
|
||||||
@ -38,18 +22,4 @@ mod tests {
|
|||||||
let ans = vec_loop(&input);
|
let ans = vec_loop(&input);
|
||||||
assert_eq!(ans, [4, 8, 12, 16, 20]);
|
assert_eq!(ans, [4, 8, 12, 16, 20]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_vec_map_example() {
|
|
||||||
let input = [1, 2, 3];
|
|
||||||
let ans = vec_map_example(&input);
|
|
||||||
assert_eq!(ans, [2, 3, 4]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_vec_map() {
|
|
||||||
let input = [2, 4, 6, 8, 10];
|
|
||||||
let ans = vec_map(&input);
|
|
||||||
assert_eq!(ans, [4, 8, 12, 16, 20]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,8 +4,6 @@ fn main() {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
// TODO: Fix the compiler errors only by reordering the lines in the test.
|
|
||||||
// Don't add, change or remove any line.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn move_semantics4() {
|
fn move_semantics4() {
|
||||||
let mut x = Vec::new();
|
let mut x = Vec::new();
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Point {
|
struct Point {
|
||||||
x: u64,
|
x: u64,
|
||||||
|
|||||||
@ -46,8 +46,8 @@ impl State {
|
|||||||
match message {
|
match message {
|
||||||
Message::Resize { width, height } => self.resize(width, height),
|
Message::Resize { width, height } => self.resize(width, height),
|
||||||
Message::Move(point) => self.move_position(point),
|
Message::Move(point) => self.move_position(point),
|
||||||
Message::Echo(s) => self.echo(s),
|
Message::Echo(string) => self.echo(string),
|
||||||
Message::ChangeColor(r, g, b) => self.change_color(r, g, b),
|
Message::ChangeColor(red, green, blue) => self.change_color(red, green, blue),
|
||||||
Message::Quit => self.quit(),
|
Message::Quit => self.quit(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,7 @@ mod tests {
|
|||||||
assert_eq!(trim_me("Hello! "), "Hello!");
|
assert_eq!(trim_me("Hello! "), "Hello!");
|
||||||
assert_eq!(trim_me(" What's up!"), "What's up!");
|
assert_eq!(trim_me(" What's up!"), "What's up!");
|
||||||
assert_eq!(trim_me(" Hola! "), "Hola!");
|
assert_eq!(trim_me(" Hola! "), "Hola!");
|
||||||
|
assert_eq!(trim_me("Hi!"), "Hi!");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -18,12 +18,11 @@ fn main() {
|
|||||||
// Here, both answers work.
|
// Here, both answers work.
|
||||||
// `.into()` converts a type into an expected type.
|
// `.into()` converts a type into an expected type.
|
||||||
// If it is called where `String` is expected, it will convert `&str` to `String`.
|
// If it is called where `String` is expected, it will convert `&str` to `String`.
|
||||||
// But if is called where `&str` is expected, then `&str` is kept `&str` since no
|
|
||||||
// conversion is needed.
|
|
||||||
string("nice weather".into());
|
string("nice weather".into());
|
||||||
|
// But if it is called where `&str` is expected, then `&str` is kept as `&str` since no conversion is needed.
|
||||||
|
// If you remove the `#[allow(…)]` line, then Clippy will tell you to remove `.into()` below since it is a useless conversion.
|
||||||
|
#[allow(clippy::useless_conversion)]
|
||||||
string_slice("nice weather".into());
|
string_slice("nice weather".into());
|
||||||
// ^^^^^^^ the compiler recommends removing the `.into()`
|
|
||||||
// call because it is a useless conversion.
|
|
||||||
|
|
||||||
string(format!("Interpolation {}", "Station"));
|
string(format!("Interpolation {}", "Station"));
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
#[allow(dead_code)]
|
|
||||||
mod delicious_snacks {
|
mod delicious_snacks {
|
||||||
// Added `pub` and used the expected alias after `as`.
|
// Added `pub` and used the expected alias after `as`.
|
||||||
pub use self::fruits::PEAR as fruit;
|
pub use self::fruits::PEAR as fruit;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// A basket of fruits in the form of a hash map needs to be defined. The key
|
// A basket of fruits in the form of a hash map needs to be defined. The key
|
||||||
// represents the name of the fruit and the value represents how many of that
|
// represents the name of the fruit and the value represents how many of that
|
||||||
// particular fruit is in the basket. You have to put at least 3 different
|
// particular fruit is in the basket. You have to put at least 3 different
|
||||||
// types of fruits (e.g apple, banana, mango) in the basket and the total count
|
// types of fruits (e.g. apple, banana, mango) in the basket and the total count
|
||||||
// of all the fruits should be at least 5.
|
// of all the fruits should be at least 5.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|||||||
@ -5,7 +5,8 @@
|
|||||||
// Apple (4), Mango (2) and Lychee (5) are already in the basket hash map. You
|
// Apple (4), Mango (2) and Lychee (5) are already in the basket hash map. You
|
||||||
// must add fruit to the basket so that there is at least one of each kind and
|
// must add fruit to the basket so that there is at least one of each kind and
|
||||||
// more than 11 in total - we have a lot of mouths to feed. You are not allowed
|
// more than 11 in total - we have a lot of mouths to feed. You are not allowed
|
||||||
// to insert any more of these fruits!
|
// to insert any more of the fruits that are already in the basket (Apple,
|
||||||
|
// Mango, and Lychee).
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
|||||||
@ -10,14 +10,14 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
// A structure to store the goal details of a team.
|
// A structure to store the goal details of a team.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct Team {
|
struct TeamScores {
|
||||||
goals_scored: u8,
|
goals_scored: u8,
|
||||||
goals_conceded: u8,
|
goals_conceded: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_scores_table(results: &str) -> HashMap<&str, Team> {
|
fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
|
||||||
// The name of the team is the key and its associated struct is the value.
|
// The name of the team is the key and its associated struct is the value.
|
||||||
let mut scores = HashMap::new();
|
let mut scores = HashMap::<&str, TeamScores>::new();
|
||||||
|
|
||||||
for line in results.lines() {
|
for line in results.lines() {
|
||||||
let mut split_iterator = line.split(',');
|
let mut split_iterator = line.split(',');
|
||||||
@ -28,13 +28,13 @@ fn build_scores_table(results: &str) -> HashMap<&str, Team> {
|
|||||||
let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap();
|
let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap();
|
||||||
|
|
||||||
// Insert the default with zeros if a team doesn't exist yet.
|
// Insert the default with zeros if a team doesn't exist yet.
|
||||||
let team_1 = scores.entry(team_1_name).or_insert_with(Team::default);
|
let team_1 = scores.entry(team_1_name).or_default();
|
||||||
// Update the values.
|
// Update the values.
|
||||||
team_1.goals_scored += team_1_score;
|
team_1.goals_scored += team_1_score;
|
||||||
team_1.goals_conceded += team_2_score;
|
team_1.goals_conceded += team_2_score;
|
||||||
|
|
||||||
// Similarely for the second team.
|
// Similarly for the second team.
|
||||||
let team_2 = scores.entry(team_2_name).or_insert_with(Team::default);
|
let team_2 = scores.entry(team_2_name).or_default();
|
||||||
team_2.goals_scored += team_2_score;
|
team_2.goals_scored += team_2_score;
|
||||||
team_2.goals_conceded += team_1_score;
|
team_2.goals_conceded += team_1_score;
|
||||||
}
|
}
|
||||||
@ -60,9 +60,11 @@ England,Spain,1,0";
|
|||||||
fn build_scores() {
|
fn build_scores() {
|
||||||
let scores = build_scores_table(RESULTS);
|
let scores = build_scores_table(RESULTS);
|
||||||
|
|
||||||
assert!(["England", "France", "Germany", "Italy", "Poland", "Spain"]
|
assert!(
|
||||||
.into_iter()
|
["England", "France", "Germany", "Italy", "Poland", "Spain"]
|
||||||
.all(|team_name| scores.contains_key(team_name)));
|
.into_iter()
|
||||||
|
.all(|team_name| scores.contains_key(team_name))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
// This function returns how much icecream there is left in the fridge.
|
// This function returns how much ice cream there is left in the fridge.
|
||||||
// If it's before 22:00 (24-hour system), then 5 scoops are left. At 22:00,
|
// If it's before 22:00 (24-hour system), then 5 scoops are left. At 22:00,
|
||||||
// someone eats it all, so no icecream is left (value 0). Return `None` if
|
// someone eats it all, so no ice cream is left (value 0). Return `None` if
|
||||||
// `hour_of_day` is higher than 23.
|
// `hour_of_day` is higher than 23.
|
||||||
fn maybe_icecream(hour_of_day: u16) -> Option<u16> {
|
fn maybe_ice_cream(hour_of_day: u16) -> Option<u16> {
|
||||||
match hour_of_day {
|
match hour_of_day {
|
||||||
0..=21 => Some(5),
|
0..=21 => Some(5),
|
||||||
22..=23 => Some(0),
|
22..=23 => Some(0),
|
||||||
@ -21,19 +21,19 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn raw_value() {
|
fn raw_value() {
|
||||||
// Using `unwrap` is fine in a test.
|
// Using `unwrap` is fine in a test.
|
||||||
let icecreams = maybe_icecream(12).unwrap();
|
let ice_creams = maybe_ice_cream(12).unwrap();
|
||||||
|
|
||||||
assert_eq!(icecreams, 5);
|
assert_eq!(ice_creams, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_icecream() {
|
fn check_ice_cream() {
|
||||||
assert_eq!(maybe_icecream(0), Some(5));
|
assert_eq!(maybe_ice_cream(0), Some(5));
|
||||||
assert_eq!(maybe_icecream(9), Some(5));
|
assert_eq!(maybe_ice_cream(9), Some(5));
|
||||||
assert_eq!(maybe_icecream(18), Some(5));
|
assert_eq!(maybe_ice_cream(18), Some(5));
|
||||||
assert_eq!(maybe_icecream(22), Some(0));
|
assert_eq!(maybe_ice_cream(22), Some(0));
|
||||||
assert_eq!(maybe_icecream(23), Some(0));
|
assert_eq!(maybe_ice_cream(23), Some(0));
|
||||||
assert_eq!(maybe_icecream(24), None);
|
assert_eq!(maybe_ice_cream(24), None);
|
||||||
assert_eq!(maybe_icecream(25), None);
|
assert_eq!(maybe_ice_cream(25), None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ fn main() {
|
|||||||
// Solution 1: Matching over the `Option` (not `&Option`) but without moving
|
// Solution 1: Matching over the `Option` (not `&Option`) but without moving
|
||||||
// out of the `Some` variant.
|
// out of the `Some` variant.
|
||||||
match optional_point {
|
match optional_point {
|
||||||
Some(ref p) => println!("Co-ordinates are {},{}", p.x, p.y),
|
Some(ref p) => println!("Coordinates are {},{}", p.x, p.y),
|
||||||
// ^^^ added
|
// ^^^ added
|
||||||
_ => panic!("No match!"),
|
_ => panic!("No match!"),
|
||||||
}
|
}
|
||||||
@ -18,7 +18,8 @@ fn main() {
|
|||||||
// Solution 2: Matching over a reference (`&Option`) by added `&` before
|
// Solution 2: Matching over a reference (`&Option`) by added `&` before
|
||||||
// `optional_point`.
|
// `optional_point`.
|
||||||
match &optional_point {
|
match &optional_point {
|
||||||
Some(p) => println!("Co-ordinates are {},{}", p.x, p.y),
|
//^ added
|
||||||
|
Some(p) => println!("Coordinates are {},{}", p.x, p.y),
|
||||||
_ => panic!("No match!"),
|
_ => panic!("No match!"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
use std::num::ParseIntError;
|
use std::num::ParseIntError;
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables, clippy::question_mark)]
|
||||||
fn total_cost(item_quantity: &str) -> Result<i32, ParseIntError> {
|
fn total_cost(item_quantity: &str) -> Result<i32, ParseIntError> {
|
||||||
let processing_fee = 1;
|
let processing_fee = 1;
|
||||||
let cost_per_item = 5;
|
let cost_per_item = 5;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
#![allow(clippy::comparison_chain)]
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
enum CreationError {
|
enum CreationError {
|
||||||
@ -11,12 +11,10 @@ struct PositiveNonzeroInteger(u64);
|
|||||||
|
|
||||||
impl PositiveNonzeroInteger {
|
impl PositiveNonzeroInteger {
|
||||||
fn new(value: i64) -> Result<Self, CreationError> {
|
fn new(value: i64) -> Result<Self, CreationError> {
|
||||||
if value == 0 {
|
match value.cmp(&0) {
|
||||||
Err(CreationError::Zero)
|
Ordering::Less => Err(CreationError::Negative),
|
||||||
} else if value < 0 {
|
Ordering::Equal => Err(CreationError::Zero),
|
||||||
Err(CreationError::Negative)
|
Ordering::Greater => Ok(Self(value as u64)),
|
||||||
} else {
|
|
||||||
Ok(Self(value as u64))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
//
|
//
|
||||||
// In short, this particular use case for boxes is for when you want to own a
|
// In short, this particular use case for boxes is for when you want to own a
|
||||||
// value and you care only that it is a type which implements a particular
|
// value and you care only that it is a type which implements a particular
|
||||||
// trait. To do so, The `Box` is declared as of type `Box<dyn Trait>` where
|
// trait. To do so, the `Box` is declared as of type `Box<dyn Trait>` where
|
||||||
// `Trait` is the trait the compiler looks for on any value used in that
|
// `Trait` is the trait the compiler looks for on any value used in that
|
||||||
// context. For this exercise, that context is the potential errors which
|
// context. For this exercise, that context is the potential errors which
|
||||||
// can be returned in a `Result`.
|
// can be returned in a `Result`.
|
||||||
|
|||||||
@ -24,11 +24,26 @@ impl ParsePosNonzeroError {
|
|||||||
Self::Creation(err)
|
Self::Creation(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_parseint(err: ParseIntError) -> Self {
|
fn from_parse_int(err: ParseIntError) -> Self {
|
||||||
Self::ParseInt(err)
|
Self::ParseInt(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// As an alternative solution, implementing the `From` trait allows for the
|
||||||
|
// automatic conversion from a `ParseIntError` into a `ParsePosNonzeroError`
|
||||||
|
// using the `?` operator, without the need to call `map_err`.
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// let x: i64 = s.parse()?;
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Traits like `From` will be dealt with in later exercises.
|
||||||
|
impl From<ParseIntError> for ParsePosNonzeroError {
|
||||||
|
fn from(err: ParseIntError) -> Self {
|
||||||
|
ParsePosNonzeroError::ParseInt(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
struct PositiveNonzeroInteger(u64);
|
struct PositiveNonzeroInteger(u64);
|
||||||
|
|
||||||
@ -44,7 +59,7 @@ impl PositiveNonzeroInteger {
|
|||||||
fn parse(s: &str) -> Result<Self, ParsePosNonzeroError> {
|
fn parse(s: &str) -> Result<Self, ParsePosNonzeroError> {
|
||||||
// Return an appropriate error instead of panicking when `parse()`
|
// Return an appropriate error instead of panicking when `parse()`
|
||||||
// returns an error.
|
// returns an error.
|
||||||
let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parseint)?;
|
let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parse_int)?;
|
||||||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
Self::new(x).map_err(ParsePosNonzeroError::from_creation)
|
Self::new(x).map_err(ParsePosNonzeroError::from_creation)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
trait Licensed {
|
trait Licensed {
|
||||||
fn licensing_info(&self) -> String {
|
fn licensing_info(&self) -> String {
|
||||||
"Default license".to_string()
|
"Default license".to_string()
|
||||||
|
|||||||
@ -5,11 +5,7 @@
|
|||||||
|
|
||||||
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
|
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
|
||||||
// ^^^^ ^^ ^^ ^^
|
// ^^^^ ^^ ^^ ^^
|
||||||
if x.len() > y.len() {
|
if x.len() > y.len() { x } else { y }
|
||||||
x
|
|
||||||
} else {
|
|
||||||
y
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|||||||
@ -1,14 +1,10 @@
|
|||||||
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
|
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
|
||||||
if x.len() > y.len() {
|
if x.len() > y.len() { x } else { y }
|
||||||
x
|
|
||||||
} else {
|
|
||||||
y
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let string1 = String::from("long string is long");
|
let string1 = String::from("long string is long");
|
||||||
// Solution1: You can move `strings2` out of the inner block so that it is
|
// Solution 1: You can move `strings2` out of the inner block so that it is
|
||||||
// not dropped before the print statement.
|
// not dropped before the print statement.
|
||||||
let string2 = String::from("xyz");
|
let string2 = String::from("xyz");
|
||||||
let result;
|
let result;
|
||||||
@ -25,7 +21,7 @@ fn main() {
|
|||||||
{
|
{
|
||||||
let string2 = String::from("xyz");
|
let string2 = String::from("xyz");
|
||||||
result = longest(&string1, &string2);
|
result = longest(&string1, &string2);
|
||||||
// Solution2: You can move the print statement into the inner block so
|
// Solution 2: You can move the print statement into the inner block so
|
||||||
// that it is executed before `string2` is dropped.
|
// that it is executed before `string2` is dropped.
|
||||||
println!("The longest string is '{result}'");
|
println!("The longest string is '{result}'");
|
||||||
// `string2` dropped here (end of the inner scope).
|
// `string2` dropped here (end of the inner scope).
|
||||||
|
|||||||
@ -9,7 +9,7 @@ impl Rectangle {
|
|||||||
if width <= 0 || height <= 0 {
|
if width <= 0 || height <= 0 {
|
||||||
// Returning a `Result` would be better here. But we want to learn
|
// Returning a `Result` would be better here. But we want to learn
|
||||||
// how to test functions that can panic.
|
// how to test functions that can panic.
|
||||||
panic!("Rectangle width and height can't be negative");
|
panic!("Rectangle width and height must be positive");
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle { width, height }
|
Rectangle { width, height }
|
||||||
|
|||||||
@ -52,6 +52,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_success() {
|
fn test_success() {
|
||||||
assert_eq!(divide(81, 9), Ok(9));
|
assert_eq!(divide(81, 9), Ok(9));
|
||||||
|
assert_eq!(divide(81, -1), Ok(-81));
|
||||||
|
assert_eq!(divide(i64::MIN, i64::MIN), Ok(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -25,6 +25,7 @@ fn factorial_fold(num: u64) -> u64 {
|
|||||||
// -> 1 * 2 is calculated, then the result 2 is multiplied by
|
// -> 1 * 2 is calculated, then the result 2 is multiplied by
|
||||||
// the second element 3 so the result 6 is returned.
|
// the second element 3 so the result 6 is returned.
|
||||||
// And so on…
|
// And so on…
|
||||||
|
#[allow(clippy::unnecessary_fold)]
|
||||||
(2..=num).fold(1, |acc, x| acc * x)
|
(2..=num).fold(1, |acc, x| acc * x)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,6 @@ use std::rc::Rc;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Sun;
|
struct Sun;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum Planet {
|
enum Planet {
|
||||||
Mercury(Rc<Sun>),
|
Mercury(Rc<Sun>),
|
||||||
@ -64,12 +63,10 @@ mod tests {
|
|||||||
println!("reference count = {}", Rc::strong_count(&sun)); // 7 references
|
println!("reference count = {}", Rc::strong_count(&sun)); // 7 references
|
||||||
saturn.details();
|
saturn.details();
|
||||||
|
|
||||||
// TODO
|
|
||||||
let uranus = Planet::Uranus(Rc::clone(&sun));
|
let uranus = Planet::Uranus(Rc::clone(&sun));
|
||||||
println!("reference count = {}", Rc::strong_count(&sun)); // 8 references
|
println!("reference count = {}", Rc::strong_count(&sun)); // 8 references
|
||||||
uranus.details();
|
uranus.details();
|
||||||
|
|
||||||
// TODO
|
|
||||||
let neptune = Planet::Neptune(Rc::clone(&sun));
|
let neptune = Planet::Neptune(Rc::clone(&sun));
|
||||||
println!("reference count = {}", Rc::strong_count(&sun)); // 9 references
|
println!("reference count = {}", Rc::strong_count(&sun)); // 9 references
|
||||||
neptune.details();
|
neptune.details();
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// This program spawns multiple threads that each run for at least 250ms, and
|
// This program spawns multiple threads that each runs for at least 250ms, and
|
||||||
// each thread returns how much time they took to complete. The program should
|
// each thread returns how much time it took to complete. The program should
|
||||||
// wait until all the spawned threads have finished and should collect their
|
// wait until all the spawned threads have finished and should collect their
|
||||||
// return values into a vector.
|
// return values into a vector.
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
use std::{sync::mpsc, thread, time::Duration};
|
use std::{sync::mpsc, thread, time::Duration};
|
||||||
|
|
||||||
struct Queue {
|
struct Queue {
|
||||||
length: u32,
|
|
||||||
first_half: Vec<u32>,
|
first_half: Vec<u32>,
|
||||||
second_half: Vec<u32>,
|
second_half: Vec<u32>,
|
||||||
}
|
}
|
||||||
@ -9,7 +8,6 @@ struct Queue {
|
|||||||
impl Queue {
|
impl Queue {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
length: 10,
|
|
||||||
first_half: vec![1, 2, 3, 4, 5],
|
first_half: vec![1, 2, 3, 4, 5],
|
||||||
second_half: vec![6, 7, 8, 9, 10],
|
second_half: vec![6, 7, 8, 9, 10],
|
||||||
}
|
}
|
||||||
@ -50,17 +48,15 @@ mod tests {
|
|||||||
fn threads3() {
|
fn threads3() {
|
||||||
let (tx, rx) = mpsc::channel();
|
let (tx, rx) = mpsc::channel();
|
||||||
let queue = Queue::new();
|
let queue = Queue::new();
|
||||||
let queue_length = queue.length;
|
|
||||||
|
|
||||||
send_tx(queue, tx);
|
send_tx(queue, tx);
|
||||||
|
|
||||||
let mut total_received: u32 = 0;
|
let mut received = Vec::with_capacity(10);
|
||||||
for received in rx {
|
for value in rx {
|
||||||
println!("Got: {received}");
|
received.push(value);
|
||||||
total_received += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Number of received values: {total_received}");
|
received.sort();
|
||||||
assert_eq!(total_received, queue_length);
|
assert_eq!(received, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Added the attribute `macro_use` attribute.
|
// Added the `macro_use` attribute.
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod macros {
|
mod macros {
|
||||||
macro_rules! my_macro {
|
macro_rules! my_macro {
|
||||||
|
|||||||
@ -1,31 +1,31 @@
|
|||||||
use std::mem;
|
use std::mem;
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[allow(unused_variables, unused_assignments)]
|
#[allow(unused_variables, unused_assignments)]
|
||||||
fn main() {
|
fn main() {
|
||||||
let my_option: Option<()> = None;
|
let my_option: Option<&str> = None;
|
||||||
// `unwrap` of an `Option` after checking if it is `None` will panic.
|
// `unwrap` of an `Option` after checking if it is `None` will panic.
|
||||||
// Use `if-let` instead.
|
// Use `if-let` instead.
|
||||||
if let Some(value) = my_option {
|
if let Some(value) = my_option {
|
||||||
println!("{value:?}");
|
println!("{value}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// A comma was missing.
|
// A comma was missing.
|
||||||
|
#[rustfmt::skip]
|
||||||
let my_arr = &[
|
let my_arr = &[
|
||||||
-1, -2, -3,
|
-1, -2, -3,
|
||||||
-4, -5, -6,
|
-4, -5, -6,
|
||||||
];
|
];
|
||||||
println!("My array! Here it is: {:?}", my_arr);
|
println!("My array! Here it is: {my_arr:?}");
|
||||||
|
|
||||||
let mut my_empty_vec = vec![1, 2, 3, 4, 5];
|
let mut my_vec = vec![1, 2, 3, 4, 5];
|
||||||
// `resize` mutates a vector instead of returning a new one.
|
// `resize` mutates a vector instead of returning a new one.
|
||||||
// `resize(0, …)` clears a vector, so it is better to use `clear`.
|
// `resize(0, …)` clears a vector, so it is better to use `clear`.
|
||||||
my_empty_vec.clear();
|
my_vec.clear();
|
||||||
println!("This Vec is empty, see? {my_empty_vec:?}");
|
println!("This Vec is empty, see? {my_vec:?}");
|
||||||
|
|
||||||
let mut value_a = 45;
|
let mut value_a = 45;
|
||||||
let mut value_b = 66;
|
let mut value_b = 66;
|
||||||
// Use `mem::swap` to correctly swap two values.
|
// Use `mem::swap` to correctly swap two values.
|
||||||
mem::swap(&mut value_a, &mut value_b);
|
mem::swap(&mut value_a, &mut value_b);
|
||||||
println!("value a: {}; value b: {}", value_a, value_b);
|
println!("value a: {value_a}; value b: {value_b}");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,9 +2,10 @@
|
|||||||
// about them at https://doc.rust-lang.org/std/convert/trait.AsRef.html and
|
// about them at https://doc.rust-lang.org/std/convert/trait.AsRef.html and
|
||||||
// https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively.
|
// https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively.
|
||||||
|
|
||||||
// Obtain the number of bytes (not characters) in the given argument.
|
// Obtain the number of bytes (not characters) in the given argument
|
||||||
|
// (`.len()` returns the number of bytes in a string).
|
||||||
fn byte_counter<T: AsRef<str>>(arg: T) -> usize {
|
fn byte_counter<T: AsRef<str>>(arg: T) -> usize {
|
||||||
arg.as_ref().as_bytes().len()
|
arg.as_ref().len()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obtain the number of characters (not bytes) in the given argument.
|
// Obtain the number of characters (not bytes) in the given argument.
|
||||||
|
|||||||
6
solutions/README.md
Normal file
6
solutions/README.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# Official Rustlings solutions
|
||||||
|
|
||||||
|
Before you finish an exercise, its solution file will only contain an empty `main` function.
|
||||||
|
The content of this file will be automatically replaced by the actual solution once you finish the exercise.
|
||||||
|
|
||||||
|
Note that these solutions are often only _one possibility_ to solve an exercise.
|
||||||
@ -62,8 +62,8 @@ mod tests {
|
|||||||
// Import `transformer`.
|
// Import `transformer`.
|
||||||
use super::my_module::transformer;
|
use super::my_module::transformer;
|
||||||
|
|
||||||
use super::my_module::transformer_iter;
|
|
||||||
use super::Command;
|
use super::Command;
|
||||||
|
use super::my_module::transformer_iter;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_works() {
|
fn it_works() {
|
||||||
|
|||||||
515
src/app_state.rs
515
src/app_state.rs
@ -1,32 +1,39 @@
|
|||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{Context, Error, Result, bail};
|
||||||
use crossterm::style::Stylize;
|
use crossterm::{QueueableCommand, cursor, terminal};
|
||||||
use serde::Deserialize;
|
|
||||||
use std::{
|
use std::{
|
||||||
fs::{self, File},
|
collections::HashSet,
|
||||||
io::{Read, StdoutLock, Write},
|
env,
|
||||||
path::{Path, PathBuf},
|
fs::{File, OpenOptions},
|
||||||
|
io::{Read, Seek, StdoutLock, Write},
|
||||||
|
path::{MAIN_SEPARATOR_STR, Path},
|
||||||
process::{Command, Stdio},
|
process::{Command, Stdio},
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicUsize, Ordering::Relaxed},
|
||||||
|
mpsc,
|
||||||
|
},
|
||||||
|
thread,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
clear_terminal,
|
clear_terminal,
|
||||||
|
cmd::CmdRunner,
|
||||||
embedded::EMBEDDED_FILES,
|
embedded::EMBEDDED_FILES,
|
||||||
exercise::{Exercise, RunnableExercise, OUTPUT_CAPACITY},
|
exercise::{Exercise, RunnableExercise},
|
||||||
info_file::ExerciseInfo,
|
info_file::ExerciseInfo,
|
||||||
DEBUG_PROFILE,
|
term::{self, CheckProgressVisualizer},
|
||||||
};
|
};
|
||||||
|
|
||||||
const STATE_FILE_NAME: &str = ".rustlings-state.txt";
|
const STATE_FILE_NAME: &str = ".rustlings-state.txt";
|
||||||
const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
|
const DEFAULT_CHECK_PARALLELISM: usize = 8;
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub enum ExercisesProgress {
|
pub enum ExercisesProgress {
|
||||||
// All exercises are done.
|
// All exercises are done.
|
||||||
AllDone,
|
AllDone,
|
||||||
// The current exercise failed and is still pending.
|
|
||||||
CurrentPending,
|
|
||||||
// A new exercise is now pending.
|
// A new exercise is now pending.
|
||||||
NewPending,
|
NewPending,
|
||||||
|
// The current exercise is still pending.
|
||||||
|
CurrentPending,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum StateFileStatus {
|
pub enum StateFileStatus {
|
||||||
@ -34,29 +41,12 @@ pub enum StateFileStatus {
|
|||||||
NotRead,
|
NotRead,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parses parts of the output of `cargo metadata`.
|
#[derive(Clone, Copy)]
|
||||||
#[derive(Deserialize)]
|
pub enum CheckProgress {
|
||||||
struct CargoMetadata {
|
None,
|
||||||
target_directory: PathBuf,
|
Checking,
|
||||||
}
|
Done,
|
||||||
|
Pending,
|
||||||
pub fn parse_target_dir() -> Result<PathBuf> {
|
|
||||||
// Get the target directory from Cargo.
|
|
||||||
let metadata_output = Command::new("cargo")
|
|
||||||
.arg("metadata")
|
|
||||||
.arg("-q")
|
|
||||||
.arg("--format-version")
|
|
||||||
.arg("1")
|
|
||||||
.arg("--no-deps")
|
|
||||||
.stdin(Stdio::null())
|
|
||||||
.stderr(Stdio::inherit())
|
|
||||||
.output()
|
|
||||||
.context(CARGO_METADATA_ERR)?
|
|
||||||
.stdout;
|
|
||||||
|
|
||||||
serde_json::de::from_slice::<CargoMetadata>(&metadata_output)
|
|
||||||
.context("Failed to read the field `target_directory` from the `cargo metadata` output")
|
|
||||||
.map(|metadata| metadata.target_directory)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
@ -65,67 +55,32 @@ pub struct AppState {
|
|||||||
// Caches the number of done exercises to avoid iterating over all exercises every time.
|
// Caches the number of done exercises to avoid iterating over all exercises every time.
|
||||||
n_done: u16,
|
n_done: u16,
|
||||||
final_message: String,
|
final_message: String,
|
||||||
|
state_file: File,
|
||||||
// Preallocated buffer for reading and writing the state file.
|
// Preallocated buffer for reading and writing the state file.
|
||||||
file_buf: Vec<u8>,
|
file_buf: Vec<u8>,
|
||||||
official_exercises: bool,
|
official_exercises: bool,
|
||||||
// Cargo's target directory.
|
cmd_runner: CmdRunner,
|
||||||
target_dir: PathBuf,
|
emit_file_links: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
// Update the app state from the state file.
|
|
||||||
fn update_from_file(&mut self) -> StateFileStatus {
|
|
||||||
self.file_buf.clear();
|
|
||||||
self.n_done = 0;
|
|
||||||
|
|
||||||
if File::open(STATE_FILE_NAME)
|
|
||||||
.and_then(|mut file| file.read_to_end(&mut self.file_buf))
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
return StateFileStatus::NotRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
// See `Self::write` for more information about the file format.
|
|
||||||
let mut lines = self.file_buf.split(|c| *c == b'\n').skip(2);
|
|
||||||
|
|
||||||
let Some(current_exercise_name) = lines.next() else {
|
|
||||||
return StateFileStatus::NotRead;
|
|
||||||
};
|
|
||||||
|
|
||||||
if current_exercise_name.is_empty() || lines.next().is_none() {
|
|
||||||
return StateFileStatus::NotRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut done_exercises = hashbrown::HashSet::with_capacity(self.exercises.len());
|
|
||||||
|
|
||||||
for done_exerise_name in lines {
|
|
||||||
if done_exerise_name.is_empty() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
done_exercises.insert(done_exerise_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (ind, exercise) in self.exercises.iter_mut().enumerate() {
|
|
||||||
if done_exercises.contains(exercise.name.as_bytes()) {
|
|
||||||
exercise.done = true;
|
|
||||||
self.n_done += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if exercise.name.as_bytes() == current_exercise_name {
|
|
||||||
self.current_exercise_ind = ind;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StateFileStatus::Read
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
exercise_infos: Vec<ExerciseInfo>,
|
exercise_infos: Vec<ExerciseInfo>,
|
||||||
final_message: String,
|
final_message: String,
|
||||||
) -> Result<(Self, StateFileStatus)> {
|
) -> Result<(Self, StateFileStatus)> {
|
||||||
let target_dir = parse_target_dir()?;
|
let cmd_runner = CmdRunner::build()?;
|
||||||
|
let mut state_file = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.truncate(false)
|
||||||
|
.open(STATE_FILE_NAME)
|
||||||
|
.with_context(|| {
|
||||||
|
format!("Failed to open or create the state file {STATE_FILE_NAME}")
|
||||||
|
})?;
|
||||||
|
|
||||||
let exercises = exercise_infos
|
let dir_canonical_path = term::canonicalize("exercises");
|
||||||
|
let mut exercises = exercise_infos
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|exercise_info| {
|
.map(|exercise_info| {
|
||||||
// Leaking to be able to borrow in the watch mode `Table`.
|
// Leaking to be able to borrow in the watch mode `Table`.
|
||||||
@ -134,33 +89,100 @@ impl AppState {
|
|||||||
let path = exercise_info.path().leak();
|
let path = exercise_info.path().leak();
|
||||||
let name = exercise_info.name.leak();
|
let name = exercise_info.name.leak();
|
||||||
let dir = exercise_info.dir.map(|dir| &*dir.leak());
|
let dir = exercise_info.dir.map(|dir| &*dir.leak());
|
||||||
|
let hint = exercise_info.hint.leak().trim_ascii();
|
||||||
|
|
||||||
let hint = exercise_info.hint.trim().to_owned();
|
let canonical_path = dir_canonical_path.as_deref().map(|dir_canonical_path| {
|
||||||
|
let mut canonical_path;
|
||||||
|
if let Some(dir) = dir {
|
||||||
|
canonical_path = String::with_capacity(
|
||||||
|
2 + dir_canonical_path.len() + dir.len() + name.len(),
|
||||||
|
);
|
||||||
|
canonical_path.push_str(dir_canonical_path);
|
||||||
|
canonical_path.push_str(MAIN_SEPARATOR_STR);
|
||||||
|
canonical_path.push_str(dir);
|
||||||
|
} else {
|
||||||
|
canonical_path =
|
||||||
|
String::with_capacity(1 + dir_canonical_path.len() + name.len());
|
||||||
|
canonical_path.push_str(dir_canonical_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
canonical_path.push_str(MAIN_SEPARATOR_STR);
|
||||||
|
canonical_path.push_str(name);
|
||||||
|
canonical_path.push_str(".rs");
|
||||||
|
canonical_path
|
||||||
|
});
|
||||||
|
|
||||||
Exercise {
|
Exercise {
|
||||||
dir,
|
dir,
|
||||||
name,
|
name,
|
||||||
path,
|
path,
|
||||||
|
canonical_path,
|
||||||
test: exercise_info.test,
|
test: exercise_info.test,
|
||||||
strict_clippy: exercise_info.strict_clippy,
|
strict_clippy: exercise_info.strict_clippy,
|
||||||
hint,
|
hint,
|
||||||
// Updated in `Self::update_from_file`.
|
// Updated below.
|
||||||
done: false,
|
done: false,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let mut slf = Self {
|
let mut current_exercise_ind = 0;
|
||||||
current_exercise_ind: 0,
|
let mut n_done = 0;
|
||||||
exercises,
|
let mut file_buf = Vec::with_capacity(2048);
|
||||||
n_done: 0,
|
let state_file_status = 'block: {
|
||||||
final_message,
|
if state_file.read_to_end(&mut file_buf).is_err() {
|
||||||
file_buf: Vec::with_capacity(2048),
|
break 'block StateFileStatus::NotRead;
|
||||||
official_exercises: !Path::new("info.toml").exists(),
|
}
|
||||||
target_dir,
|
|
||||||
|
// See `Self::write` for more information about the file format.
|
||||||
|
let mut lines = file_buf.split(|c| *c == b'\n').skip(2);
|
||||||
|
|
||||||
|
let Some(current_exercise_name) = lines.next() else {
|
||||||
|
break 'block StateFileStatus::NotRead;
|
||||||
|
};
|
||||||
|
|
||||||
|
if current_exercise_name.is_empty() || lines.next().is_none() {
|
||||||
|
break 'block StateFileStatus::NotRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut done_exercises = HashSet::with_capacity(exercises.len());
|
||||||
|
|
||||||
|
for done_exercise_name in lines {
|
||||||
|
if done_exercise_name.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
done_exercises.insert(done_exercise_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ind, exercise) in exercises.iter_mut().enumerate() {
|
||||||
|
if done_exercises.contains(exercise.name.as_bytes()) {
|
||||||
|
exercise.done = true;
|
||||||
|
n_done += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if exercise.name.as_bytes() == current_exercise_name {
|
||||||
|
current_exercise_ind = ind;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StateFileStatus::Read
|
||||||
};
|
};
|
||||||
|
|
||||||
let state_file_status = slf.update_from_file();
|
file_buf.clear();
|
||||||
|
file_buf.extend_from_slice(STATE_FILE_HEADER);
|
||||||
|
|
||||||
|
let slf = Self {
|
||||||
|
current_exercise_ind,
|
||||||
|
exercises,
|
||||||
|
n_done,
|
||||||
|
final_message,
|
||||||
|
state_file,
|
||||||
|
file_buf,
|
||||||
|
official_exercises: !Path::new("info.toml").exists(),
|
||||||
|
cmd_runner,
|
||||||
|
// VS Code has its own file link handling
|
||||||
|
emit_file_links: env::var_os("TERM_PROGRAM").is_none_or(|v| v != "vscode"),
|
||||||
|
};
|
||||||
|
|
||||||
Ok((slf, state_file_status))
|
Ok((slf, state_file_status))
|
||||||
}
|
}
|
||||||
@ -180,14 +202,24 @@ impl AppState {
|
|||||||
self.n_done
|
self.n_done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn n_pending(&self) -> u16 {
|
||||||
|
self.exercises.len() as u16 - self.n_done
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn current_exercise(&self) -> &Exercise {
|
pub fn current_exercise(&self) -> &Exercise {
|
||||||
&self.exercises[self.current_exercise_ind]
|
&self.exercises[self.current_exercise_ind]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn target_dir(&self) -> &Path {
|
pub fn cmd_runner(&self) -> &CmdRunner {
|
||||||
&self.target_dir
|
&self.cmd_runner
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn emit_file_links(&self) -> bool {
|
||||||
|
self.emit_file_links
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the state file.
|
// Write the state file.
|
||||||
@ -199,10 +231,8 @@ impl AppState {
|
|||||||
// - The fourth line is an empty line.
|
// - The fourth line is an empty line.
|
||||||
// - All remaining lines are the names of done exercises.
|
// - All remaining lines are the names of done exercises.
|
||||||
fn write(&mut self) -> Result<()> {
|
fn write(&mut self) -> Result<()> {
|
||||||
self.file_buf.clear();
|
self.file_buf.truncate(STATE_FILE_HEADER.len());
|
||||||
|
|
||||||
self.file_buf
|
|
||||||
.extend_from_slice(b"DON'T EDIT THIS FILE!\n\n");
|
|
||||||
self.file_buf
|
self.file_buf
|
||||||
.extend_from_slice(self.current_exercise().name.as_bytes());
|
.extend_from_slice(self.current_exercise().name.as_bytes());
|
||||||
self.file_buf.push(b'\n');
|
self.file_buf.push(b'\n');
|
||||||
@ -214,7 +244,14 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::write(STATE_FILE_NAME, &self.file_buf)
|
self.state_file
|
||||||
|
.rewind()
|
||||||
|
.with_context(|| format!("Failed to rewind the state file {STATE_FILE_NAME}"))?;
|
||||||
|
self.state_file
|
||||||
|
.set_len(0)
|
||||||
|
.with_context(|| format!("Failed to truncate the state file {STATE_FILE_NAME}"))?;
|
||||||
|
self.state_file
|
||||||
|
.write_all(&self.file_buf)
|
||||||
.with_context(|| format!("Failed to write the state file {STATE_FILE_NAME}"))?;
|
.with_context(|| format!("Failed to write the state file {STATE_FILE_NAME}"))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -246,15 +283,31 @@ impl AppState {
|
|||||||
self.write()
|
self.write()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_pending(&mut self, exercise_ind: usize) -> Result<()> {
|
// Set the status of an exercise without saving. Returns `true` if the
|
||||||
|
// status actually changed (and thus needs saving later).
|
||||||
|
pub fn set_status(&mut self, exercise_ind: usize, done: bool) -> Result<bool> {
|
||||||
let exercise = self
|
let exercise = self
|
||||||
.exercises
|
.exercises
|
||||||
.get_mut(exercise_ind)
|
.get_mut(exercise_ind)
|
||||||
.context(BAD_INDEX_ERR)?;
|
.context(BAD_INDEX_ERR)?;
|
||||||
|
|
||||||
if exercise.done {
|
if exercise.done == done {
|
||||||
exercise.done = false;
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
exercise.done = done;
|
||||||
|
if done {
|
||||||
|
self.n_done += 1;
|
||||||
|
} else {
|
||||||
self.n_done -= 1;
|
self.n_done -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the status of an exercise to "pending" and save.
|
||||||
|
pub fn set_pending(&mut self, exercise_ind: usize) -> Result<()> {
|
||||||
|
if self.set_status(exercise_ind, false)? {
|
||||||
self.write()?;
|
self.write()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,7 +315,7 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Official exercises: Dump the original file from the binary.
|
// Official exercises: Dump the original file from the binary.
|
||||||
// Third-party exercises: Reset the exercise file with `git stash`.
|
// Community exercises: Reset the exercise file with `git stash`.
|
||||||
fn reset(&self, exercise_ind: usize, path: &str) -> Result<()> {
|
fn reset(&self, exercise_ind: usize, path: &str) -> Result<()> {
|
||||||
if self.official_exercises {
|
if self.official_exercises {
|
||||||
return EMBEDDED_FILES
|
return EMBEDDED_FILES
|
||||||
@ -298,6 +351,7 @@ impl AppState {
|
|||||||
Ok(exercise.path)
|
Ok(exercise.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset the exercise by index and return its name.
|
||||||
pub fn reset_exercise_by_ind(&mut self, exercise_ind: usize) -> Result<&'static str> {
|
pub fn reset_exercise_by_ind(&mut self, exercise_ind: usize) -> Result<&'static str> {
|
||||||
if exercise_ind >= self.exercises.len() {
|
if exercise_ind >= self.exercises.len() {
|
||||||
bail!(BAD_INDEX_ERR);
|
bail!(BAD_INDEX_ERR);
|
||||||
@ -307,36 +361,33 @@ impl AppState {
|
|||||||
let exercise = &self.exercises[exercise_ind];
|
let exercise = &self.exercises[exercise_ind];
|
||||||
self.reset(exercise_ind, exercise.path)?;
|
self.reset(exercise_ind, exercise.path)?;
|
||||||
|
|
||||||
Ok(exercise.path)
|
Ok(exercise.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the index of the next pending exercise or `None` if all exercises are done.
|
// Return the index of the next pending exercise or `None` if all exercises are done.
|
||||||
fn next_pending_exercise_ind(&self) -> Option<usize> {
|
fn next_pending_exercise_ind(&self) -> Option<usize> {
|
||||||
if self.current_exercise_ind == self.exercises.len() - 1 {
|
let next_ind = self.current_exercise_ind + 1;
|
||||||
// The last exercise is done.
|
self.exercises
|
||||||
// Search for exercises not done from the start.
|
// If the exercise done isn't the last, search for pending exercises after it.
|
||||||
return self.exercises[..self.current_exercise_ind]
|
.get(next_ind..)
|
||||||
.iter()
|
.and_then(|later_exercises| {
|
||||||
.position(|exercise| !exercise.done);
|
later_exercises
|
||||||
}
|
.iter()
|
||||||
|
.position(|exercise| !exercise.done)
|
||||||
// The done exercise isn't the last one.
|
.map(|ind| next_ind + ind)
|
||||||
// Search for a pending exercise after the current one and then from the start.
|
})
|
||||||
match self.exercises[self.current_exercise_ind + 1..]
|
// Search from the start.
|
||||||
.iter()
|
.or_else(|| {
|
||||||
.position(|exercise| !exercise.done)
|
self.exercises[..self.current_exercise_ind]
|
||||||
{
|
.iter()
|
||||||
Some(ind) => Some(self.current_exercise_ind + 1 + ind),
|
.position(|exercise| !exercise.done)
|
||||||
None => self.exercises[..self.current_exercise_ind]
|
})
|
||||||
.iter()
|
|
||||||
.position(|exercise| !exercise.done),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Official exercises: Dump the solution file form the binary and return its path.
|
/// Official exercises: Dump the solution file from the binary and return its path.
|
||||||
/// Third-party exercises: Check if a solution file exists and return its path in that case.
|
/// Community exercises: Check if a solution file exists and return its path in that case.
|
||||||
pub fn current_solution_path(&self) -> Result<Option<String>> {
|
pub fn current_solution_path(&self) -> Result<Option<String>> {
|
||||||
if DEBUG_PROFILE {
|
if cfg!(debug_assertions) {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -347,24 +398,135 @@ impl AppState {
|
|||||||
.write_solution_to_disk(self.current_exercise_ind, current_exercise.name)
|
.write_solution_to_disk(self.current_exercise_ind, current_exercise.name)
|
||||||
.map(Some)
|
.map(Some)
|
||||||
} else {
|
} else {
|
||||||
let solution_path = if let Some(dir) = current_exercise.dir {
|
let sol_path = current_exercise.sol_path();
|
||||||
format!("solutions/{dir}/{}.rs", current_exercise.name)
|
|
||||||
} else {
|
|
||||||
format!("solutions/{}.rs", current_exercise.name)
|
|
||||||
};
|
|
||||||
|
|
||||||
if Path::new(&solution_path).exists() {
|
if Path::new(&sol_path).exists() {
|
||||||
return Ok(Some(solution_path));
|
return Ok(Some(sol_path));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn check_all_exercises_impl(&mut self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
|
||||||
|
let term_width = terminal::size()
|
||||||
|
.context("Failed to get the terminal size")?
|
||||||
|
.0;
|
||||||
|
let mut progress_visualizer = CheckProgressVisualizer::build(stdout, term_width)?;
|
||||||
|
|
||||||
|
let next_exercise_ind = AtomicUsize::new(0);
|
||||||
|
let mut progresses = vec![CheckProgress::None; self.exercises.len()];
|
||||||
|
|
||||||
|
thread::scope(|s| {
|
||||||
|
let (exercise_progress_sender, exercise_progress_receiver) = mpsc::channel();
|
||||||
|
let n_threads = thread::available_parallelism()
|
||||||
|
.map_or(DEFAULT_CHECK_PARALLELISM, |count| count.get());
|
||||||
|
|
||||||
|
for _ in 0..n_threads {
|
||||||
|
let exercise_progress_sender = exercise_progress_sender.clone();
|
||||||
|
let next_exercise_ind = &next_exercise_ind;
|
||||||
|
let slf = &self;
|
||||||
|
thread::Builder::new()
|
||||||
|
.spawn_scoped(s, move || {
|
||||||
|
loop {
|
||||||
|
let exercise_ind = next_exercise_ind.fetch_add(1, Relaxed);
|
||||||
|
let Some(exercise) = slf.exercises.get(exercise_ind) else {
|
||||||
|
// No more exercises.
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
if exercise_progress_sender
|
||||||
|
.send((exercise_ind, CheckProgress::Checking))
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
let success = exercise.run_exercise(None, &slf.cmd_runner);
|
||||||
|
let progress = match success {
|
||||||
|
Ok(true) => CheckProgress::Done,
|
||||||
|
Ok(false) => CheckProgress::Pending,
|
||||||
|
Err(_) => CheckProgress::None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if exercise_progress_sender
|
||||||
|
.send((exercise_ind, progress))
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.context("Failed to spawn a thread to check all exercises")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop this sender to detect when the last thread is done.
|
||||||
|
drop(exercise_progress_sender);
|
||||||
|
|
||||||
|
while let Ok((exercise_ind, progress)) = exercise_progress_receiver.recv() {
|
||||||
|
progresses[exercise_ind] = progress;
|
||||||
|
progress_visualizer.update(&progresses)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<_, Error>(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut first_pending_exercise_ind = None;
|
||||||
|
for exercise_ind in 0..progresses.len() {
|
||||||
|
match progresses[exercise_ind] {
|
||||||
|
CheckProgress::Done => {
|
||||||
|
self.set_status(exercise_ind, true)?;
|
||||||
|
}
|
||||||
|
CheckProgress::Pending => {
|
||||||
|
self.set_status(exercise_ind, false)?;
|
||||||
|
if first_pending_exercise_ind.is_none() {
|
||||||
|
first_pending_exercise_ind = Some(exercise_ind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CheckProgress::None | CheckProgress::Checking => {
|
||||||
|
// If we got an error while checking all exercises in parallel,
|
||||||
|
// it could be because we exceeded the limit of open file descriptors.
|
||||||
|
// Therefore, try running exercises with errors sequentially.
|
||||||
|
progresses[exercise_ind] = CheckProgress::Checking;
|
||||||
|
progress_visualizer.update(&progresses)?;
|
||||||
|
|
||||||
|
let exercise = &self.exercises[exercise_ind];
|
||||||
|
let success = exercise.run_exercise(None, &self.cmd_runner)?;
|
||||||
|
if success {
|
||||||
|
progresses[exercise_ind] = CheckProgress::Done;
|
||||||
|
} else {
|
||||||
|
progresses[exercise_ind] = CheckProgress::Pending;
|
||||||
|
if first_pending_exercise_ind.is_none() {
|
||||||
|
first_pending_exercise_ind = Some(exercise_ind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.set_status(exercise_ind, success)?;
|
||||||
|
progress_visualizer.update(&progresses)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.write()?;
|
||||||
|
|
||||||
|
Ok(first_pending_exercise_ind)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the exercise index of the first pending exercise found.
|
||||||
|
pub fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
|
||||||
|
stdout.queue(cursor::Hide)?;
|
||||||
|
let res = self.check_all_exercises_impl(stdout);
|
||||||
|
stdout.queue(cursor::Show)?;
|
||||||
|
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
/// Mark the current exercise as done and move on to the next pending exercise if one exists.
|
/// Mark the current exercise as done and move on to the next pending exercise if one exists.
|
||||||
/// If all exercises are marked as done, run all of them to make sure that they are actually
|
/// If all exercises are marked as done, run all of them to make sure that they are actually
|
||||||
/// done. If an exercise which is marked as done fails, mark it as pending and continue on it.
|
/// done. If an exercise which is marked as done fails, mark it as pending and continue on it.
|
||||||
pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result<ExercisesProgress> {
|
pub fn done_current_exercise<const CLEAR_BEFORE_FINAL_CHECK: bool>(
|
||||||
|
&mut self,
|
||||||
|
stdout: &mut StdoutLock,
|
||||||
|
) -> Result<ExercisesProgress> {
|
||||||
let exercise = &mut self.exercises[self.current_exercise_ind];
|
let exercise = &mut self.exercises[self.current_exercise_ind];
|
||||||
if !exercise.done {
|
if !exercise.done {
|
||||||
exercise.done = true;
|
exercise.done = true;
|
||||||
@ -373,62 +535,42 @@ impl AppState {
|
|||||||
|
|
||||||
if let Some(ind) = self.next_pending_exercise_ind() {
|
if let Some(ind) = self.next_pending_exercise_ind() {
|
||||||
self.set_current_exercise_ind(ind)?;
|
self.set_current_exercise_ind(ind)?;
|
||||||
|
return Ok(ExercisesProgress::NewPending);
|
||||||
|
}
|
||||||
|
|
||||||
|
if CLEAR_BEFORE_FINAL_CHECK {
|
||||||
|
clear_terminal(stdout)?;
|
||||||
|
} else {
|
||||||
|
stdout.write_all(b"\n")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(first_pending_exercise_ind) = self.check_all_exercises(stdout)? {
|
||||||
|
self.set_current_exercise_ind(first_pending_exercise_ind)?;
|
||||||
|
|
||||||
return Ok(ExercisesProgress::NewPending);
|
return Ok(ExercisesProgress::NewPending);
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?;
|
self.render_final_message(stdout)?;
|
||||||
|
|
||||||
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
|
|
||||||
for (exercise_ind, exercise) in self.exercises().iter().enumerate() {
|
|
||||||
write!(writer, "Running {exercise} ... ")?;
|
|
||||||
writer.flush()?;
|
|
||||||
|
|
||||||
let success = exercise.run_exercise(&mut output, &self.target_dir)?;
|
|
||||||
if !success {
|
|
||||||
writeln!(writer, "{}\n", "FAILED".red())?;
|
|
||||||
|
|
||||||
self.current_exercise_ind = exercise_ind;
|
|
||||||
|
|
||||||
// No check if the exercise is done before setting it to pending
|
|
||||||
// because no pending exercise was found.
|
|
||||||
self.exercises[exercise_ind].done = false;
|
|
||||||
self.n_done -= 1;
|
|
||||||
|
|
||||||
self.write()?;
|
|
||||||
|
|
||||||
return Ok(ExercisesProgress::NewPending);
|
|
||||||
}
|
|
||||||
|
|
||||||
writeln!(writer, "{}", "ok".green())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write that the last exercise is done.
|
|
||||||
self.write()?;
|
|
||||||
|
|
||||||
clear_terminal(writer)?;
|
|
||||||
writer.write_all(FENISH_LINE.as_bytes())?;
|
|
||||||
|
|
||||||
let final_message = self.final_message.trim();
|
|
||||||
if !final_message.is_empty() {
|
|
||||||
writer.write_all(final_message.as_bytes())?;
|
|
||||||
writer.write_all(b"\n")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ExercisesProgress::AllDone)
|
Ok(ExercisesProgress::AllDone)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render_final_message(&self, stdout: &mut StdoutLock) -> Result<()> {
|
||||||
|
clear_terminal(stdout)?;
|
||||||
|
stdout.write_all(FENISH_LINE.as_bytes())?;
|
||||||
|
|
||||||
|
let final_message = self.final_message.trim_ascii();
|
||||||
|
if !final_message.is_empty() {
|
||||||
|
stdout.write_all(final_message.as_bytes())?;
|
||||||
|
stdout.write_all(b"\n")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const CARGO_METADATA_ERR: &str = "Failed to run the command `cargo metadata …`
|
const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
|
||||||
Did you already install Rust?
|
const STATE_FILE_HEADER: &[u8] = b"DON'T EDIT THIS FILE!\n\n";
|
||||||
Try running `cargo --version` to diagnose the problem.";
|
|
||||||
|
|
||||||
const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b"
|
|
||||||
All exercises seem to be done.
|
|
||||||
Recompiling and running all exercises to make sure that all of them are actually done.
|
|
||||||
|
|
||||||
";
|
|
||||||
|
|
||||||
const FENISH_LINE: &str = "+----------------------------------------------------+
|
const FENISH_LINE: &str = "+----------------------------------------------------+
|
||||||
| You made it to the Fe-nish line! |
|
| You made it to the Fe-nish line! |
|
||||||
+-------------------------- ------------------------+
|
+-------------------------- ------------------------+
|
||||||
@ -460,9 +602,10 @@ mod tests {
|
|||||||
dir: None,
|
dir: None,
|
||||||
name: "0",
|
name: "0",
|
||||||
path: "exercises/0.rs",
|
path: "exercises/0.rs",
|
||||||
|
canonical_path: None,
|
||||||
test: false,
|
test: false,
|
||||||
strict_clippy: false,
|
strict_clippy: false,
|
||||||
hint: String::new(),
|
hint: "",
|
||||||
done: false,
|
done: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -474,9 +617,11 @@ mod tests {
|
|||||||
exercises: vec![dummy_exercise(), dummy_exercise(), dummy_exercise()],
|
exercises: vec![dummy_exercise(), dummy_exercise(), dummy_exercise()],
|
||||||
n_done: 0,
|
n_done: 0,
|
||||||
final_message: String::new(),
|
final_message: String::new(),
|
||||||
|
state_file: tempfile::tempfile().unwrap(),
|
||||||
file_buf: Vec::new(),
|
file_buf: Vec::new(),
|
||||||
official_exercises: true,
|
official_exercises: true,
|
||||||
target_dir: PathBuf::new(),
|
cmd_runner: CmdRunner::build().unwrap(),
|
||||||
|
emit_file_links: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut assert = |done: [bool; 3], expected: [Option<usize>; 3]| {
|
let mut assert = |done: [bool; 3], expected: [Option<usize>; 3]| {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::info_file::ExerciseInfo;
|
use crate::{exercise::RunnableExercise, info_file::ExerciseInfo};
|
||||||
|
|
||||||
/// Initial capacity of the bins buffer.
|
/// Initial capacity of the bins buffer.
|
||||||
pub const BINS_BUFFER_CAPACITY: usize = 1 << 14;
|
pub const BINS_BUFFER_CAPACITY: usize = 1 << 14;
|
||||||
@ -74,13 +74,13 @@ pub fn updated_cargo_toml(
|
|||||||
let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?;
|
let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?;
|
||||||
|
|
||||||
let mut updated_cargo_toml = Vec::with_capacity(BINS_BUFFER_CAPACITY);
|
let mut updated_cargo_toml = Vec::with_capacity(BINS_BUFFER_CAPACITY);
|
||||||
updated_cargo_toml.extend_from_slice(current_cargo_toml[..bins_start_ind].as_bytes());
|
updated_cargo_toml.extend_from_slice(¤t_cargo_toml.as_bytes()[..bins_start_ind]);
|
||||||
append_bins(
|
append_bins(
|
||||||
&mut updated_cargo_toml,
|
&mut updated_cargo_toml,
|
||||||
exercise_infos,
|
exercise_infos,
|
||||||
exercise_path_prefix,
|
exercise_path_prefix,
|
||||||
);
|
);
|
||||||
updated_cargo_toml.extend_from_slice(current_cargo_toml[bins_end_ind..].as_bytes());
|
updated_cargo_toml.extend_from_slice(¤t_cargo_toml.as_bytes()[bins_end_ind..]);
|
||||||
|
|
||||||
Ok(updated_cargo_toml)
|
Ok(updated_cargo_toml)
|
||||||
}
|
}
|
||||||
@ -134,7 +134,14 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
updated_cargo_toml(&exercise_infos, "abc\nbin = [xxx]\n123", b"../").unwrap(),
|
updated_cargo_toml(
|
||||||
|
&exercise_infos,
|
||||||
|
"abc\n\
|
||||||
|
bin = [xxx]\n\
|
||||||
|
123",
|
||||||
|
b"../"
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
br#"abc
|
br#"abc
|
||||||
bin = [
|
bin = [
|
||||||
{ name = "1", path = "../exercises/1.rs" },
|
{ name = "1", path = "../exercises/1.rs" },
|
||||||
|
|||||||
174
src/cmd.rs
174
src/cmd.rs
@ -1,30 +1,44 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result, bail};
|
||||||
use std::{io::Read, path::Path, process::Command};
|
use serde::Deserialize;
|
||||||
|
use std::{
|
||||||
|
io::{Read, pipe},
|
||||||
|
path::PathBuf,
|
||||||
|
process::{Command, Stdio},
|
||||||
|
};
|
||||||
|
|
||||||
/// Run a command with a description for a possible error and append the merged stdout and stderr.
|
/// Run a command with a description for a possible error and append the merged stdout and stderr.
|
||||||
/// The boolean in the returned `Result` is true if the command's exit status is success.
|
/// The boolean in the returned `Result` is true if the command's exit status is success.
|
||||||
pub fn run_cmd(mut cmd: Command, description: &str, output: &mut Vec<u8>) -> Result<bool> {
|
fn run_cmd(mut cmd: Command, description: &str, output: Option<&mut Vec<u8>>) -> Result<bool> {
|
||||||
let (mut reader, writer) = os_pipe::pipe()
|
let spawn = |mut cmd: Command| {
|
||||||
.with_context(|| format!("Failed to create a pipe to run the command `{description}``"))?;
|
// NOTE: The closure drops `cmd` which prevents a pipe deadlock.
|
||||||
|
cmd.stdin(Stdio::null())
|
||||||
|
.spawn()
|
||||||
|
.with_context(|| format!("Failed to run the command `{description}`"))
|
||||||
|
};
|
||||||
|
|
||||||
let writer_clone = writer.try_clone().with_context(|| {
|
let mut handle = if let Some(output) = output {
|
||||||
format!("Failed to clone the pipe writer for the command `{description}`")
|
let (mut reader, writer) = pipe().with_context(|| {
|
||||||
})?;
|
format!("Failed to create a pipe to run the command `{description}``")
|
||||||
|
})?;
|
||||||
|
|
||||||
let mut handle = cmd
|
let writer_clone = writer.try_clone().with_context(|| {
|
||||||
.stdout(writer_clone)
|
format!("Failed to clone the pipe writer for the command `{description}`")
|
||||||
.stderr(writer)
|
})?;
|
||||||
.spawn()
|
|
||||||
.with_context(|| format!("Failed to run the command `{description}`"))?;
|
|
||||||
|
|
||||||
// Prevent pipe deadlock.
|
cmd.stdout(writer_clone).stderr(writer);
|
||||||
drop(cmd);
|
let handle = spawn(cmd)?;
|
||||||
|
|
||||||
reader
|
reader
|
||||||
.read_to_end(output)
|
.read_to_end(output)
|
||||||
.with_context(|| format!("Failed to read the output of the command `{description}`"))?;
|
.with_context(|| format!("Failed to read the output of the command `{description}`"))?;
|
||||||
|
|
||||||
output.push(b'\n');
|
output.push(b'\n');
|
||||||
|
|
||||||
|
handle
|
||||||
|
} else {
|
||||||
|
cmd.stdout(Stdio::null()).stderr(Stdio::null());
|
||||||
|
spawn(cmd)?
|
||||||
|
};
|
||||||
|
|
||||||
handle
|
handle
|
||||||
.wait()
|
.wait()
|
||||||
@ -32,50 +46,106 @@ pub fn run_cmd(mut cmd: Command, description: &str, output: &mut Vec<u8>) -> Res
|
|||||||
.map(|status| status.success())
|
.map(|status| status.success())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CargoCmd<'a> {
|
// Parses parts of the output of `cargo metadata`.
|
||||||
pub subcommand: &'a str,
|
#[derive(Deserialize)]
|
||||||
pub args: &'a [&'a str],
|
struct CargoMetadata {
|
||||||
pub bin_name: &'a str,
|
target_directory: PathBuf,
|
||||||
pub description: &'a str,
|
|
||||||
/// RUSTFLAGS="-A warnings"
|
|
||||||
pub hide_warnings: bool,
|
|
||||||
/// Added as `--target-dir` if `Self::dev` is true.
|
|
||||||
pub target_dir: &'a Path,
|
|
||||||
/// The output buffer to append the merged stdout and stderr.
|
|
||||||
pub output: &'a mut Vec<u8>,
|
|
||||||
/// true while developing Rustlings.
|
|
||||||
pub dev: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> CargoCmd<'a> {
|
pub struct CmdRunner {
|
||||||
/// Run `cargo SUBCOMMAND --bin EXERCISE_NAME … ARGS`.
|
target_dir: PathBuf,
|
||||||
pub fn run(&mut self) -> Result<bool> {
|
}
|
||||||
|
|
||||||
|
impl CmdRunner {
|
||||||
|
pub fn build() -> Result<Self> {
|
||||||
|
// Get the target directory from Cargo.
|
||||||
|
let metadata_output = Command::new("cargo")
|
||||||
|
.arg("metadata")
|
||||||
|
.arg("-q")
|
||||||
|
.arg("--format-version")
|
||||||
|
.arg("1")
|
||||||
|
.arg("--no-deps")
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stderr(Stdio::inherit())
|
||||||
|
.output()
|
||||||
|
.context(CARGO_METADATA_ERR)?;
|
||||||
|
|
||||||
|
if !metadata_output.status.success() {
|
||||||
|
bail!("The command `cargo metadata …` failed. Are you in the `rustlings/` directory?");
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadata: CargoMetadata = serde_json::de::from_slice(&metadata_output.stdout)
|
||||||
|
.context(
|
||||||
|
"Failed to read the field `target_directory` from the output of the command `cargo metadata …`",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
target_dir: metadata.target_directory,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cargo<'out>(
|
||||||
|
&self,
|
||||||
|
subcommand: &str,
|
||||||
|
bin_name: &str,
|
||||||
|
output: Option<&'out mut Vec<u8>>,
|
||||||
|
) -> CargoSubcommand<'out> {
|
||||||
let mut cmd = Command::new("cargo");
|
let mut cmd = Command::new("cargo");
|
||||||
cmd.arg(self.subcommand);
|
cmd.arg(subcommand).arg("-q").arg("--bin").arg(bin_name);
|
||||||
|
|
||||||
// A hack to make `cargo run` work when developing Rustlings.
|
// A hack to make `cargo run` work when developing Rustlings.
|
||||||
if self.dev {
|
#[cfg(debug_assertions)]
|
||||||
cmd.arg("--manifest-path")
|
cmd.arg("--manifest-path")
|
||||||
.arg("dev/Cargo.toml")
|
.arg("dev/Cargo.toml")
|
||||||
.arg("--target-dir")
|
.arg("--target-dir")
|
||||||
.arg(self.target_dir);
|
.arg(&self.target_dir);
|
||||||
|
|
||||||
|
if output.is_some() {
|
||||||
|
cmd.arg("--color").arg("always");
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.arg("--color")
|
CargoSubcommand { cmd, output }
|
||||||
.arg("always")
|
}
|
||||||
.arg("-q")
|
|
||||||
.arg("--bin")
|
|
||||||
.arg(self.bin_name)
|
|
||||||
.args(self.args);
|
|
||||||
|
|
||||||
if self.hide_warnings {
|
/// The boolean in the returned `Result` is true if the command's exit status is success.
|
||||||
cmd.env("RUSTFLAGS", "-A warnings");
|
pub fn run_debug_bin(&self, bin_name: &str, output: Option<&mut Vec<u8>>) -> Result<bool> {
|
||||||
}
|
// 7 = "/debug/".len()
|
||||||
|
let mut bin_path =
|
||||||
|
PathBuf::with_capacity(self.target_dir.as_os_str().len() + 7 + bin_name.len());
|
||||||
|
bin_path.push(&self.target_dir);
|
||||||
|
bin_path.push("debug");
|
||||||
|
bin_path.push(bin_name);
|
||||||
|
|
||||||
run_cmd(cmd, self.description, self.output)
|
run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct CargoSubcommand<'out> {
|
||||||
|
cmd: Command,
|
||||||
|
output: Option<&'out mut Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CargoSubcommand<'_> {
|
||||||
|
#[inline]
|
||||||
|
pub fn args<'arg, I>(&mut self, args: I) -> &mut Self
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = &'arg str>,
|
||||||
|
{
|
||||||
|
self.cmd.args(args);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The boolean in the returned `Result` is true if the command's exit status is success.
|
||||||
|
#[inline]
|
||||||
|
pub fn run(self, description: &str) -> Result<bool> {
|
||||||
|
run_cmd(self.cmd, description, self.output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CARGO_METADATA_ERR: &str = "Failed to run the command `cargo metadata …`
|
||||||
|
Did you already install Rust?
|
||||||
|
Try running `cargo --version` to diagnose the problem.";
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -86,7 +156,7 @@ mod tests {
|
|||||||
cmd.arg("Hello");
|
cmd.arg("Hello");
|
||||||
|
|
||||||
let mut output = Vec::with_capacity(8);
|
let mut output = Vec::with_capacity(8);
|
||||||
run_cmd(cmd, "echo …", &mut output).unwrap();
|
run_cmd(cmd, "echo …", Some(&mut output)).unwrap();
|
||||||
|
|
||||||
assert_eq!(output, b"Hello\n\n");
|
assert_eq!(output, b"Hello\n\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,14 @@
|
|||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{Context, Result, bail};
|
||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::DEBUG_PROFILE;
|
|
||||||
|
|
||||||
mod check;
|
mod check;
|
||||||
mod new;
|
mod new;
|
||||||
mod update;
|
mod update;
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
pub enum DevCommands {
|
pub enum DevCommands {
|
||||||
/// Create a new project for third-party Rustlings exercises
|
/// Create a new project for community exercises
|
||||||
New {
|
New {
|
||||||
/// The path to create the project in
|
/// The path to create the project in
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
@ -32,7 +30,7 @@ impl DevCommands {
|
|||||||
pub fn run(self) -> Result<()> {
|
pub fn run(self) -> Result<()> {
|
||||||
match self {
|
match self {
|
||||||
Self::New { path, no_git } => {
|
Self::New { path, no_git } => {
|
||||||
if DEBUG_PROFILE {
|
if cfg!(debug_assertions) {
|
||||||
bail!("Disabled in the debug build");
|
bail!("Disabled in the debug build");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
336
src/dev/check.rs
336
src/dev/check.rs
@ -1,56 +1,65 @@
|
|||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{Context, Error, Result, anyhow, bail};
|
||||||
use std::{
|
use std::{
|
||||||
cmp::Ordering,
|
cmp::Ordering,
|
||||||
fs::{self, read_dir, OpenOptions},
|
collections::HashSet,
|
||||||
|
fs::{self, OpenOptions, read_dir},
|
||||||
io::{self, Read, Write},
|
io::{self, Read, Write},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::{
|
process::{Command, Stdio},
|
||||||
atomic::{self, AtomicBool},
|
|
||||||
Mutex,
|
|
||||||
},
|
|
||||||
thread,
|
thread,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_state::parse_target_dir,
|
CURRENT_FORMAT_VERSION,
|
||||||
cargo_toml::{append_bins, bins_start_end_ind, BINS_BUFFER_CAPACITY},
|
cargo_toml::{BINS_BUFFER_CAPACITY, append_bins, bins_start_end_ind},
|
||||||
exercise::{RunnableExercise, OUTPUT_CAPACITY},
|
cmd::CmdRunner,
|
||||||
|
exercise::{OUTPUT_CAPACITY, RunnableExercise},
|
||||||
info_file::{ExerciseInfo, InfoFile},
|
info_file::{ExerciseInfo, InfoFile},
|
||||||
CURRENT_FORMAT_VERSION, DEBUG_PROFILE,
|
term::ProgressCounter,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MAX_N_EXERCISES: usize = 999;
|
||||||
|
const MAX_EXERCISE_NAME_LEN: usize = 32;
|
||||||
|
|
||||||
// Find a char that isn't allowed in the exercise's `name` or `dir`.
|
// Find a char that isn't allowed in the exercise's `name` or `dir`.
|
||||||
fn forbidden_char(input: &str) -> Option<char> {
|
fn forbidden_char(input: &str) -> Option<char> {
|
||||||
input.chars().find(|c| !c.is_alphanumeric() && *c != '_')
|
input.chars().find(|c| !c.is_alphanumeric() && *c != '_')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that the Cargo.toml file is up-to-date.
|
// Check that the `Cargo.toml` file is up-to-date.
|
||||||
fn check_cargo_toml(
|
fn check_cargo_toml(
|
||||||
exercise_infos: &[ExerciseInfo],
|
exercise_infos: &[ExerciseInfo],
|
||||||
current_cargo_toml: &str,
|
cargo_toml_path: &str,
|
||||||
exercise_path_prefix: &[u8],
|
exercise_path_prefix: &[u8],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?;
|
let current_cargo_toml = fs::read_to_string(cargo_toml_path)
|
||||||
|
.with_context(|| format!("Failed to read the file `{cargo_toml_path}`"))?;
|
||||||
|
|
||||||
|
let (bins_start_ind, bins_end_ind) = bins_start_end_ind(¤t_cargo_toml)?;
|
||||||
|
|
||||||
let old_bins = ¤t_cargo_toml.as_bytes()[bins_start_ind..bins_end_ind];
|
let old_bins = ¤t_cargo_toml.as_bytes()[bins_start_ind..bins_end_ind];
|
||||||
let mut new_bins = Vec::with_capacity(BINS_BUFFER_CAPACITY);
|
let mut new_bins = Vec::with_capacity(BINS_BUFFER_CAPACITY);
|
||||||
append_bins(&mut new_bins, exercise_infos, exercise_path_prefix);
|
append_bins(&mut new_bins, exercise_infos, exercise_path_prefix);
|
||||||
|
|
||||||
if old_bins != new_bins {
|
if old_bins != new_bins {
|
||||||
if DEBUG_PROFILE {
|
if cfg!(debug_assertions) {
|
||||||
bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it");
|
bail!(
|
||||||
|
"The file `dev/Cargo.toml` is outdated. Run `cargo dev update` to update it. Then run `cargo run -- dev check` again"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bail!("The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it");
|
bail!(
|
||||||
|
"The file `Cargo.toml` is outdated. Run `rustlings dev update` to update it. Then run `rustlings dev check` again"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the info of all exercises and return their paths in a set.
|
// Check the info of all exercises and return their paths in a set.
|
||||||
fn check_info_file_exercises(info_file: &InfoFile) -> Result<hashbrown::HashSet<PathBuf>> {
|
fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
|
||||||
let mut names = hashbrown::HashSet::with_capacity(info_file.exercises.len());
|
let mut names = HashSet::with_capacity(info_file.exercises.len());
|
||||||
let mut paths = hashbrown::HashSet::with_capacity(info_file.exercises.len());
|
let mut paths = HashSet::with_capacity(info_file.exercises.len());
|
||||||
|
|
||||||
let mut file_buf = String::with_capacity(1 << 14);
|
let mut file_buf = String::with_capacity(1 << 14);
|
||||||
for exercise_info in &info_file.exercises {
|
for exercise_info in &info_file.exercises {
|
||||||
@ -58,6 +67,11 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<hashbrown::HashSet<
|
|||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
bail!("Found an empty exercise name in `info.toml`");
|
bail!("Found an empty exercise name in `info.toml`");
|
||||||
}
|
}
|
||||||
|
if name.len() > MAX_EXERCISE_NAME_LEN {
|
||||||
|
bail!(
|
||||||
|
"The length of the exercise name `{name}` is bigger than the maximum {MAX_EXERCISE_NAME_LEN}"
|
||||||
|
);
|
||||||
|
}
|
||||||
if let Some(c) = forbidden_char(name) {
|
if let Some(c) = forbidden_char(name) {
|
||||||
bail!("Char `{c}` in the exercise name `{name}` is not allowed");
|
bail!("Char `{c}` in the exercise name `{name}` is not allowed");
|
||||||
}
|
}
|
||||||
@ -71,8 +85,10 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<hashbrown::HashSet<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if exercise_info.hint.trim().is_empty() {
|
if exercise_info.hint.trim_ascii().is_empty() {
|
||||||
bail!("The exercise `{name}` has an empty hint. Please provide a hint or at least tell the user why a hint isn't needed for this exercise");
|
bail!(
|
||||||
|
"The exercise `{name}` has an empty hint. Please provide a hint or at least tell the user why a hint isn't needed for this exercise"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !names.insert(name) {
|
if !names.insert(name) {
|
||||||
@ -89,15 +105,30 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<hashbrown::HashSet<
|
|||||||
.with_context(|| format!("Failed to read the file {path}"))?;
|
.with_context(|| format!("Failed to read the file {path}"))?;
|
||||||
|
|
||||||
if !file_buf.contains("fn main()") {
|
if !file_buf.contains("fn main()") {
|
||||||
bail!("The `main` function is missing in the file `{path}`.\nCreate at least an empty `main` function to avoid language server errors");
|
bail!(
|
||||||
|
"The `main` function is missing in the file `{path}`.\n\
|
||||||
|
Create at least an empty `main` function to avoid language server errors"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !file_buf.contains("// TODO") {
|
if !file_buf.contains("// TODO") {
|
||||||
bail!("Didn't find any `// TODO` comment in the file `{path}`.\nYou need to have at least one such comment to guide the user.");
|
bail!(
|
||||||
|
"Didn't find any `// TODO` comment in the file `{path}`.\n\
|
||||||
|
You need to have at least one such comment to guide the user."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !exercise_info.test && file_buf.contains("#[test]") {
|
let contains_tests = file_buf.contains("#[test]\n");
|
||||||
bail!("The file `{path}` contains tests annotated with `#[test]` but the exercise `{name}` has `test = false` in the `info.toml` file");
|
if exercise_info.test {
|
||||||
|
if !contains_tests {
|
||||||
|
bail!(
|
||||||
|
"The file `{path}` doesn't contain any tests. If you don't want to add tests to this exercise, set `test = false` for this exercise in the `info.toml` file"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if contains_tests {
|
||||||
|
bail!(
|
||||||
|
"The file `{path}` contains tests annotated with `#[test]` but the exercise `{name}` has `test = false` in the `info.toml` file"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
file_buf.clear();
|
file_buf.clear();
|
||||||
@ -111,12 +142,12 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<hashbrown::HashSet<
|
|||||||
// Check `dir` for unexpected files.
|
// Check `dir` for unexpected files.
|
||||||
// Only Rust files in `allowed_rust_files` and `README.md` files are allowed.
|
// Only Rust files in `allowed_rust_files` and `README.md` files are allowed.
|
||||||
// Only one level of directory nesting is allowed.
|
// Only one level of directory nesting is allowed.
|
||||||
fn check_unexpected_files(
|
fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet<PathBuf>) -> Result<()> {
|
||||||
dir: &str,
|
|
||||||
allowed_rust_files: &hashbrown::HashSet<PathBuf>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let unexpected_file = |path: &Path| {
|
let unexpected_file = |path: &Path| {
|
||||||
anyhow!("Found the file `{}`. Only `README.md` and Rust files related to an exercise in `info.toml` are allowed in the `{dir}` directory", path.display())
|
anyhow!(
|
||||||
|
"Found the file `{}`. Only `README.md` and Rust files related to an exercise in `info.toml` are allowed in the `{dir}` directory",
|
||||||
|
path.display()
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
for entry in read_dir(dir).with_context(|| format!("Failed to open the `{dir}` directory"))? {
|
for entry in read_dir(dir).with_context(|| format!("Failed to open the `{dir}` directory"))? {
|
||||||
@ -145,7 +176,10 @@ fn check_unexpected_files(
|
|||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
|
|
||||||
if !entry.file_type().unwrap().is_file() {
|
if !entry.file_type().unwrap().is_file() {
|
||||||
bail!("Found `{}` but expected only files. Only one level of exercise nesting is allowed", path.display());
|
bail!(
|
||||||
|
"Found `{}` but expected only files. Only one level of exercise nesting is allowed",
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let file_name = path.file_name().unwrap();
|
let file_name = path.file_name().unwrap();
|
||||||
@ -162,135 +196,203 @@ fn check_unexpected_files(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_exercises_unsolved(info_file: &InfoFile, target_dir: &Path) -> Result<()> {
|
fn check_exercises_unsolved(
|
||||||
let error_occurred = AtomicBool::new(false);
|
info_file: &'static InfoFile,
|
||||||
|
cmd_runner: &'static CmdRunner,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut stdout = io::stdout().lock();
|
||||||
|
stdout.write_all(b"Running all exercises to check that they aren't already solved...\n")?;
|
||||||
|
|
||||||
println!(
|
let handles = info_file
|
||||||
"Running all exercises to check that they aren't already solved. This may take a while…\n",
|
.exercises
|
||||||
);
|
.iter()
|
||||||
thread::scope(|s| {
|
.filter_map(|exercise_info| {
|
||||||
for exercise_info in &info_file.exercises {
|
|
||||||
if exercise_info.skip_check_unsolved {
|
if exercise_info.skip_check_unsolved {
|
||||||
continue;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
s.spawn(|| {
|
Some(
|
||||||
let error = |e| {
|
thread::Builder::new()
|
||||||
let mut stderr = io::stderr().lock();
|
.spawn(|| exercise_info.run_exercise(None, cmd_runner))
|
||||||
stderr.write_all(e).unwrap();
|
.map(|handle| (exercise_info.name.as_str(), handle)),
|
||||||
stderr.write_all(b"\nProblem with the exercise ").unwrap();
|
)
|
||||||
stderr.write_all(exercise_info.name.as_bytes()).unwrap();
|
})
|
||||||
stderr.write_all(SEPARATOR).unwrap();
|
.collect::<Result<Vec<_>, _>>()
|
||||||
error_occurred.store(true, atomic::Ordering::Relaxed);
|
.context("Failed to spawn a thread to check if an exercise is already solved")?;
|
||||||
};
|
|
||||||
|
|
||||||
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
|
let mut progress_counter = ProgressCounter::new(&mut stdout, handles.len())?;
|
||||||
match exercise_info.run_exercise(&mut output, target_dir) {
|
|
||||||
Ok(true) => error(b"Already solved!"),
|
for (exercise_name, handle) in handles {
|
||||||
Ok(false) => (),
|
let Ok(result) = handle.join() else {
|
||||||
Err(e) => error(e.to_string().as_bytes()),
|
bail!("Panic while trying to run the exercise {exercise_name}");
|
||||||
}
|
};
|
||||||
});
|
|
||||||
|
match result {
|
||||||
|
Ok(true) => {
|
||||||
|
bail!(
|
||||||
|
"The exercise {exercise_name} is already solved.\n\
|
||||||
|
{SKIP_CHECK_UNSOLVED_HINT}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Ok(false) => (),
|
||||||
|
Err(e) => return Err(e),
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if error_occurred.load(atomic::Ordering::Relaxed) {
|
progress_counter.increment()?;
|
||||||
bail!(CHECK_EXERCISES_UNSOLVED_ERR);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_exercises(info_file: &InfoFile, target_dir: &Path) -> Result<()> {
|
fn check_exercises(info_file: &'static InfoFile, cmd_runner: &'static CmdRunner) -> Result<()> {
|
||||||
match info_file.format_version.cmp(&CURRENT_FORMAT_VERSION) {
|
match info_file.format_version.cmp(&CURRENT_FORMAT_VERSION) {
|
||||||
Ordering::Less => bail!("`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\nPlease migrate to the latest format version"),
|
Ordering::Less => bail!(
|
||||||
Ordering::Greater => bail!("`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\nTry updating the Rustlings program"),
|
"`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\n\
|
||||||
|
Please migrate to the latest format version"
|
||||||
|
),
|
||||||
|
Ordering::Greater => bail!(
|
||||||
|
"`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\n\
|
||||||
|
Try updating the Rustlings program"
|
||||||
|
),
|
||||||
Ordering::Equal => (),
|
Ordering::Equal => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let handle = thread::Builder::new()
|
||||||
|
.spawn(move || check_exercises_unsolved(info_file, cmd_runner))
|
||||||
|
.context("Failed to spawn a thread to check if any exercise is already solved")?;
|
||||||
|
|
||||||
let info_file_paths = check_info_file_exercises(info_file)?;
|
let info_file_paths = check_info_file_exercises(info_file)?;
|
||||||
check_unexpected_files("exercises", &info_file_paths)?;
|
check_unexpected_files("exercises", &info_file_paths)?;
|
||||||
|
|
||||||
check_exercises_unsolved(info_file, target_dir)
|
handle.join().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_solutions(require_solutions: bool, info_file: &InfoFile, target_dir: &Path) -> Result<()> {
|
enum SolutionCheck {
|
||||||
let paths = Mutex::new(hashbrown::HashSet::with_capacity(info_file.exercises.len()));
|
Success { sol_path: String },
|
||||||
let error_occurred = AtomicBool::new(false);
|
MissingOptional,
|
||||||
|
RunFailure { output: Vec<u8> },
|
||||||
|
Err(Error),
|
||||||
|
}
|
||||||
|
|
||||||
println!("Running all solutions. This may take a while…\n");
|
fn check_solutions(
|
||||||
thread::scope(|s| {
|
require_solutions: bool,
|
||||||
for exercise_info in &info_file.exercises {
|
info_file: &'static InfoFile,
|
||||||
s.spawn(|| {
|
cmd_runner: &'static CmdRunner,
|
||||||
let error = |e| {
|
) -> Result<()> {
|
||||||
let mut stderr = io::stderr().lock();
|
let mut stdout = io::stdout().lock();
|
||||||
stderr.write_all(e).unwrap();
|
stdout.write_all(b"Running all solutions...\n")?;
|
||||||
stderr
|
|
||||||
.write_all(b"\nFailed to run the solution of the exercise ")
|
|
||||||
.unwrap();
|
|
||||||
stderr.write_all(exercise_info.name.as_bytes()).unwrap();
|
|
||||||
stderr.write_all(SEPARATOR).unwrap();
|
|
||||||
error_occurred.store(true, atomic::Ordering::Relaxed);
|
|
||||||
};
|
|
||||||
|
|
||||||
let path = exercise_info.sol_path();
|
let handles = info_file
|
||||||
if !Path::new(&path).exists() {
|
.exercises
|
||||||
|
.iter()
|
||||||
|
.map(|exercise_info| {
|
||||||
|
thread::Builder::new().spawn(move || {
|
||||||
|
let sol_path = exercise_info.sol_path();
|
||||||
|
if !Path::new(&sol_path).exists() {
|
||||||
if require_solutions {
|
if require_solutions {
|
||||||
error(b"Solution missing");
|
return SolutionCheck::Err(anyhow!(
|
||||||
|
"The solution of the exercise {} is missing",
|
||||||
|
exercise_info.name,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// No solution to check.
|
return SolutionCheck::MissingOptional;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
|
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
|
||||||
match exercise_info.run_solution(&mut output, target_dir) {
|
match exercise_info.run_solution(Some(&mut output), cmd_runner) {
|
||||||
Ok(true) => {
|
Ok(true) => SolutionCheck::Success { sol_path },
|
||||||
paths.lock().unwrap().insert(PathBuf::from(path));
|
Ok(false) => SolutionCheck::RunFailure { output },
|
||||||
}
|
Err(e) => SolutionCheck::Err(e),
|
||||||
Ok(false) => error(&output),
|
|
||||||
Err(e) => error(e.to_string().as_bytes()),
|
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
})
|
||||||
});
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.context("Failed to spawn a thread to check a solution")?;
|
||||||
|
|
||||||
if error_occurred.load(atomic::Ordering::Relaxed) {
|
let mut sol_paths = HashSet::with_capacity(info_file.exercises.len());
|
||||||
bail!("At least one solution failed. See the output above.");
|
let mut fmt_cmd = Command::new("rustfmt");
|
||||||
|
fmt_cmd
|
||||||
|
.arg("--check")
|
||||||
|
.arg("--edition")
|
||||||
|
.arg("2024")
|
||||||
|
.arg("--color")
|
||||||
|
.arg("always")
|
||||||
|
.stdin(Stdio::null());
|
||||||
|
|
||||||
|
let mut progress_counter = ProgressCounter::new(&mut stdout, handles.len())?;
|
||||||
|
|
||||||
|
for (exercise_info, handle) in info_file.exercises.iter().zip(handles) {
|
||||||
|
let Ok(check_result) = handle.join() else {
|
||||||
|
bail!(
|
||||||
|
"Panic while trying to run the solution of the exercise {}",
|
||||||
|
exercise_info.name,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
match check_result {
|
||||||
|
SolutionCheck::Success { sol_path } => {
|
||||||
|
fmt_cmd.arg(&sol_path);
|
||||||
|
sol_paths.insert(PathBuf::from(sol_path));
|
||||||
|
}
|
||||||
|
SolutionCheck::MissingOptional => (),
|
||||||
|
SolutionCheck::RunFailure { output } => {
|
||||||
|
drop(progress_counter);
|
||||||
|
stdout.write_all(&output)?;
|
||||||
|
bail!(
|
||||||
|
"Running the solution of the exercise {} failed with the error above",
|
||||||
|
exercise_info.name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
SolutionCheck::Err(e) => return Err(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
progress_counter.increment()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
check_unexpected_files("solutions", &paths.into_inner().unwrap())?;
|
let n_solutions = sol_paths.len();
|
||||||
|
let handle = thread::Builder::new()
|
||||||
|
.spawn(move || check_unexpected_files("solutions", &sol_paths))
|
||||||
|
.context(
|
||||||
|
"Failed to spawn a thread to check for unexpected files in the solutions directory",
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(())
|
if n_solutions > 0
|
||||||
|
&& !fmt_cmd
|
||||||
|
.status()
|
||||||
|
.context("Failed to run `rustfmt` on all solution files")?
|
||||||
|
.success()
|
||||||
|
{
|
||||||
|
bail!("Some solutions aren't formatted. Run `rustfmt` on them");
|
||||||
|
}
|
||||||
|
|
||||||
|
handle.join().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check(require_solutions: bool) -> Result<()> {
|
pub fn check(require_solutions: bool) -> Result<()> {
|
||||||
let info_file = InfoFile::parse()?;
|
let info_file = InfoFile::parse()?;
|
||||||
|
|
||||||
// A hack to make `cargo run -- dev check` work when developing Rustlings.
|
if info_file.exercises.len() > MAX_N_EXERCISES {
|
||||||
if DEBUG_PROFILE {
|
bail!("The maximum number of exercises is {MAX_N_EXERCISES}");
|
||||||
check_cargo_toml(
|
|
||||||
&info_file.exercises,
|
|
||||||
include_str!("../../dev-Cargo.toml"),
|
|
||||||
b"../",
|
|
||||||
)?;
|
|
||||||
} else {
|
|
||||||
let current_cargo_toml =
|
|
||||||
fs::read_to_string("Cargo.toml").context("Failed to read the file `Cargo.toml`")?;
|
|
||||||
check_cargo_toml(&info_file.exercises, ¤t_cargo_toml, b"")?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let target_dir = parse_target_dir()?;
|
if cfg!(debug_assertions) {
|
||||||
check_exercises(&info_file, &target_dir)?;
|
// A hack to make `cargo dev check` work when developing Rustlings.
|
||||||
check_solutions(require_solutions, &info_file, &target_dir)?;
|
check_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../")?;
|
||||||
|
} else {
|
||||||
|
check_cargo_toml(&info_file.exercises, "Cargo.toml", b"")?;
|
||||||
|
}
|
||||||
|
|
||||||
println!("\nEverything looks fine!");
|
// Leaking is fine since they are used until the end of the program.
|
||||||
|
let cmd_runner = Box::leak(Box::new(CmdRunner::build()?));
|
||||||
|
let info_file = Box::leak(Box::new(info_file));
|
||||||
|
|
||||||
|
check_exercises(info_file, cmd_runner)?;
|
||||||
|
check_solutions(require_solutions, info_file, cmd_runner)?;
|
||||||
|
|
||||||
|
println!("Everything looks fine!");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
const SEPARATOR: &[u8] =
|
const SKIP_CHECK_UNSOLVED_HINT: &str = "If this is an introduction exercise that is intended to be already solved, add `skip_check_unsolved = true` to the exercise's metadata in the `info.toml` file";
|
||||||
b"\n========================================================================================\n";
|
|
||||||
|
|
||||||
const CHECK_EXERCISES_UNSOLVED_ERR: &str = "At least one exercise is already solved or failed to run. See the output above.
|
|
||||||
If this is an intro exercise that is intended to be already solved, add `skip_check_unsolved = true` to the exercise's metadata in the `info.toml` file.";
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{Context, Result, bail};
|
||||||
use std::{
|
use std::{
|
||||||
env::set_current_dir,
|
env::set_current_dir,
|
||||||
fs::{self, create_dir},
|
fs::{self, create_dir},
|
||||||
@ -6,7 +6,7 @@ use std::{
|
|||||||
process::Command,
|
process::Command,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::CURRENT_FORMAT_VERSION;
|
use crate::{CURRENT_FORMAT_VERSION, init::RUST_ANALYZER_TOML};
|
||||||
|
|
||||||
// Create a directory relative to the current directory and print its path.
|
// Create a directory relative to the current directory and print its path.
|
||||||
fn create_rel_dir(dir_name: &str, current_dir: &str) -> Result<()> {
|
fn create_rel_dir(dir_name: &str, current_dir: &str) -> Result<()> {
|
||||||
@ -55,13 +55,17 @@ pub fn new(path: &Path, no_git: bool) -> Result<()> {
|
|||||||
write_rel_file(
|
write_rel_file(
|
||||||
"info.toml",
|
"info.toml",
|
||||||
&dir_path_str,
|
&dir_path_str,
|
||||||
format!("{INFO_FILE_BEFORE_FORMAT_VERSION}{CURRENT_FORMAT_VERSION}{INFO_FILE_AFTER_FORMAT_VERSION}"),
|
format!(
|
||||||
|
"{INFO_FILE_BEFORE_FORMAT_VERSION}{CURRENT_FORMAT_VERSION}{INFO_FILE_AFTER_FORMAT_VERSION}"
|
||||||
|
),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
write_rel_file("Cargo.toml", &dir_path_str, CARGO_TOML)?;
|
write_rel_file("Cargo.toml", &dir_path_str, CARGO_TOML)?;
|
||||||
|
|
||||||
write_rel_file("README.md", &dir_path_str, README)?;
|
write_rel_file("README.md", &dir_path_str, README)?;
|
||||||
|
|
||||||
|
write_rel_file("rust-analyzer.toml", &dir_path_str, RUST_ANALYZER_TOML)?;
|
||||||
|
|
||||||
create_rel_dir(".vscode", &dir_path_str)?;
|
create_rel_dir(".vscode", &dir_path_str)?;
|
||||||
write_rel_file(
|
write_rel_file(
|
||||||
".vscode/extensions.json",
|
".vscode/extensions.json",
|
||||||
@ -74,18 +78,17 @@ pub fn new(path: &Path, no_git: bool) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const GITIGNORE: &[u8] = b".rustlings-state.txt
|
pub const GITIGNORE: &[u8] = b"Cargo.lock
|
||||||
Cargo.lock
|
target/
|
||||||
target
|
.vscode/
|
||||||
.vscode
|
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
";
|
";
|
||||||
|
|
||||||
const INFO_FILE_BEFORE_FORMAT_VERSION: &str =
|
const INFO_FILE_BEFORE_FORMAT_VERSION: &str =
|
||||||
"# The format version is an indicator of the compatibility of third-party exercises with the
|
"# The format version is an indicator of the compatibility of community exercises with the
|
||||||
# Rustlings program.
|
# Rustlings program.
|
||||||
# The format version is not the same as the version of the Rustlings program.
|
# The format version is not the same as the version of the Rustlings program.
|
||||||
# In case Rustlings makes an unavoidable breaking change to the expected format of third-party
|
# In case Rustlings makes an unavoidable breaking change to the expected format of community
|
||||||
# exercises, you would need to raise this version and adapt to the new format.
|
# exercises, you would need to raise this version and adapt to the new format.
|
||||||
# Otherwise, the newest version of the Rustlings program won't be able to run these exercises.
|
# Otherwise, the newest version of the Rustlings program won't be able to run these exercises.
|
||||||
format_version = ";
|
format_version = ";
|
||||||
@ -93,7 +96,7 @@ format_version = ";
|
|||||||
const INFO_FILE_AFTER_FORMAT_VERSION: &str = r#"
|
const INFO_FILE_AFTER_FORMAT_VERSION: &str = r#"
|
||||||
|
|
||||||
# Optional multi-line message to be shown to users when just starting with the exercises.
|
# Optional multi-line message to be shown to users when just starting with the exercises.
|
||||||
welcome_message = """Welcome to these third-party Rustlings exercises."""
|
welcome_message = """Welcome to these community Rustlings exercises."""
|
||||||
|
|
||||||
# Optional multi-line message to be shown to users after finishing all exercises.
|
# Optional multi-line message to be shown to users after finishing all exercises.
|
||||||
final_message = """We hope that you found the exercises helpful :D"""
|
final_message = """We hope that you found the exercises helpful :D"""
|
||||||
@ -128,7 +131,7 @@ bin = []
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "exercises"
|
name = "exercises"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
# Don't publish the exercises on crates.io!
|
# Don't publish the exercises on crates.io!
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
@ -137,7 +140,7 @@ publish = false
|
|||||||
|
|
||||||
const README: &str = "# Rustlings 🦀
|
const README: &str = "# Rustlings 🦀
|
||||||
|
|
||||||
Welcome to these third-party Rustlings exercises 😃
|
Welcome to these community Rustlings exercises 😃
|
||||||
|
|
||||||
First, [install Rustlings using the official instructions](https://github.com/rust-lang/rustlings) ✅
|
First, [install Rustlings using the official instructions](https://github.com/rust-lang/rustlings) ✅
|
||||||
|
|
||||||
|
|||||||
@ -4,18 +4,19 @@ use std::fs;
|
|||||||
use crate::{
|
use crate::{
|
||||||
cargo_toml::updated_cargo_toml,
|
cargo_toml::updated_cargo_toml,
|
||||||
info_file::{ExerciseInfo, InfoFile},
|
info_file::{ExerciseInfo, InfoFile},
|
||||||
DEBUG_PROFILE,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the `Cargo.toml` file.
|
// Update the `Cargo.toml` file.
|
||||||
fn update_cargo_toml(
|
fn update_cargo_toml(
|
||||||
exercise_infos: &[ExerciseInfo],
|
exercise_infos: &[ExerciseInfo],
|
||||||
current_cargo_toml: &str,
|
|
||||||
exercise_path_prefix: &[u8],
|
|
||||||
cargo_toml_path: &str,
|
cargo_toml_path: &str,
|
||||||
|
exercise_path_prefix: &[u8],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let current_cargo_toml = fs::read_to_string(cargo_toml_path)
|
||||||
|
.with_context(|| format!("Failed to read the file `{cargo_toml_path}`"))?;
|
||||||
|
|
||||||
let updated_cargo_toml =
|
let updated_cargo_toml =
|
||||||
updated_cargo_toml(exercise_infos, current_cargo_toml, exercise_path_prefix)?;
|
updated_cargo_toml(exercise_infos, ¤t_cargo_toml, exercise_path_prefix)?;
|
||||||
|
|
||||||
fs::write(cargo_toml_path, updated_cargo_toml)
|
fs::write(cargo_toml_path, updated_cargo_toml)
|
||||||
.context("Failed to write the `Cargo.toml` file")?;
|
.context("Failed to write the `Cargo.toml` file")?;
|
||||||
@ -26,21 +27,14 @@ fn update_cargo_toml(
|
|||||||
pub fn update() -> Result<()> {
|
pub fn update() -> Result<()> {
|
||||||
let info_file = InfoFile::parse()?;
|
let info_file = InfoFile::parse()?;
|
||||||
|
|
||||||
// A hack to make `cargo run -- dev update` work when developing Rustlings.
|
if cfg!(debug_assertions) {
|
||||||
if DEBUG_PROFILE {
|
// A hack to make `cargo dev update` work when developing Rustlings.
|
||||||
update_cargo_toml(
|
update_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../")
|
||||||
&info_file.exercises,
|
.context("Failed to update the file `dev/Cargo.toml`")?;
|
||||||
include_str!("../../dev-Cargo.toml"),
|
|
||||||
b"../",
|
|
||||||
"dev/Cargo.toml",
|
|
||||||
)
|
|
||||||
.context("Failed to update the file `dev/Cargo.toml`")?;
|
|
||||||
|
|
||||||
println!("Updated `dev/Cargo.toml`");
|
println!("Updated `dev/Cargo.toml`");
|
||||||
} else {
|
} else {
|
||||||
let current_cargo_toml =
|
update_cargo_toml(&info_file.exercises, "Cargo.toml", &[])
|
||||||
fs::read_to_string("Cargo.toml").context("Failed to read the file `Cargo.toml`")?;
|
|
||||||
update_cargo_toml(&info_file.exercises, ¤t_cargo_toml, b"", "Cargo.toml")
|
|
||||||
.context("Failed to update the file `Cargo.toml`")?;
|
.context("Failed to update the file `Cargo.toml`")?;
|
||||||
|
|
||||||
println!("Updated `Cargo.toml`");
|
println!("Updated `Cargo.toml`");
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use anyhow::{Context, Error, Result};
|
use anyhow::{Context, Error, Result};
|
||||||
use std::{
|
use std::{
|
||||||
fs::{create_dir, OpenOptions},
|
fs::{self, create_dir},
|
||||||
io::{self, Write},
|
io,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::info_file::ExerciseInfo;
|
use crate::info_file::ExerciseInfo;
|
||||||
@ -9,29 +9,6 @@ use crate::info_file::ExerciseInfo;
|
|||||||
/// Contains all embedded files.
|
/// Contains all embedded files.
|
||||||
pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!();
|
pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!();
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub enum WriteStrategy {
|
|
||||||
IfNotExists,
|
|
||||||
Overwrite,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WriteStrategy {
|
|
||||||
fn write(self, path: &str, content: &[u8]) -> Result<()> {
|
|
||||||
let file = match self {
|
|
||||||
Self::IfNotExists => OpenOptions::new().create_new(true).write(true).open(path),
|
|
||||||
Self::Overwrite => OpenOptions::new()
|
|
||||||
.create(true)
|
|
||||||
.write(true)
|
|
||||||
.truncate(true)
|
|
||||||
.open(path),
|
|
||||||
};
|
|
||||||
|
|
||||||
file.with_context(|| format!("Failed to open the file `{path}` in write mode"))?
|
|
||||||
.write_all(content)
|
|
||||||
.with_context(|| format!("Failed to write the file {path}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Files related to one exercise.
|
// Files related to one exercise.
|
||||||
struct ExerciseFiles {
|
struct ExerciseFiles {
|
||||||
// The content of the exercise file.
|
// The content of the exercise file.
|
||||||
@ -42,6 +19,16 @@ struct ExerciseFiles {
|
|||||||
dir_ind: usize,
|
dir_ind: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_dir_if_not_exists(path: &str) -> Result<()> {
|
||||||
|
if let Err(e) = create_dir(path)
|
||||||
|
&& e.kind() != io::ErrorKind::AlreadyExists
|
||||||
|
{
|
||||||
|
return Err(Error::from(e).context(format!("Failed to create the directory {path}")));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// A directory in the `exercises/` directory.
|
// A directory in the `exercises/` directory.
|
||||||
pub struct ExerciseDir {
|
pub struct ExerciseDir {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
@ -55,21 +42,13 @@ impl ExerciseDir {
|
|||||||
let mut dir_path = String::with_capacity(20 + self.name.len());
|
let mut dir_path = String::with_capacity(20 + self.name.len());
|
||||||
dir_path.push_str("exercises/");
|
dir_path.push_str("exercises/");
|
||||||
dir_path.push_str(self.name);
|
dir_path.push_str(self.name);
|
||||||
|
create_dir_if_not_exists(&dir_path)?;
|
||||||
if let Err(e) = create_dir(&dir_path) {
|
|
||||||
if e.kind() == io::ErrorKind::AlreadyExists {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
return Err(
|
|
||||||
Error::from(e).context(format!("Failed to create the directory {dir_path}"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut readme_path = dir_path;
|
let mut readme_path = dir_path;
|
||||||
readme_path.push_str("/README.md");
|
readme_path.push_str("/README.md");
|
||||||
|
|
||||||
WriteStrategy::Overwrite.write(&readme_path, self.readme)
|
fs::write(&readme_path, self.readme)
|
||||||
|
.with_context(|| format!("Failed to write the file {readme_path}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,17 +65,31 @@ impl EmbeddedFiles {
|
|||||||
pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> {
|
pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> {
|
||||||
create_dir("exercises").context("Failed to create the directory `exercises`")?;
|
create_dir("exercises").context("Failed to create the directory `exercises`")?;
|
||||||
|
|
||||||
WriteStrategy::IfNotExists.write(
|
fs::write(
|
||||||
"exercises/README.md",
|
"exercises/README.md",
|
||||||
include_bytes!("../exercises/README.md"),
|
include_bytes!("../exercises/README.md"),
|
||||||
)?;
|
)
|
||||||
|
.context("Failed to write the file exercises/README.md")?;
|
||||||
|
|
||||||
for dir in self.exercise_dirs {
|
for dir in self.exercise_dirs {
|
||||||
dir.init_on_disk()?;
|
dir.init_on_disk()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut exercise_path = String::with_capacity(64);
|
||||||
|
let prefix = "exercises/";
|
||||||
|
exercise_path.push_str(prefix);
|
||||||
|
|
||||||
for (exercise_info, exercise_files) in exercise_infos.iter().zip(self.exercise_files) {
|
for (exercise_info, exercise_files) in exercise_infos.iter().zip(self.exercise_files) {
|
||||||
WriteStrategy::IfNotExists.write(&exercise_info.path(), exercise_files.exercise)?;
|
let dir = &self.exercise_dirs[exercise_files.dir_ind];
|
||||||
|
|
||||||
|
exercise_path.truncate(prefix.len());
|
||||||
|
exercise_path.push_str(dir.name);
|
||||||
|
exercise_path.push('/');
|
||||||
|
exercise_path.push_str(&exercise_info.name);
|
||||||
|
exercise_path.push_str(".rs");
|
||||||
|
|
||||||
|
fs::write(&exercise_path, exercise_files.exercise)
|
||||||
|
.with_context(|| format!("Failed to write the exercise file {exercise_path}"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -107,7 +100,8 @@ impl EmbeddedFiles {
|
|||||||
let dir = &self.exercise_dirs[exercise_files.dir_ind];
|
let dir = &self.exercise_dirs[exercise_files.dir_ind];
|
||||||
|
|
||||||
dir.init_on_disk()?;
|
dir.init_on_disk()?;
|
||||||
WriteStrategy::Overwrite.write(path, exercise_files.exercise)
|
fs::write(path, exercise_files.exercise)
|
||||||
|
.with_context(|| format!("Failed to write the exercise file {path}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write the solution file to disk and return its path.
|
/// Write the solution file to disk and return its path.
|
||||||
@ -116,19 +110,25 @@ impl EmbeddedFiles {
|
|||||||
exercise_ind: usize,
|
exercise_ind: usize,
|
||||||
exercise_name: &str,
|
exercise_name: &str,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
|
create_dir_if_not_exists("solutions")?;
|
||||||
|
|
||||||
let exercise_files = &self.exercise_files[exercise_ind];
|
let exercise_files = &self.exercise_files[exercise_ind];
|
||||||
let dir = &self.exercise_dirs[exercise_files.dir_ind];
|
let dir = &self.exercise_dirs[exercise_files.dir_ind];
|
||||||
|
|
||||||
// 14 = 10 + 1 + 3
|
// 14 = 10 + 1 + 3
|
||||||
// solutions/ + / + .rs
|
// solutions/ + / + .rs
|
||||||
let mut solution_path = String::with_capacity(14 + dir.name.len() + exercise_name.len());
|
let mut dir_path = String::with_capacity(14 + dir.name.len() + exercise_name.len());
|
||||||
solution_path.push_str("solutions/");
|
dir_path.push_str("solutions/");
|
||||||
solution_path.push_str(dir.name);
|
dir_path.push_str(dir.name);
|
||||||
|
create_dir_if_not_exists(&dir_path)?;
|
||||||
|
|
||||||
|
let mut solution_path = dir_path;
|
||||||
solution_path.push('/');
|
solution_path.push('/');
|
||||||
solution_path.push_str(exercise_name);
|
solution_path.push_str(exercise_name);
|
||||||
solution_path.push_str(".rs");
|
solution_path.push_str(".rs");
|
||||||
|
|
||||||
WriteStrategy::Overwrite.write(&solution_path, exercise_files.solution)?;
|
fs::write(&solution_path, exercise_files.solution)
|
||||||
|
.with_context(|| format!("Failed to write the solution file {solution_path}"))?;
|
||||||
|
|
||||||
Ok(solution_path)
|
Ok(solution_path)
|
||||||
}
|
}
|
||||||
@ -152,7 +152,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dirs() {
|
fn dirs() {
|
||||||
let exercises = toml_edit::de::from_str::<InfoFile>(EMBEDDED_FILES.info_file)
|
let exercises = toml::de::from_str::<InfoFile>(EMBEDDED_FILES.info_file)
|
||||||
.expect("Failed to parse `info.toml`")
|
.expect("Failed to parse `info.toml`")
|
||||||
.exercises;
|
.exercises;
|
||||||
|
|
||||||
|
|||||||
234
src/exercise.rs
234
src/exercise.rs
@ -1,46 +1,64 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crossterm::style::{style, StyledContent, Stylize};
|
use crossterm::{
|
||||||
use std::{
|
QueueableCommand,
|
||||||
fmt::{self, Display, Formatter},
|
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
|
||||||
io::Write,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
process::Command,
|
|
||||||
};
|
};
|
||||||
|
use std::io::{self, StdoutLock, Write};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cmd::{run_cmd, CargoCmd},
|
cmd::CmdRunner,
|
||||||
in_official_repo,
|
term::{self, CountedWrite, file_path, terminal_file_link, write_ansi},
|
||||||
terminal_link::TerminalFileLink,
|
|
||||||
DEBUG_PROFILE,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The initial capacity of the output buffer.
|
/// The initial capacity of the output buffer.
|
||||||
pub const OUTPUT_CAPACITY: usize = 1 << 14;
|
pub const OUTPUT_CAPACITY: usize = 1 << 14;
|
||||||
|
|
||||||
|
pub fn solution_link_line(
|
||||||
|
stdout: &mut StdoutLock,
|
||||||
|
solution_path: &str,
|
||||||
|
emit_file_links: bool,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
stdout.queue(SetAttribute(Attribute::Bold))?;
|
||||||
|
stdout.write_all(b"Solution")?;
|
||||||
|
stdout.queue(ResetColor)?;
|
||||||
|
stdout.write_all(b" for comparison: ")?;
|
||||||
|
file_path(stdout, Color::Cyan, |writer| {
|
||||||
|
if emit_file_links && let Some(canonical_path) = term::canonicalize(solution_path) {
|
||||||
|
terminal_file_link(writer, solution_path, &canonical_path)
|
||||||
|
} else {
|
||||||
|
writer.stdout().write_all(solution_path.as_bytes())
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
stdout.write_all(b"\n")
|
||||||
|
}
|
||||||
|
|
||||||
// Run an exercise binary and append its output to the `output` buffer.
|
// Run an exercise binary and append its output to the `output` buffer.
|
||||||
// Compilation must be done before calling this method.
|
// Compilation must be done before calling this method.
|
||||||
fn run_bin(bin_name: &str, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> {
|
fn run_bin(
|
||||||
writeln!(output, "{}", "Output".underlined())?;
|
bin_name: &str,
|
||||||
|
mut output: Option<&mut Vec<u8>>,
|
||||||
|
cmd_runner: &CmdRunner,
|
||||||
|
) -> Result<bool> {
|
||||||
|
if let Some(output) = output.as_deref_mut() {
|
||||||
|
write_ansi(output, SetAttribute(Attribute::Underlined));
|
||||||
|
output.extend_from_slice(b"Output");
|
||||||
|
write_ansi(output, ResetColor);
|
||||||
|
output.push(b'\n');
|
||||||
|
}
|
||||||
|
|
||||||
// 7 = "/debug/".len()
|
let success = cmd_runner.run_debug_bin(bin_name, output.as_deref_mut())?;
|
||||||
let mut bin_path = PathBuf::with_capacity(target_dir.as_os_str().len() + 7 + bin_name.len());
|
|
||||||
bin_path.push(target_dir);
|
|
||||||
bin_path.push("debug");
|
|
||||||
bin_path.push(bin_name);
|
|
||||||
|
|
||||||
let success = run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)?;
|
if let Some(output) = output
|
||||||
|
&& !success
|
||||||
if !success {
|
{
|
||||||
// This output is important to show the user that something went wrong.
|
// This output is important to show the user that something went wrong.
|
||||||
// Otherwise, calling something like `exit(1)` in an exercise without further output
|
// Otherwise, calling something like `exit(1)` in an exercise without further output
|
||||||
// leaves the user confused about why the exercise isn't done yet.
|
// leaves the user confused about why the exercise isn't done yet.
|
||||||
writeln!(
|
write_ansi(output, SetAttribute(Attribute::Bold));
|
||||||
output,
|
write_ansi(output, SetForegroundColor(Color::Red));
|
||||||
"{}",
|
output.extend_from_slice(b"The exercise didn't run successfully (nonzero exit code)");
|
||||||
"The exercise didn't run successfully (nonzero exit code)"
|
write_ansi(output, ResetColor);
|
||||||
.bold()
|
output.push(b'\n');
|
||||||
.red(),
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(success)
|
Ok(success)
|
||||||
@ -52,114 +70,133 @@ pub struct Exercise {
|
|||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
/// Path of the exercise file starting with the `exercises/` directory.
|
/// Path of the exercise file starting with the `exercises/` directory.
|
||||||
pub path: &'static str,
|
pub path: &'static str,
|
||||||
|
pub canonical_path: Option<String>,
|
||||||
pub test: bool,
|
pub test: bool,
|
||||||
pub strict_clippy: bool,
|
pub strict_clippy: bool,
|
||||||
pub hint: String,
|
pub hint: &'static str,
|
||||||
pub done: bool,
|
pub done: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Exercise {
|
impl Exercise {
|
||||||
pub fn terminal_link(&self) -> StyledContent<TerminalFileLink<'_>> {
|
pub fn terminal_file_link<'a>(
|
||||||
style(TerminalFileLink(self.path)).underlined().blue()
|
&self,
|
||||||
}
|
writer: &mut impl CountedWrite<'a>,
|
||||||
}
|
emit_file_links: bool,
|
||||||
|
) -> io::Result<()> {
|
||||||
impl Display for Exercise {
|
file_path(writer, Color::Blue, |writer| {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
if emit_file_links && let Some(canonical_path) = self.canonical_path.as_deref() {
|
||||||
self.path.fmt(f)
|
terminal_file_link(writer, self.path, canonical_path)
|
||||||
|
} else {
|
||||||
|
writer.write_str(self.path)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait RunnableExercise {
|
pub trait RunnableExercise {
|
||||||
fn name(&self) -> &str;
|
fn name(&self) -> &str;
|
||||||
|
fn dir(&self) -> Option<&str>;
|
||||||
fn strict_clippy(&self) -> bool;
|
fn strict_clippy(&self) -> bool;
|
||||||
fn test(&self) -> bool;
|
fn test(&self) -> bool;
|
||||||
|
|
||||||
// Compile, check and run the exercise or its solution (depending on `bin_name´).
|
// Compile, check and run the exercise or its solution (depending on `bin_name´).
|
||||||
// The output is written to the `output` buffer after clearing it.
|
// The output is written to the `output` buffer after clearing it.
|
||||||
fn run(&self, bin_name: &str, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> {
|
fn run<const FORCE_STRICT_CLIPPY: bool>(
|
||||||
output.clear();
|
&self,
|
||||||
|
bin_name: &str,
|
||||||
// Developing the official Rustlings.
|
mut output: Option<&mut Vec<u8>>,
|
||||||
let dev = DEBUG_PROFILE && in_official_repo();
|
cmd_runner: &CmdRunner,
|
||||||
|
) -> Result<bool> {
|
||||||
let build_success = CargoCmd {
|
if let Some(output) = output.as_deref_mut() {
|
||||||
subcommand: "build",
|
output.clear();
|
||||||
args: &[],
|
|
||||||
bin_name,
|
|
||||||
description: "cargo build …",
|
|
||||||
hide_warnings: false,
|
|
||||||
target_dir,
|
|
||||||
output,
|
|
||||||
dev,
|
|
||||||
}
|
}
|
||||||
.run()?;
|
|
||||||
|
let build_success = cmd_runner
|
||||||
|
.cargo("build", bin_name, output.as_deref_mut())
|
||||||
|
.run("cargo build …")?;
|
||||||
if !build_success {
|
if !build_success {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Discard the output of `cargo build` because it will be shown again by Clippy.
|
// Discard the compiler output because it will be shown again by `cargo test` or Clippy.
|
||||||
output.clear();
|
if let Some(output) = output.as_deref_mut() {
|
||||||
|
output.clear();
|
||||||
|
}
|
||||||
|
|
||||||
// `--profile test` is required to also check code with `[cfg(test)]`.
|
if self.test() {
|
||||||
let clippy_args: &[&str] = if self.strict_clippy() {
|
let output_is_some = output.is_some();
|
||||||
&["--profile", "test", "--", "-D", "warnings"]
|
let mut test_cmd = cmd_runner.cargo("test", bin_name, output.as_deref_mut());
|
||||||
|
if output_is_some {
|
||||||
|
test_cmd.args(["--", "--color", "always", "--format", "pretty"]);
|
||||||
|
}
|
||||||
|
let test_success = test_cmd.run("cargo test …")?;
|
||||||
|
if !test_success {
|
||||||
|
run_bin(bin_name, output, cmd_runner)?;
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discard the compiler output because it will be shown again by Clippy.
|
||||||
|
if let Some(output) = output.as_deref_mut() {
|
||||||
|
output.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut clippy_cmd = cmd_runner.cargo("clippy", bin_name, output.as_deref_mut());
|
||||||
|
|
||||||
|
// `--profile test` is required to also check code with `#[cfg(test)]`.
|
||||||
|
if FORCE_STRICT_CLIPPY || self.strict_clippy() {
|
||||||
|
clippy_cmd.args(["--profile", "test", "--", "-D", "warnings"]);
|
||||||
} else {
|
} else {
|
||||||
&["--profile", "test"]
|
clippy_cmd.args(["--profile", "test"]);
|
||||||
};
|
|
||||||
let clippy_success = CargoCmd {
|
|
||||||
subcommand: "clippy",
|
|
||||||
args: clippy_args,
|
|
||||||
bin_name,
|
|
||||||
description: "cargo clippy …",
|
|
||||||
hide_warnings: false,
|
|
||||||
target_dir,
|
|
||||||
output,
|
|
||||||
dev,
|
|
||||||
}
|
|
||||||
.run()?;
|
|
||||||
if !clippy_success {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.test() {
|
let clippy_success = clippy_cmd.run("cargo clippy …")?;
|
||||||
return run_bin(bin_name, output, target_dir);
|
let run_success = run_bin(bin_name, output, cmd_runner)?;
|
||||||
}
|
|
||||||
|
|
||||||
let test_success = CargoCmd {
|
Ok(clippy_success && run_success)
|
||||||
subcommand: "test",
|
|
||||||
args: &["--", "--color", "always", "--show-output"],
|
|
||||||
bin_name,
|
|
||||||
description: "cargo test …",
|
|
||||||
// Hide warnings because they are shown by Clippy.
|
|
||||||
hide_warnings: true,
|
|
||||||
target_dir,
|
|
||||||
output,
|
|
||||||
dev,
|
|
||||||
}
|
|
||||||
.run()?;
|
|
||||||
|
|
||||||
let run_success = run_bin(bin_name, output, target_dir)?;
|
|
||||||
|
|
||||||
Ok(test_success && run_success)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compile, check and run the exercise.
|
/// Compile, check and run the exercise.
|
||||||
/// The output is written to the `output` buffer after clearing it.
|
/// The output is written to the `output` buffer after clearing it.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn run_exercise(&self, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> {
|
fn run_exercise(&self, output: Option<&mut Vec<u8>>, cmd_runner: &CmdRunner) -> Result<bool> {
|
||||||
self.run(self.name(), output, target_dir)
|
self.run::<false>(self.name(), output, cmd_runner)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compile, check and run the exercise's solution.
|
/// Compile, check and run the exercise's solution.
|
||||||
/// The output is written to the `output` buffer after clearing it.
|
/// The output is written to the `output` buffer after clearing it.
|
||||||
fn run_solution(&self, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> {
|
fn run_solution(&self, output: Option<&mut Vec<u8>>, cmd_runner: &CmdRunner) -> Result<bool> {
|
||||||
let name = self.name();
|
let name = self.name();
|
||||||
let mut bin_name = String::with_capacity(name.len());
|
let mut bin_name = String::with_capacity(name.len() + 4);
|
||||||
bin_name.push_str(name);
|
bin_name.push_str(name);
|
||||||
bin_name.push_str("_sol");
|
bin_name.push_str("_sol");
|
||||||
|
|
||||||
self.run(&bin_name, output, target_dir)
|
self.run::<true>(&bin_name, output, cmd_runner)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sol_path(&self) -> String {
|
||||||
|
let name = self.name();
|
||||||
|
|
||||||
|
let mut path = if let Some(dir) = self.dir() {
|
||||||
|
// 14 = 10 + 1 + 3
|
||||||
|
// solutions/ + / + .rs
|
||||||
|
let mut path = String::with_capacity(14 + dir.len() + name.len());
|
||||||
|
path.push_str("solutions/");
|
||||||
|
path.push_str(dir);
|
||||||
|
path.push('/');
|
||||||
|
path
|
||||||
|
} else {
|
||||||
|
// 13 = 10 + 3
|
||||||
|
// solutions/ + .rs
|
||||||
|
let mut path = String::with_capacity(13 + name.len());
|
||||||
|
path.push_str("solutions/");
|
||||||
|
path
|
||||||
|
};
|
||||||
|
|
||||||
|
path.push_str(name);
|
||||||
|
path.push_str(".rs");
|
||||||
|
|
||||||
|
path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,6 +206,11 @@ impl RunnableExercise for Exercise {
|
|||||||
self.name
|
self.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn dir(&self) -> Option<&str> {
|
||||||
|
self.dir
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn strict_clippy(&self) -> bool {
|
fn strict_clippy(&self) -> bool {
|
||||||
self.strict_clippy
|
self.strict_clippy
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use anyhow::{bail, Context, Error, Result};
|
use anyhow::{Context, Error, Result, bail};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{fs, io::ErrorKind};
|
use std::{fs, io::ErrorKind};
|
||||||
|
|
||||||
@ -52,30 +52,6 @@ impl ExerciseInfo {
|
|||||||
|
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Path to the solution file starting with the `solutions/` directory.
|
|
||||||
pub fn sol_path(&self) -> String {
|
|
||||||
let mut path = if let Some(dir) = &self.dir {
|
|
||||||
// 14 = 10 + 1 + 3
|
|
||||||
// solutions/ + / + .rs
|
|
||||||
let mut path = String::with_capacity(14 + dir.len() + self.name.len());
|
|
||||||
path.push_str("solutions/");
|
|
||||||
path.push_str(dir);
|
|
||||||
path.push('/');
|
|
||||||
path
|
|
||||||
} else {
|
|
||||||
// 13 = 10 + 3
|
|
||||||
// solutions/ + .rs
|
|
||||||
let mut path = String::with_capacity(13 + self.name.len());
|
|
||||||
path.push_str("solutions/");
|
|
||||||
path
|
|
||||||
};
|
|
||||||
|
|
||||||
path.push_str(&self.name);
|
|
||||||
path.push_str(".rs");
|
|
||||||
|
|
||||||
path
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RunnableExercise for ExerciseInfo {
|
impl RunnableExercise for ExerciseInfo {
|
||||||
@ -84,6 +60,11 @@ impl RunnableExercise for ExerciseInfo {
|
|||||||
&self.name
|
&self.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn dir(&self) -> Option<&str> {
|
||||||
|
self.dir.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn strict_clippy(&self) -> bool {
|
fn strict_clippy(&self) -> bool {
|
||||||
self.strict_clippy
|
self.strict_clippy
|
||||||
@ -98,7 +79,7 @@ impl RunnableExercise for ExerciseInfo {
|
|||||||
/// The deserialized `info.toml` file.
|
/// The deserialized `info.toml` file.
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct InfoFile {
|
pub struct InfoFile {
|
||||||
/// For possible breaking changes in the future for third-party exercises.
|
/// For possible breaking changes in the future for community exercises.
|
||||||
pub format_version: u8,
|
pub format_version: u8,
|
||||||
/// Shown to users when starting with the exercises.
|
/// Shown to users when starting with the exercises.
|
||||||
pub welcome_message: Option<String>,
|
pub welcome_message: Option<String>,
|
||||||
@ -110,15 +91,15 @@ pub struct InfoFile {
|
|||||||
|
|
||||||
impl InfoFile {
|
impl InfoFile {
|
||||||
/// Official exercises: Parse the embedded `info.toml` file.
|
/// Official exercises: Parse the embedded `info.toml` file.
|
||||||
/// Third-party exercises: Parse the `info.toml` file in the current directory.
|
/// Community exercises: Parse the `info.toml` file in the current directory.
|
||||||
pub fn parse() -> Result<Self> {
|
pub fn parse() -> Result<Self> {
|
||||||
// Read a local `info.toml` if it exists.
|
// Read a local `info.toml` if it exists.
|
||||||
let slf = match fs::read_to_string("info.toml") {
|
let slf = match fs::read_to_string("info.toml") {
|
||||||
Ok(file_content) => toml_edit::de::from_str::<Self>(&file_content)
|
Ok(file_content) => toml::de::from_str::<Self>(&file_content)
|
||||||
.context("Failed to parse the `info.toml` file")?,
|
.context("Failed to parse the `info.toml` file")?,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if e.kind() == ErrorKind::NotFound {
|
if e.kind() == ErrorKind::NotFound {
|
||||||
return toml_edit::de::from_str(EMBEDDED_FILES.info_file)
|
return toml::de::from_str(EMBEDDED_FILES.info_file)
|
||||||
.context("Failed to parse the embedded `info.toml` file");
|
.context("Failed to parse the embedded `info.toml` file");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,4 +116,4 @@ impl InfoFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const NO_EXERCISES_ERR: &str = "There are no exercises yet!
|
const NO_EXERCISES_ERR: &str = "There are no exercises yet!
|
||||||
If you are developing third-party exercises, add at least one exercise before testing.";
|
Add at least one exercise before testing.";
|
||||||
|
|||||||
180
src/init.rs
180
src/init.rs
@ -1,32 +1,122 @@
|
|||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{Context, Result, bail};
|
||||||
use crossterm::style::Stylize;
|
use crossterm::{
|
||||||
|
QueueableCommand,
|
||||||
|
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
use std::{
|
use std::{
|
||||||
env::set_current_dir,
|
env::set_current_dir,
|
||||||
fs::{self, create_dir},
|
fs::{self, create_dir},
|
||||||
io::ErrorKind,
|
io::{self, Write},
|
||||||
path::Path,
|
path::{Path, PathBuf},
|
||||||
process::{Command, Stdio},
|
process::{Command, Stdio},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, info_file::InfoFile};
|
use crate::{
|
||||||
|
cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, exercise::RunnableExercise,
|
||||||
|
info_file::InfoFile, term::press_enter_prompt,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct CargoLocateProject {
|
||||||
|
root: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn init() -> Result<()> {
|
pub fn init() -> Result<()> {
|
||||||
// Prevent initialization in a directory that contains the file `Cargo.toml`.
|
let rustlings_dir = Path::new("rustlings");
|
||||||
// This can mean that Rustlings was already initialized in this directory.
|
if rustlings_dir.exists() {
|
||||||
// Otherwise, this can cause problems with Cargo workspaces.
|
bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR);
|
||||||
if Path::new("Cargo.toml").exists() {
|
|
||||||
bail!(CARGO_TOML_EXISTS_ERR);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let rustlings_path = Path::new("rustlings");
|
let locate_project_output = Command::new("cargo")
|
||||||
if let Err(e) = create_dir(rustlings_path) {
|
.arg("locate-project")
|
||||||
if e.kind() == ErrorKind::AlreadyExists {
|
.arg("-q")
|
||||||
bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR);
|
.arg("--workspace")
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.output()
|
||||||
|
.context(
|
||||||
|
"Failed to run the command `cargo locate-project …`\n\
|
||||||
|
Did you already install Rust?\n\
|
||||||
|
Try running `cargo --version` to diagnose the problem.",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if !Command::new("cargo")
|
||||||
|
.arg("clippy")
|
||||||
|
.arg("--version")
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.status()
|
||||||
|
.context("Failed to run the command `cargo clippy --version`")?
|
||||||
|
.success()
|
||||||
|
{
|
||||||
|
bail!(
|
||||||
|
"Clippy, the official Rust linter, is missing.\n\
|
||||||
|
Please install it first before initializing Rustlings."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut stdout = io::stdout().lock();
|
||||||
|
let mut init_git = true;
|
||||||
|
|
||||||
|
if locate_project_output.status.success() {
|
||||||
|
if Path::new("exercises").exists() && Path::new("solutions").exists() {
|
||||||
|
bail!(IN_INITIALIZED_DIR_ERR);
|
||||||
}
|
}
|
||||||
return Err(e.into());
|
|
||||||
|
let workspace_manifest =
|
||||||
|
serde_json::de::from_slice::<CargoLocateProject>(&locate_project_output.stdout)
|
||||||
|
.context(
|
||||||
|
"Failed to read the field `root` from the output of `cargo locate-project …`",
|
||||||
|
)?
|
||||||
|
.root;
|
||||||
|
|
||||||
|
let workspace_manifest_content = fs::read_to_string(&workspace_manifest)
|
||||||
|
.with_context(|| format!("Failed to read the file {}", workspace_manifest.display()))?;
|
||||||
|
if !workspace_manifest_content.contains("[workspace]")
|
||||||
|
&& !workspace_manifest_content.contains("workspace.")
|
||||||
|
{
|
||||||
|
bail!(
|
||||||
|
"The current directory is already part of a Cargo project.\n\
|
||||||
|
Please initialize Rustlings in a different directory"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout.write_all(b"This command will create the directory `rustlings/` as a member of this Cargo workspace.\n\
|
||||||
|
Press ENTER to continue ")?;
|
||||||
|
press_enter_prompt(&mut stdout)?;
|
||||||
|
|
||||||
|
// Make sure "rustlings" is added to `workspace.members` by making
|
||||||
|
// Cargo initialize a new project.
|
||||||
|
let status = Command::new("cargo")
|
||||||
|
.arg("new")
|
||||||
|
.arg("-q")
|
||||||
|
.arg("--vcs")
|
||||||
|
.arg("none")
|
||||||
|
.arg("rustlings")
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.status()?;
|
||||||
|
if !status.success() {
|
||||||
|
bail!(
|
||||||
|
"Failed to initialize a new Cargo workspace member.\n\
|
||||||
|
Please initialize Rustlings in a different directory"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout.write_all(b"The directory `rustlings` has been added to `workspace.members` in the `Cargo.toml` file of this Cargo workspace.\n")?;
|
||||||
|
fs::remove_dir_all("rustlings")
|
||||||
|
.context("Failed to remove the temporary directory `rustlings/`")?;
|
||||||
|
init_git = false;
|
||||||
|
} else {
|
||||||
|
stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\n\
|
||||||
|
Press ENTER to continue ")?;
|
||||||
|
press_enter_prompt(&mut stdout)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
set_current_dir("rustlings")
|
create_dir(rustlings_dir).context("Failed to create the `rustlings/` directory")?;
|
||||||
|
set_current_dir(rustlings_dir)
|
||||||
.context("Failed to change the current directory to `rustlings/`")?;
|
.context("Failed to change the current directory to `rustlings/`")?;
|
||||||
|
|
||||||
let info_file = InfoFile::parse()?;
|
let info_file = InfoFile::parse()?;
|
||||||
@ -35,6 +125,11 @@ pub fn init() -> Result<()> {
|
|||||||
.context("Failed to initialize the `rustlings/exercises` directory")?;
|
.context("Failed to initialize the `rustlings/exercises` directory")?;
|
||||||
|
|
||||||
create_dir("solutions").context("Failed to create the `solutions/` directory")?;
|
create_dir("solutions").context("Failed to create the `solutions/` directory")?;
|
||||||
|
fs::write(
|
||||||
|
"solutions/README.md",
|
||||||
|
include_bytes!("../solutions/README.md"),
|
||||||
|
)
|
||||||
|
.context("Failed to create the file rustlings/solutions/README.md")?;
|
||||||
for dir in EMBEDDED_FILES.exercise_dirs {
|
for dir in EMBEDDED_FILES.exercise_dirs {
|
||||||
let mut dir_path = String::with_capacity(10 + dir.name.len());
|
let mut dir_path = String::with_capacity(10 + dir.name.len());
|
||||||
dir_path.push_str("solutions/");
|
dir_path.push_str("solutions/");
|
||||||
@ -63,6 +158,9 @@ pub fn init() -> Result<()> {
|
|||||||
fs::write("Cargo.toml", updated_cargo_toml)
|
fs::write("Cargo.toml", updated_cargo_toml)
|
||||||
.context("Failed to create the file `rustlings/Cargo.toml`")?;
|
.context("Failed to create the file `rustlings/Cargo.toml`")?;
|
||||||
|
|
||||||
|
fs::write("rust-analyzer.toml", RUST_ANALYZER_TOML)
|
||||||
|
.context("Failed to create the file `rustlings/rust-analyzer.toml`")?;
|
||||||
|
|
||||||
fs::write(".gitignore", GITIGNORE)
|
fs::write(".gitignore", GITIGNORE)
|
||||||
.context("Failed to create the file `rustlings/.gitignore`")?;
|
.context("Failed to create the file `rustlings/.gitignore`")?;
|
||||||
|
|
||||||
@ -70,18 +168,24 @@ pub fn init() -> Result<()> {
|
|||||||
fs::write(".vscode/extensions.json", VS_CODE_EXTENSIONS_JSON)
|
fs::write(".vscode/extensions.json", VS_CODE_EXTENSIONS_JSON)
|
||||||
.context("Failed to create the file `rustlings/.vscode/extensions.json`")?;
|
.context("Failed to create the file `rustlings/.vscode/extensions.json`")?;
|
||||||
|
|
||||||
// Ignore any Git error because Git initialization is not required.
|
if init_git {
|
||||||
let _ = Command::new("git")
|
// Ignore any Git error because Git initialization is not required.
|
||||||
.arg("init")
|
let _ = Command::new("git")
|
||||||
.stdin(Stdio::null())
|
.arg("init")
|
||||||
.stderr(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
.status();
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.status();
|
||||||
|
}
|
||||||
|
|
||||||
println!(
|
stdout.queue(SetForegroundColor(Color::Green))?;
|
||||||
"\n{}\n\n{}",
|
stdout.write_all("Initialization done ✓".as_bytes())?;
|
||||||
"Initialization done ✓".green(),
|
stdout.queue(ResetColor)?;
|
||||||
POST_INIT_MSG.bold(),
|
stdout.write_all(b"\n\n")?;
|
||||||
);
|
|
||||||
|
stdout.queue(SetAttribute(Attribute::Bold))?;
|
||||||
|
stdout.write_all(POST_INIT_MSG)?;
|
||||||
|
stdout.queue(ResetColor)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -92,19 +196,22 @@ const INIT_SOLUTION_FILE: &[u8] = b"fn main() {
|
|||||||
}
|
}
|
||||||
";
|
";
|
||||||
|
|
||||||
const GITIGNORE: &[u8] = b".rustlings-state.txt
|
pub const RUST_ANALYZER_TOML: &[u8] = br#"check.command = "clippy"
|
||||||
solutions
|
check.extraArgs = ["--profile", "test"]
|
||||||
Cargo.lock
|
cargo.targetDir = true
|
||||||
target
|
"#;
|
||||||
.vscode
|
|
||||||
|
const GITIGNORE: &[u8] = b"Cargo.lock
|
||||||
|
target/
|
||||||
|
.vscode/
|
||||||
";
|
";
|
||||||
|
|
||||||
pub const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#;
|
pub const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#;
|
||||||
|
|
||||||
const CARGO_TOML_EXISTS_ERR: &str = "The current directory contains the file `Cargo.toml`.
|
const IN_INITIALIZED_DIR_ERR: &str = "It looks like Rustlings is already initialized in this directory.
|
||||||
|
|
||||||
If you already initialized Rustlings, run the command `rustlings` for instructions on getting started with the exercises.
|
If you already initialized Rustlings, run the command `rustlings` for instructions on getting started with the exercises.
|
||||||
Otherwise, please run `rustlings init` again in another directory.";
|
Otherwise, please run `rustlings init` again in a different directory.";
|
||||||
|
|
||||||
const RUSTLINGS_DIR_ALREADY_EXISTS_ERR: &str =
|
const RUSTLINGS_DIR_ALREADY_EXISTS_ERR: &str =
|
||||||
"A directory with the name `rustlings` already exists in the current directory.
|
"A directory with the name `rustlings` already exists in the current directory.
|
||||||
@ -112,5 +219,6 @@ You probably already initialized Rustlings.
|
|||||||
Run `cd rustlings`
|
Run `cd rustlings`
|
||||||
Then run `rustlings` again";
|
Then run `rustlings` again";
|
||||||
|
|
||||||
const POST_INIT_MSG: &str = "Run `cd rustlings` to go into the generated directory.
|
const POST_INIT_MSG: &[u8] = b"Run `cd rustlings` to go into the generated directory.
|
||||||
Then run `rustlings` to get started.";
|
Then run `rustlings` to get started.
|
||||||
|
";
|
||||||
|
|||||||
192
src/list.rs
192
src/list.rs
@ -1,90 +1,134 @@
|
|||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
event::{self, Event, KeyCode, KeyEventKind},
|
QueueableCommand, cursor,
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
event::{
|
||||||
ExecutableCommand,
|
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, MouseEventKind,
|
||||||
|
},
|
||||||
|
terminal::{
|
||||||
|
DisableLineWrap, EnableLineWrap, EnterAlternateScreen, LeaveAlternateScreen,
|
||||||
|
disable_raw_mode, enable_raw_mode,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use ratatui::{backend::CrosstermBackend, Terminal};
|
use std::io::{self, StdoutLock, Write};
|
||||||
use std::io;
|
|
||||||
|
|
||||||
use crate::app_state::AppState;
|
use crate::app_state::AppState;
|
||||||
|
|
||||||
use self::state::{Filter, UiState};
|
use self::state::{Filter, ListState};
|
||||||
|
|
||||||
|
mod scroll_state;
|
||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
|
fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> {
|
||||||
|
let mut list_state = ListState::build(app_state, stdout)?;
|
||||||
|
let mut is_searching = false;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match event::read().context("Failed to read terminal event")? {
|
||||||
|
Event::Key(key) => {
|
||||||
|
match key.kind {
|
||||||
|
KeyEventKind::Release => continue,
|
||||||
|
KeyEventKind::Press | KeyEventKind::Repeat => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
list_state.message.clear();
|
||||||
|
|
||||||
|
if is_searching {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Esc | KeyCode::Enter => {
|
||||||
|
is_searching = false;
|
||||||
|
list_state.search_query.clear();
|
||||||
|
}
|
||||||
|
KeyCode::Char(c) => {
|
||||||
|
list_state.search_query.push(c);
|
||||||
|
list_state.apply_search_query();
|
||||||
|
}
|
||||||
|
KeyCode::Backspace => {
|
||||||
|
list_state.search_query.pop();
|
||||||
|
list_state.apply_search_query();
|
||||||
|
}
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
|
||||||
|
list_state.draw(stdout)?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('q') => return Ok(()),
|
||||||
|
KeyCode::Down | KeyCode::Char('j') => list_state.select_next(),
|
||||||
|
KeyCode::Up | KeyCode::Char('k') => list_state.select_previous(),
|
||||||
|
KeyCode::Home | KeyCode::Char('g') => list_state.select_first(),
|
||||||
|
KeyCode::End | KeyCode::Char('G') => list_state.select_last(),
|
||||||
|
KeyCode::Char('d') => {
|
||||||
|
if list_state.filter() == Filter::Done {
|
||||||
|
list_state.set_filter(Filter::None);
|
||||||
|
list_state.message.push_str("Disabled filter DONE");
|
||||||
|
} else {
|
||||||
|
list_state.set_filter(Filter::Done);
|
||||||
|
list_state.message.push_str(
|
||||||
|
"Enabled filter DONE │ Press d again to disable the filter",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char('p') => {
|
||||||
|
if list_state.filter() == Filter::Pending {
|
||||||
|
list_state.set_filter(Filter::None);
|
||||||
|
list_state.message.push_str("Disabled filter PENDING");
|
||||||
|
} else {
|
||||||
|
list_state.set_filter(Filter::Pending);
|
||||||
|
list_state.message.push_str(
|
||||||
|
"Enabled filter PENDING │ Press p again to disable the filter",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char('r') => list_state.reset_selected()?,
|
||||||
|
KeyCode::Char('c') => {
|
||||||
|
if list_state.selected_to_current_exercise()? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char('s' | '/') => {
|
||||||
|
is_searching = true;
|
||||||
|
list_state.apply_search_query();
|
||||||
|
}
|
||||||
|
// Redraw to remove the message.
|
||||||
|
KeyCode::Esc => (),
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Mouse(event) => match event.kind {
|
||||||
|
MouseEventKind::ScrollDown => list_state.select_next(),
|
||||||
|
MouseEventKind::ScrollUp => list_state.select_previous(),
|
||||||
|
_ => continue,
|
||||||
|
},
|
||||||
|
Event::Resize(width, height) => list_state.set_term_size(width, height),
|
||||||
|
// Ignore
|
||||||
|
Event::FocusGained | Event::FocusLost => continue,
|
||||||
|
}
|
||||||
|
|
||||||
|
list_state.draw(stdout)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn list(app_state: &mut AppState) -> Result<()> {
|
pub fn list(app_state: &mut AppState) -> Result<()> {
|
||||||
let mut stdout = io::stdout().lock();
|
let mut stdout = io::stdout().lock();
|
||||||
stdout.execute(EnterAlternateScreen)?;
|
stdout
|
||||||
|
.queue(EnterAlternateScreen)?
|
||||||
|
.queue(cursor::Hide)?
|
||||||
|
.queue(DisableLineWrap)?
|
||||||
|
.queue(EnableMouseCapture)?;
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
|
|
||||||
let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?;
|
let res = handle_list(app_state, &mut stdout);
|
||||||
terminal.clear()?;
|
|
||||||
|
|
||||||
let mut ui_state = UiState::new(app_state);
|
// Restore the terminal even if we got an error.
|
||||||
|
stdout
|
||||||
'outer: loop {
|
.queue(LeaveAlternateScreen)?
|
||||||
terminal.draw(|frame| ui_state.draw(frame).unwrap())?;
|
.queue(cursor::Show)?
|
||||||
|
.queue(EnableLineWrap)?
|
||||||
let key = loop {
|
.queue(DisableMouseCapture)?
|
||||||
match event::read()? {
|
.flush()?;
|
||||||
Event::Key(key) => match key.kind {
|
|
||||||
KeyEventKind::Press | KeyEventKind::Repeat => break key,
|
|
||||||
KeyEventKind::Release => (),
|
|
||||||
},
|
|
||||||
// Redraw
|
|
||||||
Event::Resize(_, _) => continue 'outer,
|
|
||||||
// Ignore
|
|
||||||
Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => (),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ui_state.message.clear();
|
|
||||||
|
|
||||||
match key.code {
|
|
||||||
KeyCode::Char('q') => break,
|
|
||||||
KeyCode::Down | KeyCode::Char('j') => ui_state.select_next(),
|
|
||||||
KeyCode::Up | KeyCode::Char('k') => ui_state.select_previous(),
|
|
||||||
KeyCode::Home | KeyCode::Char('g') => ui_state.select_first(),
|
|
||||||
KeyCode::End | KeyCode::Char('G') => ui_state.select_last(),
|
|
||||||
KeyCode::Char('d') => {
|
|
||||||
let message = if ui_state.filter == Filter::Done {
|
|
||||||
ui_state.filter = Filter::None;
|
|
||||||
"Disabled filter DONE"
|
|
||||||
} else {
|
|
||||||
ui_state.filter = Filter::Done;
|
|
||||||
"Enabled filter DONE │ Press d again to disable the filter"
|
|
||||||
};
|
|
||||||
|
|
||||||
ui_state = ui_state.with_updated_rows();
|
|
||||||
ui_state.message.push_str(message);
|
|
||||||
}
|
|
||||||
KeyCode::Char('p') => {
|
|
||||||
let message = if ui_state.filter == Filter::Pending {
|
|
||||||
ui_state.filter = Filter::None;
|
|
||||||
"Disabled filter PENDING"
|
|
||||||
} else {
|
|
||||||
ui_state.filter = Filter::Pending;
|
|
||||||
"Enabled filter PENDING │ Press p again to disable the filter"
|
|
||||||
};
|
|
||||||
|
|
||||||
ui_state = ui_state.with_updated_rows();
|
|
||||||
ui_state.message.push_str(message);
|
|
||||||
}
|
|
||||||
KeyCode::Char('r') => {
|
|
||||||
ui_state = ui_state.with_reset_selected()?;
|
|
||||||
}
|
|
||||||
KeyCode::Char('c') => {
|
|
||||||
ui_state.selected_to_current_exercise()?;
|
|
||||||
ui_state = ui_state.with_updated_rows();
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
drop(terminal);
|
|
||||||
stdout.execute(LeaveAlternateScreen)?;
|
|
||||||
disable_raw_mode()?;
|
disable_raw_mode()?;
|
||||||
|
|
||||||
Ok(())
|
res
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user