mirror of
https://github.com/rust-lang/rustlings.git
synced 2025-12-26 00:11:49 +02:00
Compare commits
263 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
4
.github/workflows/rust.yml
vendored
4
.github/workflows/rust.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
with:
|
||||
globs: "exercises/**/*.md"
|
||||
- name: Run cargo fmt
|
||||
run: cargo fmt --all -- --check
|
||||
run: cargo fmt --all --check
|
||||
test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: swatinem/rust-cache@v2
|
||||
- name: Run cargo test
|
||||
run: cargo test
|
||||
run: cargo test --workspace
|
||||
dev-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[default.extend-words]
|
||||
"earch" = "earch" # Because of <s>earch in the list footer
|
||||
|
||||
[files]
|
||||
extend-exclude = [
|
||||
"CHANGELOG.md",
|
||||
]
|
||||
|
||||
[default.extend-words]
|
||||
"ratatui" = "ratatui"
|
||||
|
||||
94
CHANGELOG.md
94
CHANGELOG.md
@@ -1,3 +1,93 @@
|
||||
<a name="6.4.0"></a>
|
||||
|
||||
## 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 third-party 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.
|
||||
|
||||
<a name="6.3.0"></a>
|
||||
|
||||
## 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.
|
||||
|
||||
<a name="6.2.0"></a>
|
||||
|
||||
## 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.
|
||||
|
||||
<a name="6.1.0"></a>
|
||||
|
||||
## 6.1.0 (2024-07-10)
|
||||
@@ -54,7 +144,7 @@ You can read about the motivations of this change in [this issue](https://github
|
||||
|
||||
### 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.
|
||||
It offers the following features:
|
||||
|
||||
@@ -755,7 +845,7 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER
|
||||
|
||||
#### 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:**
|
||||
- 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))
|
||||
|
||||
687
Cargo.lock
generated
687
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
61
Cargo.toml
61
Cargo.toml
@@ -1,27 +1,26 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
exclude = [
|
||||
"tests/fixture/failure",
|
||||
"tests/fixture/state",
|
||||
"tests/fixture/success",
|
||||
"tests/test_exercises",
|
||||
"dev",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "6.1.0"
|
||||
version = "6.4.0"
|
||||
authors = [
|
||||
"Liv <mokou@fastmail.com>",
|
||||
"Mo Bitar <mo8it@proton.me>",
|
||||
"Mo Bitar <mo8it@proton.me>", # https://github.com/mo8it
|
||||
"Liv <mokou@fastmail.com>", # https://github.com/shadows-withal
|
||||
# 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"
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
edition = "2021" # On Update: Update the edition of the `rustfmt` command that checks the solutions.
|
||||
rust-version = "1.80"
|
||||
|
||||
[workspace.dependencies]
|
||||
serde = { version = "1.0.204", features = ["derive"] }
|
||||
toml_edit = { version = "0.22.15", default-features = false, features = ["parse", "serde"] }
|
||||
serde = { version = "1.0.214", features = ["derive"] }
|
||||
toml_edit = { version = "0.22.22", default-features = false, features = ["parse", "serde"] }
|
||||
|
||||
[package]
|
||||
name = "rustlings"
|
||||
@@ -31,6 +30,7 @@ authors.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
keywords = [
|
||||
"exercise",
|
||||
"learning",
|
||||
@@ -46,21 +46,21 @@ include = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.86"
|
||||
clap = { version = "4.5.9", features = ["derive"] }
|
||||
crossterm = "0.27.0"
|
||||
hashbrown = "0.14.5"
|
||||
notify-debouncer-mini = { version = "0.4.1", default-features = false }
|
||||
os_pipe = "1.2.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"
|
||||
anyhow = "1.0.93"
|
||||
clap = { version = "4.5.20", features = ["derive"] }
|
||||
crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] }
|
||||
notify = "7.0.0"
|
||||
os_pipe = "1.2.1"
|
||||
rustlings-macros = { path = "rustlings-macros", version = "=6.4.0" }
|
||||
serde_json = "1.0.132"
|
||||
serde.workspace = true
|
||||
toml_edit.workspace = true
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
rustix = { version = "0.38.38", default-features = false, features = ["std", "stdio", "termios"] }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2.0.14"
|
||||
predicates = "3.1.0"
|
||||
tempfile = "3.14.0"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
@@ -70,3 +70,22 @@ panic = "abort"
|
||||
|
||||
[package.metadata.release]
|
||||
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"
|
||||
# TODO: Remove after the following fix is released: https://github.com/rust-lang/rust-clippy/pull/13102
|
||||
needless_option_as_deref = "allow"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
42
README.md
42
README.md
@@ -17,7 +17,7 @@ It contains code examples and exercises similar to Rustlings, but online.
|
||||
|
||||
### Installing Rust
|
||||
|
||||
Before installing Rustlings, you need to have _Rust installed_.
|
||||
Before installing Rustlings, you need to have the **latest version of 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.
|
||||
|
||||
@@ -53,6 +53,21 @@ After installing Rustlings, run the following command to initialize the `rustlin
|
||||
rustlings init
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary><strong>If the command <code>rustlings</code> can't be found…</strong> (<em>click to expand</em>)</summary>
|
||||
|
||||
You are probably using Linux and installed Rust using your package manager.
|
||||
|
||||
Cargo installs binaries to the directory `~/.cargo/bin`.
|
||||
Sadly, package managers often don't add `~/.cargo/bin` to your `PATH` environment variable.
|
||||
|
||||
The solution is to …
|
||||
|
||||
- either add `~/.cargo/bin` manually to `PATH`
|
||||
- or to uninstall Rust from the package manager and install it using the official way with `rustup`: https://www.rust-lang.org/tools/install
|
||||
|
||||
</details>
|
||||
|
||||
Now, go into the newly initialized directory and launch Rustlings for further instructions on getting started with the exercises:
|
||||
|
||||
```bash
|
||||
@@ -73,8 +88,6 @@ While working with Rustlings, please use a modern terminal for the best user exp
|
||||
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>`.
|
||||
@@ -111,21 +124,30 @@ 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)
|
||||
- `r`: Reset status and file of the selected exercise (you need to _reload/reopen_ its file in your editor afterwards)
|
||||
|
||||
See the footer of the list for all possible keys.
|
||||
|
||||
## Questions?
|
||||
|
||||
If you need any help while doing the exercises and the builtin-hints aren't helpful, feel free to ask in the [_Q&A_ category of the discussions](https://github.com/rust-lang/rustlings/discussions/categories/q-a?discussions_q=) if your question wasn't asked yet 💡
|
||||
|
||||
## Third-Party Exercises
|
||||
|
||||
Third-party exercises are a set of exercises maintained by the community.
|
||||
You can use the same `rustlings` program that you installed with `cargo install rustlings` to run them:
|
||||
|
||||
- [日本語版 Rustlings](https://github.com/sotanengel/rustlings-jp):A Japanese translation of the Rustlings 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 the guide about [third-party exercises](https://github.com/rust-lang/rustlings/blob/main/THIRD_PARTY_EXERCISES.md)!
|
||||
|
||||
## 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:
|
||||
|
||||
15
clippy.toml
Normal file
15
clippy.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
disallowed-types = [
|
||||
# Inefficient. Use `.queue(…)` instead.
|
||||
"crossterm::style::Stylize",
|
||||
"crossterm::style::styled_content::StyledContent",
|
||||
]
|
||||
|
||||
disallowed-methods = [
|
||||
# Inefficient. Use `.queue(…)` instead.
|
||||
"crossterm::style::style",
|
||||
# Use `thread::Builder::spawn` instead and handle the error.
|
||||
"std::thread::spawn",
|
||||
"std::thread::Scope::spawn",
|
||||
# Return `ExitCode` instead.
|
||||
"std::process::exit",
|
||||
]
|
||||
@@ -195,3 +195,29 @@ name = "exercises"
|
||||
edition = "2021"
|
||||
# Don't publish the exercises on crates.io!
|
||||
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
|
||||
// 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.
|
||||
|
||||
fn main() {
|
||||
println!("Hello and");
|
||||
println!(r#" welcome to... "#);
|
||||
println!(r#" Welcome to... "#);
|
||||
println!(r#" _ _ _ "#);
|
||||
println!(r#" _ __ _ _ ___| |_| (_)_ __ __ _ ___ "#);
|
||||
println!(r#" | '__| | | / __| __| | | '_ \ / _` / __| "#);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
fn main() {
|
||||
// TODO: Add missing keyword.
|
||||
// TODO: Add the missing keyword.
|
||||
x = 5;
|
||||
|
||||
println!("x has the value {x}");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
fn call_me(num: u32) {
|
||||
fn call_me(num: u8) {
|
||||
for i in 0..num {
|
||||
println!("Ring! Call number {}", i + 1);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// TODO: Fix the compiler error on this function.
|
||||
fn foo_if_fizz(fizzish: &str) -> &str {
|
||||
if fizzish == "fizz" {
|
||||
"foo"
|
||||
fn picky_eater(food: &str) -> &str {
|
||||
if food == "strawberry" {
|
||||
"Yummy!"
|
||||
} else {
|
||||
1
|
||||
}
|
||||
@@ -18,18 +18,20 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn foo_for_fizz() {
|
||||
// This means that calling `foo_if_fizz` with the argument "fizz" should return "foo".
|
||||
assert_eq!(foo_if_fizz("fizz"), "foo");
|
||||
fn yummy_food() {
|
||||
// This means that calling `picky_eater` with the argument "food" should return "Yummy!".
|
||||
assert_eq!(picky_eater("strawberry"), "Yummy!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bar_for_fuzz() {
|
||||
assert_eq!(foo_if_fizz("fuzz"), "bar");
|
||||
fn neutral_food() {
|
||||
assert_eq!(picky_eater("potato"), "I guess I can eat that.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_to_baz() {
|
||||
assert_eq!(foo_if_fizz("literally anything"), "baz");
|
||||
fn default_disliked_food() {
|
||||
assert_eq!(picky_eater("broccoli"), "No thanks!");
|
||||
assert_eq!(picky_eater("gummy bears"), "No thanks!");
|
||||
assert_eq!(picky_eater("literally anything"), "No thanks!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Point {
|
||||
x: u64,
|
||||
|
||||
@@ -4,7 +4,11 @@ struct Point {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// You can bring module paths into scopes and provide new names for them with
|
||||
// the `use` and `as` keywords.
|
||||
|
||||
#[allow(dead_code)]
|
||||
mod delicious_snacks {
|
||||
// TODO: Add the following two `use` statements after fixing them.
|
||||
// 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
|
||||
// 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,
|
||||
// Mango, and Lyche).
|
||||
// Mango, and Lychee).
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
||||
@@ -10,14 +10,14 @@ use std::collections::HashMap;
|
||||
|
||||
// A structure to store the goal details of a team.
|
||||
#[derive(Default)]
|
||||
struct Team {
|
||||
struct TeamScores {
|
||||
goals_scored: 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.
|
||||
let mut scores = HashMap::new();
|
||||
let mut scores = HashMap::<&str, TeamScores>::new();
|
||||
|
||||
for line in results.lines() {
|
||||
let mut split_iterator = line.split(',');
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// of `Option<String>`.
|
||||
fn generate_nametag_text(name: String) -> Option<String> {
|
||||
if name.is_empty() {
|
||||
// Empty names aren't allowed.
|
||||
// Empty names aren't allowed
|
||||
None
|
||||
} else {
|
||||
Some(format!("Hi! My name is {name}"))
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(clippy::comparison_chain)]
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
enum CreationError {
|
||||
Negative,
|
||||
|
||||
@@ -25,7 +25,7 @@ impl ParsePosNonzeroError {
|
||||
}
|
||||
|
||||
// TODO: Add another error conversion function here.
|
||||
// fn from_parseint(???) -> Self { ??? }
|
||||
// fn from_parse_int(???) -> Self { ??? }
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
trait Licensed {
|
||||
// TODO: Add a default implementation for `licensing_info` so that
|
||||
// implementors like the two structs below can share that default behavior
|
||||
|
||||
@@ -9,7 +9,7 @@ impl Rectangle {
|
||||
if width <= 0 || height <= 0 {
|
||||
// Returning a `Result` would be better here. But we want to learn
|
||||
// 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 }
|
||||
|
||||
@@ -8,7 +8,6 @@ use std::rc::Rc;
|
||||
#[derive(Debug)]
|
||||
struct Sun;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
enum Planet {
|
||||
Mercury(Rc<Sun>),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// This program spawns multiple threads that each run for at least 250ms, and
|
||||
// each thread returns how much time they took to complete. The program should
|
||||
// This program spawns multiple threads that each runs for at least 250ms, and
|
||||
// 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
|
||||
// return values into a vector.
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::{sync::mpsc, thread, time::Duration};
|
||||
|
||||
struct Queue {
|
||||
length: u32,
|
||||
first_half: Vec<u32>,
|
||||
second_half: Vec<u32>,
|
||||
}
|
||||
@@ -9,7 +8,6 @@ struct Queue {
|
||||
impl Queue {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
length: 10,
|
||||
first_half: vec![1, 2, 3, 4, 5],
|
||||
second_half: vec![6, 7, 8, 9, 10],
|
||||
}
|
||||
@@ -48,17 +46,15 @@ mod tests {
|
||||
fn threads3() {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let queue = Queue::new();
|
||||
let queue_length = queue.length;
|
||||
|
||||
send_tx(queue, tx);
|
||||
|
||||
let mut total_received: u32 = 0;
|
||||
for received in rx {
|
||||
println!("Got: {received}");
|
||||
total_received += 1;
|
||||
let mut received = Vec::with_capacity(10);
|
||||
for value in rx {
|
||||
received.push(value);
|
||||
}
|
||||
|
||||
println!("Number of received values: {total_received}");
|
||||
assert_eq!(total_received, queue_length);
|
||||
received.sort();
|
||||
assert_eq!(received, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
// 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.
|
||||
|
||||
// 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.
|
||||
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.
|
||||
|
||||
@@ -25,7 +25,7 @@ enum ParsePersonError {
|
||||
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".
|
||||
// Note that you'll need to parse the age component into a `u8` with something
|
||||
// like `"4".parse::<u8>()`.
|
||||
|
||||
@@ -26,7 +26,7 @@ enum Command {
|
||||
mod my_module {
|
||||
use super::Command;
|
||||
|
||||
// TODO: Complete the function.
|
||||
// TODO: Complete the function as described above.
|
||||
// pub fn transformer(input: ???) -> ??? { ??? }
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
# Error out if any command fails
|
||||
set -e
|
||||
|
||||
cargo run -- dev check
|
||||
typos
|
||||
cargo outdated -w --exit-code 1
|
||||
cargo upgrades
|
||||
|
||||
# Similar to CI
|
||||
cargo clippy -- --deny warnings
|
||||
cargo fmt --all --check
|
||||
cargo test --workspace --all-targets
|
||||
cargo run -- dev check --require-solutions
|
||||
|
||||
# MSRV
|
||||
cargo +1.80 run -- dev check --require-solutions
|
||||
|
||||
@@ -6,6 +6,7 @@ authors.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
include = [
|
||||
"/src/",
|
||||
"/info.toml",
|
||||
@@ -15,6 +16,9 @@ include = [
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
quote = "1.0.36"
|
||||
quote = "1.0.37"
|
||||
serde.workspace = true
|
||||
toml_edit.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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
|
||||
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!
|
||||
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
|
||||
the exercise file in your editor, fix errors and save the file. Rustlings will
|
||||
automatically detect the file change and rerun the exercise. If all errors are
|
||||
fixed, Rustlings will ask you to move on to the next exercise.
|
||||
the exercise file in your editor, fix errors and save the file. Rustlings
|
||||
will automatically detect the file change and rerun the exercise. If all
|
||||
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.
|
||||
4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub!
|
||||
(https://github.com/rust-lang/rustlings). We look at every issue, and sometimes,
|
||||
other learners do too so you can help each other out!"""
|
||||
4. If an exercise doesn't make sense to you, feel free to open an issue on
|
||||
GitHub! (https://github.com/rust-lang/rustlings). We look at every issue, and
|
||||
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.
|
||||
You can also contribute your own exercises to help the greater community!
|
||||
|
||||
@@ -120,10 +122,10 @@ dir = "01_variables"
|
||||
test = false
|
||||
hint = """
|
||||
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
|
||||
of `let`.
|
||||
Constants are always immutable. They are declared with the keyword `const`
|
||||
instead of `let`.
|
||||
|
||||
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:
|
||||
```
|
||||
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`
|
||||
@@ -319,7 +321,8 @@ hint = """
|
||||
In the first function, we create an empty vector and want to push new elements
|
||||
to it.
|
||||
|
||||
In the second function, we map the values of the input and collect them into a vector.
|
||||
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.
|
||||
@@ -332,8 +335,8 @@ What do you think is the more commonly used pattern under Rust developers?"""
|
||||
name = "move_semantics1"
|
||||
dir = "06_move_semantics"
|
||||
hint = """
|
||||
So you've got the "cannot borrow `vec` as mutable, as it is not declared as mutable"
|
||||
error on the line where we push an element to the vector, right?
|
||||
So you've got the "cannot borrow `vec` as mutable, as it is not declared as
|
||||
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 line where we push to the vector (where the error is).
|
||||
@@ -369,7 +372,8 @@ hint = """
|
||||
Carefully reason about the range in which each mutable reference is in
|
||||
scope. Does it help to update the value of `x` immediately after
|
||||
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."""
|
||||
|
||||
[[exercises]]
|
||||
@@ -498,14 +502,18 @@ some of them:
|
||||
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
|
||||
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]]
|
||||
name = "strings4"
|
||||
dir = "09_strings"
|
||||
test = false
|
||||
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:
|
||||
`placeholder("blue");`
|
||||
@@ -567,12 +575,8 @@ https://doc.rust-lang.org/book/ch08-03-hash-maps.html#only-inserting-a-value-if-
|
||||
name = "hashmaps3"
|
||||
dir = "11_hashmaps"
|
||||
hint = """
|
||||
Hint 1: Use the `entry()` and `or_insert()` (or `or_insert_with()`) methods of
|
||||
`HashMap` to insert the default value of `Team` if a team doesn't
|
||||
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 1: Use the `entry()` and `or_default()` methods of `HashMap` to insert the
|
||||
default value of `TeamScores` if a team doesn't exist in the table yet.
|
||||
|
||||
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.
|
||||
@@ -1136,7 +1140,7 @@ constants, but clippy recognizes those imprecise mathematical constants as a
|
||||
source of potential error.
|
||||
|
||||
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]]
|
||||
name = "clippy2"
|
||||
@@ -1197,7 +1201,8 @@ hint = """
|
||||
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?
|
||||
|
||||
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]]
|
||||
name = "as_ref_mut"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
fn call_me(num: u32) {
|
||||
fn call_me(num: u8) {
|
||||
for i in 0..num {
|
||||
println!("Ring! Call number {}", i + 1);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
fn foo_if_fizz(fizzish: &str) -> &str {
|
||||
if fizzish == "fizz" {
|
||||
"foo"
|
||||
} else if fizzish == "fuzz" {
|
||||
"bar"
|
||||
fn picky_eater(food: &str) -> &str {
|
||||
if food == "strawberry" {
|
||||
"Yummy!"
|
||||
} else if food == "potato" {
|
||||
"I guess I can eat that."
|
||||
} else {
|
||||
"baz"
|
||||
"No thanks!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,17 +17,19 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn foo_for_fizz() {
|
||||
assert_eq!(foo_if_fizz("fizz"), "foo");
|
||||
fn yummy_food() {
|
||||
assert_eq!(picky_eater("strawberry"), "Yummy!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bar_for_fuzz() {
|
||||
assert_eq!(foo_if_fizz("fuzz"), "bar");
|
||||
fn neutral_food() {
|
||||
assert_eq!(picky_eater("potato"), "I guess I can eat that.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_to_baz() {
|
||||
assert_eq!(foo_if_fizz("literally anything"), "baz");
|
||||
fn default_disliked_food() {
|
||||
assert_eq!(picky_eater("broccoli"), "No thanks!");
|
||||
assert_eq!(picky_eater("gummy bears"), "No thanks!");
|
||||
assert_eq!(picky_eater("literally anything"), "No thanks!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Point {
|
||||
x: u64,
|
||||
|
||||
@@ -46,8 +46,8 @@ impl State {
|
||||
match message {
|
||||
Message::Resize { width, height } => self.resize(width, height),
|
||||
Message::Move(point) => self.move_position(point),
|
||||
Message::Echo(s) => self.echo(s),
|
||||
Message::ChangeColor(r, g, b) => self.change_color(r, g, b),
|
||||
Message::Echo(string) => self.echo(string),
|
||||
Message::ChangeColor(red, green, blue) => self.change_color(red, green, blue),
|
||||
Message::Quit => self.quit(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,11 @@ fn main() {
|
||||
// Here, both answers work.
|
||||
// `.into()` converts a type into an expected type.
|
||||
// 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());
|
||||
// But if it is called where `&str` is expected, then `&str` is kept `&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());
|
||||
// ^^^^^^^ the compiler recommends removing the `.into()`
|
||||
// call because it is a useless conversion.
|
||||
|
||||
string(format!("Interpolation {}", "Station"));
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#[allow(dead_code)]
|
||||
mod delicious_snacks {
|
||||
// Added `pub` and used the expected alias after `as`.
|
||||
pub use self::fruits::PEAR as fruit;
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
// 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
|
||||
// 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;
|
||||
|
||||
|
||||
@@ -10,14 +10,14 @@ use std::collections::HashMap;
|
||||
|
||||
// A structure to store the goal details of a team.
|
||||
#[derive(Default)]
|
||||
struct Team {
|
||||
struct TeamScores {
|
||||
goals_scored: 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.
|
||||
let mut scores = HashMap::new();
|
||||
let mut scores = HashMap::<&str, TeamScores>::new();
|
||||
|
||||
for line in results.lines() {
|
||||
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();
|
||||
|
||||
// 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.
|
||||
team_1.goals_scored += team_1_score;
|
||||
team_1.goals_conceded += team_2_score;
|
||||
|
||||
// Similarely for the second team.
|
||||
let team_2 = scores.entry(team_2_name).or_insert_with(Team::default);
|
||||
// Similarly for the second team.
|
||||
let team_2 = scores.entry(team_2_name).or_default();
|
||||
team_2.goals_scored += team_2_score;
|
||||
team_2.goals_conceded += team_1_score;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#![allow(clippy::comparison_chain)]
|
||||
use std::cmp::Ordering;
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
enum CreationError {
|
||||
@@ -11,12 +11,10 @@ struct PositiveNonzeroInteger(u64);
|
||||
|
||||
impl PositiveNonzeroInteger {
|
||||
fn new(value: i64) -> Result<Self, CreationError> {
|
||||
if value == 0 {
|
||||
Err(CreationError::Zero)
|
||||
} else if value < 0 {
|
||||
Err(CreationError::Negative)
|
||||
} else {
|
||||
Ok(Self(value as u64))
|
||||
match value.cmp(&0) {
|
||||
Ordering::Less => Err(CreationError::Negative),
|
||||
Ordering::Equal => Err(CreationError::Zero),
|
||||
Ordering::Greater => Ok(Self(value as u64)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ impl ParsePosNonzeroError {
|
||||
Self::Creation(err)
|
||||
}
|
||||
|
||||
fn from_parseint(err: ParseIntError) -> Self {
|
||||
fn from_parse_int(err: ParseIntError) -> Self {
|
||||
Self::ParseInt(err)
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ impl PositiveNonzeroInteger {
|
||||
fn parse(s: &str) -> Result<Self, ParsePosNonzeroError> {
|
||||
// Return an appropriate error instead of panicking when `parse()`
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
trait Licensed {
|
||||
fn licensing_info(&self) -> String {
|
||||
"Default license".to_string()
|
||||
|
||||
@@ -9,7 +9,7 @@ impl Rectangle {
|
||||
if width <= 0 || height <= 0 {
|
||||
// Returning a `Result` would be better here. But we want to learn
|
||||
// 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 }
|
||||
|
||||
@@ -25,6 +25,7 @@ fn factorial_fold(num: u64) -> u64 {
|
||||
// -> 1 * 2 is calculated, then the result 2 is multiplied by
|
||||
// the second element 3 so the result 6 is returned.
|
||||
// And so on…
|
||||
#[allow(clippy::unnecessary_fold)]
|
||||
(2..=num).fold(1, |acc, x| acc * x)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ use std::rc::Rc;
|
||||
#[derive(Debug)]
|
||||
struct Sun;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
enum Planet {
|
||||
Mercury(Rc<Sun>),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// This program spawns multiple threads that each run for at least 250ms, and
|
||||
// each thread returns how much time they took to complete. The program should
|
||||
// This program spawns multiple threads that each runs for at least 250ms, and
|
||||
// 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
|
||||
// return values into a vector.
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::{sync::mpsc, thread, time::Duration};
|
||||
|
||||
struct Queue {
|
||||
length: u32,
|
||||
first_half: Vec<u32>,
|
||||
second_half: Vec<u32>,
|
||||
}
|
||||
@@ -9,7 +8,6 @@ struct Queue {
|
||||
impl Queue {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
length: 10,
|
||||
first_half: vec![1, 2, 3, 4, 5],
|
||||
second_half: vec![6, 7, 8, 9, 10],
|
||||
}
|
||||
@@ -50,17 +48,15 @@ mod tests {
|
||||
fn threads3() {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let queue = Queue::new();
|
||||
let queue_length = queue.length;
|
||||
|
||||
send_tx(queue, tx);
|
||||
|
||||
let mut total_received: u32 = 0;
|
||||
for received in rx {
|
||||
println!("Got: {received}");
|
||||
total_received += 1;
|
||||
let mut received = Vec::with_capacity(10);
|
||||
for value in rx {
|
||||
received.push(value);
|
||||
}
|
||||
|
||||
println!("Number of received values: {total_received}");
|
||||
assert_eq!(total_received, queue_length);
|
||||
received.sort();
|
||||
assert_eq!(received, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
// 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.
|
||||
|
||||
// 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 {
|
||||
arg.as_ref().as_bytes().len()
|
||||
arg.as_ref().len()
|
||||
}
|
||||
|
||||
// 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.
|
||||
509
src/app_state.rs
509
src/app_state.rs
@@ -1,32 +1,39 @@
|
||||
use anyhow::{bail, Context, Result};
|
||||
use crossterm::style::Stylize;
|
||||
use serde::Deserialize;
|
||||
use anyhow::{bail, Context, Error, Result};
|
||||
use crossterm::{cursor, terminal, QueueableCommand};
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::{Read, StdoutLock, Write},
|
||||
path::{Path, PathBuf},
|
||||
collections::HashSet,
|
||||
env,
|
||||
fs::{File, OpenOptions},
|
||||
io::{Read, Seek, StdoutLock, Write},
|
||||
path::{Path, MAIN_SEPARATOR_STR},
|
||||
process::{Command, Stdio},
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering::Relaxed},
|
||||
mpsc,
|
||||
},
|
||||
thread,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
clear_terminal,
|
||||
cmd::CmdRunner,
|
||||
embedded::EMBEDDED_FILES,
|
||||
exercise::{Exercise, RunnableExercise, OUTPUT_CAPACITY},
|
||||
exercise::{Exercise, RunnableExercise},
|
||||
info_file::ExerciseInfo,
|
||||
DEBUG_PROFILE,
|
||||
term::{self, CheckProgressVisualizer},
|
||||
};
|
||||
|
||||
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]
|
||||
pub enum ExercisesProgress {
|
||||
// All exercises are done.
|
||||
AllDone,
|
||||
// The current exercise failed and is still pending.
|
||||
CurrentPending,
|
||||
// A new exercise is now pending.
|
||||
NewPending,
|
||||
// The current exercise is still pending.
|
||||
CurrentPending,
|
||||
}
|
||||
|
||||
pub enum StateFileStatus {
|
||||
@@ -34,29 +41,12 @@ pub enum StateFileStatus {
|
||||
NotRead,
|
||||
}
|
||||
|
||||
// Parses parts of the output of `cargo metadata`.
|
||||
#[derive(Deserialize)]
|
||||
struct CargoMetadata {
|
||||
target_directory: PathBuf,
|
||||
}
|
||||
|
||||
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)
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum CheckProgress {
|
||||
None,
|
||||
Checking,
|
||||
Done,
|
||||
Pending,
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
@@ -65,67 +55,33 @@ pub struct AppState {
|
||||
// Caches the number of done exercises to avoid iterating over all exercises every time.
|
||||
n_done: u16,
|
||||
final_message: String,
|
||||
state_file: File,
|
||||
// Preallocated buffer for reading and writing the state file.
|
||||
file_buf: Vec<u8>,
|
||||
official_exercises: bool,
|
||||
// Cargo's target directory.
|
||||
target_dir: PathBuf,
|
||||
cmd_runner: CmdRunner,
|
||||
// Running in VS Code.
|
||||
vs_code: bool,
|
||||
}
|
||||
|
||||
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(
|
||||
exercise_infos: Vec<ExerciseInfo>,
|
||||
final_message: String,
|
||||
) -> 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()
|
||||
.map(|exercise_info| {
|
||||
// Leaking to be able to borrow in the watch mode `Table`.
|
||||
@@ -134,33 +90,99 @@ impl AppState {
|
||||
let path = exercise_info.path().leak();
|
||||
let name = exercise_info.name.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 {
|
||||
dir,
|
||||
name,
|
||||
path,
|
||||
canonical_path,
|
||||
test: exercise_info.test,
|
||||
strict_clippy: exercise_info.strict_clippy,
|
||||
hint,
|
||||
// Updated in `Self::update_from_file`.
|
||||
// Updated below.
|
||||
done: false,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut slf = Self {
|
||||
current_exercise_ind: 0,
|
||||
exercises,
|
||||
n_done: 0,
|
||||
final_message,
|
||||
file_buf: Vec::with_capacity(2048),
|
||||
official_exercises: !Path::new("info.toml").exists(),
|
||||
target_dir,
|
||||
let mut current_exercise_ind = 0;
|
||||
let mut n_done = 0;
|
||||
let mut file_buf = Vec::with_capacity(2048);
|
||||
let state_file_status = 'block: {
|
||||
if state_file.read_to_end(&mut file_buf).is_err() {
|
||||
break 'block StateFileStatus::NotRead;
|
||||
}
|
||||
|
||||
// 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: env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode"),
|
||||
};
|
||||
|
||||
Ok((slf, state_file_status))
|
||||
}
|
||||
@@ -180,14 +202,24 @@ impl AppState {
|
||||
self.n_done
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn n_pending(&self) -> u16 {
|
||||
self.exercises.len() as u16 - self.n_done
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn current_exercise(&self) -> &Exercise {
|
||||
&self.exercises[self.current_exercise_ind]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn target_dir(&self) -> &Path {
|
||||
&self.target_dir
|
||||
pub fn cmd_runner(&self) -> &CmdRunner {
|
||||
&self.cmd_runner
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn vs_code(&self) -> bool {
|
||||
self.vs_code
|
||||
}
|
||||
|
||||
// Write the state file.
|
||||
@@ -199,10 +231,8 @@ impl AppState {
|
||||
// - The fourth line is an empty line.
|
||||
// - All remaining lines are the names of done exercises.
|
||||
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
|
||||
.extend_from_slice(self.current_exercise().name.as_bytes());
|
||||
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}"))?;
|
||||
|
||||
Ok(())
|
||||
@@ -246,15 +283,31 @@ impl AppState {
|
||||
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
|
||||
.exercises
|
||||
.get_mut(exercise_ind)
|
||||
.context(BAD_INDEX_ERR)?;
|
||||
|
||||
if exercise.done {
|
||||
exercise.done = false;
|
||||
if exercise.done == done {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
exercise.done = done;
|
||||
if done {
|
||||
self.n_done += 1;
|
||||
} else {
|
||||
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()?;
|
||||
}
|
||||
|
||||
@@ -298,6 +351,7 @@ impl AppState {
|
||||
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> {
|
||||
if exercise_ind >= self.exercises.len() {
|
||||
bail!(BAD_INDEX_ERR);
|
||||
@@ -307,36 +361,33 @@ impl AppState {
|
||||
let exercise = &self.exercises[exercise_ind];
|
||||
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.
|
||||
fn next_pending_exercise_ind(&self) -> Option<usize> {
|
||||
if self.current_exercise_ind == self.exercises.len() - 1 {
|
||||
// The last exercise is done.
|
||||
// Search for exercises not done from the start.
|
||||
return self.exercises[..self.current_exercise_ind]
|
||||
.iter()
|
||||
.position(|exercise| !exercise.done);
|
||||
}
|
||||
|
||||
// The done exercise isn't the last one.
|
||||
// Search for a pending exercise after the current one and then from the start.
|
||||
match self.exercises[self.current_exercise_ind + 1..]
|
||||
.iter()
|
||||
.position(|exercise| !exercise.done)
|
||||
{
|
||||
Some(ind) => Some(self.current_exercise_ind + 1 + ind),
|
||||
None => self.exercises[..self.current_exercise_ind]
|
||||
.iter()
|
||||
.position(|exercise| !exercise.done),
|
||||
}
|
||||
let next_ind = self.current_exercise_ind + 1;
|
||||
self.exercises
|
||||
// If the exercise done isn't the last, search for pending exercises after it.
|
||||
.get(next_ind..)
|
||||
.and_then(|later_exercises| {
|
||||
later_exercises
|
||||
.iter()
|
||||
.position(|exercise| !exercise.done)
|
||||
.map(|ind| next_ind + ind)
|
||||
})
|
||||
// Search from the start.
|
||||
.or_else(|| {
|
||||
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.
|
||||
pub fn current_solution_path(&self) -> Result<Option<String>> {
|
||||
if DEBUG_PROFILE {
|
||||
if cfg!(debug_assertions) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -347,24 +398,133 @@ impl AppState {
|
||||
.write_solution_to_disk(self.current_exercise_ind, current_exercise.name)
|
||||
.map(Some)
|
||||
} else {
|
||||
let solution_path = if let Some(dir) = current_exercise.dir {
|
||||
format!("solutions/{dir}/{}.rs", current_exercise.name)
|
||||
} else {
|
||||
format!("solutions/{}.rs", current_exercise.name)
|
||||
};
|
||||
let sol_path = current_exercise.sol_path();
|
||||
|
||||
if Path::new(&solution_path).exists() {
|
||||
return Ok(Some(solution_path));
|
||||
if Path::new(&sol_path).exists() {
|
||||
return Ok(Some(sol_path));
|
||||
}
|
||||
|
||||
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.
|
||||
/// 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.
|
||||
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];
|
||||
if !exercise.done {
|
||||
exercise.done = true;
|
||||
@@ -373,62 +533,42 @@ impl AppState {
|
||||
|
||||
if let Some(ind) = self.next_pending_exercise_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);
|
||||
}
|
||||
|
||||
writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?;
|
||||
|
||||
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")?;
|
||||
}
|
||||
self.render_final_message(stdout)?;
|
||||
|
||||
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 …`
|
||||
Did you already install Rust?
|
||||
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 BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
|
||||
const STATE_FILE_HEADER: &[u8] = b"DON'T EDIT THIS FILE!\n\n";
|
||||
const FENISH_LINE: &str = "+----------------------------------------------------+
|
||||
| You made it to the Fe-nish line! |
|
||||
+-------------------------- ------------------------+
|
||||
@@ -460,9 +600,10 @@ mod tests {
|
||||
dir: None,
|
||||
name: "0",
|
||||
path: "exercises/0.rs",
|
||||
canonical_path: None,
|
||||
test: false,
|
||||
strict_clippy: false,
|
||||
hint: String::new(),
|
||||
hint: "",
|
||||
done: false,
|
||||
}
|
||||
}
|
||||
@@ -474,9 +615,11 @@ mod tests {
|
||||
exercises: vec![dummy_exercise(), dummy_exercise(), dummy_exercise()],
|
||||
n_done: 0,
|
||||
final_message: String::new(),
|
||||
state_file: tempfile::tempfile().unwrap(),
|
||||
file_buf: Vec::new(),
|
||||
official_exercises: true,
|
||||
target_dir: PathBuf::new(),
|
||||
cmd_runner: CmdRunner::build().unwrap(),
|
||||
vs_code: false,
|
||||
};
|
||||
|
||||
let mut assert = |done: [bool; 3], expected: [Option<usize>; 3]| {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::info_file::ExerciseInfo;
|
||||
use crate::{exercise::RunnableExercise, info_file::ExerciseInfo};
|
||||
|
||||
/// Initial capacity of the bins buffer.
|
||||
pub const BINS_BUFFER_CAPACITY: usize = 1 << 14;
|
||||
|
||||
174
src/cmd.rs
174
src/cmd.rs
@@ -1,30 +1,44 @@
|
||||
use anyhow::{Context, Result};
|
||||
use std::{io::Read, path::Path, process::Command};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
io::Read,
|
||||
path::PathBuf,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
/// 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.
|
||||
pub fn run_cmd(mut cmd: Command, description: &str, output: &mut Vec<u8>) -> Result<bool> {
|
||||
let (mut reader, writer) = os_pipe::pipe()
|
||||
.with_context(|| format!("Failed to create a pipe to run the command `{description}``"))?;
|
||||
fn run_cmd(mut cmd: Command, description: &str, output: Option<&mut Vec<u8>>) -> Result<bool> {
|
||||
let spawn = |mut cmd: Command| {
|
||||
// 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(|| {
|
||||
format!("Failed to clone the pipe writer for the command `{description}`")
|
||||
})?;
|
||||
let mut handle = if let Some(output) = output {
|
||||
let (mut reader, writer) = os_pipe::pipe().with_context(|| {
|
||||
format!("Failed to create a pipe to run the command `{description}``")
|
||||
})?;
|
||||
|
||||
let mut handle = cmd
|
||||
.stdout(writer_clone)
|
||||
.stderr(writer)
|
||||
.spawn()
|
||||
.with_context(|| format!("Failed to run the command `{description}`"))?;
|
||||
let writer_clone = writer.try_clone().with_context(|| {
|
||||
format!("Failed to clone the pipe writer for the command `{description}`")
|
||||
})?;
|
||||
|
||||
// Prevent pipe deadlock.
|
||||
drop(cmd);
|
||||
cmd.stdout(writer_clone).stderr(writer);
|
||||
let handle = spawn(cmd)?;
|
||||
|
||||
reader
|
||||
.read_to_end(output)
|
||||
.with_context(|| format!("Failed to read the output of the command `{description}`"))?;
|
||||
reader
|
||||
.read_to_end(output)
|
||||
.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
|
||||
.wait()
|
||||
@@ -32,50 +46,106 @@ pub fn run_cmd(mut cmd: Command, description: &str, output: &mut Vec<u8>) -> Res
|
||||
.map(|status| status.success())
|
||||
}
|
||||
|
||||
pub struct CargoCmd<'a> {
|
||||
pub subcommand: &'a str,
|
||||
pub args: &'a [&'a str],
|
||||
pub bin_name: &'a str,
|
||||
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,
|
||||
// Parses parts of the output of `cargo metadata`.
|
||||
#[derive(Deserialize)]
|
||||
struct CargoMetadata {
|
||||
target_directory: PathBuf,
|
||||
}
|
||||
|
||||
impl<'a> CargoCmd<'a> {
|
||||
/// Run `cargo SUBCOMMAND --bin EXERCISE_NAME … ARGS`.
|
||||
pub fn run(&mut self) -> Result<bool> {
|
||||
pub struct CmdRunner {
|
||||
target_dir: PathBuf,
|
||||
}
|
||||
|
||||
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");
|
||||
cmd.arg(self.subcommand);
|
||||
cmd.arg(subcommand).arg("-q").arg("--bin").arg(bin_name);
|
||||
|
||||
// A hack to make `cargo run` work when developing Rustlings.
|
||||
if self.dev {
|
||||
cmd.arg("--manifest-path")
|
||||
.arg("dev/Cargo.toml")
|
||||
.arg("--target-dir")
|
||||
.arg(self.target_dir);
|
||||
#[cfg(debug_assertions)]
|
||||
cmd.arg("--manifest-path")
|
||||
.arg("dev/Cargo.toml")
|
||||
.arg("--target-dir")
|
||||
.arg(&self.target_dir);
|
||||
|
||||
if output.is_some() {
|
||||
cmd.arg("--color").arg("always");
|
||||
}
|
||||
|
||||
cmd.arg("--color")
|
||||
.arg("always")
|
||||
.arg("-q")
|
||||
.arg("--bin")
|
||||
.arg(self.bin_name)
|
||||
.args(self.args);
|
||||
CargoSubcommand { cmd, output }
|
||||
}
|
||||
|
||||
if self.hide_warnings {
|
||||
cmd.env("RUSTFLAGS", "-A warnings");
|
||||
}
|
||||
/// The boolean in the returned `Result` is true if the command's exit status is success.
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -86,7 +156,7 @@ mod tests {
|
||||
cmd.arg("Hello");
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ use anyhow::{bail, Context, Result};
|
||||
use clap::Subcommand;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::DEBUG_PROFILE;
|
||||
|
||||
mod check;
|
||||
mod new;
|
||||
mod update;
|
||||
@@ -32,7 +30,7 @@ impl DevCommands {
|
||||
pub fn run(self) -> Result<()> {
|
||||
match self {
|
||||
Self::New { path, no_git } => {
|
||||
if DEBUG_PROFILE {
|
||||
if cfg!(debug_assertions) {
|
||||
bail!("Disabled in the debug build");
|
||||
}
|
||||
|
||||
|
||||
292
src/dev/check.rs
292
src/dev/check.rs
@@ -1,56 +1,60 @@
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use anyhow::{anyhow, bail, Context, Error, Result};
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
collections::HashSet,
|
||||
fs::{self, read_dir, OpenOptions},
|
||||
io::{self, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
atomic::{self, AtomicBool},
|
||||
Mutex,
|
||||
},
|
||||
process::{Command, Stdio},
|
||||
thread,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app_state::parse_target_dir,
|
||||
cargo_toml::{append_bins, bins_start_end_ind, BINS_BUFFER_CAPACITY},
|
||||
cmd::CmdRunner,
|
||||
exercise::{RunnableExercise, OUTPUT_CAPACITY},
|
||||
info_file::{ExerciseInfo, InfoFile},
|
||||
CURRENT_FORMAT_VERSION, DEBUG_PROFILE,
|
||||
CURRENT_FORMAT_VERSION,
|
||||
};
|
||||
|
||||
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`.
|
||||
fn forbidden_char(input: &str) -> Option<char> {
|
||||
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(
|
||||
exercise_infos: &[ExerciseInfo],
|
||||
current_cargo_toml: &str,
|
||||
cargo_toml_path: &str,
|
||||
exercise_path_prefix: &[u8],
|
||||
) -> 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 mut new_bins = Vec::with_capacity(BINS_BUFFER_CAPACITY);
|
||||
append_bins(&mut new_bins, exercise_infos, exercise_path_prefix);
|
||||
|
||||
if old_bins != new_bins {
|
||||
if DEBUG_PROFILE {
|
||||
bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it");
|
||||
if cfg!(debug_assertions) {
|
||||
bail!("The file `dev/Cargo.toml` is outdated. Run `cargo run -- 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(())
|
||||
}
|
||||
|
||||
// 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>> {
|
||||
let mut names = hashbrown::HashSet::with_capacity(info_file.exercises.len());
|
||||
let mut paths = hashbrown::HashSet::with_capacity(info_file.exercises.len());
|
||||
fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
|
||||
let mut names = 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);
|
||||
for exercise_info in &info_file.exercises {
|
||||
@@ -58,6 +62,9 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<hashbrown::HashSet<
|
||||
if name.is_empty() {
|
||||
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) {
|
||||
bail!("Char `{c}` in the exercise name `{name}` is not allowed");
|
||||
}
|
||||
@@ -71,7 +78,7 @@ 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");
|
||||
}
|
||||
|
||||
@@ -96,7 +103,12 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<hashbrown::HashSet<
|
||||
bail!("Didn't find any `// TODO` comment in the file `{path}`.\nYou 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");
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -111,10 +123,7 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<hashbrown::HashSet<
|
||||
// Check `dir` for unexpected files.
|
||||
// Only Rust files in `allowed_rust_files` and `README.md` files are allowed.
|
||||
// Only one level of directory nesting is allowed.
|
||||
fn check_unexpected_files(
|
||||
dir: &str,
|
||||
allowed_rust_files: &hashbrown::HashSet<PathBuf>,
|
||||
) -> Result<()> {
|
||||
fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet<PathBuf>) -> Result<()> {
|
||||
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())
|
||||
};
|
||||
@@ -162,135 +171,204 @@ fn check_unexpected_files(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_exercises_unsolved(info_file: &InfoFile, target_dir: &Path) -> Result<()> {
|
||||
let error_occurred = AtomicBool::new(false);
|
||||
fn check_exercises_unsolved(
|
||||
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!(
|
||||
"Running all exercises to check that they aren't already solved. This may take a while…\n",
|
||||
);
|
||||
thread::scope(|s| {
|
||||
for exercise_info in &info_file.exercises {
|
||||
let handles = info_file
|
||||
.exercises
|
||||
.iter()
|
||||
.filter_map(|exercise_info| {
|
||||
if exercise_info.skip_check_unsolved {
|
||||
continue;
|
||||
return None;
|
||||
}
|
||||
|
||||
s.spawn(|| {
|
||||
let error = |e| {
|
||||
let mut stderr = io::stderr().lock();
|
||||
stderr.write_all(e).unwrap();
|
||||
stderr.write_all(b"\nProblem with the exercise ").unwrap();
|
||||
stderr.write_all(exercise_info.name.as_bytes()).unwrap();
|
||||
stderr.write_all(SEPARATOR).unwrap();
|
||||
error_occurred.store(true, atomic::Ordering::Relaxed);
|
||||
};
|
||||
Some(
|
||||
thread::Builder::new()
|
||||
.spawn(|| exercise_info.run_exercise(None, cmd_runner))
|
||||
.map(|handle| (exercise_info.name.as_str(), handle)),
|
||||
)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.context("Failed to spawn a thread to check if an exercise is already solved")?;
|
||||
|
||||
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
|
||||
match exercise_info.run_exercise(&mut output, target_dir) {
|
||||
Ok(true) => error(b"Already solved!"),
|
||||
Ok(false) => (),
|
||||
Err(e) => error(e.to_string().as_bytes()),
|
||||
}
|
||||
});
|
||||
let n_handles = handles.len();
|
||||
write!(stdout, "Progress: 0/{n_handles}")?;
|
||||
stdout.flush()?;
|
||||
let mut handle_num = 1;
|
||||
|
||||
for (exercise_name, handle) in handles {
|
||||
let Ok(result) = handle.join() else {
|
||||
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) {
|
||||
bail!(CHECK_EXERCISES_UNSOLVED_ERR);
|
||||
write!(stdout, "\rProgress: {handle_num}/{n_handles}")?;
|
||||
stdout.flush()?;
|
||||
handle_num += 1;
|
||||
}
|
||||
stdout.write_all(b"\n")?;
|
||||
|
||||
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) {
|
||||
Ordering::Less => bail!("`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\nPlease migrate to the latest format version"),
|
||||
Ordering::Greater => bail!("`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\nTry updating the Rustlings program"),
|
||||
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)?;
|
||||
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<()> {
|
||||
let paths = Mutex::new(hashbrown::HashSet::with_capacity(info_file.exercises.len()));
|
||||
let error_occurred = AtomicBool::new(false);
|
||||
enum SolutionCheck {
|
||||
Success { sol_path: String },
|
||||
MissingOptional,
|
||||
RunFailure { output: Vec<u8> },
|
||||
Err(Error),
|
||||
}
|
||||
|
||||
println!("Running all solutions. This may take a while…\n");
|
||||
thread::scope(|s| {
|
||||
for exercise_info in &info_file.exercises {
|
||||
s.spawn(|| {
|
||||
let error = |e| {
|
||||
let mut stderr = io::stderr().lock();
|
||||
stderr.write_all(e).unwrap();
|
||||
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);
|
||||
};
|
||||
fn check_solutions(
|
||||
require_solutions: bool,
|
||||
info_file: &'static InfoFile,
|
||||
cmd_runner: &'static CmdRunner,
|
||||
) -> Result<()> {
|
||||
let mut stdout = io::stdout().lock();
|
||||
stdout.write_all(b"Running all solutions...\n")?;
|
||||
|
||||
let path = exercise_info.sol_path();
|
||||
if !Path::new(&path).exists() {
|
||||
let handles = info_file
|
||||
.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 {
|
||||
error(b"Solution missing");
|
||||
return SolutionCheck::Err(anyhow!(
|
||||
"The solution of the exercise {} is missing",
|
||||
exercise_info.name,
|
||||
));
|
||||
}
|
||||
|
||||
// No solution to check.
|
||||
return;
|
||||
return SolutionCheck::MissingOptional;
|
||||
}
|
||||
|
||||
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
|
||||
match exercise_info.run_solution(&mut output, target_dir) {
|
||||
Ok(true) => {
|
||||
paths.lock().unwrap().insert(PathBuf::from(path));
|
||||
}
|
||||
Ok(false) => error(&output),
|
||||
Err(e) => error(e.to_string().as_bytes()),
|
||||
match exercise_info.run_solution(Some(&mut output), cmd_runner) {
|
||||
Ok(true) => SolutionCheck::Success { sol_path },
|
||||
Ok(false) => SolutionCheck::RunFailure { output },
|
||||
Err(e) => SolutionCheck::Err(e),
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.context("Failed to spawn a thread to check a solution")?;
|
||||
|
||||
if error_occurred.load(atomic::Ordering::Relaxed) {
|
||||
bail!("At least one solution failed. See the output above.");
|
||||
let mut sol_paths = HashSet::with_capacity(info_file.exercises.len());
|
||||
let mut fmt_cmd = Command::new("rustfmt");
|
||||
fmt_cmd
|
||||
.arg("--check")
|
||||
.arg("--edition")
|
||||
.arg("2021")
|
||||
.arg("--color")
|
||||
.arg("always")
|
||||
.stdin(Stdio::null());
|
||||
|
||||
let n_handles = handles.len();
|
||||
write!(stdout, "Progress: 0/{n_handles}")?;
|
||||
stdout.flush()?;
|
||||
let mut handle_num = 1;
|
||||
|
||||
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 } => {
|
||||
stdout.write_all(b"\n\n")?;
|
||||
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),
|
||||
}
|
||||
|
||||
write!(stdout, "\rProgress: {handle_num}/{n_handles}")?;
|
||||
stdout.flush()?;
|
||||
handle_num += 1;
|
||||
}
|
||||
stdout.write_all(b"\n")?;
|
||||
|
||||
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",
|
||||
)?;
|
||||
|
||||
if !fmt_cmd
|
||||
.status()
|
||||
.context("Failed to run `rustfmt` on all solution files")?
|
||||
.success()
|
||||
{
|
||||
bail!("Some solutions aren't formatted. Run `rustfmt` on them");
|
||||
}
|
||||
|
||||
check_unexpected_files("solutions", &paths.into_inner().unwrap())?;
|
||||
|
||||
Ok(())
|
||||
handle.join().unwrap()
|
||||
}
|
||||
|
||||
pub fn check(require_solutions: bool) -> Result<()> {
|
||||
let info_file = InfoFile::parse()?;
|
||||
|
||||
// A hack to make `cargo run -- dev check` work when developing Rustlings.
|
||||
if DEBUG_PROFILE {
|
||||
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"")?;
|
||||
if info_file.exercises.len() > MAX_N_EXERCISES {
|
||||
bail!("The maximum number of exercises is {MAX_N_EXERCISES}");
|
||||
}
|
||||
|
||||
let target_dir = parse_target_dir()?;
|
||||
check_exercises(&info_file, &target_dir)?;
|
||||
check_solutions(require_solutions, &info_file, &target_dir)?;
|
||||
if cfg!(debug_assertions) {
|
||||
// A hack to make `cargo run -- dev check` work when developing Rustlings.
|
||||
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(())
|
||||
}
|
||||
|
||||
const SEPARATOR: &[u8] =
|
||||
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.";
|
||||
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";
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::{
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use crate::CURRENT_FORMAT_VERSION;
|
||||
use crate::{init::RUST_ANALYZER_TOML, CURRENT_FORMAT_VERSION};
|
||||
|
||||
// Create a directory relative to the current directory and print its path.
|
||||
fn create_rel_dir(dir_name: &str, current_dir: &str) -> Result<()> {
|
||||
@@ -62,6 +62,8 @@ pub fn new(path: &Path, no_git: bool) -> Result<()> {
|
||||
|
||||
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)?;
|
||||
write_rel_file(
|
||||
".vscode/extensions.json",
|
||||
@@ -76,8 +78,8 @@ pub fn new(path: &Path, no_git: bool) -> Result<()> {
|
||||
|
||||
pub const GITIGNORE: &[u8] = b".rustlings-state.txt
|
||||
Cargo.lock
|
||||
target
|
||||
.vscode
|
||||
target/
|
||||
.vscode/
|
||||
!.vscode/extensions.json
|
||||
";
|
||||
|
||||
|
||||
@@ -4,18 +4,19 @@ use std::fs;
|
||||
use crate::{
|
||||
cargo_toml::updated_cargo_toml,
|
||||
info_file::{ExerciseInfo, InfoFile},
|
||||
DEBUG_PROFILE,
|
||||
};
|
||||
|
||||
// Update the `Cargo.toml` file.
|
||||
fn update_cargo_toml(
|
||||
exercise_infos: &[ExerciseInfo],
|
||||
current_cargo_toml: &str,
|
||||
exercise_path_prefix: &[u8],
|
||||
cargo_toml_path: &str,
|
||||
exercise_path_prefix: &[u8],
|
||||
) -> 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 =
|
||||
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)
|
||||
.context("Failed to write the `Cargo.toml` file")?;
|
||||
@@ -26,21 +27,14 @@ fn update_cargo_toml(
|
||||
pub fn update() -> Result<()> {
|
||||
let info_file = InfoFile::parse()?;
|
||||
|
||||
// A hack to make `cargo run -- dev update` work when developing Rustlings.
|
||||
if DEBUG_PROFILE {
|
||||
update_cargo_toml(
|
||||
&info_file.exercises,
|
||||
include_str!("../../dev-Cargo.toml"),
|
||||
b"../",
|
||||
"dev/Cargo.toml",
|
||||
)
|
||||
.context("Failed to update the file `dev/Cargo.toml`")?;
|
||||
if cfg!(debug_assertions) {
|
||||
// A hack to make `cargo run -- dev update` work when developing Rustlings.
|
||||
update_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../")
|
||||
.context("Failed to update the file `dev/Cargo.toml`")?;
|
||||
|
||||
println!("Updated `dev/Cargo.toml`");
|
||||
} else {
|
||||
let current_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")
|
||||
update_cargo_toml(&info_file.exercises, "Cargo.toml", &[])
|
||||
.context("Failed to update the file `Cargo.toml`")?;
|
||||
|
||||
println!("Updated `Cargo.toml`");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::{Context, Error, Result};
|
||||
use std::{
|
||||
fs::{create_dir, OpenOptions},
|
||||
io::{self, Write},
|
||||
fs::{self, create_dir},
|
||||
io,
|
||||
};
|
||||
|
||||
use crate::info_file::ExerciseInfo;
|
||||
@@ -9,29 +9,6 @@ use crate::info_file::ExerciseInfo;
|
||||
/// Contains all embedded 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.
|
||||
struct ExerciseFiles {
|
||||
// The content of the exercise file.
|
||||
@@ -42,6 +19,16 @@ struct ExerciseFiles {
|
||||
dir_ind: usize,
|
||||
}
|
||||
|
||||
fn create_dir_if_not_exists(path: &str) -> Result<()> {
|
||||
if let Err(e) = create_dir(path) {
|
||||
if 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.
|
||||
pub struct ExerciseDir {
|
||||
pub name: &'static str,
|
||||
@@ -55,21 +42,13 @@ impl ExerciseDir {
|
||||
let mut dir_path = String::with_capacity(20 + self.name.len());
|
||||
dir_path.push_str("exercises/");
|
||||
dir_path.push_str(self.name);
|
||||
|
||||
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}"))
|
||||
);
|
||||
}
|
||||
create_dir_if_not_exists(&dir_path)?;
|
||||
|
||||
let mut readme_path = dir_path;
|
||||
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<()> {
|
||||
create_dir("exercises").context("Failed to create the directory `exercises`")?;
|
||||
|
||||
WriteStrategy::IfNotExists.write(
|
||||
fs::write(
|
||||
"exercises/README.md",
|
||||
include_bytes!("../exercises/README.md"),
|
||||
)?;
|
||||
)
|
||||
.context("Failed to write the file exercises/README.md")?;
|
||||
|
||||
for dir in self.exercise_dirs {
|
||||
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) {
|
||||
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(())
|
||||
@@ -107,7 +100,8 @@ impl EmbeddedFiles {
|
||||
let dir = &self.exercise_dirs[exercise_files.dir_ind];
|
||||
|
||||
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.
|
||||
@@ -116,19 +110,25 @@ impl EmbeddedFiles {
|
||||
exercise_ind: usize,
|
||||
exercise_name: &str,
|
||||
) -> Result<String> {
|
||||
create_dir_if_not_exists("solutions")?;
|
||||
|
||||
let exercise_files = &self.exercise_files[exercise_ind];
|
||||
let dir = &self.exercise_dirs[exercise_files.dir_ind];
|
||||
|
||||
// 14 = 10 + 1 + 3
|
||||
// solutions/ + / + .rs
|
||||
let mut solution_path = String::with_capacity(14 + dir.name.len() + exercise_name.len());
|
||||
solution_path.push_str("solutions/");
|
||||
solution_path.push_str(dir.name);
|
||||
let mut dir_path = String::with_capacity(14 + dir.name.len() + exercise_name.len());
|
||||
dir_path.push_str("solutions/");
|
||||
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_str(exercise_name);
|
||||
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)
|
||||
}
|
||||
|
||||
226
src/exercise.rs
226
src/exercise.rs
@@ -1,46 +1,58 @@
|
||||
use anyhow::Result;
|
||||
use crossterm::style::{style, StyledContent, Stylize};
|
||||
use std::{
|
||||
fmt::{self, Display, Formatter},
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
use crossterm::{
|
||||
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
|
||||
QueueableCommand,
|
||||
};
|
||||
use std::io::{self, StdoutLock, Write};
|
||||
|
||||
use crate::{
|
||||
cmd::{run_cmd, CargoCmd},
|
||||
in_official_repo,
|
||||
terminal_link::TerminalFileLink,
|
||||
DEBUG_PROFILE,
|
||||
cmd::CmdRunner,
|
||||
term::{self, terminal_file_link, write_ansi, CountedWrite},
|
||||
};
|
||||
|
||||
/// The initial capacity of the output buffer.
|
||||
pub const OUTPUT_CAPACITY: usize = 1 << 14;
|
||||
|
||||
pub fn solution_link_line(stdout: &mut StdoutLock, solution_path: &str) -> io::Result<()> {
|
||||
stdout.queue(SetAttribute(Attribute::Bold))?;
|
||||
stdout.write_all(b"Solution")?;
|
||||
stdout.queue(ResetColor)?;
|
||||
stdout.write_all(b" for comparison: ")?;
|
||||
if let Some(canonical_path) = term::canonicalize(solution_path) {
|
||||
terminal_file_link(stdout, solution_path, &canonical_path, Color::Cyan)?;
|
||||
} else {
|
||||
stdout.write_all(solution_path.as_bytes())?;
|
||||
}
|
||||
stdout.write_all(b"\n")
|
||||
}
|
||||
|
||||
// Run an exercise binary and append its output to the `output` buffer.
|
||||
// Compilation must be done before calling this method.
|
||||
fn run_bin(bin_name: &str, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> {
|
||||
writeln!(output, "{}", "Output".underlined())?;
|
||||
fn run_bin(
|
||||
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 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 = cmd_runner.run_debug_bin(bin_name, output.as_deref_mut())?;
|
||||
|
||||
let success = run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)?;
|
||||
|
||||
if !success {
|
||||
// This output is important to show the user that something went wrong.
|
||||
// Otherwise, calling something like `exit(1)` in an exercise without further output
|
||||
// leaves the user confused about why the exercise isn't done yet.
|
||||
writeln!(
|
||||
output,
|
||||
"{}",
|
||||
"The exercise didn't run successfully (nonzero exit code)"
|
||||
.bold()
|
||||
.red(),
|
||||
)?;
|
||||
if let Some(output) = output {
|
||||
if !success {
|
||||
// This output is important to show the user that something went wrong.
|
||||
// Otherwise, calling something like `exit(1)` in an exercise without further output
|
||||
// leaves the user confused about why the exercise isn't done yet.
|
||||
write_ansi(output, SetAttribute(Attribute::Bold));
|
||||
write_ansi(output, SetForegroundColor(Color::Red));
|
||||
output.extend_from_slice(b"The exercise didn't run successfully (nonzero exit code)");
|
||||
write_ansi(output, ResetColor);
|
||||
output.push(b'\n');
|
||||
}
|
||||
}
|
||||
|
||||
Ok(success)
|
||||
@@ -52,114 +64,127 @@ pub struct Exercise {
|
||||
pub name: &'static str,
|
||||
/// Path of the exercise file starting with the `exercises/` directory.
|
||||
pub path: &'static str,
|
||||
pub canonical_path: Option<String>,
|
||||
pub test: bool,
|
||||
pub strict_clippy: bool,
|
||||
pub hint: String,
|
||||
pub hint: &'static str,
|
||||
pub done: bool,
|
||||
}
|
||||
|
||||
impl Exercise {
|
||||
pub fn terminal_link(&self) -> StyledContent<TerminalFileLink<'_>> {
|
||||
style(TerminalFileLink(self.path)).underlined().blue()
|
||||
}
|
||||
}
|
||||
pub fn terminal_file_link<'a>(&self, writer: &mut impl CountedWrite<'a>) -> io::Result<()> {
|
||||
if let Some(canonical_path) = self.canonical_path.as_deref() {
|
||||
return terminal_file_link(writer, self.path, canonical_path, Color::Blue);
|
||||
}
|
||||
|
||||
impl Display for Exercise {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
self.path.fmt(f)
|
||||
writer.write_str(self.path)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RunnableExercise {
|
||||
fn name(&self) -> &str;
|
||||
fn dir(&self) -> Option<&str>;
|
||||
fn strict_clippy(&self) -> bool;
|
||||
fn test(&self) -> bool;
|
||||
|
||||
// Compile, check and run the exercise or its solution (depending on `bin_name´).
|
||||
// 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> {
|
||||
output.clear();
|
||||
|
||||
// Developing the official Rustlings.
|
||||
let dev = DEBUG_PROFILE && in_official_repo();
|
||||
|
||||
let build_success = CargoCmd {
|
||||
subcommand: "build",
|
||||
args: &[],
|
||||
bin_name,
|
||||
description: "cargo build …",
|
||||
hide_warnings: false,
|
||||
target_dir,
|
||||
output,
|
||||
dev,
|
||||
fn run<const FORCE_STRICT_CLIPPY: bool>(
|
||||
&self,
|
||||
bin_name: &str,
|
||||
mut output: Option<&mut Vec<u8>>,
|
||||
cmd_runner: &CmdRunner,
|
||||
) -> Result<bool> {
|
||||
if let Some(output) = output.as_deref_mut() {
|
||||
output.clear();
|
||||
}
|
||||
.run()?;
|
||||
|
||||
let build_success = cmd_runner
|
||||
.cargo("build", bin_name, output.as_deref_mut())
|
||||
.run("cargo build …")?;
|
||||
if !build_success {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Discard the output of `cargo build` because it will be shown again by Clippy.
|
||||
output.clear();
|
||||
// Discard the compiler output because it will be shown again by `cargo test` or Clippy.
|
||||
if let Some(output) = output.as_deref_mut() {
|
||||
output.clear();
|
||||
}
|
||||
|
||||
// `--profile test` is required to also check code with `[cfg(test)]`.
|
||||
let clippy_args: &[&str] = if self.strict_clippy() {
|
||||
&["--profile", "test", "--", "-D", "warnings"]
|
||||
if self.test() {
|
||||
let output_is_some = output.is_some();
|
||||
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 {
|
||||
&["--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);
|
||||
clippy_cmd.args(["--profile", "test"]);
|
||||
}
|
||||
|
||||
if !self.test() {
|
||||
return run_bin(bin_name, output, target_dir);
|
||||
}
|
||||
let clippy_success = clippy_cmd.run("cargo clippy …")?;
|
||||
let run_success = run_bin(bin_name, output, cmd_runner)?;
|
||||
|
||||
let test_success = CargoCmd {
|
||||
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)
|
||||
Ok(clippy_success && run_success)
|
||||
}
|
||||
|
||||
/// Compile, check and run the exercise.
|
||||
/// The output is written to the `output` buffer after clearing it.
|
||||
#[inline]
|
||||
fn run_exercise(&self, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> {
|
||||
self.run(self.name(), output, target_dir)
|
||||
fn run_exercise(&self, output: Option<&mut Vec<u8>>, cmd_runner: &CmdRunner) -> Result<bool> {
|
||||
self.run::<false>(self.name(), output, cmd_runner)
|
||||
}
|
||||
|
||||
/// Compile, check and run the exercise's solution.
|
||||
/// 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 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("_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 +194,11 @@ impl RunnableExercise for Exercise {
|
||||
self.name
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn dir(&self) -> Option<&str> {
|
||||
self.dir
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn strict_clippy(&self) -> bool {
|
||||
self.strict_clippy
|
||||
|
||||
@@ -52,30 +52,6 @@ impl ExerciseInfo {
|
||||
|
||||
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 {
|
||||
@@ -84,6 +60,11 @@ impl RunnableExercise for ExerciseInfo {
|
||||
&self.name
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn dir(&self) -> Option<&str> {
|
||||
self.dir.as_deref()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn strict_clippy(&self) -> bool {
|
||||
self.strict_clippy
|
||||
@@ -135,4 +116,4 @@ impl InfoFile {
|
||||
}
|
||||
|
||||
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.";
|
||||
|
||||
153
src/init.rs
153
src/init.rs
@@ -1,32 +1,94 @@
|
||||
use anyhow::{bail, Context, Result};
|
||||
use crossterm::style::Stylize;
|
||||
use crossterm::{
|
||||
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
|
||||
QueueableCommand,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
env::set_current_dir,
|
||||
fs::{self, create_dir},
|
||||
io::ErrorKind,
|
||||
path::Path,
|
||||
io::{self, Write},
|
||||
path::{Path, PathBuf},
|
||||
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<()> {
|
||||
// Prevent initialization in a directory that contains the file `Cargo.toml`.
|
||||
// This can mean that Rustlings was already initialized in this directory.
|
||||
// Otherwise, this can cause problems with Cargo workspaces.
|
||||
if Path::new("Cargo.toml").exists() {
|
||||
bail!(CARGO_TOML_EXISTS_ERR);
|
||||
let rustlings_dir = Path::new("rustlings");
|
||||
if rustlings_dir.exists() {
|
||||
bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR);
|
||||
}
|
||||
|
||||
let rustlings_path = Path::new("rustlings");
|
||||
if let Err(e) = create_dir(rustlings_path) {
|
||||
if e.kind() == ErrorKind::AlreadyExists {
|
||||
bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR);
|
||||
let locate_project_output = Command::new("cargo")
|
||||
.arg("locate-project")
|
||||
.arg("-q")
|
||||
.arg("--workspace")
|
||||
.stdin(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.output()
|
||||
.context(CARGO_LOCATE_PROJECT_ERR)?;
|
||||
|
||||
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]\n")
|
||||
&& !workspace_manifest_content.contains("workspace.")
|
||||
{
|
||||
bail!("The current directory is already part of a Cargo project.\nPlease initialize Rustlings in a different directory");
|
||||
}
|
||||
|
||||
stdout.write_all(b"This command will create the directory `rustlings/` as a member of this Cargo workspace.\nPress 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.\nPlease 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.\nPress 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/`")?;
|
||||
|
||||
let info_file = InfoFile::parse()?;
|
||||
@@ -35,6 +97,11 @@ pub fn init() -> Result<()> {
|
||||
.context("Failed to initialize the `rustlings/exercises` 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 {
|
||||
let mut dir_path = String::with_capacity(10 + dir.name.len());
|
||||
dir_path.push_str("solutions/");
|
||||
@@ -63,6 +130,9 @@ pub fn init() -> Result<()> {
|
||||
fs::write("Cargo.toml", updated_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)
|
||||
.context("Failed to create the file `rustlings/.gitignore`")?;
|
||||
|
||||
@@ -70,41 +140,53 @@ pub fn init() -> Result<()> {
|
||||
fs::write(".vscode/extensions.json", VS_CODE_EXTENSIONS_JSON)
|
||||
.context("Failed to create the file `rustlings/.vscode/extensions.json`")?;
|
||||
|
||||
// Ignore any Git error because Git initialization is not required.
|
||||
let _ = Command::new("git")
|
||||
.arg("init")
|
||||
.stdin(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status();
|
||||
if init_git {
|
||||
// Ignore any Git error because Git initialization is not required.
|
||||
let _ = Command::new("git")
|
||||
.arg("init")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status();
|
||||
}
|
||||
|
||||
println!(
|
||||
"\n{}\n\n{}",
|
||||
"Initialization done ✓".green(),
|
||||
POST_INIT_MSG.bold(),
|
||||
);
|
||||
stdout.queue(SetForegroundColor(Color::Green))?;
|
||||
stdout.write_all("Initialization done ✓".as_bytes())?;
|
||||
stdout.queue(ResetColor)?;
|
||||
stdout.write_all(b"\n\n")?;
|
||||
|
||||
stdout.queue(SetAttribute(Attribute::Bold))?;
|
||||
stdout.write_all(POST_INIT_MSG)?;
|
||||
stdout.queue(ResetColor)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const CARGO_LOCATE_PROJECT_ERR: &str = "Failed to run the command `cargo locate-project …`
|
||||
Did you already install Rust?
|
||||
Try running `cargo --version` to diagnose the problem.";
|
||||
|
||||
const INIT_SOLUTION_FILE: &[u8] = b"fn main() {
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
";
|
||||
|
||||
const GITIGNORE: &[u8] = b".rustlings-state.txt
|
||||
solutions
|
||||
Cargo.lock
|
||||
target
|
||||
.vscode
|
||||
pub const RUST_ANALYZER_TOML: &[u8] = br#"check.command = "clippy"
|
||||
check.extraArgs = ["--profile", "test"]
|
||||
"#;
|
||||
|
||||
const GITIGNORE: &[u8] = b"Cargo.lock
|
||||
target/
|
||||
.vscode/
|
||||
";
|
||||
|
||||
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.
|
||||
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 =
|
||||
"A directory with the name `rustlings` already exists in the current directory.
|
||||
@@ -112,5 +194,6 @@ You probably already initialized Rustlings.
|
||||
Run `cd rustlings`
|
||||
Then run `rustlings` again";
|
||||
|
||||
const POST_INIT_MSG: &str = "Run `cd rustlings` to go into the generated directory.
|
||||
Then run `rustlings` to get started.";
|
||||
const POST_INIT_MSG: &[u8] = b"Run `cd rustlings` to go into the generated directory.
|
||||
Then run `rustlings` to get started.
|
||||
";
|
||||
|
||||
193
src/list.rs
193
src/list.rs
@@ -1,90 +1,135 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
cursor,
|
||||
event::{
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, MouseEventKind,
|
||||
},
|
||||
terminal::{
|
||||
disable_raw_mode, enable_raw_mode, DisableLineWrap, EnableLineWrap, EnterAlternateScreen,
|
||||
LeaveAlternateScreen,
|
||||
},
|
||||
QueueableCommand,
|
||||
};
|
||||
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
use std::io;
|
||||
use std::io::{self, StdoutLock, Write};
|
||||
|
||||
use crate::app_state::AppState;
|
||||
|
||||
use self::state::{Filter, UiState};
|
||||
use self::state::{Filter, ListState};
|
||||
|
||||
mod scroll_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<()> {
|
||||
let mut stdout = io::stdout().lock();
|
||||
stdout.execute(EnterAlternateScreen)?;
|
||||
stdout
|
||||
.queue(EnterAlternateScreen)?
|
||||
.queue(cursor::Hide)?
|
||||
.queue(DisableLineWrap)?
|
||||
.queue(EnableMouseCapture)?;
|
||||
enable_raw_mode()?;
|
||||
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?;
|
||||
terminal.clear()?;
|
||||
let res = handle_list(app_state, &mut stdout);
|
||||
|
||||
let mut ui_state = UiState::new(app_state);
|
||||
|
||||
'outer: loop {
|
||||
terminal.draw(|frame| ui_state.draw(frame).unwrap())?;
|
||||
|
||||
let key = loop {
|
||||
match event::read()? {
|
||||
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)?;
|
||||
// Restore the terminal even if we got an error.
|
||||
stdout
|
||||
.queue(LeaveAlternateScreen)?
|
||||
.queue(cursor::Show)?
|
||||
.queue(EnableLineWrap)?
|
||||
.queue(DisableMouseCapture)?
|
||||
.flush()?;
|
||||
disable_raw_mode()?;
|
||||
|
||||
Ok(())
|
||||
res
|
||||
}
|
||||
|
||||
104
src/list/scroll_state.rs
Normal file
104
src/list/scroll_state.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
pub struct ScrollState {
|
||||
n_rows: usize,
|
||||
max_n_rows_to_display: usize,
|
||||
selected: Option<usize>,
|
||||
offset: usize,
|
||||
scroll_padding: usize,
|
||||
max_scroll_padding: usize,
|
||||
}
|
||||
|
||||
impl ScrollState {
|
||||
pub fn new(n_rows: usize, selected: Option<usize>, max_scroll_padding: usize) -> Self {
|
||||
Self {
|
||||
n_rows,
|
||||
max_n_rows_to_display: 0,
|
||||
selected,
|
||||
offset: selected.map_or(0, |selected| selected.saturating_sub(max_scroll_padding)),
|
||||
scroll_padding: 0,
|
||||
max_scroll_padding,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn offset(&self) -> usize {
|
||||
self.offset
|
||||
}
|
||||
|
||||
fn update_offset(&mut self) {
|
||||
let Some(selected) = self.selected else {
|
||||
return;
|
||||
};
|
||||
|
||||
let min_offset = (selected + self.scroll_padding)
|
||||
.saturating_sub(self.max_n_rows_to_display.saturating_sub(1));
|
||||
let max_offset = selected.saturating_sub(self.scroll_padding);
|
||||
let global_max_offset = self.n_rows.saturating_sub(self.max_n_rows_to_display);
|
||||
|
||||
self.offset = self
|
||||
.offset
|
||||
.max(min_offset)
|
||||
.min(max_offset)
|
||||
.min(global_max_offset);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn selected(&self) -> Option<usize> {
|
||||
self.selected
|
||||
}
|
||||
|
||||
pub fn set_selected(&mut self, selected: usize) {
|
||||
self.selected = Some(selected);
|
||||
self.update_offset();
|
||||
}
|
||||
|
||||
pub fn select_next(&mut self) {
|
||||
if let Some(selected) = self.selected {
|
||||
self.set_selected((selected + 1).min(self.n_rows - 1));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_previous(&mut self) {
|
||||
if let Some(selected) = self.selected {
|
||||
self.set_selected(selected.saturating_sub(1));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_first(&mut self) {
|
||||
if self.n_rows > 0 {
|
||||
self.set_selected(0);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_last(&mut self) {
|
||||
if self.n_rows > 0 {
|
||||
self.set_selected(self.n_rows - 1);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_n_rows(&mut self, n_rows: usize) {
|
||||
self.n_rows = n_rows;
|
||||
|
||||
if self.n_rows == 0 {
|
||||
self.selected = None;
|
||||
return;
|
||||
}
|
||||
|
||||
self.set_selected(self.selected.map_or(0, |selected| selected.min(n_rows - 1)));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn update_scroll_padding(&mut self) {
|
||||
self.scroll_padding = (self.max_n_rows_to_display / 4).min(self.max_scroll_padding);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn max_n_rows_to_display(&self) -> usize {
|
||||
self.max_n_rows_to_display
|
||||
}
|
||||
|
||||
pub fn set_max_n_rows_to_display(&mut self, max_n_rows_to_display: usize) {
|
||||
self.max_n_rows_to_display = max_n_rows_to_display;
|
||||
self.update_scroll_padding();
|
||||
self.update_offset();
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,36 @@
|
||||
use anyhow::{Context, Result};
|
||||
use ratatui::{
|
||||
layout::{Constraint, Rect},
|
||||
style::{Style, Stylize},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, HighlightSpacing, Paragraph, Row, Table, TableState},
|
||||
Frame,
|
||||
use crossterm::{
|
||||
cursor::{MoveTo, MoveToNextLine},
|
||||
style::{
|
||||
Attribute, Attributes, Color, ResetColor, SetAttribute, SetAttributes, SetForegroundColor,
|
||||
},
|
||||
terminal::{self, BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate},
|
||||
QueueableCommand,
|
||||
};
|
||||
use std::{
|
||||
fmt::Write as _,
|
||||
io::{self, StdoutLock, Write},
|
||||
};
|
||||
use std::fmt::Write;
|
||||
|
||||
use crate::{app_state::AppState, progress_bar::progress_bar_ratatui};
|
||||
use crate::{
|
||||
app_state::AppState,
|
||||
exercise::Exercise,
|
||||
term::{progress_bar, CountedWrite, MaxLenWriter},
|
||||
};
|
||||
|
||||
use super::scroll_state::ScrollState;
|
||||
|
||||
const COL_SPACING: usize = 2;
|
||||
const SELECTED_ROW_ATTRIBUTES: Attributes = Attributes::none()
|
||||
.with(Attribute::Reverse)
|
||||
.with(Attribute::Bold);
|
||||
|
||||
fn next_ln(stdout: &mut StdoutLock) -> io::Result<()> {
|
||||
stdout
|
||||
.queue(Clear(ClearType::UntilNewLine))?
|
||||
.queue(MoveToNextLine(1))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Filter {
|
||||
@@ -17,255 +39,386 @@ pub enum Filter {
|
||||
None,
|
||||
}
|
||||
|
||||
pub struct UiState<'a> {
|
||||
pub table: Table<'static>,
|
||||
pub struct ListState<'a> {
|
||||
/// Footer message to be displayed if not empty.
|
||||
pub message: String,
|
||||
pub filter: Filter,
|
||||
pub search_query: String,
|
||||
app_state: &'a mut AppState,
|
||||
table_state: TableState,
|
||||
n_rows: usize,
|
||||
scroll_state: ScrollState,
|
||||
name_col_padding: Vec<u8>,
|
||||
path_col_padding: Vec<u8>,
|
||||
filter: Filter,
|
||||
term_width: u16,
|
||||
term_height: u16,
|
||||
show_footer: bool,
|
||||
}
|
||||
|
||||
impl<'a> UiState<'a> {
|
||||
pub fn with_updated_rows(mut self) -> Self {
|
||||
let current_exercise_ind = self.app_state.current_exercise_ind();
|
||||
impl<'a> ListState<'a> {
|
||||
pub fn build(app_state: &'a mut AppState, stdout: &mut StdoutLock) -> Result<Self> {
|
||||
stdout.queue(Clear(ClearType::All))?;
|
||||
|
||||
self.n_rows = 0;
|
||||
let rows = self
|
||||
.app_state
|
||||
.exercises()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(ind, exercise)| {
|
||||
let exercise_state = if exercise.done {
|
||||
if self.filter == Filter::Pending {
|
||||
return None;
|
||||
}
|
||||
|
||||
"DONE".green()
|
||||
} else {
|
||||
if self.filter == Filter::Done {
|
||||
return None;
|
||||
}
|
||||
|
||||
"PENDING".yellow()
|
||||
};
|
||||
|
||||
self.n_rows += 1;
|
||||
|
||||
let next = if ind == current_exercise_ind {
|
||||
">>>>".bold().red()
|
||||
} else {
|
||||
Span::default()
|
||||
};
|
||||
|
||||
Some(Row::new([
|
||||
next,
|
||||
exercise_state,
|
||||
Span::raw(exercise.name),
|
||||
Span::raw(exercise.path),
|
||||
]))
|
||||
});
|
||||
|
||||
self.table = self.table.rows(rows);
|
||||
|
||||
if self.n_rows == 0 {
|
||||
self.table_state.select(None);
|
||||
} else {
|
||||
self.table_state.select(Some(
|
||||
self.table_state
|
||||
.selected()
|
||||
.map_or(0, |selected| selected.min(self.n_rows - 1)),
|
||||
));
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn new(app_state: &'a mut AppState) -> Self {
|
||||
let header = Row::new(["Next", "State", "Name", "Path"]);
|
||||
|
||||
let max_name_len = app_state
|
||||
.exercises()
|
||||
.iter()
|
||||
.map(|exercise| exercise.name.len())
|
||||
.max()
|
||||
.unwrap_or(4) as u16;
|
||||
|
||||
let widths = [
|
||||
Constraint::Length(4),
|
||||
Constraint::Length(7),
|
||||
Constraint::Length(max_name_len),
|
||||
Constraint::Fill(1),
|
||||
];
|
||||
|
||||
let table = Table::default()
|
||||
.widths(widths)
|
||||
.header(header)
|
||||
.column_spacing(2)
|
||||
.highlight_spacing(HighlightSpacing::Always)
|
||||
.highlight_style(Style::new().bg(ratatui::style::Color::Rgb(50, 50, 50)))
|
||||
.highlight_symbol("🦀")
|
||||
.block(Block::default().borders(Borders::BOTTOM));
|
||||
|
||||
let selected = app_state.current_exercise_ind();
|
||||
let table_state = TableState::default()
|
||||
.with_offset(selected.saturating_sub(10))
|
||||
.with_selected(Some(selected));
|
||||
let name_col_title_len = 4;
|
||||
let path_col_title_len = 4;
|
||||
let (name_col_width, path_col_width) = app_state.exercises().iter().fold(
|
||||
(name_col_title_len, path_col_title_len),
|
||||
|(name_col_width, path_col_width), exercise| {
|
||||
(
|
||||
name_col_width.max(exercise.name.len()),
|
||||
path_col_width.max(exercise.path.len()),
|
||||
)
|
||||
},
|
||||
);
|
||||
let name_col_padding = vec![b' '; name_col_width + COL_SPACING];
|
||||
let path_col_padding = vec![b' '; path_col_width];
|
||||
|
||||
let filter = Filter::None;
|
||||
let n_rows = app_state.exercises().len();
|
||||
let n_rows_with_filter = app_state.exercises().len();
|
||||
let selected = app_state.current_exercise_ind();
|
||||
|
||||
let slf = Self {
|
||||
table,
|
||||
let (width, height) = terminal::size().context("Failed to get the terminal size")?;
|
||||
let scroll_state = ScrollState::new(n_rows_with_filter, Some(selected), 5);
|
||||
|
||||
let mut slf = Self {
|
||||
message: String::with_capacity(128),
|
||||
filter,
|
||||
search_query: String::new(),
|
||||
app_state,
|
||||
table_state,
|
||||
n_rows,
|
||||
scroll_state,
|
||||
name_col_padding,
|
||||
path_col_padding,
|
||||
filter,
|
||||
// Set by `set_term_size`
|
||||
term_width: 0,
|
||||
term_height: 0,
|
||||
show_footer: true,
|
||||
};
|
||||
|
||||
slf.with_updated_rows()
|
||||
slf.set_term_size(width, height);
|
||||
slf.draw(stdout)?;
|
||||
|
||||
Ok(slf)
|
||||
}
|
||||
|
||||
pub fn select_next(&mut self) {
|
||||
if self.n_rows > 0 {
|
||||
let next = self
|
||||
.table_state
|
||||
.selected()
|
||||
.map_or(0, |selected| (selected + 1).min(self.n_rows - 1));
|
||||
self.table_state.select(Some(next));
|
||||
pub fn set_term_size(&mut self, width: u16, height: u16) {
|
||||
self.term_width = width;
|
||||
self.term_height = height;
|
||||
|
||||
if height == 0 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_previous(&mut self) {
|
||||
if self.n_rows > 0 {
|
||||
let previous = self
|
||||
.table_state
|
||||
.selected()
|
||||
.map_or(0, |selected| selected.saturating_sub(1));
|
||||
self.table_state.select(Some(previous));
|
||||
}
|
||||
}
|
||||
let header_height = 1;
|
||||
// 1 progress bar, 2 footer message lines.
|
||||
let footer_height = 3;
|
||||
self.show_footer = height > header_height + footer_height;
|
||||
|
||||
pub fn select_first(&mut self) {
|
||||
if self.n_rows > 0 {
|
||||
self.table_state.select(Some(0));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_last(&mut self) {
|
||||
if self.n_rows > 0 {
|
||||
self.table_state.select(Some(self.n_rows - 1));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw(&mut self, frame: &mut Frame) -> Result<()> {
|
||||
let area = frame.size();
|
||||
|
||||
frame.render_stateful_widget(
|
||||
&self.table,
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: area.width,
|
||||
height: area.height - 3,
|
||||
},
|
||||
&mut self.table_state,
|
||||
self.scroll_state.set_max_n_rows_to_display(
|
||||
height.saturating_sub(header_height + u16::from(self.show_footer) * footer_height)
|
||||
as usize,
|
||||
);
|
||||
}
|
||||
|
||||
frame.render_widget(
|
||||
Paragraph::new(progress_bar_ratatui(
|
||||
fn draw_exercise_name(&self, writer: &mut MaxLenWriter, exercise: &Exercise) -> io::Result<()> {
|
||||
if !self.search_query.is_empty() {
|
||||
if let Some((pre_highlight, highlight, post_highlight)) = exercise
|
||||
.name
|
||||
.find(&self.search_query)
|
||||
.and_then(|ind| exercise.name.split_at_checked(ind))
|
||||
.and_then(|(pre_highlight, rest)| {
|
||||
rest.split_at_checked(self.search_query.len())
|
||||
.map(|x| (pre_highlight, x.0, x.1))
|
||||
})
|
||||
{
|
||||
writer.write_str(pre_highlight)?;
|
||||
writer.stdout.queue(SetForegroundColor(Color::Magenta))?;
|
||||
writer.write_str(highlight)?;
|
||||
writer.stdout.queue(SetForegroundColor(Color::Reset))?;
|
||||
return writer.write_str(post_highlight);
|
||||
}
|
||||
}
|
||||
|
||||
writer.write_str(exercise.name)
|
||||
}
|
||||
|
||||
fn draw_rows(
|
||||
&self,
|
||||
stdout: &mut StdoutLock,
|
||||
filtered_exercises: impl Iterator<Item = (usize, &'a Exercise)>,
|
||||
) -> io::Result<usize> {
|
||||
let current_exercise_ind = self.app_state.current_exercise_ind();
|
||||
let row_offset = self.scroll_state.offset();
|
||||
let mut n_displayed_rows = 0;
|
||||
|
||||
for (exercise_ind, exercise) in filtered_exercises
|
||||
.skip(row_offset)
|
||||
.take(self.scroll_state.max_n_rows_to_display())
|
||||
{
|
||||
let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
|
||||
|
||||
if self.scroll_state.selected() == Some(row_offset + n_displayed_rows) {
|
||||
// The crab emoji has the width of two ascii chars.
|
||||
writer.add_to_len(2);
|
||||
writer.stdout.write_all("🦀".as_bytes())?;
|
||||
writer
|
||||
.stdout
|
||||
.queue(SetAttributes(SELECTED_ROW_ATTRIBUTES))?;
|
||||
} else {
|
||||
writer.write_ascii(b" ")?;
|
||||
}
|
||||
|
||||
if exercise_ind == current_exercise_ind {
|
||||
writer.stdout.queue(SetForegroundColor(Color::Red))?;
|
||||
writer.write_ascii(b">>>>>>> ")?;
|
||||
} else {
|
||||
writer.write_ascii(b" ")?;
|
||||
}
|
||||
|
||||
if exercise.done {
|
||||
writer.stdout.queue(SetForegroundColor(Color::Green))?;
|
||||
writer.write_ascii(b"DONE ")?;
|
||||
} else {
|
||||
writer.stdout.queue(SetForegroundColor(Color::Yellow))?;
|
||||
writer.write_ascii(b"PENDING")?;
|
||||
}
|
||||
writer.stdout.queue(SetForegroundColor(Color::Reset))?;
|
||||
writer.write_ascii(b" ")?;
|
||||
|
||||
self.draw_exercise_name(&mut writer, exercise)?;
|
||||
|
||||
writer.write_ascii(&self.name_col_padding[exercise.name.len()..])?;
|
||||
|
||||
// The list links aren't shown correctly in VS Code on Windows.
|
||||
// But VS Code shows its own links anyway.
|
||||
if self.app_state.vs_code() {
|
||||
writer.write_str(exercise.path)?;
|
||||
} else {
|
||||
exercise.terminal_file_link(&mut writer)?;
|
||||
}
|
||||
|
||||
writer.write_ascii(&self.path_col_padding[exercise.path.len()..])?;
|
||||
|
||||
next_ln(stdout)?;
|
||||
stdout.queue(ResetColor)?;
|
||||
n_displayed_rows += 1;
|
||||
}
|
||||
|
||||
Ok(n_displayed_rows)
|
||||
}
|
||||
|
||||
pub fn draw(&mut self, stdout: &mut StdoutLock) -> io::Result<()> {
|
||||
if self.term_height == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
stdout.queue(BeginSynchronizedUpdate)?.queue(MoveTo(0, 0))?;
|
||||
|
||||
// Header
|
||||
let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
|
||||
writer.write_ascii(b" Current State Name")?;
|
||||
writer.write_ascii(&self.name_col_padding[4..])?;
|
||||
writer.write_ascii(b"Path")?;
|
||||
next_ln(stdout)?;
|
||||
|
||||
// Rows
|
||||
let iter = self.app_state.exercises().iter().enumerate();
|
||||
let n_displayed_rows = match self.filter {
|
||||
Filter::Done => self.draw_rows(stdout, iter.filter(|(_, exercise)| exercise.done))?,
|
||||
Filter::Pending => {
|
||||
self.draw_rows(stdout, iter.filter(|(_, exercise)| !exercise.done))?
|
||||
}
|
||||
Filter::None => self.draw_rows(stdout, iter)?,
|
||||
};
|
||||
|
||||
for _ in 0..self.scroll_state.max_n_rows_to_display() - n_displayed_rows {
|
||||
next_ln(stdout)?;
|
||||
}
|
||||
|
||||
if self.show_footer {
|
||||
progress_bar(
|
||||
&mut MaxLenWriter::new(stdout, self.term_width as usize),
|
||||
self.app_state.n_done(),
|
||||
self.app_state.exercises().len() as u16,
|
||||
area.width,
|
||||
)?)
|
||||
.block(Block::default().borders(Borders::BOTTOM)),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: area.height - 3,
|
||||
width: area.width,
|
||||
height: 2,
|
||||
},
|
||||
);
|
||||
self.term_width,
|
||||
)?;
|
||||
next_ln(stdout)?;
|
||||
|
||||
let message = if self.message.is_empty() {
|
||||
// Help footer.
|
||||
let mut spans = Vec::with_capacity(4);
|
||||
spans.push(Span::raw(
|
||||
"↓/j ↑/k home/g end/G │ <c>ontinue at │ <r>eset │ filter ",
|
||||
));
|
||||
match self.filter {
|
||||
Filter::Done => {
|
||||
spans.push("<d>one".underlined().magenta());
|
||||
spans.push(Span::raw("/<p>ending"));
|
||||
let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
|
||||
if self.message.is_empty() {
|
||||
// Help footer message
|
||||
if self.scroll_state.selected().is_some() {
|
||||
writer.write_str("↓/j ↑/k home/g end/G | <c>ontinue at | <r>eset exercise")?;
|
||||
next_ln(stdout)?;
|
||||
writer = MaxLenWriter::new(stdout, self.term_width as usize);
|
||||
|
||||
writer.write_ascii(b"<s>earch | filter ")?;
|
||||
} else {
|
||||
// Nothing selected (and nothing shown), so only display filter and quit.
|
||||
writer.write_ascii(b"filter ")?;
|
||||
}
|
||||
Filter::Pending => {
|
||||
spans.push(Span::raw("<d>one/"));
|
||||
spans.push("<p>ending".underlined().magenta());
|
||||
|
||||
match self.filter {
|
||||
Filter::Done => {
|
||||
writer
|
||||
.stdout
|
||||
.queue(SetForegroundColor(Color::Magenta))?
|
||||
.queue(SetAttribute(Attribute::Underlined))?;
|
||||
writer.write_ascii(b"<d>one")?;
|
||||
writer.stdout.queue(ResetColor)?;
|
||||
writer.write_ascii(b"/<p>ending")?;
|
||||
}
|
||||
Filter::Pending => {
|
||||
writer.write_ascii(b"<d>one/")?;
|
||||
writer
|
||||
.stdout
|
||||
.queue(SetForegroundColor(Color::Magenta))?
|
||||
.queue(SetAttribute(Attribute::Underlined))?;
|
||||
writer.write_ascii(b"<p>ending")?;
|
||||
writer.stdout.queue(ResetColor)?;
|
||||
}
|
||||
Filter::None => writer.write_ascii(b"<d>one/<p>ending")?,
|
||||
}
|
||||
Filter::None => spans.push(Span::raw("<d>one/<p>ending")),
|
||||
|
||||
writer.write_ascii(b" | <q>uit list")?;
|
||||
} else {
|
||||
writer.stdout.queue(SetForegroundColor(Color::Magenta))?;
|
||||
writer.write_str(&self.message)?;
|
||||
stdout.queue(ResetColor)?;
|
||||
next_ln(stdout)?;
|
||||
}
|
||||
spans.push(Span::raw(" │ <q>uit"));
|
||||
Line::from(spans)
|
||||
} else {
|
||||
Line::from(self.message.as_str().light_blue())
|
||||
|
||||
next_ln(stdout)?;
|
||||
}
|
||||
|
||||
stdout.queue(EndSynchronizedUpdate)?.flush()
|
||||
}
|
||||
|
||||
fn update_rows(&mut self) {
|
||||
let n_rows = match self.filter {
|
||||
Filter::Done => self
|
||||
.app_state
|
||||
.exercises()
|
||||
.iter()
|
||||
.filter(|exercise| exercise.done)
|
||||
.count(),
|
||||
Filter::Pending => self
|
||||
.app_state
|
||||
.exercises()
|
||||
.iter()
|
||||
.filter(|exercise| !exercise.done)
|
||||
.count(),
|
||||
Filter::None => self.app_state.exercises().len(),
|
||||
};
|
||||
frame.render_widget(
|
||||
message,
|
||||
Rect {
|
||||
x: 0,
|
||||
y: area.height - 1,
|
||||
width: area.width,
|
||||
height: 1,
|
||||
},
|
||||
);
|
||||
|
||||
self.scroll_state.set_n_rows(n_rows);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn filter(&self) -> Filter {
|
||||
self.filter
|
||||
}
|
||||
|
||||
pub fn set_filter(&mut self, filter: Filter) {
|
||||
self.filter = filter;
|
||||
self.update_rows();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn select_next(&mut self) {
|
||||
self.scroll_state.select_next();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn select_previous(&mut self) {
|
||||
self.scroll_state.select_previous();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn select_first(&mut self) {
|
||||
self.scroll_state.select_first();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn select_last(&mut self) {
|
||||
self.scroll_state.select_last();
|
||||
}
|
||||
|
||||
fn selected_to_exercise_ind(&self, selected: usize) -> Result<usize> {
|
||||
match self.filter {
|
||||
Filter::Done => self
|
||||
.app_state
|
||||
.exercises()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, exercise)| exercise.done)
|
||||
.nth(selected)
|
||||
.context("Invalid selection index")
|
||||
.map(|(ind, _)| ind),
|
||||
Filter::Pending => self
|
||||
.app_state
|
||||
.exercises()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, exercise)| !exercise.done)
|
||||
.nth(selected)
|
||||
.context("Invalid selection index")
|
||||
.map(|(ind, _)| ind),
|
||||
Filter::None => Ok(selected),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_selected(&mut self) -> Result<()> {
|
||||
let Some(selected) = self.scroll_state.selected() else {
|
||||
self.message.push_str("Nothing selected to reset!");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let exercise_ind = self.selected_to_exercise_ind(selected)?;
|
||||
let exercise_name = self.app_state.reset_exercise_by_ind(exercise_ind)?;
|
||||
self.update_rows();
|
||||
write!(
|
||||
self.message,
|
||||
"The exercise `{exercise_name}` has been reset",
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn with_reset_selected(mut self) -> Result<Self> {
|
||||
let Some(selected) = self.table_state.selected() else {
|
||||
return Ok(self);
|
||||
pub fn apply_search_query(&mut self) {
|
||||
self.message.push_str("search:");
|
||||
self.message.push_str(&self.search_query);
|
||||
self.message.push('|');
|
||||
|
||||
if self.search_query.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let is_search_result = |exercise: &Exercise| exercise.name.contains(&self.search_query);
|
||||
let mut iter = self.app_state.exercises().iter();
|
||||
let ind = match self.filter {
|
||||
Filter::None => iter.position(is_search_result),
|
||||
Filter::Done => iter
|
||||
.filter(|exercise| exercise.done)
|
||||
.position(is_search_result),
|
||||
Filter::Pending => iter
|
||||
.filter(|exercise| !exercise.done)
|
||||
.position(is_search_result),
|
||||
};
|
||||
|
||||
let ind = self
|
||||
.app_state
|
||||
.exercises()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(ind, exercise)| match self.filter {
|
||||
Filter::Done => exercise.done.then_some(ind),
|
||||
Filter::Pending => (!exercise.done).then_some(ind),
|
||||
Filter::None => Some(ind),
|
||||
})
|
||||
.nth(selected)
|
||||
.context("Invalid selection index")?;
|
||||
|
||||
let exercise_path = self.app_state.reset_exercise_by_ind(ind)?;
|
||||
write!(self.message, "The exercise {exercise_path} has been reset")?;
|
||||
|
||||
Ok(self.with_updated_rows())
|
||||
match ind {
|
||||
Some(exercise_ind) => self.scroll_state.set_selected(exercise_ind),
|
||||
None => self.message.push_str(" (not found)"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selected_to_current_exercise(&mut self) -> Result<()> {
|
||||
let Some(selected) = self.table_state.selected() else {
|
||||
return Ok(());
|
||||
// Return `true` if there was something to select.
|
||||
pub fn selected_to_current_exercise(&mut self) -> Result<bool> {
|
||||
let Some(selected) = self.scroll_state.selected() else {
|
||||
self.message.push_str("Nothing selected to continue at!");
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let ind = self
|
||||
.app_state
|
||||
.exercises()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(ind, exercise)| match self.filter {
|
||||
Filter::Done => exercise.done.then_some(ind),
|
||||
Filter::Pending => (!exercise.done).then_some(ind),
|
||||
Filter::None => Some(ind),
|
||||
})
|
||||
.nth(selected)
|
||||
.context("Invalid selection index")?;
|
||||
let exercise_ind = self.selected_to_exercise_ind(selected)?;
|
||||
self.app_state.set_current_exercise_ind(exercise_ind)?;
|
||||
|
||||
self.app_state.set_current_exercise_ind(ind)
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
116
src/main.rs
116
src/main.rs
@@ -2,12 +2,13 @@ use anyhow::{bail, Context, Result};
|
||||
use app_state::StateFileStatus;
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::{
|
||||
io::{self, BufRead, IsTerminal, StdoutLock, Write},
|
||||
io::{self, IsTerminal, Write},
|
||||
path::Path,
|
||||
process::exit,
|
||||
process::ExitCode,
|
||||
};
|
||||
use term::{clear_terminal, press_enter_prompt};
|
||||
|
||||
use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit};
|
||||
use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile};
|
||||
|
||||
mod app_state;
|
||||
mod cargo_toml;
|
||||
@@ -18,37 +19,11 @@ mod exercise;
|
||||
mod info_file;
|
||||
mod init;
|
||||
mod list;
|
||||
mod progress_bar;
|
||||
mod run;
|
||||
mod terminal_link;
|
||||
mod term;
|
||||
mod watch;
|
||||
|
||||
const CURRENT_FORMAT_VERSION: u8 = 1;
|
||||
const DEBUG_PROFILE: bool = {
|
||||
#[allow(unused_assignments, unused_mut)]
|
||||
let mut debug_profile = false;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
debug_profile = true;
|
||||
}
|
||||
|
||||
debug_profile
|
||||
};
|
||||
|
||||
// The current directory is the official Rustligns repository.
|
||||
fn in_official_repo() -> bool {
|
||||
Path::new("dev/rustlings-repo.txt").exists()
|
||||
}
|
||||
|
||||
fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> {
|
||||
stdout.write_all(b"\x1b[H\x1b[2J\x1b[3J")
|
||||
}
|
||||
|
||||
fn press_enter_prompt() -> io::Result<()> {
|
||||
io::stdin().lock().read_until(b'\n', &mut Vec::new())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rustlings is a collection of small exercises to get you used to writing and reading Rust code
|
||||
#[derive(Parser)]
|
||||
@@ -71,6 +46,8 @@ enum Subcommands {
|
||||
/// The name of the exercise
|
||||
name: Option<String>,
|
||||
},
|
||||
/// Check all the exercises, marking them as done or pending accordingly.
|
||||
CheckAll,
|
||||
/// Reset a single exercise
|
||||
Reset {
|
||||
/// The name of the exercise
|
||||
@@ -86,36 +63,26 @@ enum Subcommands {
|
||||
Dev(DevCommands),
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
fn main() -> Result<ExitCode> {
|
||||
let args = Args::parse();
|
||||
|
||||
if !DEBUG_PROFILE && in_official_repo() {
|
||||
if cfg!(not(debug_assertions)) && Path::new("dev/rustlings-repo.txt").exists() {
|
||||
bail!("{OLD_METHOD_ERR}");
|
||||
}
|
||||
|
||||
match args.command {
|
||||
Some(Subcommands::Init) => {
|
||||
if DEBUG_PROFILE {
|
||||
bail!("Disabled in the debug build");
|
||||
}
|
||||
|
||||
{
|
||||
let mut stdout = io::stdout().lock();
|
||||
stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\nPress ENTER to continue ")?;
|
||||
stdout.flush()?;
|
||||
press_enter_prompt()?;
|
||||
stdout.write_all(b"\n")?;
|
||||
}
|
||||
|
||||
return init::init().context("Initialization failed");
|
||||
'priority_cmd: {
|
||||
match args.command {
|
||||
Some(Subcommands::Init) => init::init().context("Initialization failed")?,
|
||||
Some(Subcommands::Dev(dev_command)) => dev_command.run()?,
|
||||
_ => break 'priority_cmd,
|
||||
}
|
||||
Some(Subcommands::Dev(dev_command)) => return dev_command.run(),
|
||||
_ => (),
|
||||
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
if !Path::new("exercises").is_dir() {
|
||||
println!("{PRE_INIT_MSG}");
|
||||
exit(1);
|
||||
return Ok(ExitCode::FAILURE);
|
||||
}
|
||||
|
||||
let info_file = InfoFile::parse()?;
|
||||
@@ -136,11 +103,12 @@ fn main() -> Result<()> {
|
||||
let mut stdout = io::stdout().lock();
|
||||
clear_terminal(&mut stdout)?;
|
||||
|
||||
let welcome_message = welcome_message.trim();
|
||||
let welcome_message = welcome_message.trim_ascii();
|
||||
write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?;
|
||||
stdout.flush()?;
|
||||
press_enter_prompt()?;
|
||||
press_enter_prompt(&mut stdout)?;
|
||||
clear_terminal(&mut stdout)?;
|
||||
// Flush to be able to show errors occurring before printing a newline to stdout.
|
||||
stdout.flush()?;
|
||||
}
|
||||
StateFileStatus::Read => (),
|
||||
}
|
||||
@@ -167,21 +135,41 @@ fn main() -> Result<()> {
|
||||
)
|
||||
};
|
||||
|
||||
loop {
|
||||
match watch::watch(&mut app_state, notify_exercise_names)? {
|
||||
WatchExit::Shutdown => break,
|
||||
// It is much easier to exit the watch mode, launch the list mode and then restart
|
||||
// the watch mode instead of trying to pause the watch threads and correct the
|
||||
// watch state.
|
||||
WatchExit::List => list::list(&mut app_state)?,
|
||||
}
|
||||
}
|
||||
watch::watch(&mut app_state, notify_exercise_names)?;
|
||||
}
|
||||
Some(Subcommands::Run { name }) => {
|
||||
if let Some(name) = name {
|
||||
app_state.set_current_exercise_by_name(&name)?;
|
||||
}
|
||||
run::run(&mut app_state)?;
|
||||
return run::run(&mut app_state);
|
||||
}
|
||||
Some(Subcommands::CheckAll) => {
|
||||
let mut stdout = io::stdout().lock();
|
||||
if let Some(first_pending_exercise_ind) = app_state.check_all_exercises(&mut stdout)? {
|
||||
if app_state.current_exercise().done {
|
||||
app_state.set_current_exercise_ind(first_pending_exercise_ind)?;
|
||||
}
|
||||
|
||||
stdout.write_all(b"\n\n")?;
|
||||
let pending = app_state.n_pending();
|
||||
if pending == 1 {
|
||||
stdout.write_all(b"One exercise pending: ")?;
|
||||
} else {
|
||||
write!(
|
||||
stdout,
|
||||
"{pending}/{} exercises pending. The first: ",
|
||||
app_state.exercises().len(),
|
||||
)?;
|
||||
}
|
||||
app_state
|
||||
.current_exercise()
|
||||
.terminal_file_link(&mut stdout)?;
|
||||
stdout.write_all(b"\n")?;
|
||||
|
||||
return Ok(ExitCode::FAILURE);
|
||||
} else {
|
||||
app_state.render_final_message(&mut stdout)?;
|
||||
}
|
||||
}
|
||||
Some(Subcommands::Reset { name }) => {
|
||||
app_state.set_current_exercise_by_name(&name)?;
|
||||
@@ -198,7 +186,7 @@ fn main() -> Result<()> {
|
||||
Some(Subcommands::Init | Subcommands::Dev(_)) => (),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
|
||||
const OLD_METHOD_ERR: &str =
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
use anyhow::{bail, Result};
|
||||
use ratatui::text::{Line, Span};
|
||||
use std::fmt::Write;
|
||||
|
||||
const PREFIX: &str = "Progress: [";
|
||||
const PREFIX_WIDTH: u16 = PREFIX.len() as u16;
|
||||
// Leaving the last char empty (_) for `total` > 99.
|
||||
const POSTFIX_WIDTH: u16 = "] xxx/xx exercises_".len() as u16;
|
||||
const WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH;
|
||||
const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4;
|
||||
|
||||
const PROGRESS_EXCEEDS_MAX_ERR: &str =
|
||||
"The progress of the progress bar is higher than the maximum";
|
||||
|
||||
/// Terminal progress bar to be used when not using Ratataui.
|
||||
pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result<String> {
|
||||
use crossterm::style::Stylize;
|
||||
|
||||
if progress > total {
|
||||
bail!(PROGRESS_EXCEEDS_MAX_ERR);
|
||||
}
|
||||
|
||||
if line_width < MIN_LINE_WIDTH {
|
||||
return Ok(format!("Progress: {progress}/{total} exercises"));
|
||||
}
|
||||
|
||||
let mut line = String::with_capacity(usize::from(line_width));
|
||||
line.push_str(PREFIX);
|
||||
|
||||
let width = line_width - WRAPPER_WIDTH;
|
||||
let filled = (width * progress) / total;
|
||||
|
||||
let mut green_part = String::with_capacity(usize::from(filled + 1));
|
||||
for _ in 0..filled {
|
||||
green_part.push('#');
|
||||
}
|
||||
|
||||
if filled < width {
|
||||
green_part.push('>');
|
||||
}
|
||||
write!(line, "{}", green_part.green()).unwrap();
|
||||
|
||||
let width_minus_filled = width - filled;
|
||||
if width_minus_filled > 1 {
|
||||
let red_part_width = width_minus_filled - 1;
|
||||
let mut red_part = String::with_capacity(usize::from(red_part_width));
|
||||
for _ in 0..red_part_width {
|
||||
red_part.push('-');
|
||||
}
|
||||
write!(line, "{}", red_part.red()).unwrap();
|
||||
}
|
||||
|
||||
writeln!(line, "] {progress:>3}/{total} exercises").unwrap();
|
||||
|
||||
Ok(line)
|
||||
}
|
||||
|
||||
/// Progress bar to be used with Ratataui.
|
||||
// Not using Ratatui's Gauge widget to keep the progress bar consistent.
|
||||
pub fn progress_bar_ratatui(progress: u16, total: u16, line_width: u16) -> Result<Line<'static>> {
|
||||
use ratatui::style::Stylize;
|
||||
|
||||
if progress > total {
|
||||
bail!(PROGRESS_EXCEEDS_MAX_ERR);
|
||||
}
|
||||
|
||||
if line_width < MIN_LINE_WIDTH {
|
||||
return Ok(Line::raw(format!("Progress: {progress}/{total} exercises")));
|
||||
}
|
||||
|
||||
let mut spans = Vec::with_capacity(4);
|
||||
spans.push(Span::raw(PREFIX));
|
||||
|
||||
let width = line_width - WRAPPER_WIDTH;
|
||||
let filled = (width * progress) / total;
|
||||
|
||||
let mut green_part = String::with_capacity(usize::from(filled + 1));
|
||||
for _ in 0..filled {
|
||||
green_part.push('#');
|
||||
}
|
||||
|
||||
if filled < width {
|
||||
green_part.push('>');
|
||||
}
|
||||
spans.push(green_part.green());
|
||||
|
||||
let width_minus_filled = width - filled;
|
||||
if width_minus_filled > 1 {
|
||||
let red_part_width = width_minus_filled - 1;
|
||||
let mut red_part = String::with_capacity(usize::from(red_part_width));
|
||||
for _ in 0..red_part_width {
|
||||
red_part.push('-');
|
||||
}
|
||||
spans.push(red_part.red());
|
||||
}
|
||||
|
||||
spans.push(Span::raw(format!("] {progress:>3}/{total} exercises")));
|
||||
|
||||
Ok(Line::from(spans))
|
||||
}
|
||||
63
src/run.rs
63
src/run.rs
@@ -1,17 +1,22 @@
|
||||
use anyhow::{bail, Result};
|
||||
use crossterm::style::{style, Stylize};
|
||||
use std::io::{self, Write};
|
||||
use anyhow::Result;
|
||||
use crossterm::{
|
||||
style::{Color, ResetColor, SetForegroundColor},
|
||||
QueueableCommand,
|
||||
};
|
||||
use std::{
|
||||
io::{self, Write},
|
||||
process::ExitCode,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app_state::{AppState, ExercisesProgress},
|
||||
exercise::{RunnableExercise, OUTPUT_CAPACITY},
|
||||
terminal_link::TerminalFileLink,
|
||||
exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY},
|
||||
};
|
||||
|
||||
pub fn run(app_state: &mut AppState) -> Result<()> {
|
||||
pub fn run(app_state: &mut AppState) -> Result<ExitCode> {
|
||||
let exercise = app_state.current_exercise();
|
||||
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
|
||||
let success = exercise.run_exercise(&mut output, app_state.target_dir())?;
|
||||
let success = exercise.run_exercise(Some(&mut output), app_state.cmd_runner())?;
|
||||
|
||||
let mut stdout = io::stdout().lock();
|
||||
stdout.write_all(&output)?;
|
||||
@@ -19,33 +24,37 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
|
||||
if !success {
|
||||
app_state.set_pending(app_state.current_exercise_ind())?;
|
||||
|
||||
bail!(
|
||||
"Ran {} with errors",
|
||||
app_state.current_exercise().terminal_link(),
|
||||
);
|
||||
stdout.write_all(b"Ran ")?;
|
||||
app_state
|
||||
.current_exercise()
|
||||
.terminal_file_link(&mut stdout)?;
|
||||
stdout.write_all(b" with errors\n")?;
|
||||
|
||||
return Ok(ExitCode::FAILURE);
|
||||
}
|
||||
|
||||
writeln!(
|
||||
stdout,
|
||||
"{}{}",
|
||||
"✓ Successfully ran ".green(),
|
||||
exercise.path.green(),
|
||||
)?;
|
||||
stdout.queue(SetForegroundColor(Color::Green))?;
|
||||
stdout.write_all("✓ Successfully ran ".as_bytes())?;
|
||||
stdout.write_all(exercise.path.as_bytes())?;
|
||||
stdout.queue(ResetColor)?;
|
||||
stdout.write_all(b"\n")?;
|
||||
|
||||
if let Some(solution_path) = app_state.current_solution_path()? {
|
||||
println!(
|
||||
"\nA solution file can be found at {}\n",
|
||||
style(TerminalFileLink(&solution_path)).underlined().green(),
|
||||
);
|
||||
stdout.write_all(b"\n")?;
|
||||
solution_link_line(&mut stdout, &solution_path)?;
|
||||
stdout.write_all(b"\n")?;
|
||||
}
|
||||
|
||||
match app_state.done_current_exercise(&mut stdout)? {
|
||||
match app_state.done_current_exercise::<false>(&mut stdout)? {
|
||||
ExercisesProgress::NewPending | ExercisesProgress::CurrentPending => {
|
||||
stdout.write_all(b"Next exercise: ")?;
|
||||
app_state
|
||||
.current_exercise()
|
||||
.terminal_file_link(&mut stdout)?;
|
||||
stdout.write_all(b"\n")?;
|
||||
}
|
||||
ExercisesProgress::AllDone => (),
|
||||
ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => println!(
|
||||
"Next exercise: {}",
|
||||
app_state.current_exercise().terminal_link(),
|
||||
),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
|
||||
279
src/term.rs
Normal file
279
src/term.rs
Normal file
@@ -0,0 +1,279 @@
|
||||
use crossterm::{
|
||||
cursor::MoveTo,
|
||||
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
|
||||
terminal::{Clear, ClearType},
|
||||
Command, QueueableCommand,
|
||||
};
|
||||
use std::{
|
||||
fmt, fs,
|
||||
io::{self, BufRead, StdoutLock, Write},
|
||||
};
|
||||
|
||||
use crate::app_state::CheckProgress;
|
||||
|
||||
pub struct MaxLenWriter<'a, 'lock> {
|
||||
pub stdout: &'a mut StdoutLock<'lock>,
|
||||
len: usize,
|
||||
max_len: usize,
|
||||
}
|
||||
|
||||
impl<'a, 'lock> MaxLenWriter<'a, 'lock> {
|
||||
#[inline]
|
||||
pub fn new(stdout: &'a mut StdoutLock<'lock>, max_len: usize) -> Self {
|
||||
Self {
|
||||
stdout,
|
||||
len: 0,
|
||||
max_len,
|
||||
}
|
||||
}
|
||||
|
||||
// Additional is for emojis that take more space.
|
||||
#[inline]
|
||||
pub fn add_to_len(&mut self, additional: usize) {
|
||||
self.len += additional;
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CountedWrite<'lock> {
|
||||
fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()>;
|
||||
fn write_str(&mut self, unicode: &str) -> io::Result<()>;
|
||||
fn stdout(&mut self) -> &mut StdoutLock<'lock>;
|
||||
}
|
||||
|
||||
impl<'lock> CountedWrite<'lock> for MaxLenWriter<'_, 'lock> {
|
||||
fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()> {
|
||||
let n = ascii.len().min(self.max_len.saturating_sub(self.len));
|
||||
if n > 0 {
|
||||
self.stdout.write_all(&ascii[..n])?;
|
||||
self.len += n;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_str(&mut self, unicode: &str) -> io::Result<()> {
|
||||
if let Some((ind, c)) = unicode
|
||||
.char_indices()
|
||||
.take(self.max_len.saturating_sub(self.len))
|
||||
.last()
|
||||
{
|
||||
self.stdout
|
||||
.write_all(&unicode.as_bytes()[..ind + c.len_utf8()])?;
|
||||
self.len += ind + 1;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn stdout(&mut self) -> &mut StdoutLock<'lock> {
|
||||
self.stdout
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> CountedWrite<'a> for StdoutLock<'a> {
|
||||
#[inline]
|
||||
fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()> {
|
||||
self.write_all(ascii)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn write_str(&mut self, unicode: &str) -> io::Result<()> {
|
||||
self.write_all(unicode.as_bytes())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn stdout(&mut self) -> &mut StdoutLock<'a> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CheckProgressVisualizer<'a, 'lock> {
|
||||
stdout: &'a mut StdoutLock<'lock>,
|
||||
n_cols: usize,
|
||||
}
|
||||
|
||||
impl<'a, 'lock> CheckProgressVisualizer<'a, 'lock> {
|
||||
const CHECKING_COLOR: Color = Color::Blue;
|
||||
const DONE_COLOR: Color = Color::Green;
|
||||
const PENDING_COLOR: Color = Color::Red;
|
||||
|
||||
pub fn build(stdout: &'a mut StdoutLock<'lock>, term_width: u16) -> io::Result<Self> {
|
||||
clear_terminal(stdout)?;
|
||||
stdout.write_all("Checking all exercises…\n".as_bytes())?;
|
||||
|
||||
// Legend
|
||||
stdout.write_all(b"Color of exercise number: ")?;
|
||||
stdout.queue(SetForegroundColor(Self::CHECKING_COLOR))?;
|
||||
stdout.write_all(b"Checking")?;
|
||||
stdout.queue(ResetColor)?;
|
||||
stdout.write_all(b" - ")?;
|
||||
stdout.queue(SetForegroundColor(Self::DONE_COLOR))?;
|
||||
stdout.write_all(b"Done")?;
|
||||
stdout.queue(ResetColor)?;
|
||||
stdout.write_all(b" - ")?;
|
||||
stdout.queue(SetForegroundColor(Self::PENDING_COLOR))?;
|
||||
stdout.write_all(b"Pending")?;
|
||||
stdout.queue(ResetColor)?;
|
||||
stdout.write_all(b"\n")?;
|
||||
|
||||
// Exercise numbers with up to 3 digits.
|
||||
// +1 because the last column doesn't end with a whitespace.
|
||||
let n_cols = usize::from(term_width + 1) / 4;
|
||||
|
||||
Ok(Self { stdout, n_cols })
|
||||
}
|
||||
|
||||
pub fn update(&mut self, progresses: &[CheckProgress]) -> io::Result<()> {
|
||||
self.stdout.queue(MoveTo(0, 2))?;
|
||||
|
||||
let mut exercise_num = 1;
|
||||
for exercise_progress in progresses {
|
||||
match exercise_progress {
|
||||
CheckProgress::None => (),
|
||||
CheckProgress::Checking => {
|
||||
self.stdout
|
||||
.queue(SetForegroundColor(Self::CHECKING_COLOR))?;
|
||||
}
|
||||
CheckProgress::Done => {
|
||||
self.stdout.queue(SetForegroundColor(Self::DONE_COLOR))?;
|
||||
}
|
||||
CheckProgress::Pending => {
|
||||
self.stdout.queue(SetForegroundColor(Self::PENDING_COLOR))?;
|
||||
}
|
||||
}
|
||||
|
||||
write!(self.stdout, "{exercise_num:<3}")?;
|
||||
self.stdout.queue(ResetColor)?;
|
||||
|
||||
if exercise_num != progresses.len() {
|
||||
if exercise_num % self.n_cols == 0 {
|
||||
self.stdout.write_all(b"\n")?;
|
||||
} else {
|
||||
self.stdout.write_all(b" ")?;
|
||||
}
|
||||
|
||||
exercise_num += 1;
|
||||
}
|
||||
}
|
||||
|
||||
self.stdout.flush()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn progress_bar<'a>(
|
||||
writer: &mut impl CountedWrite<'a>,
|
||||
progress: u16,
|
||||
total: u16,
|
||||
term_width: u16,
|
||||
) -> io::Result<()> {
|
||||
debug_assert!(total <= 999);
|
||||
debug_assert!(progress <= total);
|
||||
|
||||
const PREFIX: &[u8] = b"Progress: [";
|
||||
const PREFIX_WIDTH: u16 = PREFIX.len() as u16;
|
||||
const POSTFIX_WIDTH: u16 = "] xxx/xxx".len() as u16;
|
||||
const WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH;
|
||||
const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4;
|
||||
|
||||
if term_width < MIN_LINE_WIDTH {
|
||||
writer.write_ascii(b"Progress: ")?;
|
||||
// Integers are in ASCII.
|
||||
return writer.write_ascii(format!("{progress}/{total}").as_bytes());
|
||||
}
|
||||
|
||||
let stdout = writer.stdout();
|
||||
stdout.write_all(PREFIX)?;
|
||||
|
||||
let width = term_width - WRAPPER_WIDTH;
|
||||
let filled = (width * progress) / total;
|
||||
|
||||
stdout.queue(SetForegroundColor(Color::Green))?;
|
||||
for _ in 0..filled {
|
||||
stdout.write_all(b"#")?;
|
||||
}
|
||||
|
||||
if filled < width {
|
||||
stdout.write_all(b">")?;
|
||||
}
|
||||
|
||||
let width_minus_filled = width - filled;
|
||||
if width_minus_filled > 1 {
|
||||
let red_part_width = width_minus_filled - 1;
|
||||
stdout.queue(SetForegroundColor(Color::Red))?;
|
||||
for _ in 0..red_part_width {
|
||||
stdout.write_all(b"-")?;
|
||||
}
|
||||
}
|
||||
|
||||
stdout.queue(SetForegroundColor(Color::Reset))?;
|
||||
|
||||
write!(stdout, "] {progress:>3}/{total}")
|
||||
}
|
||||
|
||||
pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> {
|
||||
stdout
|
||||
.queue(MoveTo(0, 0))?
|
||||
.queue(Clear(ClearType::All))?
|
||||
.queue(Clear(ClearType::Purge))
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> {
|
||||
stdout.flush()?;
|
||||
io::stdin().lock().read_until(b'\n', &mut Vec::new())?;
|
||||
stdout.write_all(b"\n")
|
||||
}
|
||||
|
||||
/// Canonicalize, convert to string and remove verbatim part on Windows.
|
||||
pub fn canonicalize(path: &str) -> Option<String> {
|
||||
fs::canonicalize(path)
|
||||
.ok()?
|
||||
.into_os_string()
|
||||
.into_string()
|
||||
.ok()
|
||||
.map(|mut path| {
|
||||
// Windows itself can't handle its verbatim paths.
|
||||
if cfg!(windows) && path.as_bytes().starts_with(br"\\?\") {
|
||||
path.drain(..4);
|
||||
}
|
||||
|
||||
path
|
||||
})
|
||||
}
|
||||
|
||||
pub fn terminal_file_link<'a>(
|
||||
writer: &mut impl CountedWrite<'a>,
|
||||
path: &str,
|
||||
canonical_path: &str,
|
||||
color: Color,
|
||||
) -> io::Result<()> {
|
||||
writer
|
||||
.stdout()
|
||||
.queue(SetForegroundColor(color))?
|
||||
.queue(SetAttribute(Attribute::Underlined))?;
|
||||
writer.stdout().write_all(b"\x1b]8;;file://")?;
|
||||
writer.stdout().write_all(canonical_path.as_bytes())?;
|
||||
writer.stdout().write_all(b"\x1b\\")?;
|
||||
// Only this part is visible.
|
||||
writer.write_str(path)?;
|
||||
writer.stdout().write_all(b"\x1b]8;;\x1b\\")?;
|
||||
writer
|
||||
.stdout()
|
||||
.queue(SetForegroundColor(Color::Reset))?
|
||||
.queue(SetAttribute(Attribute::NoUnderline))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_ansi(output: &mut Vec<u8>, command: impl Command) {
|
||||
struct FmtWriter<'a>(&'a mut Vec<u8>);
|
||||
|
||||
impl fmt::Write for FmtWriter<'_> {
|
||||
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||
self.0.extend_from_slice(s.as_bytes());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
let _ = command.write_ansi(&mut FmtWriter(output));
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
use std::{
|
||||
fmt::{self, Display, Formatter},
|
||||
fs,
|
||||
};
|
||||
|
||||
pub struct TerminalFileLink<'a>(pub &'a str);
|
||||
|
||||
impl<'a> Display for TerminalFileLink<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let path = fs::canonicalize(self.0);
|
||||
|
||||
if let Some(path) = path.as_deref().ok().and_then(|path| path.to_str()) {
|
||||
// Windows itself can't handle its verbatim paths.
|
||||
#[cfg(windows)]
|
||||
let path = if path.len() > 5 && &path[0..4] == r"\\?\" {
|
||||
&path[4..]
|
||||
} else {
|
||||
path
|
||||
};
|
||||
|
||||
write!(f, "\x1b]8;;file://{path}\x1b\\{}\x1b]8;;\x1b\\", self.0)
|
||||
} else {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
164
src/watch.rs
164
src/watch.rs
@@ -1,110 +1,127 @@
|
||||
use anyhow::{Error, Result};
|
||||
use notify_debouncer_mini::{
|
||||
new_debouncer,
|
||||
notify::{self, RecursiveMode},
|
||||
};
|
||||
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use std::{
|
||||
io::{self, Write},
|
||||
path::Path,
|
||||
sync::mpsc::channel,
|
||||
thread,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering::Relaxed},
|
||||
mpsc::channel,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::app_state::{AppState, ExercisesProgress};
|
||||
|
||||
use self::{
|
||||
notify_event::NotifyEventHandler,
|
||||
state::WatchState,
|
||||
terminal_event::{terminal_event_handler, InputEvent},
|
||||
use crate::{
|
||||
app_state::{AppState, ExercisesProgress},
|
||||
list,
|
||||
};
|
||||
|
||||
use self::{notify_event::NotifyEventHandler, state::WatchState, terminal_event::InputEvent};
|
||||
|
||||
mod notify_event;
|
||||
mod state;
|
||||
mod terminal_event;
|
||||
|
||||
static EXERCISE_RUNNING: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
// Private unit type to force using the constructor function.
|
||||
#[must_use = "When the guard is dropped, the input is unpaused"]
|
||||
pub struct InputPauseGuard(());
|
||||
|
||||
impl InputPauseGuard {
|
||||
#[inline]
|
||||
pub fn scoped_pause() -> Self {
|
||||
EXERCISE_RUNNING.store(true, Relaxed);
|
||||
Self(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for InputPauseGuard {
|
||||
#[inline]
|
||||
fn drop(&mut self) {
|
||||
EXERCISE_RUNNING.store(false, Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
enum WatchEvent {
|
||||
Input(InputEvent),
|
||||
FileChange { exercise_ind: usize },
|
||||
TerminalResize,
|
||||
TerminalResize { width: u16 },
|
||||
NotifyErr(notify::Error),
|
||||
TerminalEventErr(io::Error),
|
||||
}
|
||||
|
||||
/// Returned by the watch mode to indicate what to do afterwards.
|
||||
#[must_use]
|
||||
pub enum WatchExit {
|
||||
enum WatchExit {
|
||||
/// Exit the program.
|
||||
Shutdown,
|
||||
/// Enter the list mode and restart the watch mode afterwards.
|
||||
List,
|
||||
}
|
||||
|
||||
/// `notify_exercise_names` as None activates the manual run mode.
|
||||
pub fn watch(
|
||||
fn run_watch(
|
||||
app_state: &mut AppState,
|
||||
notify_exercise_names: Option<&'static [&'static [u8]]>,
|
||||
) -> Result<WatchExit> {
|
||||
let (tx, rx) = channel();
|
||||
let (watch_event_sender, watch_event_receiver) = channel();
|
||||
|
||||
let mut manual_run = false;
|
||||
// Prevent dropping the guard until the end of the function.
|
||||
// Otherwise, the file watcher exits.
|
||||
let _debouncer_guard = if let Some(exercise_names) = notify_exercise_names {
|
||||
let mut debouncer = new_debouncer(
|
||||
Duration::from_millis(200),
|
||||
NotifyEventHandler {
|
||||
tx: tx.clone(),
|
||||
exercise_names,
|
||||
},
|
||||
let _watcher_guard = if let Some(exercise_names) = notify_exercise_names {
|
||||
let notify_event_handler =
|
||||
NotifyEventHandler::build(watch_event_sender.clone(), exercise_names)?;
|
||||
|
||||
let mut watcher = RecommendedWatcher::new(
|
||||
notify_event_handler,
|
||||
Config::default().with_poll_interval(Duration::from_secs(1)),
|
||||
)
|
||||
.inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;
|
||||
debouncer
|
||||
.watcher()
|
||||
|
||||
watcher
|
||||
.watch(Path::new("exercises"), RecursiveMode::Recursive)
|
||||
.inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;
|
||||
|
||||
Some(debouncer)
|
||||
Some(watcher)
|
||||
} else {
|
||||
manual_run = true;
|
||||
None
|
||||
};
|
||||
|
||||
let mut watch_state = WatchState::new(app_state, manual_run);
|
||||
let mut watch_state = WatchState::build(app_state, watch_event_sender, manual_run)?;
|
||||
let mut stdout = io::stdout().lock();
|
||||
|
||||
watch_state.run_current_exercise()?;
|
||||
watch_state.run_current_exercise(&mut stdout)?;
|
||||
|
||||
thread::spawn(move || terminal_event_handler(tx, manual_run));
|
||||
|
||||
while let Ok(event) = rx.recv() {
|
||||
while let Ok(event) = watch_event_receiver.recv() {
|
||||
match event {
|
||||
WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise()? {
|
||||
WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise(&mut stdout)? {
|
||||
ExercisesProgress::AllDone => break,
|
||||
ExercisesProgress::CurrentPending => watch_state.render()?,
|
||||
ExercisesProgress::NewPending => watch_state.run_current_exercise()?,
|
||||
ExercisesProgress::NewPending => watch_state.run_current_exercise(&mut stdout)?,
|
||||
ExercisesProgress::CurrentPending => (),
|
||||
},
|
||||
WatchEvent::Input(InputEvent::Hint) => {
|
||||
watch_state.show_hint()?;
|
||||
}
|
||||
WatchEvent::Input(InputEvent::List) => {
|
||||
return Ok(WatchExit::List);
|
||||
}
|
||||
WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise(&mut stdout)?,
|
||||
WatchEvent::Input(InputEvent::Hint) => watch_state.show_hint(&mut stdout)?,
|
||||
WatchEvent::Input(InputEvent::List) => return Ok(WatchExit::List),
|
||||
WatchEvent::Input(InputEvent::CheckAll) => match watch_state
|
||||
.check_all_exercises(&mut stdout)?
|
||||
{
|
||||
ExercisesProgress::AllDone => break,
|
||||
ExercisesProgress::NewPending => watch_state.run_current_exercise(&mut stdout)?,
|
||||
ExercisesProgress::CurrentPending => watch_state.render(&mut stdout)?,
|
||||
},
|
||||
WatchEvent::Input(InputEvent::Reset) => watch_state.reset_exercise(&mut stdout)?,
|
||||
WatchEvent::Input(InputEvent::Quit) => {
|
||||
watch_state.into_writer().write_all(QUIT_MSG)?;
|
||||
stdout.write_all(QUIT_MSG)?;
|
||||
break;
|
||||
}
|
||||
WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise()?,
|
||||
WatchEvent::Input(InputEvent::Unrecognized) => watch_state.render()?,
|
||||
WatchEvent::FileChange { exercise_ind } => {
|
||||
watch_state.handle_file_change(exercise_ind)?;
|
||||
watch_state.handle_file_change(exercise_ind, &mut stdout)?;
|
||||
}
|
||||
WatchEvent::TerminalResize => {
|
||||
watch_state.render()?;
|
||||
}
|
||||
WatchEvent::NotifyErr(e) => {
|
||||
watch_state.into_writer().write_all(NOTIFY_ERR.as_bytes())?;
|
||||
return Err(Error::from(e));
|
||||
WatchEvent::TerminalResize { width } => {
|
||||
watch_state.update_term_width(width, &mut stdout)?;
|
||||
}
|
||||
WatchEvent::NotifyErr(e) => return Err(Error::from(e).context(NOTIFY_ERR)),
|
||||
WatchEvent::TerminalEventErr(e) => {
|
||||
return Err(Error::from(e).context("Terminal event listener failed"));
|
||||
}
|
||||
@@ -114,9 +131,52 @@ pub fn watch(
|
||||
Ok(WatchExit::Shutdown)
|
||||
}
|
||||
|
||||
fn watch_list_loop(
|
||||
app_state: &mut AppState,
|
||||
notify_exercise_names: Option<&'static [&'static [u8]]>,
|
||||
) -> Result<()> {
|
||||
loop {
|
||||
match run_watch(app_state, notify_exercise_names)? {
|
||||
WatchExit::Shutdown => break Ok(()),
|
||||
// It is much easier to exit the watch mode, launch the list mode and then restart
|
||||
// the watch mode instead of trying to pause the watch threads and correct the
|
||||
// watch state.
|
||||
WatchExit::List => list::list(app_state)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `notify_exercise_names` as None activates the manual run mode.
|
||||
pub fn watch(
|
||||
app_state: &mut AppState,
|
||||
notify_exercise_names: Option<&'static [&'static [u8]]>,
|
||||
) -> Result<()> {
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let stdin_fd = rustix::stdio::stdin();
|
||||
let mut termios = rustix::termios::tcgetattr(stdin_fd)?;
|
||||
let original_local_modes = termios.local_modes;
|
||||
// Disable stdin line buffering and hide input.
|
||||
termios.local_modes -=
|
||||
rustix::termios::LocalModes::ICANON | rustix::termios::LocalModes::ECHO;
|
||||
rustix::termios::tcsetattr(stdin_fd, rustix::termios::OptionalActions::Now, &termios)?;
|
||||
|
||||
let res = watch_list_loop(app_state, notify_exercise_names);
|
||||
|
||||
termios.local_modes = original_local_modes;
|
||||
rustix::termios::tcsetattr(stdin_fd, rustix::termios::OptionalActions::Now, &termios)?;
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
watch_list_loop(app_state, notify_exercise_names)
|
||||
}
|
||||
|
||||
const QUIT_MSG: &[u8] = b"
|
||||
|
||||
We hope you're enjoying learning Rust!
|
||||
If you want to continue working on the exercises at a later point, you can simply run `rustlings` again.
|
||||
If you want to continue working on the exercises at a later point, you can simply run `rustlings` again in this directory.
|
||||
";
|
||||
|
||||
const NOTIFY_ERR: &str = "
|
||||
|
||||
@@ -1,52 +1,132 @@
|
||||
use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind};
|
||||
use std::sync::mpsc::Sender;
|
||||
use anyhow::{Context, Result};
|
||||
use notify::{
|
||||
event::{AccessKind, AccessMode, MetadataKind, ModifyKind, RenameMode},
|
||||
Event, EventKind,
|
||||
};
|
||||
use std::{
|
||||
sync::{
|
||||
atomic::Ordering::Relaxed,
|
||||
mpsc::{sync_channel, RecvTimeoutError, Sender, SyncSender},
|
||||
},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use super::WatchEvent;
|
||||
use super::{WatchEvent, EXERCISE_RUNNING};
|
||||
|
||||
const DEBOUNCE_DURATION: Duration = Duration::from_millis(200);
|
||||
|
||||
pub struct NotifyEventHandler {
|
||||
pub tx: Sender<WatchEvent>,
|
||||
/// Used to report which exercise was modified.
|
||||
pub exercise_names: &'static [&'static [u8]],
|
||||
error_sender: Sender<WatchEvent>,
|
||||
// Sends the index of the updated exercise.
|
||||
update_sender: SyncSender<usize>,
|
||||
// Used to report which exercise was modified.
|
||||
exercise_names: &'static [&'static [u8]],
|
||||
}
|
||||
|
||||
impl notify_debouncer_mini::DebounceEventHandler for NotifyEventHandler {
|
||||
fn handle_event(&mut self, input_event: DebounceEventResult) {
|
||||
let output_event = match input_event {
|
||||
Ok(input_event) => {
|
||||
let Some(exercise_ind) = input_event
|
||||
.iter()
|
||||
.filter_map(|input_event| {
|
||||
if input_event.kind != DebouncedEventKind::Any {
|
||||
return None;
|
||||
impl NotifyEventHandler {
|
||||
pub fn build(
|
||||
watch_event_sender: Sender<WatchEvent>,
|
||||
exercise_names: &'static [&'static [u8]],
|
||||
) -> Result<Self> {
|
||||
let (update_sender, update_receiver) = sync_channel(0);
|
||||
let error_sender = watch_event_sender.clone();
|
||||
|
||||
// Debouncer
|
||||
thread::Builder::new()
|
||||
.spawn(move || {
|
||||
let mut exercise_updated = vec![false; exercise_names.len()];
|
||||
|
||||
loop {
|
||||
match update_receiver.recv_timeout(DEBOUNCE_DURATION) {
|
||||
Ok(exercise_ind) => exercise_updated[exercise_ind] = true,
|
||||
Err(RecvTimeoutError::Timeout) => {
|
||||
for (exercise_ind, updated) in exercise_updated.iter_mut().enumerate() {
|
||||
if *updated {
|
||||
if watch_event_sender
|
||||
.send(WatchEvent::FileChange { exercise_ind })
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
*updated = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(RecvTimeoutError::Disconnected) => break,
|
||||
}
|
||||
}
|
||||
})
|
||||
.context("Failed to spawn a thread to debounce file changes")?;
|
||||
|
||||
let file_name = input_event.path.file_name()?.to_str()?.as_bytes();
|
||||
|
||||
if file_name.len() < 4 {
|
||||
return None;
|
||||
}
|
||||
let (file_name_without_ext, ext) = file_name.split_at(file_name.len() - 3);
|
||||
|
||||
if ext != b".rs" {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.exercise_names
|
||||
.iter()
|
||||
.position(|exercise_name| *exercise_name == file_name_without_ext)
|
||||
})
|
||||
.min()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
WatchEvent::FileChange { exercise_ind }
|
||||
}
|
||||
Err(e) => WatchEvent::NotifyErr(e),
|
||||
};
|
||||
|
||||
// An error occurs when the receiver is dropped.
|
||||
// After dropping the receiver, the debouncer guard should also be dropped.
|
||||
let _ = self.tx.send(output_event);
|
||||
Ok(Self {
|
||||
error_sender,
|
||||
update_sender,
|
||||
exercise_names,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl notify::EventHandler for NotifyEventHandler {
|
||||
fn handle_event(&mut self, input_event: notify::Result<Event>) {
|
||||
if EXERCISE_RUNNING.load(Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let input_event = match input_event {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
// An error occurs when the receiver is dropped.
|
||||
// After dropping the receiver, the watcher guard should also be dropped.
|
||||
let _ = self.error_sender.send(WatchEvent::NotifyErr(e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match input_event.kind {
|
||||
EventKind::Any => (),
|
||||
EventKind::Modify(modify_kind) => match modify_kind {
|
||||
ModifyKind::Any | ModifyKind::Data(_) => (),
|
||||
ModifyKind::Name(rename_mode) => match rename_mode {
|
||||
RenameMode::Any | RenameMode::To => (),
|
||||
RenameMode::From | RenameMode::Both | RenameMode::Other => return,
|
||||
},
|
||||
ModifyKind::Metadata(metadata_kind) => match metadata_kind {
|
||||
MetadataKind::Any | MetadataKind::WriteTime => (),
|
||||
MetadataKind::AccessTime
|
||||
| MetadataKind::Permissions
|
||||
| MetadataKind::Ownership
|
||||
| MetadataKind::Extended
|
||||
| MetadataKind::Other => return,
|
||||
},
|
||||
ModifyKind::Other => return,
|
||||
},
|
||||
EventKind::Access(access_kind) => match access_kind {
|
||||
AccessKind::Any => (),
|
||||
AccessKind::Close(access_mode) => match access_mode {
|
||||
AccessMode::Any | AccessMode::Write => (),
|
||||
AccessMode::Execute | AccessMode::Read | AccessMode::Other => return,
|
||||
},
|
||||
AccessKind::Read | AccessKind::Open(_) | AccessKind::Other => return,
|
||||
},
|
||||
EventKind::Create(_) | EventKind::Remove(_) | EventKind::Other => return,
|
||||
}
|
||||
|
||||
let _ = input_event
|
||||
.paths
|
||||
.into_iter()
|
||||
.filter_map(|path| {
|
||||
let file_name = path.file_name()?.to_str()?.as_bytes();
|
||||
|
||||
let [file_name_without_ext @ .., b'.', b'r', b's'] = file_name else {
|
||||
return None;
|
||||
};
|
||||
|
||||
self.exercise_names
|
||||
.iter()
|
||||
.position(|exercise_name| *exercise_name == file_name_without_ext)
|
||||
})
|
||||
.try_for_each(|exercise_ind| self.update_sender.send(exercise_ind));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use crossterm::{
|
||||
style::{style, Stylize},
|
||||
terminal,
|
||||
style::{
|
||||
Attribute, Attributes, Color, ResetColor, SetAttribute, SetAttributes, SetForegroundColor,
|
||||
},
|
||||
terminal, QueueableCommand,
|
||||
};
|
||||
use std::{
|
||||
io::{self, Read, StdoutLock, Write},
|
||||
sync::mpsc::{sync_channel, Sender, SyncSender},
|
||||
thread,
|
||||
};
|
||||
use std::io::{self, StdoutLock, Write};
|
||||
|
||||
use crate::{
|
||||
app_state::{AppState, ExercisesProgress},
|
||||
clear_terminal,
|
||||
exercise::{RunnableExercise, OUTPUT_CAPACITY},
|
||||
progress_bar::progress_bar,
|
||||
terminal_link::TerminalFileLink,
|
||||
exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY},
|
||||
term::progress_bar,
|
||||
};
|
||||
|
||||
use super::{terminal_event::terminal_event_handler, InputPauseGuard, WatchEvent};
|
||||
|
||||
const HEADING_ATTRIBUTES: Attributes = Attributes::none()
|
||||
.with(Attribute::Bold)
|
||||
.with(Attribute::Underlined);
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
enum DoneStatus {
|
||||
DoneWithSolution(String),
|
||||
@@ -21,40 +32,65 @@ enum DoneStatus {
|
||||
}
|
||||
|
||||
pub struct WatchState<'a> {
|
||||
writer: StdoutLock<'a>,
|
||||
app_state: &'a mut AppState,
|
||||
output: Vec<u8>,
|
||||
show_hint: bool,
|
||||
done_status: DoneStatus,
|
||||
manual_run: bool,
|
||||
term_width: u16,
|
||||
terminal_event_unpause_sender: SyncSender<()>,
|
||||
}
|
||||
|
||||
impl<'a> WatchState<'a> {
|
||||
pub fn new(app_state: &'a mut AppState, manual_run: bool) -> Self {
|
||||
let writer = io::stdout().lock();
|
||||
pub fn build(
|
||||
app_state: &'a mut AppState,
|
||||
watch_event_sender: Sender<WatchEvent>,
|
||||
manual_run: bool,
|
||||
) -> Result<Self> {
|
||||
let term_width = terminal::size()
|
||||
.context("Failed to get the terminal size")?
|
||||
.0;
|
||||
|
||||
Self {
|
||||
writer,
|
||||
let (terminal_event_unpause_sender, terminal_event_unpause_receiver) = sync_channel(0);
|
||||
|
||||
thread::Builder::new()
|
||||
.spawn(move || {
|
||||
terminal_event_handler(
|
||||
watch_event_sender,
|
||||
terminal_event_unpause_receiver,
|
||||
manual_run,
|
||||
)
|
||||
})
|
||||
.context("Failed to spawn a thread to handle terminal events")?;
|
||||
|
||||
Ok(Self {
|
||||
app_state,
|
||||
output: Vec::with_capacity(OUTPUT_CAPACITY),
|
||||
show_hint: false,
|
||||
done_status: DoneStatus::Pending,
|
||||
manual_run,
|
||||
}
|
||||
term_width,
|
||||
terminal_event_unpause_sender,
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn into_writer(self) -> StdoutLock<'a> {
|
||||
self.writer
|
||||
}
|
||||
pub fn run_current_exercise(&mut self, stdout: &mut StdoutLock) -> Result<()> {
|
||||
// Ignore any input until running the exercise is done.
|
||||
let _input_pause_guard = InputPauseGuard::scoped_pause();
|
||||
|
||||
pub fn run_current_exercise(&mut self) -> Result<()> {
|
||||
self.show_hint = false;
|
||||
|
||||
writeln!(
|
||||
stdout,
|
||||
"\nChecking the exercise `{}`. Please wait…",
|
||||
self.app_state.current_exercise().name,
|
||||
)?;
|
||||
|
||||
let success = self
|
||||
.app_state
|
||||
.current_exercise()
|
||||
.run_exercise(&mut self.output, self.app_state.target_dir())?;
|
||||
.run_exercise(Some(&mut self.output), self.app_state.cmd_runner())?;
|
||||
self.output.push(b'\n');
|
||||
if success {
|
||||
self.done_status =
|
||||
if let Some(solution_path) = self.app_state.current_solution_path()? {
|
||||
@@ -69,106 +105,194 @@ impl<'a> WatchState<'a> {
|
||||
self.done_status = DoneStatus::Pending;
|
||||
}
|
||||
|
||||
self.render()
|
||||
self.render(stdout)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_file_change(&mut self, exercise_ind: usize) -> Result<()> {
|
||||
// Don't skip exercises on file changes to avoid confusion from missing exercises.
|
||||
// Skipping exercises must be explicit in the interactive list.
|
||||
// But going back to an earlier exercise on file change is fine.
|
||||
if self.app_state.current_exercise_ind() < exercise_ind {
|
||||
return Ok(());
|
||||
pub fn reset_exercise(&mut self, stdout: &mut StdoutLock) -> Result<()> {
|
||||
clear_terminal(stdout)?;
|
||||
|
||||
stdout.write_all(b"Resetting will undo all your changes to the file ")?;
|
||||
stdout.write_all(self.app_state.current_exercise().path.as_bytes())?;
|
||||
stdout.write_all(b"\nReset (y/n)? ")?;
|
||||
stdout.flush()?;
|
||||
|
||||
{
|
||||
let mut stdin = io::stdin().lock();
|
||||
let mut answer = [0];
|
||||
loop {
|
||||
stdin
|
||||
.read_exact(&mut answer)
|
||||
.context("Failed to read the user's input")?;
|
||||
|
||||
match answer[0] {
|
||||
b'y' | b'Y' => {
|
||||
self.app_state.reset_current_exercise()?;
|
||||
|
||||
// The file watcher reruns the exercise otherwise.
|
||||
if self.manual_run {
|
||||
self.run_current_exercise(stdout)?;
|
||||
}
|
||||
}
|
||||
b'n' | b'N' => self.render(stdout)?,
|
||||
_ => continue,
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.app_state.set_current_exercise_ind(exercise_ind)?;
|
||||
self.run_current_exercise()
|
||||
}
|
||||
|
||||
/// Move on to the next exercise if the current one is done.
|
||||
pub fn next_exercise(&mut self) -> Result<ExercisesProgress> {
|
||||
if self.done_status == DoneStatus::Pending {
|
||||
return Ok(ExercisesProgress::CurrentPending);
|
||||
}
|
||||
|
||||
self.app_state.done_current_exercise(&mut self.writer)
|
||||
}
|
||||
|
||||
fn show_prompt(&mut self) -> io::Result<()> {
|
||||
self.writer.write_all(b"\n")?;
|
||||
|
||||
if self.manual_run {
|
||||
write!(self.writer, "{}:run / ", 'r'.bold())?;
|
||||
}
|
||||
|
||||
if self.done_status != DoneStatus::Pending {
|
||||
write!(self.writer, "{}:{} / ", 'n'.bold(), "next".underlined())?;
|
||||
}
|
||||
|
||||
if !self.show_hint {
|
||||
write!(self.writer, "{}:hint / ", 'h'.bold())?;
|
||||
}
|
||||
|
||||
write!(self.writer, "{}:list / {}:quit ? ", 'l'.bold(), 'q'.bold())?;
|
||||
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
pub fn render(&mut self) -> Result<()> {
|
||||
// Prevent having the first line shifted if clearing wasn't successful.
|
||||
self.writer.write_all(b"\n")?;
|
||||
|
||||
clear_terminal(&mut self.writer)?;
|
||||
|
||||
self.writer.write_all(&self.output)?;
|
||||
self.writer.write_all(b"\n")?;
|
||||
|
||||
if self.show_hint {
|
||||
writeln!(
|
||||
self.writer,
|
||||
"{}\n{}\n",
|
||||
"Hint".bold().cyan().underlined(),
|
||||
self.app_state.current_exercise().hint,
|
||||
)?;
|
||||
}
|
||||
|
||||
if self.done_status != DoneStatus::Pending {
|
||||
writeln!(
|
||||
self.writer,
|
||||
"{}\n",
|
||||
"Exercise done ✓
|
||||
When you are done experimenting, enter `n` to move on to the next exercise 🦀"
|
||||
.bold()
|
||||
.green(),
|
||||
)?;
|
||||
}
|
||||
|
||||
if let DoneStatus::DoneWithSolution(solution_path) = &self.done_status {
|
||||
writeln!(
|
||||
self.writer,
|
||||
"A solution file can be found at {}\n",
|
||||
style(TerminalFileLink(solution_path)).underlined().green(),
|
||||
)?;
|
||||
}
|
||||
|
||||
let line_width = terminal::size()?.0;
|
||||
let progress_bar = progress_bar(
|
||||
self.app_state.n_done(),
|
||||
self.app_state.exercises().len() as u16,
|
||||
line_width,
|
||||
)?;
|
||||
writeln!(
|
||||
self.writer,
|
||||
"{progress_bar}Current exercise: {}",
|
||||
self.app_state.current_exercise().terminal_link(),
|
||||
)?;
|
||||
|
||||
self.show_prompt()?;
|
||||
self.terminal_event_unpause_sender.send(())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn show_hint(&mut self) -> Result<()> {
|
||||
self.show_hint = true;
|
||||
self.render()
|
||||
pub fn handle_file_change(
|
||||
&mut self,
|
||||
exercise_ind: usize,
|
||||
stdout: &mut StdoutLock,
|
||||
) -> Result<()> {
|
||||
if self.app_state.current_exercise_ind() != exercise_ind {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.run_current_exercise(stdout)
|
||||
}
|
||||
|
||||
/// Move on to the next exercise if the current one is done.
|
||||
pub fn next_exercise(&mut self, stdout: &mut StdoutLock) -> Result<ExercisesProgress> {
|
||||
match self.done_status {
|
||||
DoneStatus::DoneWithSolution(_) | DoneStatus::DoneWithoutSolution => (),
|
||||
DoneStatus::Pending => return Ok(ExercisesProgress::CurrentPending),
|
||||
}
|
||||
|
||||
self.app_state.done_current_exercise::<true>(stdout)
|
||||
}
|
||||
|
||||
fn show_prompt(&self, stdout: &mut StdoutLock) -> io::Result<()> {
|
||||
if self.done_status != DoneStatus::Pending {
|
||||
stdout.queue(SetAttribute(Attribute::Bold))?;
|
||||
stdout.write_all(b"n")?;
|
||||
stdout.queue(ResetColor)?;
|
||||
stdout.write_all(b":")?;
|
||||
stdout.queue(SetAttribute(Attribute::Underlined))?;
|
||||
stdout.write_all(b"next")?;
|
||||
stdout.queue(ResetColor)?;
|
||||
stdout.write_all(b" / ")?;
|
||||
}
|
||||
|
||||
let mut show_key = |key, postfix| {
|
||||
stdout.queue(SetAttribute(Attribute::Bold))?;
|
||||
stdout.write_all(&[key])?;
|
||||
stdout.queue(ResetColor)?;
|
||||
stdout.write_all(postfix)
|
||||
};
|
||||
|
||||
if self.manual_run {
|
||||
show_key(b'r', b":run / ")?;
|
||||
}
|
||||
|
||||
if !self.show_hint {
|
||||
show_key(b'h', b":hint / ")?;
|
||||
}
|
||||
|
||||
show_key(b'l', b":list / ")?;
|
||||
show_key(b'c', b":check all / ")?;
|
||||
show_key(b'x', b":reset / ")?;
|
||||
show_key(b'q', b":quit ? ")?;
|
||||
|
||||
stdout.flush()
|
||||
}
|
||||
|
||||
pub fn render(&self, stdout: &mut StdoutLock) -> io::Result<()> {
|
||||
// Prevent having the first line shifted if clearing wasn't successful.
|
||||
stdout.write_all(b"\n")?;
|
||||
clear_terminal(stdout)?;
|
||||
|
||||
stdout.write_all(&self.output)?;
|
||||
|
||||
if self.show_hint {
|
||||
stdout
|
||||
.queue(SetAttributes(HEADING_ATTRIBUTES))?
|
||||
.queue(SetForegroundColor(Color::Cyan))?;
|
||||
stdout.write_all(b"Hint")?;
|
||||
stdout.queue(ResetColor)?;
|
||||
stdout.write_all(b"\n")?;
|
||||
|
||||
stdout.write_all(self.app_state.current_exercise().hint.as_bytes())?;
|
||||
stdout.write_all(b"\n\n")?;
|
||||
}
|
||||
|
||||
if self.done_status != DoneStatus::Pending {
|
||||
stdout
|
||||
.queue(SetAttribute(Attribute::Bold))?
|
||||
.queue(SetForegroundColor(Color::Green))?;
|
||||
stdout.write_all("Exercise done ✓".as_bytes())?;
|
||||
stdout.queue(ResetColor)?;
|
||||
stdout.write_all(b"\n")?;
|
||||
|
||||
if let DoneStatus::DoneWithSolution(solution_path) = &self.done_status {
|
||||
solution_link_line(stdout, solution_path)?;
|
||||
}
|
||||
|
||||
stdout.write_all(
|
||||
"When done experimenting, enter `n` to move on to the next exercise 🦀\n\n"
|
||||
.as_bytes(),
|
||||
)?;
|
||||
}
|
||||
|
||||
progress_bar(
|
||||
stdout,
|
||||
self.app_state.n_done(),
|
||||
self.app_state.exercises().len() as u16,
|
||||
self.term_width,
|
||||
)?;
|
||||
|
||||
stdout.write_all(b"\nCurrent exercise: ")?;
|
||||
self.app_state
|
||||
.current_exercise()
|
||||
.terminal_file_link(stdout)?;
|
||||
stdout.write_all(b"\n\n")?;
|
||||
|
||||
self.show_prompt(stdout)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn show_hint(&mut self, stdout: &mut StdoutLock) -> io::Result<()> {
|
||||
if !self.show_hint {
|
||||
self.show_hint = true;
|
||||
self.render(stdout)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result<ExercisesProgress> {
|
||||
// Ignore any input until checking all exercises is done.
|
||||
let _input_pause_guard = InputPauseGuard::scoped_pause();
|
||||
|
||||
if let Some(first_pending_exercise_ind) = self.app_state.check_all_exercises(stdout)? {
|
||||
// Only change exercise if the current one is done.
|
||||
if self.app_state.current_exercise().done {
|
||||
self.app_state
|
||||
.set_current_exercise_ind(first_pending_exercise_ind)?;
|
||||
Ok(ExercisesProgress::NewPending)
|
||||
} else {
|
||||
Ok(ExercisesProgress::CurrentPending)
|
||||
}
|
||||
} else {
|
||||
self.app_state.render_final_message(stdout)?;
|
||||
Ok(ExercisesProgress::AllDone)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_term_width(&mut self, width: u16, stdout: &mut StdoutLock) -> io::Result<()> {
|
||||
if self.term_width != width {
|
||||
self.term_width = width;
|
||||
self.render(stdout)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,86 +1,73 @@
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
|
||||
use std::sync::mpsc::Sender;
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
|
||||
use std::sync::{
|
||||
atomic::Ordering::Relaxed,
|
||||
mpsc::{Receiver, Sender},
|
||||
};
|
||||
|
||||
use super::WatchEvent;
|
||||
use super::{WatchEvent, EXERCISE_RUNNING};
|
||||
|
||||
pub enum InputEvent {
|
||||
Run,
|
||||
Next,
|
||||
Run,
|
||||
Hint,
|
||||
List,
|
||||
CheckAll,
|
||||
Reset,
|
||||
Quit,
|
||||
Unrecognized,
|
||||
}
|
||||
|
||||
pub fn terminal_event_handler(tx: Sender<WatchEvent>, manual_run: bool) {
|
||||
// Only send `Unrecognized` on ENTER if the last input wasn't valid.
|
||||
let mut last_input_valid = false;
|
||||
|
||||
let last_input_event = loop {
|
||||
let terminal_event = match event::read() {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
// If `send` returns an error, then the receiver is dropped and
|
||||
// a shutdown has been already initialized.
|
||||
let _ = tx.send(WatchEvent::TerminalEventErr(e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match terminal_event {
|
||||
Event::Key(key) => {
|
||||
pub fn terminal_event_handler(
|
||||
sender: Sender<WatchEvent>,
|
||||
unpause_receiver: Receiver<()>,
|
||||
manual_run: bool,
|
||||
) {
|
||||
let last_watch_event = loop {
|
||||
match event::read() {
|
||||
Ok(Event::Key(key)) => {
|
||||
match key.kind {
|
||||
KeyEventKind::Release | KeyEventKind::Repeat => continue,
|
||||
KeyEventKind::Press => (),
|
||||
}
|
||||
|
||||
if key.modifiers != KeyModifiers::NONE {
|
||||
last_input_valid = false;
|
||||
if EXERCISE_RUNNING.load(Relaxed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let input_event = match key.code {
|
||||
KeyCode::Enter => {
|
||||
if last_input_valid {
|
||||
continue;
|
||||
KeyCode::Char('n') => InputEvent::Next,
|
||||
KeyCode::Char('r') if manual_run => InputEvent::Run,
|
||||
KeyCode::Char('h') => InputEvent::Hint,
|
||||
KeyCode::Char('l') => break WatchEvent::Input(InputEvent::List),
|
||||
KeyCode::Char('c') => InputEvent::CheckAll,
|
||||
KeyCode::Char('x') => {
|
||||
if sender.send(WatchEvent::Input(InputEvent::Reset)).is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
InputEvent::Unrecognized
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
let input_event = match c {
|
||||
'n' => InputEvent::Next,
|
||||
'h' => InputEvent::Hint,
|
||||
'l' => break InputEvent::List,
|
||||
'q' => break InputEvent::Quit,
|
||||
'r' if manual_run => InputEvent::Run,
|
||||
_ => {
|
||||
last_input_valid = false;
|
||||
continue;
|
||||
}
|
||||
// Pause input until quitting the confirmation prompt.
|
||||
if unpause_receiver.recv().is_err() {
|
||||
return;
|
||||
};
|
||||
|
||||
last_input_valid = true;
|
||||
input_event
|
||||
}
|
||||
_ => {
|
||||
last_input_valid = false;
|
||||
continue;
|
||||
}
|
||||
KeyCode::Char('q') => break WatchEvent::Input(InputEvent::Quit),
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
if tx.send(WatchEvent::Input(input_event)).is_err() {
|
||||
if sender.send(WatchEvent::Input(input_event)).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Event::Resize(_, _) => {
|
||||
if tx.send(WatchEvent::TerminalResize).is_err() {
|
||||
Ok(Event::Resize(width, _)) => {
|
||||
if sender.send(WatchEvent::TerminalResize { width }).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => continue,
|
||||
Ok(Event::FocusGained | Event::FocusLost | Event::Mouse(_)) => continue,
|
||||
Err(e) => break WatchEvent::TerminalEventErr(e),
|
||||
}
|
||||
};
|
||||
|
||||
let _ = tx.send(WatchEvent::Input(last_input_event));
|
||||
let _ = sender.send(last_watch_event);
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
[package]
|
||||
name = "failure"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "compFailure"
|
||||
path = "exercises/compFailure.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "compNoExercise"
|
||||
path = "exercises/compNoExercise.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "testFailure"
|
||||
path = "exercises/testFailure.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "testNotPassed"
|
||||
path = "exercises/testNotPassed.rs"
|
||||
@@ -1,6 +0,0 @@
|
||||
fn main() {}
|
||||
|
||||
#[test]
|
||||
fn passing() {
|
||||
asset!(true);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
fn main() {}
|
||||
|
||||
#[test]
|
||||
fn not_passing() {
|
||||
assert!(false);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
format_version = 1
|
||||
|
||||
[[exercises]]
|
||||
name = "compFailure"
|
||||
test = false
|
||||
hint = ""
|
||||
|
||||
[[exercises]]
|
||||
name = "testFailure"
|
||||
hint = "Hello!"
|
||||
@@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "state"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "pending_exercise"
|
||||
path = "exercises/pending_exercise.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "pending_test_exercise"
|
||||
path = "exercises/pending_test_exercise.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "finished_exercise"
|
||||
path = "exercises/finished_exercise.rs"
|
||||
@@ -1 +0,0 @@
|
||||
fn main() {}
|
||||
@@ -1,4 +0,0 @@
|
||||
fn main() {}
|
||||
|
||||
#[test]
|
||||
fn it_works() {}
|
||||
@@ -1,15 +0,0 @@
|
||||
format_version = 1
|
||||
|
||||
[[exercises]]
|
||||
name = "pending_exercise"
|
||||
test = false
|
||||
hint = """"""
|
||||
|
||||
[[exercises]]
|
||||
name = "pending_test_exercise"
|
||||
hint = """"""
|
||||
|
||||
[[exercises]]
|
||||
name = "finished_exercise"
|
||||
test = false
|
||||
hint = """"""
|
||||
@@ -1,12 +0,0 @@
|
||||
[package]
|
||||
name = "success"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "compSuccess"
|
||||
path = "exercises/compSuccess.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "testSuccess"
|
||||
path = "exercises/testSuccess.rs"
|
||||
@@ -1 +0,0 @@
|
||||
fn main() {}
|
||||
@@ -1,7 +0,0 @@
|
||||
fn main() {}
|
||||
|
||||
#[test]
|
||||
fn passing() {
|
||||
println!("THIS TEST TOO SHALL PASS");
|
||||
assert!(true);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
format_version = 1
|
||||
|
||||
[[exercises]]
|
||||
name = "compSuccess"
|
||||
test = false
|
||||
hint = """"""
|
||||
|
||||
[[exercises]]
|
||||
name = "testSuccess"
|
||||
hint = """"""
|
||||
@@ -1,134 +1,182 @@
|
||||
use assert_cmd::prelude::*;
|
||||
use std::process::Command;
|
||||
use std::{
|
||||
env::{self, consts::EXE_SUFFIX},
|
||||
process::{Command, Stdio},
|
||||
str::from_utf8,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn fails_when_in_wrong_dir() {
|
||||
Command::cargo_bin("rustlings")
|
||||
.unwrap()
|
||||
.current_dir("tests/")
|
||||
.assert()
|
||||
.code(1);
|
||||
enum Output<'a> {
|
||||
FullStdout(&'a str),
|
||||
PartialStdout(&'a str),
|
||||
PartialStderr(&'a str),
|
||||
}
|
||||
|
||||
use Output::*;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Cmd<'a> {
|
||||
current_dir: Option<&'a str>,
|
||||
args: &'a [&'a str],
|
||||
output: Option<Output<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Cmd<'a> {
|
||||
#[inline]
|
||||
fn current_dir(&mut self, current_dir: &'a str) -> &mut Self {
|
||||
self.current_dir = Some(current_dir);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn args(&mut self, args: &'a [&'a str]) -> &mut Self {
|
||||
self.args = args;
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn output(&mut self, output: Output<'a>) -> &mut Self {
|
||||
self.output = Some(output);
|
||||
self
|
||||
}
|
||||
|
||||
fn assert(&self, success: bool) {
|
||||
let rustlings_bin = {
|
||||
let mut path = env::current_exe().unwrap();
|
||||
// Pop test binary name
|
||||
path.pop();
|
||||
// Pop `/deps`
|
||||
path.pop();
|
||||
|
||||
path.push("rustlings");
|
||||
let mut path = path.into_os_string();
|
||||
path.push(EXE_SUFFIX);
|
||||
path
|
||||
};
|
||||
|
||||
let mut cmd = Command::new(rustlings_bin);
|
||||
|
||||
if let Some(current_dir) = self.current_dir {
|
||||
cmd.current_dir(current_dir);
|
||||
}
|
||||
|
||||
cmd.args(self.args).stdin(Stdio::null());
|
||||
|
||||
let status = match self.output {
|
||||
None => cmd
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.unwrap(),
|
||||
Some(FullStdout(stdout)) => {
|
||||
let output = cmd.stderr(Stdio::null()).output().unwrap();
|
||||
assert_eq!(from_utf8(&output.stdout).unwrap(), stdout);
|
||||
output.status
|
||||
}
|
||||
Some(PartialStdout(stdout)) => {
|
||||
let output = cmd.stderr(Stdio::null()).output().unwrap();
|
||||
assert!(from_utf8(&output.stdout).unwrap().contains(stdout));
|
||||
output.status
|
||||
}
|
||||
Some(PartialStderr(stderr)) => {
|
||||
let output = cmd.stdout(Stdio::null()).output().unwrap();
|
||||
assert!(from_utf8(&output.stderr).unwrap().contains(stderr));
|
||||
output.status
|
||||
}
|
||||
};
|
||||
|
||||
assert_eq!(status.success(), success, "{cmd:?}");
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn success(&self) {
|
||||
self.assert(true);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn fail(&self) {
|
||||
self.assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_single_compile_success() {
|
||||
Command::cargo_bin("rustlings")
|
||||
.unwrap()
|
||||
.args(["run", "compSuccess"])
|
||||
.current_dir("tests/fixture/success/")
|
||||
.assert()
|
||||
fn run_compilation_success() {
|
||||
Cmd::default()
|
||||
.current_dir("tests/test_exercises")
|
||||
.args(&["run", "compilation_success"])
|
||||
.success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_single_compile_failure() {
|
||||
Command::cargo_bin("rustlings")
|
||||
.unwrap()
|
||||
.args(["run", "compFailure"])
|
||||
.current_dir("tests/fixture/failure/")
|
||||
.assert()
|
||||
.code(1);
|
||||
fn run_compilation_failure() {
|
||||
Cmd::default()
|
||||
.current_dir("tests/test_exercises")
|
||||
.args(&["run", "compilation_failure"])
|
||||
.fail();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_single_test_success() {
|
||||
Command::cargo_bin("rustlings")
|
||||
.unwrap()
|
||||
.args(["run", "testSuccess"])
|
||||
.current_dir("tests/fixture/success/")
|
||||
.assert()
|
||||
fn run_test_success() {
|
||||
Cmd::default()
|
||||
.current_dir("tests/test_exercises")
|
||||
.args(&["run", "test_success"])
|
||||
.output(PartialStdout("\nOutput from `main` function\n"))
|
||||
.success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_single_test_failure() {
|
||||
Command::cargo_bin("rustlings")
|
||||
.unwrap()
|
||||
.args(["run", "testFailure"])
|
||||
.current_dir("tests/fixture/failure/")
|
||||
.assert()
|
||||
.code(1);
|
||||
fn run_test_failure() {
|
||||
Cmd::default()
|
||||
.current_dir("tests/test_exercises")
|
||||
.args(&["run", "test_failure"])
|
||||
.fail();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_single_test_not_passed() {
|
||||
Command::cargo_bin("rustlings")
|
||||
.unwrap()
|
||||
.args(["run", "testNotPassed.rs"])
|
||||
.current_dir("tests/fixture/failure/")
|
||||
.assert()
|
||||
.code(1);
|
||||
fn run_exercise_not_in_info() {
|
||||
Cmd::default()
|
||||
.current_dir("tests/test_exercises")
|
||||
.args(&["run", "not_in_info"])
|
||||
.fail();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_single_test_no_exercise() {
|
||||
Command::cargo_bin("rustlings")
|
||||
.unwrap()
|
||||
.args(["run", "compNoExercise.rs"])
|
||||
.current_dir("tests/fixture/failure")
|
||||
.assert()
|
||||
.code(1);
|
||||
fn reset_without_exercise_name() {
|
||||
Cmd::default().args(&["reset"]).fail();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_single_exercise() {
|
||||
Command::cargo_bin("rustlings")
|
||||
.unwrap()
|
||||
.args(["reset", "intro1"])
|
||||
.assert()
|
||||
.code(0);
|
||||
fn hint() {
|
||||
Cmd::default()
|
||||
.current_dir("tests/test_exercises")
|
||||
.args(&["hint", "test_failure"])
|
||||
.output(FullStdout("The answer to everything: 42\n"))
|
||||
.success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_no_exercise() {
|
||||
Command::cargo_bin("rustlings")
|
||||
.unwrap()
|
||||
.arg("reset")
|
||||
.assert()
|
||||
.code(2)
|
||||
.stderr(predicates::str::contains(
|
||||
"required arguments were not provided",
|
||||
));
|
||||
}
|
||||
fn init() {
|
||||
let test_dir = tempfile::TempDir::new().unwrap();
|
||||
let test_dir = test_dir.path().to_str().unwrap();
|
||||
|
||||
#[test]
|
||||
fn get_hint_for_single_test() {
|
||||
Command::cargo_bin("rustlings")
|
||||
.unwrap()
|
||||
.args(["hint", "testFailure"])
|
||||
.current_dir("tests/fixture/failure")
|
||||
.assert()
|
||||
.code(0)
|
||||
.stdout("Hello!\n");
|
||||
}
|
||||
Cmd::default().current_dir(test_dir).fail();
|
||||
|
||||
#[test]
|
||||
fn run_compile_exercise_does_not_prompt() {
|
||||
Command::cargo_bin("rustlings")
|
||||
.unwrap()
|
||||
.args(["run", "pending_exercise"])
|
||||
.current_dir("tests/fixture/state")
|
||||
.assert()
|
||||
.code(0);
|
||||
}
|
||||
Cmd::default()
|
||||
.current_dir(test_dir)
|
||||
.args(&["init"])
|
||||
.success();
|
||||
|
||||
#[test]
|
||||
fn run_test_exercise_does_not_prompt() {
|
||||
Command::cargo_bin("rustlings")
|
||||
.unwrap()
|
||||
.args(["run", "pending_test_exercise"])
|
||||
.current_dir("tests/fixture/state")
|
||||
.assert()
|
||||
.code(0);
|
||||
}
|
||||
// Running `init` after a successful initialization.
|
||||
Cmd::default()
|
||||
.current_dir(test_dir)
|
||||
.args(&["init"])
|
||||
.output(PartialStderr("`cd rustlings`"))
|
||||
.fail();
|
||||
|
||||
#[test]
|
||||
fn run_single_test_success_with_output() {
|
||||
Command::cargo_bin("rustlings")
|
||||
.unwrap()
|
||||
.args(["run", "testSuccess"])
|
||||
.current_dir("tests/fixture/success/")
|
||||
.assert()
|
||||
.code(0)
|
||||
.stdout(predicates::str::contains("THIS TEST TOO SHALL PASS"));
|
||||
let initialized_dir = format!("{test_dir}/rustlings");
|
||||
|
||||
// Running `init` in the initialized directory.
|
||||
Cmd::default()
|
||||
.current_dir(&initialized_dir)
|
||||
.args(&["init"])
|
||||
.output(PartialStderr("already initialized"))
|
||||
.fail();
|
||||
}
|
||||
|
||||
11
tests/test_exercises/dev/Cargo.toml
Normal file
11
tests/test_exercises/dev/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
bin = [
|
||||
{ name = "compilation_success", path = "../exercises/compilation_success.rs" },
|
||||
{ name = "compilation_failure", path = "../exercises/compilation_failure.rs" },
|
||||
{ name = "test_success", path = "../exercises/test_success.rs" },
|
||||
{ name = "test_failure", path = "../exercises/test_failure.rs" },
|
||||
]
|
||||
|
||||
[package]
|
||||
name = "test_exercises"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
9
tests/test_exercises/exercises/test_failure.rs
Normal file
9
tests/test_exercises/exercises/test_failure.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
fn main() {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn fails() {
|
||||
assert!(false);
|
||||
}
|
||||
}
|
||||
9
tests/test_exercises/exercises/test_success.rs
Normal file
9
tests/test_exercises/exercises/test_success.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
fn main() {
|
||||
println!("Output from `main` function");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn passes() {}
|
||||
}
|
||||
19
tests/test_exercises/info.toml
Normal file
19
tests/test_exercises/info.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
format_version = 1
|
||||
|
||||
[[exercises]]
|
||||
name = "compilation_success"
|
||||
test = false
|
||||
hint = ""
|
||||
|
||||
[[exercises]]
|
||||
name = "compilation_failure"
|
||||
test = false
|
||||
hint = ""
|
||||
|
||||
[[exercises]]
|
||||
name = "test_success"
|
||||
hint = ""
|
||||
|
||||
[[exercises]]
|
||||
name = "test_failure"
|
||||
hint = "The answer to everything: 42"
|
||||
Reference in New Issue
Block a user