mirror of
https://github.com/rust-lang/rustlings.git
synced 2025-12-28 06:49:19 +00:00
Changing some files
This commit is contained in:
parent
98ef272777
commit
f90ea4c65b
20
.gitignore
vendored
20
.gitignore
vendored
@ -1,19 +1,3 @@
|
||||
# Cargo
|
||||
target/
|
||||
Cargo.lock
|
||||
!/Cargo.lock
|
||||
|
||||
# State file
|
||||
.rustlings-state.txt
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
.direnv/
|
||||
|
||||
# Editor
|
||||
*.swp
|
||||
.idea
|
||||
*.iml
|
||||
|
||||
# Ignore file for editors like Helix
|
||||
.ignore
|
||||
target/
|
||||
.vscode/
|
||||
|
||||
60
.rustlings-state.txt
Normal file
60
.rustlings-state.txt
Normal file
@ -0,0 +1,60 @@
|
||||
DON'T EDIT THIS FILE!
|
||||
|
||||
generics1
|
||||
|
||||
intro1
|
||||
intro2
|
||||
variables1
|
||||
variables2
|
||||
variables3
|
||||
variables4
|
||||
variables5
|
||||
variables6
|
||||
functions1
|
||||
functions2
|
||||
functions3
|
||||
functions4
|
||||
functions5
|
||||
if1
|
||||
if2
|
||||
if3
|
||||
quiz1
|
||||
primitive_types1
|
||||
primitive_types2
|
||||
primitive_types3
|
||||
primitive_types4
|
||||
primitive_types5
|
||||
primitive_types6
|
||||
vecs1
|
||||
vecs2
|
||||
move_semantics1
|
||||
move_semantics2
|
||||
move_semantics3
|
||||
move_semantics4
|
||||
move_semantics5
|
||||
structs1
|
||||
structs2
|
||||
structs3
|
||||
enums1
|
||||
enums2
|
||||
enums3
|
||||
strings1
|
||||
strings2
|
||||
strings3
|
||||
strings4
|
||||
modules1
|
||||
modules2
|
||||
modules3
|
||||
hashmaps1
|
||||
hashmaps2
|
||||
hashmaps3
|
||||
quiz2
|
||||
options1
|
||||
options2
|
||||
options3
|
||||
errors1
|
||||
errors2
|
||||
errors3
|
||||
errors4
|
||||
errors5
|
||||
errors6
|
||||
@ -1,7 +0,0 @@
|
||||
[default.extend-words]
|
||||
"earch" = "earch" # Because of <s>earch in the list footer
|
||||
|
||||
[files]
|
||||
extend-exclude = [
|
||||
"CHANGELOG.md",
|
||||
]
|
||||
976
CHANGELOG.md
976
CHANGELOG.md
@ -1,976 +0,0 @@
|
||||
## Unreleased
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgrade to Rust edition 2024
|
||||
- Raise the minimum supported Rust version to `1.87`
|
||||
|
||||
## 6.4.0 (2024-11-11)
|
||||
|
||||
### Added
|
||||
|
||||
- The list of exercises is now searchable by pressing `s` or `/` 🔍️ (thanks to [@frroossst](https://github.com/frroossst))
|
||||
- New option `c` in the prompt to manually check all exercises ✅ (thanks to [@Nahor](https://github.com/Nahor))
|
||||
- New command `check-all` to manually check all exercises ✅ (thanks to [@Nahor](https://github.com/Nahor))
|
||||
- Addictive animation for showing the progress of checking all exercises. A nice showcase of parallelism in Rust ✨
|
||||
- New option `x` in the prompt to reset the file of the current exercise 🔄
|
||||
- Allow `dead_code` for all exercises and solutions ⚰️ (thanks to [@huss4in](https://github.com/huss4in))
|
||||
- Pause input while running an exercise to avoid unexpected prompt interactions ⏸️
|
||||
- Limit the maximum number of exercises to 999. Any community exercises willing to reach that limit? 🔝
|
||||
|
||||
### Changed
|
||||
|
||||
- `enums3`: Remove redundant enum definition task (thanks to [@senekor](https://github.com/senekor))
|
||||
- `if2`: Make the exercise less confusing by avoiding "fizz", "fuzz", "foo", "bar" and "baz" (thanks to [@senekor](https://github.com/senekor))
|
||||
- `hashmap3`: Use the method `Entry::or_default`.
|
||||
- Update the state of all exercises when checking all of them (thanks to [@Nahor](https://github.com/Nahor))
|
||||
- The main prompt doesn't need a confirmation with ENTER on Unix-like systems anymore.
|
||||
- No more jumping back to a previous exercise when its file is changed. Use the list to jump between exercises.
|
||||
- Dump the solution file after an exercise is done even if the solution's directory doesn't exist.
|
||||
- Rework the footer in the list.
|
||||
- Optimize the file watcher.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix bad contrast in the list on terminals with a light theme.
|
||||
|
||||
## 6.3.0 (2024-08-29)
|
||||
|
||||
### Added
|
||||
|
||||
- Add the following exercise lints:
|
||||
- `forbid(unsafe_code)`: You shouldn't write unsafe code in Rustlings.
|
||||
- `forbid(unstable_features)`: You don't need unstable features in Rustlings and shouldn't rely on them while learning Rust.
|
||||
- `forbid(todo)`: You forgot a `todo!()`.
|
||||
- `forbid(empty_loop)`: This can only happen by mistake in Rustlings.
|
||||
- `deny(infinite_loop)`: No infinite loops are needed in Rustlings.
|
||||
- `deny(mem_forget)`: You shouldn't leak memory while still learning Rust.
|
||||
- Show a link to every exercise file in the list.
|
||||
- Add scroll padding in the list.
|
||||
- Break the help footer of the list into two lines when the terminal width isn't big enough.
|
||||
- Enable scrolling with the mouse in the list.
|
||||
- `dev check`: Show the progress of checks.
|
||||
- `dev check`: Check that the length of all exercise names is lower than 32.
|
||||
- `dev check`: Check if exercise contains no tests and isn't marked with `test = false`.
|
||||
|
||||
### Changed
|
||||
|
||||
- The compilation time when installing Rustlings is reduced.
|
||||
- Pressing `c` in the list for "continue on" now quits the list after setting the selected exercise as the current one.
|
||||
- Better highlighting of the solution file after an exercise is done.
|
||||
- Don't show the output of successful tests anymore. Instead, show the pretty output for tests.
|
||||
- Be explicit about `q` only quitting the list and not the whole program in the list.
|
||||
- Be explicit about `r` only resetting one exercise (the selected one) in the list.
|
||||
- Ignore the standard output of `git init`.
|
||||
- `threads3`: Remove the queue length and improve tests.
|
||||
- `errors4`: Use match instead of a comparison chain in the solution.
|
||||
- `functions3`: Only take `u8` to avoid using a too high number of iterations by mistake.
|
||||
- `dev check`: Always check with strict Clippy (warnings to errors) when checking the solutions.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix the error on some systems about too many open files during the final check of all exercises.
|
||||
- Fix the list when the terminal height is too low.
|
||||
- Restore the terminal after an error in the list.
|
||||
|
||||
## 6.2.0 (2024-08-09)
|
||||
|
||||
### Added
|
||||
|
||||
- Show a message before checking and running an exercise. This gives the user instant feedback and avoids confusion if the checks take too long.
|
||||
- Show a helpful error message when trying to install Rustlings with a Rust version lower than the minimum one that Rustlings supports.
|
||||
- Add a `README.md` file to the `solutions/` directory.
|
||||
- Allow initializing Rustlings in a Cargo workspace.
|
||||
- `dev check`: Check that all solutions are formatted with `rustfmt`.
|
||||
|
||||
### Changed
|
||||
|
||||
- Remove the state file and the solutions directory from the generated `.gitignore` file.
|
||||
- Run the final check of all exercises in parallel.
|
||||
- Small exercise improvements.
|
||||
|
||||
## 6.1.0 (2024-07-10)
|
||||
|
||||
#### Added
|
||||
|
||||
- `dev check`: Check that all exercises (including community ones) include at least one `TODO` comment.
|
||||
- `dev check`: Check that all exercises actually fail to run (not already solved).
|
||||
|
||||
#### Changed
|
||||
|
||||
- Make enum variants more consistent between enum exercises.
|
||||
- `iterators3`: Teach about the possible case of integer overflow during division.
|
||||
|
||||
#### Fixed
|
||||
|
||||
- Exit with a helpful error message on missing/unsupported terminal/TTY.
|
||||
- Mark the last exercise as done.
|
||||
|
||||
## 6.0.1 (2024-07-04)
|
||||
|
||||
Small exercise improvements and fixes.
|
||||
Most importantly, fixed that the exercise `clippy1` was already solved 😅
|
||||
|
||||
## 6.0.0 (2024-07-03)
|
||||
|
||||
This release is the result of a complete rewrite to deliver a ton of new features and improvements ✨
|
||||
The most important changes are highlighted below.
|
||||
|
||||
### Installation
|
||||
|
||||
The installation has been simplified a lot!
|
||||
To install Rustlings after installing Rust, all what you need to do now is running the following command:
|
||||
|
||||
```bash
|
||||
cargo install rustlings
|
||||
```
|
||||
|
||||
Yes, this means that Rustlings is now on [crates.io](https://crates.io/crates/rustlings) 🎉
|
||||
|
||||
You can read about the motivations of this change in [this issue](https://github.com/rust-lang/rustlings/issues/1919).
|
||||
|
||||
### UI/UX
|
||||
|
||||
- The UI is now responsive when the terminal is resized.
|
||||
- The progress bar was moved to the bottom so that you can always see your progress and the current exercise to work on.
|
||||
- The current exercise path is now a terminal link. It will open the exercise file in your default editor when you click on it.
|
||||
- A small prompt is now always shown at the bottom. It allows you to choose an action by entering a character. For example, entering `h` will show you the hint of the current exercise.
|
||||
- The comment "I AM NOT DONE!" doesn't exist anymore. Instead of needing to remove it to go to the next exercise, you need to enter `n` in the terminal.
|
||||
|
||||
### List mode
|
||||
|
||||
A new list mode was added!
|
||||
You can enter it by entering `l` in the watch mode.
|
||||
It offers the following features:
|
||||
|
||||
- Browse all exercises and see their state (pending/done).
|
||||
- Filter exercises based on their state (pending/done).
|
||||
- Continue at another exercise. This allows you to skip some exercises or go back to previous ones.
|
||||
- Reset an exercise so you can start over and revert your changes.
|
||||
|
||||
### Solutions
|
||||
|
||||
After finishing an exercise, a solution file will be available and Rustlings will show you its path in green.
|
||||
This allows you to compare your solution with an idiomatic solution and maybe learn about other ways to solve a problem.
|
||||
|
||||
While writing the solutions, all exercises have been polished 🌟
|
||||
For example, every exercise now contains `TODO` comments to highlight what the user needs to change and where.
|
||||
|
||||
### LSP support out of the box
|
||||
|
||||
Instead of creating a `project.json` file using `rustlings lsp`, Rustlings now works with a `Cargo.toml` file out of the box.
|
||||
No actions are needed to activate the language server `rust-analyzer`.
|
||||
|
||||
This should avoid issues related to the language server or to running exercises, especially the ones with Clippy.
|
||||
|
||||
### Clippy
|
||||
|
||||
Clippy lints are now shown on all exercises, not only the Clippy exercises 📎
|
||||
Make Clippy your friend from early on 🥰
|
||||
|
||||
### Community Exercises
|
||||
|
||||
Rustlings now supports community 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 [community exercises](https://rustlings.rust-lang.org/community-exercises)!
|
||||
|
||||
## 5.6.1 (2023-09-18)
|
||||
|
||||
#### Changed
|
||||
|
||||
- Converted all exercises with assertions to test mode.
|
||||
|
||||
#### Fixed
|
||||
|
||||
- `cow1`: Reverted regression introduced by calling `to_mut` where it
|
||||
shouldn't have been called, and clarified comment.
|
||||
- `primitive_types3`: Require at least an array of 100 elements.
|
||||
- Removed hint comments when no hint exists for the exercise.
|
||||
- `as_ref_mut`: Fixed a typo in a test function name.
|
||||
- `enums3`: Fixed formatting with `rustfmt`.
|
||||
|
||||
## 5.6.0 (2023-09-04)
|
||||
|
||||
#### Added
|
||||
|
||||
- New exercise: `if3`, teaching the user about `if let` statements.
|
||||
- `hashmaps2`: Added an extra test function to check if the amount of fruits is higher than zero.
|
||||
- `enums3`: Added a test for `Message`.
|
||||
- `if1`: Added a test case to check equal values.
|
||||
- `if3`: Added a note specifying that there are no test changes needed.
|
||||
|
||||
#### Changed
|
||||
|
||||
- Swapped the order of threads and smart pointer exercises.
|
||||
- Rewrote the CLI to use `clap` - it's matured much since we switched to `argh` :)
|
||||
- `structs3`: Switched from i32 to u32.
|
||||
- `move_semantics`: Switched 1-4 to tests, and rewrote them to be way simpler, while still teaching about the same
|
||||
concepts.
|
||||
|
||||
#### Fixed
|
||||
|
||||
- `iterators5`:
|
||||
- Removed an outdated part of the hint.
|
||||
- Renamed variables to use snake_case.
|
||||
- `vecs2`: Updated the hint to reference the renamed loop variable.
|
||||
- `enums3`: Changed message string in test so that it gets properly tested.
|
||||
- `strings2`: Corrected line number in hint, then removed it (this both happened as part of this release cycle).
|
||||
- `primitive_types4`: Updated hint to the correct ending index.
|
||||
- `quiz1`: Removed duplicated sentence from exercise comments.
|
||||
- `errors4`: Improved comment.
|
||||
- `from_into`: Fixed test values.
|
||||
- `cow1`: Added `.to_mut()` to distinguish from the previous test case.
|
||||
- `threads2`: Updated hint text to reference the correct book heading.
|
||||
|
||||
#### Housekeeping
|
||||
|
||||
- Cleaned up the explanation paragraphs at the start of each exercise.
|
||||
- Lots of Nix housekeeping that I don't feel qualified to write about!
|
||||
- Improved CI workflows, we're now testing on multiple platforms at once.
|
||||
|
||||
## 5.5.1 (2023-05-17)
|
||||
|
||||
#### Fixed
|
||||
|
||||
- Reverted `rust-project.json` path generation due to an upstream `rust-analyzer` fix.
|
||||
|
||||
## 5.5.0 (2023-05-17)
|
||||
|
||||
#### Added
|
||||
|
||||
- `strings2`: Added a reference to the book chapter for reference conversion
|
||||
- `lifetimes`: Added a link to the lifetimekata project
|
||||
- Added a new `tests4` exercises, which teaches about testing for panics
|
||||
- Added a `!` prefix command to watch mode that runs an external command
|
||||
- Added a `--success-hints` option to watch mode that shows hints on exercise success
|
||||
|
||||
#### Changed
|
||||
|
||||
- `vecs2`: Renamed iterator variable bindings for clarify
|
||||
- `lifetimes`: Changed order of book references
|
||||
- `hashmaps2`: Clarified instructions in the todo block
|
||||
- Moved lifetime exercises before test exercises (via the recommended book ordering)
|
||||
- `options2`: Improved tests for layering options
|
||||
- `modules2`: Added more information to the hint
|
||||
|
||||
#### Fixed
|
||||
|
||||
- `errors2`: Corrected a comment wording
|
||||
- `iterators2`: Fixed a spelling mistake in the hint text
|
||||
- `variables`: Wrapped the mut keyword with backticks for readability
|
||||
- `move_semantics2`: Removed references to line numbers
|
||||
- `cow1`: Clarified the `owned_no_mutation` comments
|
||||
- `options3`: Changed exercise to panic when no match is found
|
||||
- `rustlings lsp` now generates absolute paths, which should fix VSCode `rust-analyzer` usage on Windows
|
||||
|
||||
#### Housekeeping
|
||||
|
||||
- Added a markdown linter to run on GitHub actions
|
||||
- Split quick installation section into two code blocks
|
||||
|
||||
## 5.4.1 (2023-03-10)
|
||||
|
||||
#### Changed
|
||||
|
||||
- `vecs`: Added links to `iter_mut` and `map` to README.md
|
||||
- `cow1`: Changed main to tests
|
||||
- `iterators1`: Formatted according to rustfmt
|
||||
|
||||
#### Fixed
|
||||
|
||||
- `errors5`: Unified undisclosed type notation
|
||||
- `arc1`: Improved readability by avoiding implicit dereference
|
||||
- `macros4`: Prevented auto-fix by adding `#[rustfmt::skip]`
|
||||
- `cli`: Actually show correct progress percentages
|
||||
|
||||
## 5.4.0 (2023-02-12)
|
||||
|
||||
#### Changed
|
||||
|
||||
- Reordered exercises
|
||||
- Unwrapped `standard_library_types` into `iterators` and `smart_pointers`
|
||||
- Moved smart pointer exercises behind threads
|
||||
- Ordered `rc1` before `arc1`
|
||||
- **intro1**: Added a note on `rustlings lsp`
|
||||
- **threads1**: Panic if threads are not joined
|
||||
- **cli**:
|
||||
- Made progress bar update proportional to amount of files verified
|
||||
- Decreased `watch` delay from 2 to 1 second
|
||||
|
||||
#### Fixed
|
||||
|
||||
- Capitalized "Rust" in exercise hints
|
||||
- **enums3**: Removed superfluous tuple brackets
|
||||
- **quiz2, clippy1, iterators1**: Fixed a typo
|
||||
- **rc1**: Fixed a prompt error
|
||||
- **cli**:
|
||||
- Fixed a typo in a method name
|
||||
- Specified the edition in `rustc` commands
|
||||
|
||||
#### Housekeeping
|
||||
|
||||
- Bumped min Rust version to 1.58 in installation script
|
||||
|
||||
## 5.3.0 (2022-12-23)
|
||||
|
||||
#### Added
|
||||
|
||||
- **cli**: Added a percentage display in watch mode
|
||||
- Added a `flake.nix` for Nix users
|
||||
|
||||
#### Changed
|
||||
|
||||
- **structs3**: Added an additional test
|
||||
- **macros**: Added a link to MacroKata in the README
|
||||
|
||||
#### Fixed
|
||||
|
||||
- **strings3**: Added a link to `std` in the hint
|
||||
- **threads1**: Corrected a hint link
|
||||
- **iterators1**: Clarified hint steps
|
||||
- **errors5**: Fix a typo in the hint
|
||||
- **options1**: Clarified on the usage of the 24-hour system
|
||||
- **threads2, threads3**: Explicitly use `Arc::clone`
|
||||
- **structs3**: Clarifed the hint
|
||||
- **quiz2, as_ref_mut, options1, traits1, traits2**: Clarified hints
|
||||
- **traits1, traits2, cli**: Tidied up unmatching backticks
|
||||
- **enums2**: Removed unnecessary indirection of self
|
||||
- **enums3**: Added an extra tuple comment
|
||||
|
||||
#### Housekeeping
|
||||
|
||||
- Added a VSCode extension recommendation
|
||||
- Applied some Clippy and rustfmt formatting
|
||||
- Added a note on Windows PowerShell and other shell compatibility
|
||||
|
||||
## 5.2.1 (2022-09-06)
|
||||
|
||||
#### Fixed
|
||||
|
||||
- **quiz1**: Reworded the comment to actually reflect what's going on in the tests.
|
||||
Also added another assert just to make sure.
|
||||
- **rc1**: Fixed a typo in the hint.
|
||||
- **lifetimes**: Add quotes to the `println!` output, for readability.
|
||||
|
||||
#### Housekeeping
|
||||
|
||||
- Fixed a typo in README.md
|
||||
|
||||
## 5.2.0 (2022-08-27)
|
||||
|
||||
#### Added
|
||||
|
||||
- Added a `reset` command
|
||||
|
||||
#### Changed
|
||||
|
||||
- **options2**: Convert the exercise to use tests
|
||||
|
||||
#### Fixed
|
||||
|
||||
- **threads3**: Fixed a typo
|
||||
- **quiz1**: Adjusted the explanations to be consistent with
|
||||
the tests
|
||||
|
||||
## 5.1.1 (2022-08-17)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- Fixed an incorrect assertion in options1
|
||||
|
||||
## 5.1.0 (2022-08-16)
|
||||
|
||||
#### Features
|
||||
|
||||
- Added a new `rc1` exercise.
|
||||
- Added a new `cow1` exercise.
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **variables5**: Corrected reference to previous exercise
|
||||
- **functions4**: Fixed line number reference
|
||||
- **strings3**: Clarified comment wording
|
||||
- **traits4, traits5**: Fixed line number reference
|
||||
- **traits5**:
|
||||
- Fixed typo in "parameter"
|
||||
- Made exercise prefer a traits-based solution
|
||||
- **lifetimes2**: Improved hint
|
||||
- **threads3**: Fixed typo in hint
|
||||
- **box1**: Replaced `unimplemented!` with `todo!`
|
||||
- **errors5**: Provided an explanation for usage of `Box<dyn Error>`
|
||||
- **quiz2**: Fixed a typo
|
||||
- **macros**: Updated the macros book link
|
||||
- **options1**:
|
||||
- Removed unused code
|
||||
- Added more granular tests
|
||||
- Fixed some comment syntax shenanigans in info.toml
|
||||
|
||||
#### Housekeeping
|
||||
|
||||
- Fixed a typo in .editorconfig
|
||||
- Fixed a typo in integration_tests.rs
|
||||
- Clarified manual installation instructions using `cargo install --path .`
|
||||
- Added a link to our Zulip in the readme file
|
||||
|
||||
## 5.0.0 (2022-07-16)
|
||||
|
||||
#### Features
|
||||
|
||||
- Hint comments in exercises now also include a reference to the
|
||||
`hint` watch mode subcommand.
|
||||
- **intro1**: Added more hints to point the user to the source file.
|
||||
- **variables**: Switched variables3 and variables4.
|
||||
- Moved `vec` and `primitive_types` exercises before `move_semantics`.
|
||||
- Renamed `vec` to `vecs` to be more in line with the naming in general.
|
||||
- Split up the `collections` exercises in their own folders.
|
||||
- **vec2**: Added a second part of the function that provides an alternative,
|
||||
immutable way of modifying vec values.
|
||||
- **enums3**: Added a hint.
|
||||
- Moved `strings` before `modules`.
|
||||
- Added a `strings3` exercise to teach modifying strings.
|
||||
- Added a `hashmaps3` exercise for some advanced usage of hashmaps.
|
||||
- Moved the original `quiz2` to be `strings4`, since it only tested strings
|
||||
anyways.
|
||||
- Reworked `quiz2` into a new exercise that tests more chapters.
|
||||
- Renamed `option` to `options`.
|
||||
- **options1**: Rewrote parts of the exercise to remove the weird array
|
||||
iteration stuff.
|
||||
- Moved `generics3` to be `quiz3`.
|
||||
- Moved box/arc exercises behind `iterators`.
|
||||
- **iterators4**: Added a test for factorials of zero.
|
||||
- Split `threads1` between two exercises, the first one focusing more on
|
||||
`JoinHandle`s.
|
||||
- Added a `threads3` exercises that uses `std::sync::mpsc`.
|
||||
- Added a `clippy3` exercises with some more interesting checks.
|
||||
- **as_ref_mut**: Added a section that actually tests `AsMut`.
|
||||
- Added 3 new lifetimes exercises.
|
||||
- Added 3 new traits exercises.
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **variables2**: Made output messages more verbose.
|
||||
- **variables5**: Added a nudging hint about shadowing.
|
||||
- **variables6**: Fixed link to book.
|
||||
- **functions**: Clarified the README wording. Generally cleaned up
|
||||
some hints and added some extra comments.
|
||||
- **if2**: Renamed function name to `foo_if_fizz`.
|
||||
- **move_semantics**: Clarified some hints.
|
||||
- **quiz1**: Renamed the function name to be more verbose.
|
||||
- **structs1**: Use an integer type instead of strings. Renamed "unit structs"
|
||||
to "unit-like structs", as is used in the book.
|
||||
- **structs3**: Added the `panic!` statement in from the beginning.
|
||||
- **errors1**: Use `is_empty()` instead of `len() > 0`
|
||||
- **errors3**: Improved the hint.
|
||||
- **errors5**: Improved exercise instructions and the hint.
|
||||
- **errors6**: Provided the skeleton of one of the functions that's supposed
|
||||
to be implemented.
|
||||
- **iterators3**: Inserted `todo!` into `divide()` to keep a compiler error
|
||||
from happening.
|
||||
- **from_str**: Added a hint comment about string error message conversion with
|
||||
`Box<dyn Error>`.
|
||||
- **try_from_into**: Fixed the function name in comment.
|
||||
|
||||
#### Removed
|
||||
|
||||
- Removed the legacy LSP feature that was using `mod.rs` files.
|
||||
- Removed `quiz4`.
|
||||
- Removed `advanced_errs`. These were the last exercises in the recommended
|
||||
order, and I've always felt like they didn't quite fit in with the mostly
|
||||
simple, book-following style we've had in Rustlings.
|
||||
|
||||
#### Housekeeping
|
||||
|
||||
- Added missing exercises to the book index.
|
||||
- Updated spacing in Cargo.toml.
|
||||
- Added a GitHub actions config so that tests run on every PR/commit.
|
||||
|
||||
## 4.8.0 (2022-07-01)
|
||||
|
||||
#### Features
|
||||
|
||||
- Added a progress indicator for `rustlings watch`.
|
||||
- The installation script now checks for Rustup being installed.
|
||||
- Added a `rustlings lsp` command to enable `rust-analyzer`.
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **move_semantics5**: Replaced "in vogue" with "in scope" in hint.
|
||||
- **if2**: Fixed a typo in the hint.
|
||||
- **variables1**: Fixed an incorrect line reference in the hint.
|
||||
- Fixed an out of bounds check in the installation Bash script.
|
||||
|
||||
#### Housekeeping
|
||||
|
||||
- Replaced the git.io URL with the fully qualified URL because of git.io's sunsetting.
|
||||
- Removed the deprecated Rust GitPod extension.
|
||||
|
||||
## 4.7.1 (2022-04-20)
|
||||
|
||||
#### Features
|
||||
|
||||
- The amount of dependency crates that need to be compiled went down from ~65 to
|
||||
~45 by bumping dependency versions.
|
||||
- The minimum Rust version in the install scripts has been bumped to 1.56.0 (this isn't in
|
||||
the release itself, since install scripts don't really get versioned)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **arc1**: A small part has been rewritten using a more functional code style (#968).
|
||||
- **using_as**: A small part has been refactored to use `sum` instead of `fold`, resulting
|
||||
in better readability.
|
||||
|
||||
#### Housekeeping
|
||||
|
||||
- The changelog will now be manually written instead of being automatically generated by the
|
||||
Git log.
|
||||
|
||||
## 4.7.0 (2022-04-14)
|
||||
|
||||
#### Features
|
||||
|
||||
- Add move_semantics6.rs exercise (#908) ([3f0e1303](https://github.com/rust-lang/rustlings/commit/3f0e1303e0b3bf3fecc0baced3c8b8a37f83c184))
|
||||
- **intro:** Add intro section. ([21c9f441](https://github.com/rust-lang/rustlings/commit/21c9f44168394e08338fd470b5f49b1fd235986f))
|
||||
- Include exercises folder in the project structure behind a feature, enabling rust-analyzer to work (#917) ([179a75a6](https://github.com/rust-lang/rustlings/commit/179a75a68d03ac9518dec2297fb17f91a4fc506b))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- Fix a few spelling mistakes ([1c0fe3cb](https://github.com/rust-lang/rustlings/commit/1c0fe3cbcca85f90b3985985b8e265ee872a2ab2))
|
||||
- **cli:**
|
||||
- Move long text strings into constants. ([f78c4802](https://github.com/rust-lang/rustlings/commit/f78c48020830d7900dd8d81f355606581670446d))
|
||||
- Replace `filter_map()` with `find_map()` ([9b27e8d](https://github.com/rust-lang/rustlings/commit/9b27e8d993ca20232fe38a412750c3f845a83b65))
|
||||
- **clippy1:**
|
||||
- Set clippy::float_cmp lint to deny (#907) ([71a06044](https://github.com/rust-lang/rustlings/commit/71a06044e6a96ff756dc31d7b0ed665ae4badb57))
|
||||
- Updated code to test correctness clippy lint with approx_constant lint rule ([f2650de3](https://github.com/rust-lang/rustlings/commit/f2650de369810867d2763e935ac0963c32ec420e))
|
||||
- **errors1:**
|
||||
- Add a comment to make the purpose more clear (#486) ([cbcde345](https://github.com/rust-lang/rustlings/commit/cbcde345409c3e550112e449242848eaa3391bb6))
|
||||
- Don't modify tests (#958) ([60bb7cc](https://github.com/rust-lang/rustlings/commit/60bb7cc3931d21d3986ad52b2b302e632a93831c))
|
||||
- **errors6:** Remove existing answer code ([43d0623](https://github.com/rust-lang/rustlings/commit/43d0623086edbc46fe896ba59c7afa22c3da9f7a))
|
||||
- **functions5:** Remove wrong new line and small English improvements (#885) ([8ef4869b](https://github.com/rust-lang/rustlings/commit/8ef4869b264094e5a9b50452b4534823a9df19c3))
|
||||
- **install:** protect path with whitespaces using quotes and stop at the first error ([d114847f](https://github.com/rust-lang/rustlings/commit/d114847f256c5f571c0b4c87e04b04bce3435509))
|
||||
- **intro1:** Add compiler error explanation. ([9b8de655](https://github.com/rust-lang/rustlings/commit/9b8de65525a5576b78cf0c8e4098cdd34296338f))
|
||||
- **iterators1:** reorder TODO steps ([0bd7a063](https://github.com/rust-lang/rustlings/commit/0bd7a0631a17a9d69af5746795a30efc9cf64e6e))
|
||||
- **move_semantics2:** Add comment ([89650f80](https://github.com/rust-lang/rustlings/commit/89650f808af23a32c9a2c6d46592b77547a6a464))
|
||||
- **move_semantics5:** correct typo (#857) ([46c28d5c](https://github.com/rust-lang/rustlings/commit/46c28d5cef3d8446b5a356b19d8dbc725f91a3a0))
|
||||
- **quiz1:** update to say quiz covers "If" ([1622e8c1](https://github.com/rust-lang/rustlings/commit/1622e8c198d89739765c915203efff0091bdeb78))
|
||||
- **structs3:**
|
||||
- Add a hint for panic (#608) ([4f7ff5d9](https://github.com/rust-lang/rustlings/commit/4f7ff5d9c7b2d8b045194c1a9469d37e30257c4a))
|
||||
- remove redundant 'return' (#852) ([bf33829d](https://github.com/rust-lang/rustlings/commit/bf33829da240375d086f96267fc2e02fa6b07001))
|
||||
- Assigned value to `cents_per_gram` in test ([d1ee2da](https://github.com/rust-lang/rustlings/commit/d1ee2daf14f19105e6db3f9c610f44293d688532))
|
||||
- **structs3.rs:** assigned value to cents_per_gram in test ([d1ee2daf](https://github.com/rust-lang/rustlings/commit/d1ee2daf14f19105e6db3f9c610f44293d688532))
|
||||
- **traits1:** rename test functions to snake case (#854) ([1663a16e](https://github.com/rust-lang/rustlings/commit/1663a16eade6ca646b6ed061735f7982434d530d))
|
||||
|
||||
#### Documentation improvements
|
||||
|
||||
- Add hints on how to get GCC installed (#741) ([bc56861](https://github.com/rust-lang/rustlings/commit/bc5686174463ad6f4f6b824b0e9b97c3039d4886))
|
||||
- Fix some code blocks that were not highlighted ([17f9d74](https://github.com/rust-lang/rustlings/commit/17f9d7429ccd133a72e815fb5618e0ce79560929))
|
||||
|
||||
## 4.6.0 (2021-09-25)
|
||||
|
||||
#### Features
|
||||
|
||||
- add advanced_errs2 ([abd6b70c](https://github.com/rust-lang/rustlings/commit/abd6b70c72dc6426752ff41f09160b839e5c449e))
|
||||
- add advanced_errs1 ([882d535b](https://github.com/rust-lang/rustlings/commit/882d535ba8628d5e0b37e8664b3e2f26260b2671))
|
||||
- Add a farewell message when quitting `watch` ([1caef0b4](https://github.com/rust-lang/rustlings/commit/1caef0b43494c8b8cdd6c9260147e70d510f1aca))
|
||||
- add more watch commands ([a7dc080b](https://github.com/rust-lang/rustlings/commit/a7dc080b95e49146fbaafe6922a6de2f8cb1582a), closes [#842](https://github.com/rust-lang/rustlings/issues/842))
|
||||
- **modules:** update exercises, add modules3 (#822) ([dfd2fab4](https://github.com/rust-lang/rustlings/commit/dfd2fab4f33d1bf59e2e5ee03123c0c9a67a9481))
|
||||
- **quiz1:** add default function name in comment (#838) ([0a11bad7](https://github.com/rust-lang/rustlings/commit/0a11bad71402b5403143d642f439f57931278c07))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- Correct small typo in exercises/conversions/from_str.rs ([86cc8529](https://github.com/rust-lang/rustlings/commit/86cc85295ae36948963ae52882e285d7e3e29323))
|
||||
- **cli:** typo in exercise.rs (#848) ([06d5c097](https://github.com/rust-lang/rustlings/commit/06d5c0973a3dffa3c6c6f70acb775d4c6630323c))
|
||||
- **from_str, try_from_into:** custom error types ([2dc93cad](https://github.com/rust-lang/rustlings/commit/2dc93caddad43821743e4903d89b355df58d7a49))
|
||||
- **modules2:** fix typo (#835) ([1c3beb0a](https://github.com/rust-lang/rustlings/commit/1c3beb0a59178c950dc05fe8ee2346b017429ae0))
|
||||
- **move_semantics5:**
|
||||
- change &mut \*y to &mut x (#814) ([d75759e8](https://github.com/rust-lang/rustlings/commit/d75759e829fdcd64ef071cf4b6eae2a011a7718b))
|
||||
- Clarify instructions ([df25684c](https://github.com/rust-lang/rustlings/commit/df25684cb79f8413915e00b5efef29369849cef1))
|
||||
- **quiz1:** Fix inconsistent wording (#826) ([03131a3d](https://github.com/rust-lang/rustlings/commit/03131a3d35d9842598150f9da817f7cc26e2669a))
|
||||
|
||||
## 4.5.0 (2021-07-07)
|
||||
|
||||
#### Features
|
||||
|
||||
- Add move_semantics5 exercise. (#746) ([399ab328](https://github.com/rust-lang/rustlings/commit/399ab328d8d407265c09563aa4ef4534b2503ff2))
|
||||
- **cli:** Add "next" to run the next unsolved exercise. (#785) ([d20e413a](https://github.com/rust-lang/rustlings/commit/d20e413a68772cd493561f2651cf244e822b7ca5))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- rename result1 to errors4 ([50ab289d](https://github.com/rust-lang/rustlings/commit/50ab289da6b9eb19a7486c341b00048c516b88c0))
|
||||
- move_semantics5 hints ([1b858285](https://github.com/rust-lang/rustlings/commit/1b85828548f46f58b622b5e0c00f8c989f928807))
|
||||
- remove trailing whitespaces from iterators1 ([4d4fa774](https://github.com/rust-lang/rustlings/commit/4d4fa77459392acd3581c6068aa8be9a02de12fc))
|
||||
- add hints to generics1 and generics2 exercises ([31457940](https://github.com/rust-lang/rustlings/commit/31457940846b3844d78d4a4d2b074bc8d6aaf1eb))
|
||||
- remove trailing whitespace ([d9b69bd1](https://github.com/rust-lang/rustlings/commit/d9b69bd1a0a7a99f2c0d80933ad2eea44c8c71b2))
|
||||
- **installation:** first PowerShell command ([aa9a943d](https://github.com/rust-lang/rustlings/commit/aa9a943ddf3ae260782e73c26bcc9db60e5894b6))
|
||||
- **iterators5:** derive Clone, Copy ([91fc9e31](https://github.com/rust-lang/rustlings/commit/91fc9e3118f4af603c9911698cc2a234725cb032))
|
||||
- **quiz1:** Updated question description (#794) ([d8766496](https://github.com/rust-lang/rustlings/commit/d876649616cc8a8dd5f539f8bc1a5434b960b1e9))
|
||||
- **try_from_into, from_str:** hints for dyn Error ([11d2cf0d](https://github.com/rust-lang/rustlings/commit/11d2cf0d604dee3f5023c17802d69438e69fa50e))
|
||||
- **variables5:** confine the answer further ([48ffcbd2](https://github.com/rust-lang/rustlings/commit/48ffcbd2c4cc4d936c2c7480019190f179813cc5))
|
||||
|
||||
## 4.4.0 (2021-04-24)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- Fix spelling error in main.rs ([91ee27f2](https://github.com/rust-lang/rustlings/commit/91ee27f22bd3797a9db57e5fd430801c170c5db8))
|
||||
- typo in default out text ([644c49f1](https://github.com/rust-lang/rustlings/commit/644c49f1e04cbb24e95872b3a52b07d692ae3bc8))
|
||||
- **collections:** Naming exercises for vectors and hashmap ([bef39b12](https://github.com/rust-lang/rustlings/commit/bef39b125961310b34b34871e480a82e82af4678))
|
||||
- **from_str:**
|
||||
- Correct typos ([5f7c89f8](https://github.com/rust-lang/rustlings/commit/5f7c89f85db1f33da01911eaa479c3a2d4721678))
|
||||
- test for error instead of unwrap/should_panic ([15e71535](https://github.com/rust-lang/rustlings/commit/15e71535f37cfaed36e22eb778728d186e2104ab))
|
||||
- use trait objects for from_str ([c3e7b831](https://github.com/rust-lang/rustlings/commit/c3e7b831786c9172ed8bd5d150f3c432f242fba9))
|
||||
- **functions3:** improve function argument type (#687) ([a6509cc4](https://github.com/rust-lang/rustlings/commit/a6509cc4d545d8825f01ddf7ee37823b372154dd))
|
||||
- **hashmap2:** Update incorrect assertion (#660) ([72aaa15e](https://github.com/rust-lang/rustlings/commit/72aaa15e6ab4b72b3422f1c6356396e20a2a2bb8))
|
||||
- **info:** Fix typo (#635) ([cddc1e86](https://github.com/rust-lang/rustlings/commit/cddc1e86e7ec744ee644cc774a4887b1a0ded3e8))
|
||||
- **iterators2:** Moved errors out of tests. ([baf4ba17](https://github.com/rust-lang/rustlings/commit/baf4ba175ba6eb92989e3dd54ecbec4bedc9a863), closes [#359](https://github.com/rust-lang/rustlings/issues/359))
|
||||
- **iterators3:** Enabled iterators3.rs to run without commented out tests. ([c6712dfc](https://github.com/rust-lang/rustlings/commit/c6712dfccd1a093e590ad22bbc4f49edc417dac0))
|
||||
- **main:** Let find_exercise work with borrows ([347f30bd](https://github.com/rust-lang/rustlings/commit/347f30bd867343c5ace1097e085a1f7e356553f7))
|
||||
- **move_semantics4:**
|
||||
- Remove redundant "instead" (#640) ([cc266d7d](https://github.com/rust-lang/rustlings/commit/cc266d7d80b91e79df3f61984f231b7f1587218e))
|
||||
- Small readbility improvement (#617) ([10965920](https://github.com/rust-lang/rustlings/commit/10965920fbdf8a1efc85bed869e55a1787006404))
|
||||
- **option2:** Rename uninformative variables (#675) ([b4de6594](https://github.com/rust-lang/rustlings/commit/b4de6594380636817d13c2677ec6f472a964cf43))
|
||||
- **quiz3:** Force an answer to Q2 (#672) ([0d894e6f](https://github.com/rust-lang/rustlings/commit/0d894e6ff739943901e1ae8c904582e5c2f843bd))
|
||||
- **structs:** Add 5.3 to structs/README (#652) ([6bd791f2](https://github.com/rust-lang/rustlings/commit/6bd791f2f44aa7f0ad926df767f6b1fa8f12a9a9))
|
||||
- **structs2:** correct grammar in hint (#663) ([ebdb66c7](https://github.com/rust-lang/rustlings/commit/ebdb66c7bfb6d687a14cc511a559a222e6fc5de4))
|
||||
- **structs3:**
|
||||
- reword heading comment (#664) ([9f3e8c2d](https://github.com/rust-lang/rustlings/commit/9f3e8c2dde645e5264c2d2200e68842b5f47bfa3))
|
||||
- add check to prevent naive implementation of is_international ([05a753fe](https://github.com/rust-lang/rustlings/commit/05a753fe6333d36dbee5f68c21dec04eacdc75df))
|
||||
- **threads1:** line number correction ([7857b0a6](https://github.com/rust-lang/rustlings/commit/7857b0a689b0847f48d8c14cbd1865e3b812d5ca))
|
||||
- **try_from_into:** use trait objects ([2e93a588](https://github.com/rust-lang/rustlings/commit/2e93a588e0abe8badb7eafafb9e7d073c2be5df8))
|
||||
|
||||
#### Features
|
||||
|
||||
- Replace clap with argh ([7928122f](https://github.com/rust-lang/rustlings/commit/7928122fcef9ca7834d988b1ec8ca0687478beeb))
|
||||
- Replace emojis when NO_EMOJI env variable present ([8d62a996](https://github.com/rust-lang/rustlings/commit/8d62a9963708dbecd9312e8bcc4b47049c72d155))
|
||||
- Added iterators5.rs exercise. ([b29ea17e](https://github.com/rust-lang/rustlings/commit/b29ea17ea94d1862114af2cf5ced0e09c197dc35))
|
||||
- **arc1:** Add more details to description and hint (#710) ([81be4044](https://github.com/rust-lang/rustlings/commit/81be40448777fa338ebced3b0bfc1b32d6370313))
|
||||
- **cli:** Improve the list command with options, and then some ([8bbe4ff1](https://github.com/rust-lang/rustlings/commit/8bbe4ff1385c5c169c90cd3ff9253f9a91daaf8e))
|
||||
- **list:**
|
||||
- updated progress percentage ([1c6f7e4b](https://github.com/rust-lang/rustlings/commit/1c6f7e4b7b9b3bd36f4da2bb2b69c549cc8bd913))
|
||||
- added progress info ([c0e3daac](https://github.com/rust-lang/rustlings/commit/c0e3daacaf6850811df5bc57fa43e0f249d5cfa4))
|
||||
|
||||
## 4.3.0 (2020-12-29)
|
||||
|
||||
#### Features
|
||||
|
||||
- Rewrite default out text ([44d39112](https://github.com/rust-lang/rustlings/commit/44d39112ff122b29c9793fe52e605df1612c6490))
|
||||
- match exercise order to book chapters (#541) ([033bf119](https://github.com/rust-lang/rustlings/commit/033bf1198fc8bfce1b570e49da7cde010aa552e3))
|
||||
- Crab? (#586) ([fa9f522b](https://github.com/rust-lang/rustlings/commit/fa9f522b7f043d7ef73a39f003a9272dfe72c4f4))
|
||||
- add "rustlings list" command ([838f9f30](https://github.com/rust-lang/rustlings/commit/838f9f30083d0b23fd67503dcf0fbeca498e6647))
|
||||
- **try_from_into:** remove duplicate annotation ([04f1d079](https://github.com/rust-lang/rustlings/commit/04f1d079aa42a2f49af694bc92c67d731d31a53f))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- update structs README ([bcf14cf6](https://github.com/rust-lang/rustlings/commit/bcf14cf677adb3a38a3ac3ca53f3c69f61153025))
|
||||
- added missing exercises to info.toml ([90cfb6ff](https://github.com/rust-lang/rustlings/commit/90cfb6ff28377531bfc34acb70547bdb13374f6b))
|
||||
- gives a bit more context to magic number ([30644c9a](https://github.com/rust-lang/rustlings/commit/30644c9a062b825c0ea89435dc59f0cad86b110e))
|
||||
- **functions2:** Change signature to trigger precise error message: (#605) ([0ef95947](https://github.com/rust-lang/rustlings/commit/0ef95947cc30482e63a7045be6cc2fb6f6dcb4cc))
|
||||
- **structs1:** Adjust wording (#573) ([9334783d](https://github.com/rust-lang/rustlings/commit/9334783da31d821cc59174fbe8320df95828926c))
|
||||
- **try_from_into:**
|
||||
- type error ([4f4cfcf3](https://github.com/rust-lang/rustlings/commit/4f4cfcf3c36c8718c7c170c9c3a6935e6ef0618c))
|
||||
- Update description (#584) ([96347df9](https://github.com/rust-lang/rustlings/commit/96347df9df294f01153b29d9ad4ba361f665c755))
|
||||
- **vec1:** Have test compare every element in a and v ([9b6c6293](https://github.com/rust-lang/rustlings/commit/9b6c629397b24b944f484f5b2bbd8144266b5695))
|
||||
|
||||
## 4.2.0 (2020-11-07)
|
||||
|
||||
#### Features
|
||||
|
||||
- Add HashMap exercises ([633c00cf](https://github.com/rust-lang/rustlings/commit/633c00cf8071e1e82959a3010452a32f34f29fc9))
|
||||
- Add Vec exercises ([0c12fa31](https://github.com/rust-lang/rustlings/commit/0c12fa31c57c03c6287458a0a8aca7afd057baf6))
|
||||
- **primitive_types6:** Add a test (#548) ([2b1fb2b7](https://github.com/rust-lang/rustlings/commit/2b1fb2b739bf9ad8d6b7b12af25fee173011bfc4))
|
||||
- **try_from_into:** Add tests (#571) ([95ccd926](https://github.com/rust-lang/rustlings/commit/95ccd92616ae79ba287cce221101e0bbe4f68cdc))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- log error output when inotify limit is exceeded ([d61b4e5a](https://github.com/rust-lang/rustlings/commit/d61b4e5a13b44d72d004082f523fa1b6b24c1aca))
|
||||
- more unique temp_file ([5643ef05](https://github.com/rust-lang/rustlings/commit/5643ef05bc81e4a840e9456f4406a769abbe1392))
|
||||
- **installation:** Update the MinRustVersion ([21bfb2d4](https://github.com/rust-lang/rustlings/commit/21bfb2d4777429c87d8d3b5fbf0ce66006dcd034))
|
||||
- **iterators2:** Update description (#578) ([197d3a3d](https://github.com/rust-lang/rustlings/commit/197d3a3d8961b2465579218a6749b2b2cefa8ddd))
|
||||
- **primitive_types6:**
|
||||
- remove 'unused doc comment' warning ([472d8592](https://github.com/rust-lang/rustlings/commit/472d8592d65c8275332a20dfc269e7ac0d41bc88))
|
||||
- missing comma in test ([4fb230da](https://github.com/rust-lang/rustlings/commit/4fb230daf1251444fcf29e085cee222a91f8a37e))
|
||||
- **quiz3:** Second test is for odd numbers, not even. (#553) ([18e0bfef](https://github.com/rust-lang/rustlings/commit/18e0bfef1de53071e353ba1ec5837002ff7290e6))
|
||||
|
||||
## 4.1.0 (2020-10-05)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- Update rustlings version in Cargo.lock ([1cc40bc9](https://github.com/rust-lang/rustlings/commit/1cc40bc9ce95c23d56f6d91fa1c4deb646231fef))
|
||||
- **arc1:** index mod should equal thread count ([b4062ef6](https://github.com/rust-lang/rustlings/commit/b4062ef6993e80dac107c4093ea85166ad3ee0fa))
|
||||
- **enums3:** Update Message::ChangeColor to take a tuple. (#457) ([4b6540c7](https://github.com/rust-lang/rustlings/commit/4b6540c71adabad647de8a09e57295e7c7c7d794))
|
||||
- **exercises:** adding question mark to quiz2 ([101072ab](https://github.com/rust-lang/rustlings/commit/101072ab9f8c80b40b8b88cb06cbe38aca2481c5))
|
||||
- **generics3:** clarify grade change ([47f7672c](https://github.com/rust-lang/rustlings/commit/47f7672c0307732056e7426e81d351f0dd7e22e5))
|
||||
- **structs3:** Small adjustment of variable name ([114b54cb](https://github.com/rust-lang/rustlings/commit/114b54cbdb977234b39e5f180d937c14c78bb8b2))
|
||||
- **using_as:** Add test so that proper type is returned. (#512) ([3286c5ec](https://github.com/rust-lang/rustlings/commit/3286c5ec19ea5fb7ded81d047da5f8594108a490))
|
||||
|
||||
#### Features
|
||||
|
||||
- Added iterators1.rs exercise ([9642f5a3](https://github.com/rust-lang/rustlings/commit/9642f5a3f686270a4f8f6ba969919ddbbc4f7fdd))
|
||||
- Add ability to run rustlings on repl.it (#471) ([8f7b5bd0](https://github.com/rust-lang/rustlings/commit/8f7b5bd00eb83542b959830ef55192d2d76db90a))
|
||||
- Add gitpod support (#473) ([4821a8be](https://github.com/rust-lang/rustlings/commit/4821a8be94af4f669042a06ab917934cfacd032f))
|
||||
- Remind the user of the hint option (#425) ([816b1f5e](https://github.com/rust-lang/rustlings/commit/816b1f5e85d6cc6e72673813a85d0ada2a8f84af))
|
||||
- Remind the user of the hint option (#425) ([9f61db5d](https://github.com/rust-lang/rustlings/commit/9f61db5dbe38538cf06571fcdd5f806e7901e83a))
|
||||
- **cli:** Added 'cls' command to 'watch' mode (#474) ([4f2468e1](https://github.com/rust-lang/rustlings/commit/4f2468e14f574a93a2e9b688367b5752ed96ae7b))
|
||||
- **try_from_into:** Add insufficient length test (#469) ([523d18b8](https://github.com/rust-lang/rustlings/commit/523d18b873a319f7c09262f44bd40e2fab1830e5))
|
||||
|
||||
## 4.0.0 (2020-07-08)
|
||||
|
||||
#### Breaking Changes
|
||||
|
||||
- Add a --nocapture option to display test harnesses' outputs ([8ad5f9bf](https://github.com/rust-lang/rustlings/commit/8ad5f9bf531a4848b1104b7b389a20171624c82f))
|
||||
- Rename test to quiz, fixes #244 ([010a0456](https://github.com/rust-lang/rustlings/commit/010a04569282149cea7f7a76fc4d7f4c9f0f08dd))
|
||||
|
||||
#### Features
|
||||
|
||||
- Add traits README ([173bb141](https://github.com/rust-lang/rustlings/commit/173bb14140c5530cbdb59e53ace3991a99d804af))
|
||||
- Add box1.rs exercise ([7479a473](https://github.com/rust-lang/rustlings/commit/7479a4737bdcac347322ad0883ca528c8675e720))
|
||||
- Rewrite try_from_into (#393) ([763aa6e3](https://github.com/rust-lang/rustlings/commit/763aa6e378a586caae2d8d63755a85eeba227933))
|
||||
- Add if2 exercise ([1da84b5f](https://github.com/rust-lang/rustlings/commit/1da84b5f7c489f65bd683c244f13c7d1ee812df0))
|
||||
- Added exercise structs3.rs ([b66e2e09](https://github.com/rust-lang/rustlings/commit/b66e2e09622243e086a0f1258dd27e1a2d61c891))
|
||||
- Add exercise variables6 covering const (#352) ([5999acd2](https://github.com/rust-lang/rustlings/commit/5999acd24a4f203292be36e0fd18d385887ec481))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- Change then to than ([ddd98ad7](https://github.com/rust-lang/rustlings/commit/ddd98ad75d3668fbb10eff74374148aa5ed2344d))
|
||||
- rename quiz1 to tests1 in info (#420) ([0dd1c6ca](https://github.com/rust-lang/rustlings/commit/0dd1c6ca6b389789e0972aa955fe17aa15c95f29))
|
||||
- fix quiz naming inconsistency (#421) ([5563adbb](https://github.com/rust-lang/rustlings/commit/5563adbb890587fc48fbbc9c4028642687f1e85b))
|
||||
- confine the user further in variable exercises ([06ef4cc6](https://github.com/rust-lang/rustlings/commit/06ef4cc654e75d22a526812919ee49b8956280bf))
|
||||
- update iterator and macro text for typos and clarity ([95900828](https://github.com/rust-lang/rustlings/commit/959008284834bece0196a01e17ac69a7e3590116))
|
||||
- update generics2 closes #362 ([964c974a](https://github.com/rust-lang/rustlings/commit/964c974a0274199d755073b917c2bc5da0c9b4f1))
|
||||
- confusing comment in conversions/try_from_into.rs ([c9e4f2cf](https://github.com/rust-lang/rustlings/commit/c9e4f2cfb4c48d0b7451263cfb43b9426438122d))
|
||||
- **arc1:** Passively introduce attributes (#429) ([113cdae2](https://github.com/rust-lang/rustlings/commit/113cdae2d4e4c55905e8056ad326ede7fd7de356))
|
||||
- **box1:** fix comment typo (#426) ([bb2ca251](https://github.com/rust-lang/rustlings/commit/bb2ca251106b27a7272d9a30872904dd1376654c))
|
||||
- **errorsn:** Try harder to confine the user. (#388) ([2b20c8a0](https://github.com/rust-lang/rustlings/commit/2b20c8a0f5774d07c58d110d75879f33fc6273b5))
|
||||
- **from_into.rs:** typo ([a901499e](https://github.com/rust-lang/rustlings/commit/a901499ededd3ce1995164700514fe4e9a0373ea))
|
||||
- **generics2:** Guide students to the answer (#430) ([e6bd8021](https://github.com/rust-lang/rustlings/commit/e6bd8021d9a7dd06feebc30c9d5f953901d7b419))
|
||||
- **installation:**
|
||||
- Provide a backup git reference when tag can't be curl ([9e4fb100](https://github.com/rust-lang/rustlings/commit/9e4fb1009f1c9e3433915c03e22c2af422e5c5fe))
|
||||
- Check if python is available while checking for git,rustc and cargo ([9cfb617d](https://github.com/rust-lang/rustlings/commit/9cfb617d5b0451b4b51644a1298965390cda9884))
|
||||
- **option1:**
|
||||
- Don't add only zeros to the numbers array ([cce6a442](https://github.com/rust-lang/rustlings/commit/cce6a4427718724a9096800754cd3abeca6a1580))
|
||||
- Add cast to usize, as it is confusing in the context of an exercise about Option ([f6cffc7e](https://github.com/rust-lang/rustlings/commit/f6cffc7e487b42f15a6f958e49704c93a8d4465b))
|
||||
- **option2:** Add TODO to comments (#400) ([10967bce](https://github.com/rust-lang/rustlings/commit/10967bce57682812dc0891a9f9757da1a9d87404))
|
||||
- **options1:** Add hint about Array Initialization (#389) ([9f75554f](https://github.com/rust-lang/rustlings/commit/9f75554f2a30295996f03f0160b98c0458305502))
|
||||
- **test2:** name of type String and &str (#394) ([d6c0a688](https://github.com/rust-lang/rustlings/commit/d6c0a688e6a96f93ad60d540d4b326f342fc0d45))
|
||||
- **variables6:** minor typo (#419) ([524e17df](https://github.com/rust-lang/rustlings/commit/524e17df10db95f7b90a0f75cc8997182a8a4094))
|
||||
|
||||
## 3.0.0 (2020-04-11)
|
||||
|
||||
#### Breaking Changes
|
||||
|
||||
- make "compile" exercises print output (#278) ([3b6d5c](https://github.com/fmoko/rustlings/commit/3b6d5c3aaa27a242a832799eb66e96897d26fde3))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **primitive_types:** revert primitive_types4 (#296) ([b3a3351e](https://github.com/rust-lang/rustlings/commit/b3a3351e8e6a0bdee07077d7b0382953821649ae))
|
||||
- **run:** compile clippy exercise files (#295) ([3ab084a4](https://github.com/rust-lang/rustlings/commit/3ab084a421c0f140ae83bf1fc3f47b39342e7373))
|
||||
- **conversions:**
|
||||
- add additional test to meet exercise rules (#284) ([bc22ec3](https://github.com/fmoko/rustlings/commit/bc22ec382f843347333ef1301fc1bad773657f38))
|
||||
- remove duplicate not done comment (#292) ([dab90f](https://github.com/fmoko/rustlings/commit/dab90f7b91a6000fe874e3d664f244048e5fa342))
|
||||
- don't hardcode documentation version for traits (#288) ([30e6af](https://github.com/fmoko/rustlings/commit/30e6af60690c326fb5d3a9b7335f35c69c09137d))
|
||||
|
||||
#### Features
|
||||
|
||||
- add Option2 exercise (#290) ([86b5c08b](https://github.com/rust-lang/rustlings/commit/86b5c08b9bea1576127a7c5f599f5752072c087d))
|
||||
- add exercise for option (#282) ([135e5d47](https://github.com/rust-lang/rustlings/commit/135e5d47a7c395aece6f6022117fb20c82f2d3d4))
|
||||
- add new exercises for generics (#280) ([76be5e4e](https://github.com/rust-lang/rustlings/commit/76be5e4e991160f5fd9093f03ee2ba260e8f7229))
|
||||
- **ci:** add buildkite config ([b049fa2c](https://github.com/rust-lang/rustlings/commit/b049fa2c84dba0f0c8906ac44e28fd45fba51a71))
|
||||
|
||||
### 2.2.1 (2020-02-27)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- Re-add cloning the repo to install scripts ([3d9b03c5](https://github.com/rust-lang/rustlings/commit/3d9b03c52b8dc51b140757f6fd25ad87b5782ef5))
|
||||
|
||||
#### Features
|
||||
|
||||
- Add clippy lints (#269) ([1e2fd9c9](https://github.com/rust-lang/rustlings/commit/1e2fd9c92f8cd6e389525ca1a999fca4c90b5921))
|
||||
|
||||
## 2.2.0 (2020-02-25)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- 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))
|
||||
- Updated iterators readme to account for iterators4 exercise (#273) ([bec8e3a](https://github.com/rust-lang/rustlings/commit/bec8e3a644cbd88db1c73ea5f1d8a364f4a34016))
|
||||
- **installation:** make fatal errors more obvious (#272) ([17d0951e](https://github.com/rust-lang/rustlings/commit/17d0951e66fda8e11b204d5c4c41a0d5e22e78f7))
|
||||
- **iterators2:**
|
||||
- Remove reference to missing iterators2.rs (#245) ([419f7797](https://github.com/rust-lang/rustlings/commit/419f7797f294e4ce6a2b883199731b5bde77d262))
|
||||
- **as_ref_mut:** Enable a test and improve per clippy's suggestion (#256) ([dfdf809](https://github.com/rust-lang/rustlings/commit/dfdf8093ebbd4145864995627b812780de52f902))
|
||||
- **tests1:**
|
||||
- Change test command ([fe10e06c](https://github.com/rust-lang/rustlings/commit/fe10e06c3733ddb4a21e90d09bf79bfe618e97ce)
|
||||
- Correct test command in tests1.rs comment (#263) ([39fa7ae](https://github.com/rust-lang/rustlings/commit/39fa7ae8b70ad468da49b06f11b2383135a63bcf))
|
||||
|
||||
#### Features
|
||||
|
||||
- Add variables5.rs exercise (#264) ([0c73609e](https://github.com/rust-lang/rustlings/commit/0c73609e6f2311295e95d6f96f8c747cfc4cba03))
|
||||
- Show a completion message when watching (#253) ([d25ee55a](https://github.com/rust-lang/rustlings/commit/d25ee55a3205882d35782e370af855051b39c58c))
|
||||
- Add type conversion and parsing exercises (#249) ([0c85dc11](https://github.com/rust-lang/rustlings/commit/0c85dc1193978b5165491b99cc4922caf8d14a65))
|
||||
- Created consistent money unit (#258) ([fd57f8f](https://github.com/rust-lang/rustlings/commit/fd57f8f2c1da2af8ddbebbccec214e6f40f4dbab))
|
||||
- Enable test for exercise test4 (#276) ([8b971ff](https://github.com/rust-lang/rustlings/commit/8b971ffab6079a706ac925f5917f987932b55c07))
|
||||
- Added traits exercises (#274 but specifically #216, which originally added
|
||||
this :heart:) ([b559cdd](https://github.com/rust-lang/rustlings/commit/b559cdd73f32c0d0cfc1feda39f82b3e3583df17))
|
||||
|
||||
## 2.1.0 (2019-11-27)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- add line numbers in several exercises and hints ([b565c4d3](https://github.com/rust-lang/rustlings/commit/b565c4d3e74e8e110bef201a082fa1302722a7c3))
|
||||
- **arc1:** Fix some words in the comment ([c42c3b21](https://github.com/rust-lang/rustlings/commit/c42c3b2101df9164c8cd7bb344def921e5ba3e61))
|
||||
- **enums:** Add link to chapter on pattern syntax (#242) ([615ce327](https://github.com/rust-lang/rustlings/commit/615ce3279800c56d89f19d218ccb7ef576624feb))
|
||||
- **primitive_types4:**
|
||||
- update outdated hint ([4c5189df](https://github.com/rust-lang/rustlings/commit/4c5189df2bdd9a231f6b2611919ba5aa14da0d3f))
|
||||
- update outdated comment ([ded2c034](https://github.com/rust-lang/rustlings/commit/ded2c034ba93fa1e3c2c2ea16b83abc1a57265e8))
|
||||
- **strings2:** update line number in hint ([a09f684f](https://github.com/rust-lang/rustlings/commit/a09f684f05c58d239a6fc59ec5f81c2533e8b820))
|
||||
- **variables1:** Correct wrong word in comment ([fda5a470](https://github.com/rust-lang/rustlings/commit/fda5a47069e0954f16a04e8e50945e03becb71a5))
|
||||
|
||||
#### Features
|
||||
|
||||
- **watch:** show hint while watching ([8143d57b](https://github.com/rust-lang/rustlings/commit/8143d57b4e88c51341dd4a18a14c536042cc009c))
|
||||
|
||||
## 2.0.0 (2019-11-12)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **default:** Clarify the installation procedure ([c371b853](https://github.com/rust-lang/rustlings/commit/c371b853afa08947ddeebec0edd074b171eeaae0))
|
||||
- **info:** Fix trailing newlines for hints ([795b6e34](https://github.com/rust-lang/rustlings/commit/795b6e348094a898e9227a14f6232f7bb94c8d31))
|
||||
- **run:** make `run` never prompt ([4b265465](https://github.com/rust-lang/rustlings/commit/4b26546589f7d2b50455429482cf1f386ceae8b3))
|
||||
|
||||
#### Breaking Changes
|
||||
|
||||
- Refactor hint system ([9bdb0a12](https://github.com/rust-lang/rustlings/commit/9bdb0a12e45a8e9f9f6a4bd4a9c172c5376c7f60))
|
||||
- improve `watch` execution mode ([2cdd6129](https://github.com/rust-lang/rustlings/commit/2cdd61294f0d9a53775ee24ad76435bec8a21e60))
|
||||
- Index exercises by name ([627cdc07](https://github.com/rust-lang/rustlings/commit/627cdc07d07dfe6a740e885e0ddf6900e7ec336b))
|
||||
- **run:** makes `run` never prompt ([4b265465](https://github.com/rust-lang/rustlings/commit/4b26546589f7d2b50455429482cf1f386ceae8b3))
|
||||
|
||||
#### Features
|
||||
|
||||
- **cli:** check for rustc before doing anything ([36a033b8](https://github.com/rust-lang/rustlings/commit/36a033b87a6549c1e5639c908bf7381c84f4f425))
|
||||
- **hint:** Add test for hint ([ce9fa6eb](https://github.com/rust-lang/rustlings/commit/ce9fa6ebbfdc3e7585d488d9409797285708316f))
|
||||
|
||||
### 1.5.1 (2019-11-11)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **errors3:** Update hint ([dcfb427b](https://github.com/rust-lang/rustlings/commit/dcfb427b09585f0193f0a294443fdf99f11c64cb), closes [#185](https://github.com/rust-lang/rustlings/issues/185))
|
||||
- **if1:** Remove `return` reference ([ad03d180](https://github.com/rust-lang/rustlings/commit/ad03d180c9311c0093e56a3531eec1a9a70cdb45))
|
||||
- **strings:** Move Strings before Structs ([6dcecb38](https://github.com/rust-lang/rustlings/commit/6dcecb38a4435593beb87c8e12d6314143631482), closes [#204](https://github.com/rust-lang/rustlings/issues/204))
|
||||
- **structs1:** Remove misleading comment ([f72e5a8f](https://github.com/rust-lang/rustlings/commit/f72e5a8f05568dde04eaeac10b9a69872f21cb37))
|
||||
- **threads:** Move Threads behind SLT ([fbe91a67](https://github.com/rust-lang/rustlings/commit/fbe91a67a482bfe64cbcdd58d06ba830a0f39da3), closes [#205](https://github.com/rust-lang/rustlings/issues/205))
|
||||
- **watch:** clear screen before each `verify()` ([3aff590](https://github.com/rust-lang/rustlings/commit/3aff59085586c24196a547c2693adbdcf4432648))
|
||||
|
||||
## 1.5.0 (2019-11-09)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **test1:** Rewrite logic ([79a56942](https://github.com/rust-lang/rustlings/commit/79a569422c8309cfc9e4aed25bf4ab3b3859996b))
|
||||
- **installation:** Fix rustlings installation check ([7a252c47](https://github.com/rust-lang/rustlings/commit/7a252c475551486efb52f949b8af55803b700bc6))
|
||||
- **iterators:** Rename iterator3.rs ([433d2115](https://github.com/rust-lang/rustlings/commit/433d2115bc1c04b6d34a335a18c9a8f3e2672bc6))
|
||||
- **iterators2:** Remove syntax resulting in misleading error message ([4cde8664](https://github.com/rust-lang/rustlings/commit/4cde86643e12db162a66e62f23b78962986046ac))
|
||||
- **option1:**
|
||||
- Fix arguments passed to assert! macro (#222) ([4c2cf6da](https://github.com/rust-lang/rustlings/commit/4c2cf6da755efe02725e05ecc3a303304c10a6da))
|
||||
- Fix arguments passed to assert! macro ([ead4f7af](https://github.com/rust-lang/rustlings/commit/ead4f7af9e10e53418efdde5c359159347282afd))
|
||||
- Add test for prematurely passing exercise ([a750e4a1](https://github.com/rust-lang/rustlings/commit/a750e4a1a3006227292bb17d57d78ce84da6bfc6))
|
||||
- **primitive_types4:** Fail on a slice covering the wrong area ([5b1e673c](https://github.com/rust-lang/rustlings/commit/5b1e673cec1658afc4ebbbc800213847804facf5))
|
||||
- **readme:** http to https ([70946b85](https://github.com/rust-lang/rustlings/commit/70946b85e536e80e70ed9505cb650ca0a3a1fbb5))
|
||||
- **test1:**
|
||||
- Swap assertion parameter order ([4086d463](https://github.com/rust-lang/rustlings/commit/4086d463a981e81d97781851d17db2ced290f446))
|
||||
- renamed function name to snake case closes #180 ([89d5186c](https://github.com/rust-lang/rustlings/commit/89d5186c0dae8135ecabf90ee8bb35949bc2d29b))
|
||||
|
||||
#### Features
|
||||
|
||||
- Add enums exercises ([dc150321](https://github.com/rust-lang/rustlings/commit/dc15032112fc485226a573a18139e5ce928b1755))
|
||||
- Added exercise for struct update syntax ([1c4c8764](https://github.com/rust-lang/rustlings/commit/1c4c8764ed118740cd4cee73272ddc6cceb9d959))
|
||||
- **iterators2:** adds iterators2 exercise including config ([9288fccf](https://github.com/rust-lang/rustlings/commit/9288fccf07a2c5043b76d0fd6491e4cf72d76031))
|
||||
|
||||
### 1.4.1 (2019-08-13)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **iterators2:** Remove syntax resulting in misleading error message ([4cde8664](https://github.com/rust-lang/rustlings/commit/4cde86643e12db162a66e62f23b78962986046ac))
|
||||
- **option1:** Add test for prematurely passing exercise ([a750e4a1](https://github.com/rust-lang/rustlings/commit/a750e4a1a3006227292bb17d57d78ce84da6bfc6))
|
||||
- **test1:** Swap assertion parameter order ([4086d463](https://github.com/rust-lang/rustlings/commit/4086d463a981e81d97781851d17db2ced290f446))
|
||||
|
||||
## 1.4.0 (2019-07-13)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **installation:** Fix rustlings installation check ([7a252c47](https://github.com/rust-lang/rustlings/commit/7a252c475551486efb52f949b8af55803b700bc6))
|
||||
- **iterators:** Rename iterator3.rs ([433d2115](https://github.com/rust-lang/rustlings/commit/433d2115bc1c04b6d34a335a18c9a8f3e2672bc6))
|
||||
- **readme:** http to https ([70946b85](https://github.com/rust-lang/rustlings/commit/70946b85e536e80e70ed9505cb650ca0a3a1fbb5))
|
||||
- **test1:** renamed function name to snake case ([89d5186c](https://github.com/rust-lang/rustlings/commit/89d5186c0dae8135ecabf90ee8bb35949bc2d29b))
|
||||
- **cli:** Check if changed exercise file exists before calling verify ([ba85ca3](https://github.com/rust-lang/rustlings/commit/ba85ca32c4cfc61de46851ab89f9c58a28f33c88))
|
||||
- **structs1:** Fix the irrefutable let pattern warning ([cc6a141](https://github.com/rust-lang/rustlings/commit/cc6a14104d7c034eadc98297eaaa972d09c50b1f))
|
||||
|
||||
#### Features
|
||||
|
||||
- **changelog:** Use clog for changelogs ([34e31232](https://github.com/rust-lang/rustlings/commit/34e31232dfddde284a341c9609b33cd27d9d5724))
|
||||
- **iterators2:** adds iterators2 exercise including config ([9288fccf](https://github.com/rust-lang/rustlings/commit/9288fccf07a2c5043b76d0fd6491e4cf72d76031))
|
||||
|
||||
### 1.3.0 (2019-06-05)
|
||||
|
||||
#### Features
|
||||
|
||||
- Adds a simple exercise for structures (#163, @briankung)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- Add Result type signature as it is difficult for new comers to understand Generics and Error all at once. (#157, @veggiemonk)
|
||||
- Rustfmt and whitespace fixes (#161, @eddyp)
|
||||
- errorsn.rs: Separate also the hints from each other to avoid accidental viewing (#162, @eddyp)
|
||||
- fixed outdated links (#165, @gushroom)
|
||||
- Fix broken link (#164, @HanKruiger)
|
||||
- Remove highlighting and syntect (#167, @komaeda)
|
||||
|
||||
### 1.2.2 (2019-05-07)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- Reverted `--nocapture` flag since it was causing tests to pass unconditionally
|
||||
|
||||
### 1.2.1 (2019-04-22)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- Fix the `--nocapture` feature (@komaeda)
|
||||
- Provide a nicer error message for when you're in the wrong directory
|
||||
|
||||
### 1.2.0 (2019-04-22)
|
||||
|
||||
#### Features
|
||||
|
||||
- Add errors to exercises that compile without user changes (@yvan-sraka)
|
||||
- Use --nocapture when testing, enabling `println!` when running (@komaeda)
|
||||
|
||||
### 1.1.1 (2019-04-14)
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
- Fix permissions on exercise files (@zacanger, #133)
|
||||
- Make installation checks more thorough (@komaeda, 1b3469f236bc6979c27f6e1a04e4138a88e55de3)
|
||||
- Fix order of true/false in tests for executables (@mgeier, #137)
|
||||
- Stop run from panicking when compile fails (@cjpearce, #141)
|
||||
- Fix intermittent test failure caused by race condition (@cjpearce, #140)
|
||||
- Fix links by deleting book version (@diodfr, #142)
|
||||
- Canonicalize paths to fix path matching (@cjpearce, #143)
|
||||
|
||||
### 1.1.0 (2019-03-20)
|
||||
|
||||
- errors2.rs: update link to Rust book (#124)
|
||||
- Start verification at most recently modified file (#120)
|
||||
- Watch for file creation events in watch mode (#117)
|
||||
- Add standard library types to exercises suite (#119)
|
||||
- Give a warning when Rustlings isn't run from the right directory (#123)
|
||||
- Verify that rust version is recent enough to install Rustlings (#131)
|
||||
|
||||
### 1.0.1 (2019-03-06)
|
||||
|
||||
- Adds a way to install Rustlings in one command (`curl -L https://git.io/rustlings | bash`)
|
||||
- Makes `rustlings watch` react to create file events (@shaunbennett, #117)
|
||||
- Reworks the exercise management to use an external TOML file instead of just listing them in the code
|
||||
|
||||
### 1.0.0 (2019-03-06)
|
||||
|
||||
Initial release.
|
||||
@ -1,61 +0,0 @@
|
||||
# Contributing to Rustlings
|
||||
|
||||
First off, thanks for taking the time to contribute! ❤️
|
||||
|
||||
## Quick Reference
|
||||
|
||||
I want to …
|
||||
|
||||
- _report a bug!_ ➡️ [open an issue](#issues)
|
||||
- _fix a bug!_ ➡️ [open a pull request](#pull-requests)
|
||||
- _implement a new feature!_ ➡️ [open an issue to discuss it first, then a pull request](#issues)
|
||||
- _add an exercise!_ ➡️ [read this](#adding-an-exercise)
|
||||
- _update an outdated exercise!_ ➡️ [open a pull request](#pull-requests)
|
||||
|
||||
## Issues
|
||||
|
||||
You can open an issue [here](https://github.com/rust-lang/rustlings/issues/new).
|
||||
If you're reporting a bug, please include the output of the following commands:
|
||||
|
||||
- `cargo --version`
|
||||
- `rustlings --version`
|
||||
- `ls -la`
|
||||
- Your OS name and version
|
||||
|
||||
## Pull Requests
|
||||
|
||||
You are welcome to open a pull request, but unless it is small and trivial, **please open an issue to discuss your idea first** 🙏🏼
|
||||
|
||||
Opening a pull request is as easy as forking the repository and committing your changes.
|
||||
If you need any help with it or face any Git related problems, don't hesitate to ask for help 🤗
|
||||
|
||||
It may take time to review your pull request.
|
||||
Please be patient 😇
|
||||
|
||||
When updating an exercise, check if its solution needs to be updated.
|
||||
|
||||
## Adding An Exercise
|
||||
|
||||
- Name the file `exercises/yourTopic/yourTopicN.rs`.
|
||||
- Make sure to put in some helpful links, and link to sections of The Book in `exercises/yourTopic/README.md`.
|
||||
- In the exercise, add a `// TODO: …` comment where user changes are required.
|
||||
- Add a solution at `solutions/yourTopic/yourTopicN.rs` with comments explaining it.
|
||||
- Add the [metadata for your exercise](#exercise-metadata) in the `rustlings-macros/info.toml` file.
|
||||
- Make sure your exercise runs with `rustlings run yourTopicN`.
|
||||
- [Open a pull request](#pull-requests).
|
||||
|
||||
### Exercise Metadata
|
||||
|
||||
The exercise metadata should contain the following:
|
||||
|
||||
```toml
|
||||
[[exercises]]
|
||||
name = "yourTopicN"
|
||||
dir = "yourTopic"
|
||||
hint = """
|
||||
A useful (multi-line) hint for your exercise.
|
||||
Include links to a section in The Book or a documentation page."""
|
||||
```
|
||||
|
||||
If your exercise doesn't contain any test, add `test = false` to the exercise metadata.
|
||||
But adding tests is recommended.
|
||||
793
Cargo.lock
generated
793
Cargo.lock
generated
@ -3,794 +3,5 @@
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"crossterm_winapi",
|
||||
"document-features",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"rustix",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "document-features"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d"
|
||||
dependencies = [
|
||||
"litrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
|
||||
dependencies = [
|
||||
"kqueue-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue-sys"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.172"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
|
||||
|
||||
[[package]]
|
||||
name = "litrs"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "8.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"filetime",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio",
|
||||
"notify-types",
|
||||
"walkdir",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-types"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustlings"
|
||||
version = "6.4.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"crossterm",
|
||||
"notify",
|
||||
"rustix",
|
||||
"rustlings-macros",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustlings-macros"
|
||||
version = "6.4.0"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"serde",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.140"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.14.2+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
|
||||
dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
]
|
||||
name = "exercises"
|
||||
version = "0.0.0"
|
||||
|
||||
278
Cargo.toml
278
Cargo.toml
@ -1,65 +1,199 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
exclude = [
|
||||
"tests/test_exercises",
|
||||
"dev",
|
||||
bin = [
|
||||
{ name = "intro1", path = "exercises/00_intro/intro1.rs" },
|
||||
{ name = "intro1_sol", path = "solutions/00_intro/intro1.rs" },
|
||||
{ name = "intro2", path = "exercises/00_intro/intro2.rs" },
|
||||
{ name = "intro2_sol", path = "solutions/00_intro/intro2.rs" },
|
||||
{ name = "variables1", path = "exercises/01_variables/variables1.rs" },
|
||||
{ name = "variables1_sol", path = "solutions/01_variables/variables1.rs" },
|
||||
{ name = "variables2", path = "exercises/01_variables/variables2.rs" },
|
||||
{ name = "variables2_sol", path = "solutions/01_variables/variables2.rs" },
|
||||
{ name = "variables3", path = "exercises/01_variables/variables3.rs" },
|
||||
{ name = "variables3_sol", path = "solutions/01_variables/variables3.rs" },
|
||||
{ name = "variables4", path = "exercises/01_variables/variables4.rs" },
|
||||
{ name = "variables4_sol", path = "solutions/01_variables/variables4.rs" },
|
||||
{ name = "variables5", path = "exercises/01_variables/variables5.rs" },
|
||||
{ name = "variables5_sol", path = "solutions/01_variables/variables5.rs" },
|
||||
{ name = "variables6", path = "exercises/01_variables/variables6.rs" },
|
||||
{ name = "variables6_sol", path = "solutions/01_variables/variables6.rs" },
|
||||
{ name = "functions1", path = "exercises/02_functions/functions1.rs" },
|
||||
{ name = "functions1_sol", path = "solutions/02_functions/functions1.rs" },
|
||||
{ name = "functions2", path = "exercises/02_functions/functions2.rs" },
|
||||
{ name = "functions2_sol", path = "solutions/02_functions/functions2.rs" },
|
||||
{ name = "functions3", path = "exercises/02_functions/functions3.rs" },
|
||||
{ name = "functions3_sol", path = "solutions/02_functions/functions3.rs" },
|
||||
{ name = "functions4", path = "exercises/02_functions/functions4.rs" },
|
||||
{ name = "functions4_sol", path = "solutions/02_functions/functions4.rs" },
|
||||
{ name = "functions5", path = "exercises/02_functions/functions5.rs" },
|
||||
{ name = "functions5_sol", path = "solutions/02_functions/functions5.rs" },
|
||||
{ name = "if1", path = "exercises/03_if/if1.rs" },
|
||||
{ name = "if1_sol", path = "solutions/03_if/if1.rs" },
|
||||
{ name = "if2", path = "exercises/03_if/if2.rs" },
|
||||
{ name = "if2_sol", path = "solutions/03_if/if2.rs" },
|
||||
{ name = "if3", path = "exercises/03_if/if3.rs" },
|
||||
{ name = "if3_sol", path = "solutions/03_if/if3.rs" },
|
||||
{ name = "quiz1", path = "exercises/quizzes/quiz1.rs" },
|
||||
{ name = "quiz1_sol", path = "solutions/quizzes/quiz1.rs" },
|
||||
{ name = "primitive_types1", path = "exercises/04_primitive_types/primitive_types1.rs" },
|
||||
{ name = "primitive_types1_sol", path = "solutions/04_primitive_types/primitive_types1.rs" },
|
||||
{ name = "primitive_types2", path = "exercises/04_primitive_types/primitive_types2.rs" },
|
||||
{ name = "primitive_types2_sol", path = "solutions/04_primitive_types/primitive_types2.rs" },
|
||||
{ name = "primitive_types3", path = "exercises/04_primitive_types/primitive_types3.rs" },
|
||||
{ name = "primitive_types3_sol", path = "solutions/04_primitive_types/primitive_types3.rs" },
|
||||
{ name = "primitive_types4", path = "exercises/04_primitive_types/primitive_types4.rs" },
|
||||
{ name = "primitive_types4_sol", path = "solutions/04_primitive_types/primitive_types4.rs" },
|
||||
{ name = "primitive_types5", path = "exercises/04_primitive_types/primitive_types5.rs" },
|
||||
{ name = "primitive_types5_sol", path = "solutions/04_primitive_types/primitive_types5.rs" },
|
||||
{ name = "primitive_types6", path = "exercises/04_primitive_types/primitive_types6.rs" },
|
||||
{ name = "primitive_types6_sol", path = "solutions/04_primitive_types/primitive_types6.rs" },
|
||||
{ name = "vecs1", path = "exercises/05_vecs/vecs1.rs" },
|
||||
{ name = "vecs1_sol", path = "solutions/05_vecs/vecs1.rs" },
|
||||
{ name = "vecs2", path = "exercises/05_vecs/vecs2.rs" },
|
||||
{ name = "vecs2_sol", path = "solutions/05_vecs/vecs2.rs" },
|
||||
{ name = "move_semantics1", path = "exercises/06_move_semantics/move_semantics1.rs" },
|
||||
{ name = "move_semantics1_sol", path = "solutions/06_move_semantics/move_semantics1.rs" },
|
||||
{ name = "move_semantics2", path = "exercises/06_move_semantics/move_semantics2.rs" },
|
||||
{ name = "move_semantics2_sol", path = "solutions/06_move_semantics/move_semantics2.rs" },
|
||||
{ name = "move_semantics3", path = "exercises/06_move_semantics/move_semantics3.rs" },
|
||||
{ name = "move_semantics3_sol", path = "solutions/06_move_semantics/move_semantics3.rs" },
|
||||
{ name = "move_semantics4", path = "exercises/06_move_semantics/move_semantics4.rs" },
|
||||
{ name = "move_semantics4_sol", path = "solutions/06_move_semantics/move_semantics4.rs" },
|
||||
{ name = "move_semantics5", path = "exercises/06_move_semantics/move_semantics5.rs" },
|
||||
{ name = "move_semantics5_sol", path = "solutions/06_move_semantics/move_semantics5.rs" },
|
||||
{ name = "structs1", path = "exercises/07_structs/structs1.rs" },
|
||||
{ name = "structs1_sol", path = "solutions/07_structs/structs1.rs" },
|
||||
{ name = "structs2", path = "exercises/07_structs/structs2.rs" },
|
||||
{ name = "structs2_sol", path = "solutions/07_structs/structs2.rs" },
|
||||
{ name = "structs3", path = "exercises/07_structs/structs3.rs" },
|
||||
{ name = "structs3_sol", path = "solutions/07_structs/structs3.rs" },
|
||||
{ name = "enums1", path = "exercises/08_enums/enums1.rs" },
|
||||
{ name = "enums1_sol", path = "solutions/08_enums/enums1.rs" },
|
||||
{ name = "enums2", path = "exercises/08_enums/enums2.rs" },
|
||||
{ name = "enums2_sol", path = "solutions/08_enums/enums2.rs" },
|
||||
{ name = "enums3", path = "exercises/08_enums/enums3.rs" },
|
||||
{ name = "enums3_sol", path = "solutions/08_enums/enums3.rs" },
|
||||
{ name = "strings1", path = "exercises/09_strings/strings1.rs" },
|
||||
{ name = "strings1_sol", path = "solutions/09_strings/strings1.rs" },
|
||||
{ name = "strings2", path = "exercises/09_strings/strings2.rs" },
|
||||
{ name = "strings2_sol", path = "solutions/09_strings/strings2.rs" },
|
||||
{ name = "strings3", path = "exercises/09_strings/strings3.rs" },
|
||||
{ name = "strings3_sol", path = "solutions/09_strings/strings3.rs" },
|
||||
{ name = "strings4", path = "exercises/09_strings/strings4.rs" },
|
||||
{ name = "strings4_sol", path = "solutions/09_strings/strings4.rs" },
|
||||
{ name = "modules1", path = "exercises/10_modules/modules1.rs" },
|
||||
{ name = "modules1_sol", path = "solutions/10_modules/modules1.rs" },
|
||||
{ name = "modules2", path = "exercises/10_modules/modules2.rs" },
|
||||
{ name = "modules2_sol", path = "solutions/10_modules/modules2.rs" },
|
||||
{ name = "modules3", path = "exercises/10_modules/modules3.rs" },
|
||||
{ name = "modules3_sol", path = "solutions/10_modules/modules3.rs" },
|
||||
{ name = "hashmaps1", path = "exercises/11_hashmaps/hashmaps1.rs" },
|
||||
{ name = "hashmaps1_sol", path = "solutions/11_hashmaps/hashmaps1.rs" },
|
||||
{ name = "hashmaps2", path = "exercises/11_hashmaps/hashmaps2.rs" },
|
||||
{ name = "hashmaps2_sol", path = "solutions/11_hashmaps/hashmaps2.rs" },
|
||||
{ name = "hashmaps3", path = "exercises/11_hashmaps/hashmaps3.rs" },
|
||||
{ name = "hashmaps3_sol", path = "solutions/11_hashmaps/hashmaps3.rs" },
|
||||
{ name = "quiz2", path = "exercises/quizzes/quiz2.rs" },
|
||||
{ name = "quiz2_sol", path = "solutions/quizzes/quiz2.rs" },
|
||||
{ name = "options1", path = "exercises/12_options/options1.rs" },
|
||||
{ name = "options1_sol", path = "solutions/12_options/options1.rs" },
|
||||
{ name = "options2", path = "exercises/12_options/options2.rs" },
|
||||
{ name = "options2_sol", path = "solutions/12_options/options2.rs" },
|
||||
{ name = "options3", path = "exercises/12_options/options3.rs" },
|
||||
{ name = "options3_sol", path = "solutions/12_options/options3.rs" },
|
||||
{ name = "errors1", path = "exercises/13_error_handling/errors1.rs" },
|
||||
{ name = "errors1_sol", path = "solutions/13_error_handling/errors1.rs" },
|
||||
{ name = "errors2", path = "exercises/13_error_handling/errors2.rs" },
|
||||
{ name = "errors2_sol", path = "solutions/13_error_handling/errors2.rs" },
|
||||
{ name = "errors3", path = "exercises/13_error_handling/errors3.rs" },
|
||||
{ name = "errors3_sol", path = "solutions/13_error_handling/errors3.rs" },
|
||||
{ name = "errors4", path = "exercises/13_error_handling/errors4.rs" },
|
||||
{ name = "errors4_sol", path = "solutions/13_error_handling/errors4.rs" },
|
||||
{ name = "errors5", path = "exercises/13_error_handling/errors5.rs" },
|
||||
{ name = "errors5_sol", path = "solutions/13_error_handling/errors5.rs" },
|
||||
{ name = "errors6", path = "exercises/13_error_handling/errors6.rs" },
|
||||
{ name = "errors6_sol", path = "solutions/13_error_handling/errors6.rs" },
|
||||
{ name = "generics1", path = "exercises/14_generics/generics1.rs" },
|
||||
{ name = "generics1_sol", path = "solutions/14_generics/generics1.rs" },
|
||||
{ name = "generics2", path = "exercises/14_generics/generics2.rs" },
|
||||
{ name = "generics2_sol", path = "solutions/14_generics/generics2.rs" },
|
||||
{ name = "traits1", path = "exercises/15_traits/traits1.rs" },
|
||||
{ name = "traits1_sol", path = "solutions/15_traits/traits1.rs" },
|
||||
{ name = "traits2", path = "exercises/15_traits/traits2.rs" },
|
||||
{ name = "traits2_sol", path = "solutions/15_traits/traits2.rs" },
|
||||
{ name = "traits3", path = "exercises/15_traits/traits3.rs" },
|
||||
{ name = "traits3_sol", path = "solutions/15_traits/traits3.rs" },
|
||||
{ name = "traits4", path = "exercises/15_traits/traits4.rs" },
|
||||
{ name = "traits4_sol", path = "solutions/15_traits/traits4.rs" },
|
||||
{ name = "traits5", path = "exercises/15_traits/traits5.rs" },
|
||||
{ name = "traits5_sol", path = "solutions/15_traits/traits5.rs" },
|
||||
{ name = "quiz3", path = "exercises/quizzes/quiz3.rs" },
|
||||
{ name = "quiz3_sol", path = "solutions/quizzes/quiz3.rs" },
|
||||
{ name = "lifetimes1", path = "exercises/16_lifetimes/lifetimes1.rs" },
|
||||
{ name = "lifetimes1_sol", path = "solutions/16_lifetimes/lifetimes1.rs" },
|
||||
{ name = "lifetimes2", path = "exercises/16_lifetimes/lifetimes2.rs" },
|
||||
{ name = "lifetimes2_sol", path = "solutions/16_lifetimes/lifetimes2.rs" },
|
||||
{ name = "lifetimes3", path = "exercises/16_lifetimes/lifetimes3.rs" },
|
||||
{ name = "lifetimes3_sol", path = "solutions/16_lifetimes/lifetimes3.rs" },
|
||||
{ name = "tests1", path = "exercises/17_tests/tests1.rs" },
|
||||
{ name = "tests1_sol", path = "solutions/17_tests/tests1.rs" },
|
||||
{ name = "tests2", path = "exercises/17_tests/tests2.rs" },
|
||||
{ name = "tests2_sol", path = "solutions/17_tests/tests2.rs" },
|
||||
{ name = "tests3", path = "exercises/17_tests/tests3.rs" },
|
||||
{ name = "tests3_sol", path = "solutions/17_tests/tests3.rs" },
|
||||
{ name = "iterators1", path = "exercises/18_iterators/iterators1.rs" },
|
||||
{ name = "iterators1_sol", path = "solutions/18_iterators/iterators1.rs" },
|
||||
{ name = "iterators2", path = "exercises/18_iterators/iterators2.rs" },
|
||||
{ name = "iterators2_sol", path = "solutions/18_iterators/iterators2.rs" },
|
||||
{ name = "iterators3", path = "exercises/18_iterators/iterators3.rs" },
|
||||
{ name = "iterators3_sol", path = "solutions/18_iterators/iterators3.rs" },
|
||||
{ name = "iterators4", path = "exercises/18_iterators/iterators4.rs" },
|
||||
{ name = "iterators4_sol", path = "solutions/18_iterators/iterators4.rs" },
|
||||
{ name = "iterators5", path = "exercises/18_iterators/iterators5.rs" },
|
||||
{ name = "iterators5_sol", path = "solutions/18_iterators/iterators5.rs" },
|
||||
{ name = "box1", path = "exercises/19_smart_pointers/box1.rs" },
|
||||
{ name = "box1_sol", path = "solutions/19_smart_pointers/box1.rs" },
|
||||
{ name = "rc1", path = "exercises/19_smart_pointers/rc1.rs" },
|
||||
{ name = "rc1_sol", path = "solutions/19_smart_pointers/rc1.rs" },
|
||||
{ name = "arc1", path = "exercises/19_smart_pointers/arc1.rs" },
|
||||
{ name = "arc1_sol", path = "solutions/19_smart_pointers/arc1.rs" },
|
||||
{ name = "cow1", path = "exercises/19_smart_pointers/cow1.rs" },
|
||||
{ name = "cow1_sol", path = "solutions/19_smart_pointers/cow1.rs" },
|
||||
{ name = "threads1", path = "exercises/20_threads/threads1.rs" },
|
||||
{ name = "threads1_sol", path = "solutions/20_threads/threads1.rs" },
|
||||
{ name = "threads2", path = "exercises/20_threads/threads2.rs" },
|
||||
{ name = "threads2_sol", path = "solutions/20_threads/threads2.rs" },
|
||||
{ name = "threads3", path = "exercises/20_threads/threads3.rs" },
|
||||
{ name = "threads3_sol", path = "solutions/20_threads/threads3.rs" },
|
||||
{ name = "macros1", path = "exercises/21_macros/macros1.rs" },
|
||||
{ name = "macros1_sol", path = "solutions/21_macros/macros1.rs" },
|
||||
{ name = "macros2", path = "exercises/21_macros/macros2.rs" },
|
||||
{ name = "macros2_sol", path = "solutions/21_macros/macros2.rs" },
|
||||
{ name = "macros3", path = "exercises/21_macros/macros3.rs" },
|
||||
{ name = "macros3_sol", path = "solutions/21_macros/macros3.rs" },
|
||||
{ name = "macros4", path = "exercises/21_macros/macros4.rs" },
|
||||
{ name = "macros4_sol", path = "solutions/21_macros/macros4.rs" },
|
||||
{ name = "clippy1", path = "exercises/22_clippy/clippy1.rs" },
|
||||
{ name = "clippy1_sol", path = "solutions/22_clippy/clippy1.rs" },
|
||||
{ name = "clippy2", path = "exercises/22_clippy/clippy2.rs" },
|
||||
{ name = "clippy2_sol", path = "solutions/22_clippy/clippy2.rs" },
|
||||
{ name = "clippy3", path = "exercises/22_clippy/clippy3.rs" },
|
||||
{ name = "clippy3_sol", path = "solutions/22_clippy/clippy3.rs" },
|
||||
{ name = "using_as", path = "exercises/23_conversions/using_as.rs" },
|
||||
{ name = "using_as_sol", path = "solutions/23_conversions/using_as.rs" },
|
||||
{ name = "from_into", path = "exercises/23_conversions/from_into.rs" },
|
||||
{ name = "from_into_sol", path = "solutions/23_conversions/from_into.rs" },
|
||||
{ name = "from_str", path = "exercises/23_conversions/from_str.rs" },
|
||||
{ name = "from_str_sol", path = "solutions/23_conversions/from_str.rs" },
|
||||
{ name = "try_from_into", path = "exercises/23_conversions/try_from_into.rs" },
|
||||
{ name = "try_from_into_sol", path = "solutions/23_conversions/try_from_into.rs" },
|
||||
{ name = "as_ref_mut", path = "exercises/23_conversions/as_ref_mut.rs" },
|
||||
{ name = "as_ref_mut_sol", path = "solutions/23_conversions/as_ref_mut.rs" },
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "6.4.0"
|
||||
authors = [
|
||||
"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>", # https://github.com/carols10cents
|
||||
]
|
||||
repository = "https://github.com/rust-lang/rustlings"
|
||||
license = "MIT"
|
||||
edition = "2024" # On Update: Update the edition of `rustfmt` in `dev check` and `CARGO_TOML` in `dev new`.
|
||||
rust-version = "1.87"
|
||||
|
||||
[workspace.dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
toml_edit = { version = "0.22", default-features = false, features = ["parse", "serde"] }
|
||||
|
||||
[package]
|
||||
name = "rustlings"
|
||||
description = "Small exercises to get you used to reading and writing Rust code!"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
keywords = [
|
||||
"exercise",
|
||||
"learning",
|
||||
]
|
||||
include = [
|
||||
"/src/",
|
||||
"/exercises/",
|
||||
"/solutions/",
|
||||
# A symlink to be able to include `dev/Cargo.toml` although `dev` is excluded.
|
||||
"/dev-Cargo.toml",
|
||||
"/README.md",
|
||||
"/LICENSE",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
crossterm = { version = "0.29", default-features = false, features = ["windows", "events"] }
|
||||
notify = "8.0"
|
||||
rustlings-macros = { path = "rustlings-macros", version = "=6.4.0" }
|
||||
serde_json = "1.0"
|
||||
serde.workspace = true
|
||||
toml_edit.workspace = true
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
rustix = { version = "1.0", default-features = false, features = ["std", "stdio", "termios"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.19"
|
||||
name = "exercises"
|
||||
edition = "2021"
|
||||
# Don't publish the exercises on crates.io!
|
||||
publish = false
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
@ -67,22 +201,22 @@ panic = "abort"
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
|
||||
[package.metadata.release]
|
||||
pre-release-hook = ["./release-hook.sh"]
|
||||
pre-release-commit-message = "Release 🎉"
|
||||
|
||||
[workspace.lints.rust]
|
||||
[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"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
[lints.clippy]
|
||||
# You forgot a `todo!()`!
|
||||
todo = "forbid"
|
||||
# This can only happen by mistake in Rustlings.
|
||||
empty_loop = "forbid"
|
||||
disallowed-types = "deny"
|
||||
disallowed-methods = "deny"
|
||||
# No infinite loops are needed in Rustlings.
|
||||
infinite_loop = "deny"
|
||||
# You shouldn't leak memory while still learning Rust!
|
||||
mem_forget = "deny"
|
||||
dbg_macro = "warn"
|
||||
todo = "warn"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
# Currently, there are no disallowed methods. This line avoids problems when developing Rustlings.
|
||||
disallowed_methods = "allow"
|
||||
|
||||
22
LICENSE
22
LICENSE
@ -1,22 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Carol (Nichols || Goulding)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
@ -1,7 +1 @@
|
||||
# [Rustlings](https://rustlings.rust-lang.org) 🦀
|
||||
|
||||
Small exercises to get you used to reading and writing [Rust](https://www.rust-lang.org) code - _Recommended in parallel to reading [the official Rust book](https://doc.rust-lang.org/book) 📚️_
|
||||
|
||||
Visit the **website** for a demo, info about setup and more:
|
||||
|
||||
## ➡️ [rustlings.rust-lang.org](https://rustlings.rust-lang.org) ⬅️
|
||||
# rustlings
|
||||
|
||||
5
build.rs
5
build.rs
@ -1,5 +0,0 @@
|
||||
fn main() {
|
||||
// Fix building from source on Windows because it can't handle file links.
|
||||
#[cfg(windows)]
|
||||
let _ = std::fs::copy("dev/Cargo.toml", "dev-Cargo.toml");
|
||||
}
|
||||
15
clippy.toml
15
clippy.toml
@ -1,15 +0,0 @@
|
||||
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",
|
||||
]
|
||||
@ -1 +0,0 @@
|
||||
dev/Cargo.toml
|
||||
223
dev/Cargo.toml
223
dev/Cargo.toml
@ -1,223 +0,0 @@
|
||||
# Don't edit the `bin` list manually! It is updated by `cargo dev update`. This comment line will be stripped in `rustlings init`.
|
||||
bin = [
|
||||
{ name = "intro1", path = "../exercises/00_intro/intro1.rs" },
|
||||
{ name = "intro1_sol", path = "../solutions/00_intro/intro1.rs" },
|
||||
{ name = "intro2", path = "../exercises/00_intro/intro2.rs" },
|
||||
{ name = "intro2_sol", path = "../solutions/00_intro/intro2.rs" },
|
||||
{ name = "variables1", path = "../exercises/01_variables/variables1.rs" },
|
||||
{ name = "variables1_sol", path = "../solutions/01_variables/variables1.rs" },
|
||||
{ name = "variables2", path = "../exercises/01_variables/variables2.rs" },
|
||||
{ name = "variables2_sol", path = "../solutions/01_variables/variables2.rs" },
|
||||
{ name = "variables3", path = "../exercises/01_variables/variables3.rs" },
|
||||
{ name = "variables3_sol", path = "../solutions/01_variables/variables3.rs" },
|
||||
{ name = "variables4", path = "../exercises/01_variables/variables4.rs" },
|
||||
{ name = "variables4_sol", path = "../solutions/01_variables/variables4.rs" },
|
||||
{ name = "variables5", path = "../exercises/01_variables/variables5.rs" },
|
||||
{ name = "variables5_sol", path = "../solutions/01_variables/variables5.rs" },
|
||||
{ name = "variables6", path = "../exercises/01_variables/variables6.rs" },
|
||||
{ name = "variables6_sol", path = "../solutions/01_variables/variables6.rs" },
|
||||
{ name = "functions1", path = "../exercises/02_functions/functions1.rs" },
|
||||
{ name = "functions1_sol", path = "../solutions/02_functions/functions1.rs" },
|
||||
{ name = "functions2", path = "../exercises/02_functions/functions2.rs" },
|
||||
{ name = "functions2_sol", path = "../solutions/02_functions/functions2.rs" },
|
||||
{ name = "functions3", path = "../exercises/02_functions/functions3.rs" },
|
||||
{ name = "functions3_sol", path = "../solutions/02_functions/functions3.rs" },
|
||||
{ name = "functions4", path = "../exercises/02_functions/functions4.rs" },
|
||||
{ name = "functions4_sol", path = "../solutions/02_functions/functions4.rs" },
|
||||
{ name = "functions5", path = "../exercises/02_functions/functions5.rs" },
|
||||
{ name = "functions5_sol", path = "../solutions/02_functions/functions5.rs" },
|
||||
{ name = "if1", path = "../exercises/03_if/if1.rs" },
|
||||
{ name = "if1_sol", path = "../solutions/03_if/if1.rs" },
|
||||
{ name = "if2", path = "../exercises/03_if/if2.rs" },
|
||||
{ name = "if2_sol", path = "../solutions/03_if/if2.rs" },
|
||||
{ name = "if3", path = "../exercises/03_if/if3.rs" },
|
||||
{ name = "if3_sol", path = "../solutions/03_if/if3.rs" },
|
||||
{ name = "quiz1", path = "../exercises/quizzes/quiz1.rs" },
|
||||
{ name = "quiz1_sol", path = "../solutions/quizzes/quiz1.rs" },
|
||||
{ name = "primitive_types1", path = "../exercises/04_primitive_types/primitive_types1.rs" },
|
||||
{ name = "primitive_types1_sol", path = "../solutions/04_primitive_types/primitive_types1.rs" },
|
||||
{ name = "primitive_types2", path = "../exercises/04_primitive_types/primitive_types2.rs" },
|
||||
{ name = "primitive_types2_sol", path = "../solutions/04_primitive_types/primitive_types2.rs" },
|
||||
{ name = "primitive_types3", path = "../exercises/04_primitive_types/primitive_types3.rs" },
|
||||
{ name = "primitive_types3_sol", path = "../solutions/04_primitive_types/primitive_types3.rs" },
|
||||
{ name = "primitive_types4", path = "../exercises/04_primitive_types/primitive_types4.rs" },
|
||||
{ name = "primitive_types4_sol", path = "../solutions/04_primitive_types/primitive_types4.rs" },
|
||||
{ name = "primitive_types5", path = "../exercises/04_primitive_types/primitive_types5.rs" },
|
||||
{ name = "primitive_types5_sol", path = "../solutions/04_primitive_types/primitive_types5.rs" },
|
||||
{ name = "primitive_types6", path = "../exercises/04_primitive_types/primitive_types6.rs" },
|
||||
{ name = "primitive_types6_sol", path = "../solutions/04_primitive_types/primitive_types6.rs" },
|
||||
{ name = "vecs1", path = "../exercises/05_vecs/vecs1.rs" },
|
||||
{ name = "vecs1_sol", path = "../solutions/05_vecs/vecs1.rs" },
|
||||
{ name = "vecs2", path = "../exercises/05_vecs/vecs2.rs" },
|
||||
{ name = "vecs2_sol", path = "../solutions/05_vecs/vecs2.rs" },
|
||||
{ name = "move_semantics1", path = "../exercises/06_move_semantics/move_semantics1.rs" },
|
||||
{ name = "move_semantics1_sol", path = "../solutions/06_move_semantics/move_semantics1.rs" },
|
||||
{ name = "move_semantics2", path = "../exercises/06_move_semantics/move_semantics2.rs" },
|
||||
{ name = "move_semantics2_sol", path = "../solutions/06_move_semantics/move_semantics2.rs" },
|
||||
{ name = "move_semantics3", path = "../exercises/06_move_semantics/move_semantics3.rs" },
|
||||
{ name = "move_semantics3_sol", path = "../solutions/06_move_semantics/move_semantics3.rs" },
|
||||
{ name = "move_semantics4", path = "../exercises/06_move_semantics/move_semantics4.rs" },
|
||||
{ name = "move_semantics4_sol", path = "../solutions/06_move_semantics/move_semantics4.rs" },
|
||||
{ name = "move_semantics5", path = "../exercises/06_move_semantics/move_semantics5.rs" },
|
||||
{ name = "move_semantics5_sol", path = "../solutions/06_move_semantics/move_semantics5.rs" },
|
||||
{ name = "structs1", path = "../exercises/07_structs/structs1.rs" },
|
||||
{ name = "structs1_sol", path = "../solutions/07_structs/structs1.rs" },
|
||||
{ name = "structs2", path = "../exercises/07_structs/structs2.rs" },
|
||||
{ name = "structs2_sol", path = "../solutions/07_structs/structs2.rs" },
|
||||
{ name = "structs3", path = "../exercises/07_structs/structs3.rs" },
|
||||
{ name = "structs3_sol", path = "../solutions/07_structs/structs3.rs" },
|
||||
{ name = "enums1", path = "../exercises/08_enums/enums1.rs" },
|
||||
{ name = "enums1_sol", path = "../solutions/08_enums/enums1.rs" },
|
||||
{ name = "enums2", path = "../exercises/08_enums/enums2.rs" },
|
||||
{ name = "enums2_sol", path = "../solutions/08_enums/enums2.rs" },
|
||||
{ name = "enums3", path = "../exercises/08_enums/enums3.rs" },
|
||||
{ name = "enums3_sol", path = "../solutions/08_enums/enums3.rs" },
|
||||
{ name = "strings1", path = "../exercises/09_strings/strings1.rs" },
|
||||
{ name = "strings1_sol", path = "../solutions/09_strings/strings1.rs" },
|
||||
{ name = "strings2", path = "../exercises/09_strings/strings2.rs" },
|
||||
{ name = "strings2_sol", path = "../solutions/09_strings/strings2.rs" },
|
||||
{ name = "strings3", path = "../exercises/09_strings/strings3.rs" },
|
||||
{ name = "strings3_sol", path = "../solutions/09_strings/strings3.rs" },
|
||||
{ name = "strings4", path = "../exercises/09_strings/strings4.rs" },
|
||||
{ name = "strings4_sol", path = "../solutions/09_strings/strings4.rs" },
|
||||
{ name = "modules1", path = "../exercises/10_modules/modules1.rs" },
|
||||
{ name = "modules1_sol", path = "../solutions/10_modules/modules1.rs" },
|
||||
{ name = "modules2", path = "../exercises/10_modules/modules2.rs" },
|
||||
{ name = "modules2_sol", path = "../solutions/10_modules/modules2.rs" },
|
||||
{ name = "modules3", path = "../exercises/10_modules/modules3.rs" },
|
||||
{ name = "modules3_sol", path = "../solutions/10_modules/modules3.rs" },
|
||||
{ name = "hashmaps1", path = "../exercises/11_hashmaps/hashmaps1.rs" },
|
||||
{ name = "hashmaps1_sol", path = "../solutions/11_hashmaps/hashmaps1.rs" },
|
||||
{ name = "hashmaps2", path = "../exercises/11_hashmaps/hashmaps2.rs" },
|
||||
{ name = "hashmaps2_sol", path = "../solutions/11_hashmaps/hashmaps2.rs" },
|
||||
{ name = "hashmaps3", path = "../exercises/11_hashmaps/hashmaps3.rs" },
|
||||
{ name = "hashmaps3_sol", path = "../solutions/11_hashmaps/hashmaps3.rs" },
|
||||
{ name = "quiz2", path = "../exercises/quizzes/quiz2.rs" },
|
||||
{ name = "quiz2_sol", path = "../solutions/quizzes/quiz2.rs" },
|
||||
{ name = "options1", path = "../exercises/12_options/options1.rs" },
|
||||
{ name = "options1_sol", path = "../solutions/12_options/options1.rs" },
|
||||
{ name = "options2", path = "../exercises/12_options/options2.rs" },
|
||||
{ name = "options2_sol", path = "../solutions/12_options/options2.rs" },
|
||||
{ name = "options3", path = "../exercises/12_options/options3.rs" },
|
||||
{ name = "options3_sol", path = "../solutions/12_options/options3.rs" },
|
||||
{ name = "errors1", path = "../exercises/13_error_handling/errors1.rs" },
|
||||
{ name = "errors1_sol", path = "../solutions/13_error_handling/errors1.rs" },
|
||||
{ name = "errors2", path = "../exercises/13_error_handling/errors2.rs" },
|
||||
{ name = "errors2_sol", path = "../solutions/13_error_handling/errors2.rs" },
|
||||
{ name = "errors3", path = "../exercises/13_error_handling/errors3.rs" },
|
||||
{ name = "errors3_sol", path = "../solutions/13_error_handling/errors3.rs" },
|
||||
{ name = "errors4", path = "../exercises/13_error_handling/errors4.rs" },
|
||||
{ name = "errors4_sol", path = "../solutions/13_error_handling/errors4.rs" },
|
||||
{ name = "errors5", path = "../exercises/13_error_handling/errors5.rs" },
|
||||
{ name = "errors5_sol", path = "../solutions/13_error_handling/errors5.rs" },
|
||||
{ name = "errors6", path = "../exercises/13_error_handling/errors6.rs" },
|
||||
{ name = "errors6_sol", path = "../solutions/13_error_handling/errors6.rs" },
|
||||
{ name = "generics1", path = "../exercises/14_generics/generics1.rs" },
|
||||
{ name = "generics1_sol", path = "../solutions/14_generics/generics1.rs" },
|
||||
{ name = "generics2", path = "../exercises/14_generics/generics2.rs" },
|
||||
{ name = "generics2_sol", path = "../solutions/14_generics/generics2.rs" },
|
||||
{ name = "traits1", path = "../exercises/15_traits/traits1.rs" },
|
||||
{ name = "traits1_sol", path = "../solutions/15_traits/traits1.rs" },
|
||||
{ name = "traits2", path = "../exercises/15_traits/traits2.rs" },
|
||||
{ name = "traits2_sol", path = "../solutions/15_traits/traits2.rs" },
|
||||
{ name = "traits3", path = "../exercises/15_traits/traits3.rs" },
|
||||
{ name = "traits3_sol", path = "../solutions/15_traits/traits3.rs" },
|
||||
{ name = "traits4", path = "../exercises/15_traits/traits4.rs" },
|
||||
{ name = "traits4_sol", path = "../solutions/15_traits/traits4.rs" },
|
||||
{ name = "traits5", path = "../exercises/15_traits/traits5.rs" },
|
||||
{ name = "traits5_sol", path = "../solutions/15_traits/traits5.rs" },
|
||||
{ name = "quiz3", path = "../exercises/quizzes/quiz3.rs" },
|
||||
{ name = "quiz3_sol", path = "../solutions/quizzes/quiz3.rs" },
|
||||
{ name = "lifetimes1", path = "../exercises/16_lifetimes/lifetimes1.rs" },
|
||||
{ name = "lifetimes1_sol", path = "../solutions/16_lifetimes/lifetimes1.rs" },
|
||||
{ name = "lifetimes2", path = "../exercises/16_lifetimes/lifetimes2.rs" },
|
||||
{ name = "lifetimes2_sol", path = "../solutions/16_lifetimes/lifetimes2.rs" },
|
||||
{ name = "lifetimes3", path = "../exercises/16_lifetimes/lifetimes3.rs" },
|
||||
{ name = "lifetimes3_sol", path = "../solutions/16_lifetimes/lifetimes3.rs" },
|
||||
{ name = "tests1", path = "../exercises/17_tests/tests1.rs" },
|
||||
{ name = "tests1_sol", path = "../solutions/17_tests/tests1.rs" },
|
||||
{ name = "tests2", path = "../exercises/17_tests/tests2.rs" },
|
||||
{ name = "tests2_sol", path = "../solutions/17_tests/tests2.rs" },
|
||||
{ name = "tests3", path = "../exercises/17_tests/tests3.rs" },
|
||||
{ name = "tests3_sol", path = "../solutions/17_tests/tests3.rs" },
|
||||
{ name = "iterators1", path = "../exercises/18_iterators/iterators1.rs" },
|
||||
{ name = "iterators1_sol", path = "../solutions/18_iterators/iterators1.rs" },
|
||||
{ name = "iterators2", path = "../exercises/18_iterators/iterators2.rs" },
|
||||
{ name = "iterators2_sol", path = "../solutions/18_iterators/iterators2.rs" },
|
||||
{ name = "iterators3", path = "../exercises/18_iterators/iterators3.rs" },
|
||||
{ name = "iterators3_sol", path = "../solutions/18_iterators/iterators3.rs" },
|
||||
{ name = "iterators4", path = "../exercises/18_iterators/iterators4.rs" },
|
||||
{ name = "iterators4_sol", path = "../solutions/18_iterators/iterators4.rs" },
|
||||
{ name = "iterators5", path = "../exercises/18_iterators/iterators5.rs" },
|
||||
{ name = "iterators5_sol", path = "../solutions/18_iterators/iterators5.rs" },
|
||||
{ name = "box1", path = "../exercises/19_smart_pointers/box1.rs" },
|
||||
{ name = "box1_sol", path = "../solutions/19_smart_pointers/box1.rs" },
|
||||
{ name = "rc1", path = "../exercises/19_smart_pointers/rc1.rs" },
|
||||
{ name = "rc1_sol", path = "../solutions/19_smart_pointers/rc1.rs" },
|
||||
{ name = "arc1", path = "../exercises/19_smart_pointers/arc1.rs" },
|
||||
{ name = "arc1_sol", path = "../solutions/19_smart_pointers/arc1.rs" },
|
||||
{ name = "cow1", path = "../exercises/19_smart_pointers/cow1.rs" },
|
||||
{ name = "cow1_sol", path = "../solutions/19_smart_pointers/cow1.rs" },
|
||||
{ name = "threads1", path = "../exercises/20_threads/threads1.rs" },
|
||||
{ name = "threads1_sol", path = "../solutions/20_threads/threads1.rs" },
|
||||
{ name = "threads2", path = "../exercises/20_threads/threads2.rs" },
|
||||
{ name = "threads2_sol", path = "../solutions/20_threads/threads2.rs" },
|
||||
{ name = "threads3", path = "../exercises/20_threads/threads3.rs" },
|
||||
{ name = "threads3_sol", path = "../solutions/20_threads/threads3.rs" },
|
||||
{ name = "macros1", path = "../exercises/21_macros/macros1.rs" },
|
||||
{ name = "macros1_sol", path = "../solutions/21_macros/macros1.rs" },
|
||||
{ name = "macros2", path = "../exercises/21_macros/macros2.rs" },
|
||||
{ name = "macros2_sol", path = "../solutions/21_macros/macros2.rs" },
|
||||
{ name = "macros3", path = "../exercises/21_macros/macros3.rs" },
|
||||
{ name = "macros3_sol", path = "../solutions/21_macros/macros3.rs" },
|
||||
{ name = "macros4", path = "../exercises/21_macros/macros4.rs" },
|
||||
{ name = "macros4_sol", path = "../solutions/21_macros/macros4.rs" },
|
||||
{ name = "clippy1", path = "../exercises/22_clippy/clippy1.rs" },
|
||||
{ name = "clippy1_sol", path = "../solutions/22_clippy/clippy1.rs" },
|
||||
{ name = "clippy2", path = "../exercises/22_clippy/clippy2.rs" },
|
||||
{ name = "clippy2_sol", path = "../solutions/22_clippy/clippy2.rs" },
|
||||
{ name = "clippy3", path = "../exercises/22_clippy/clippy3.rs" },
|
||||
{ name = "clippy3_sol", path = "../solutions/22_clippy/clippy3.rs" },
|
||||
{ name = "using_as", path = "../exercises/23_conversions/using_as.rs" },
|
||||
{ name = "using_as_sol", path = "../solutions/23_conversions/using_as.rs" },
|
||||
{ name = "from_into", path = "../exercises/23_conversions/from_into.rs" },
|
||||
{ name = "from_into_sol", path = "../solutions/23_conversions/from_into.rs" },
|
||||
{ name = "from_str", path = "../exercises/23_conversions/from_str.rs" },
|
||||
{ name = "from_str_sol", path = "../solutions/23_conversions/from_str.rs" },
|
||||
{ name = "try_from_into", path = "../exercises/23_conversions/try_from_into.rs" },
|
||||
{ name = "try_from_into_sol", path = "../solutions/23_conversions/try_from_into.rs" },
|
||||
{ name = "as_ref_mut", path = "../exercises/23_conversions/as_ref_mut.rs" },
|
||||
{ name = "as_ref_mut_sol", path = "../solutions/23_conversions/as_ref_mut.rs" },
|
||||
]
|
||||
|
||||
[package]
|
||||
name = "exercises"
|
||||
edition = "2024"
|
||||
# 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 +0,0 @@
|
||||
This file is used to check if the user tries to run Rustlings in the repository (the method before version 6)
|
||||
@ -1,16 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Error out if any command fails
|
||||
set -e
|
||||
|
||||
typos
|
||||
cargo upgrades
|
||||
|
||||
# Similar to CI
|
||||
cargo clippy -- --deny warnings
|
||||
cargo fmt --all --check
|
||||
cargo test --workspace
|
||||
cargo dev check --require-solutions
|
||||
|
||||
# MSRV
|
||||
cargo +1.87 dev check --require-solutions
|
||||
2
rust-analyzer.toml
Normal file
2
rust-analyzer.toml
Normal file
@ -0,0 +1,2 @@
|
||||
check.command = "clippy"
|
||||
check.extraArgs = ["--profile", "test"]
|
||||
@ -1,24 +0,0 @@
|
||||
[package]
|
||||
name = "rustlings-macros"
|
||||
description = "A macros crate intended to be used only by Rustlings"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
include = [
|
||||
"/src/",
|
||||
"/info.toml",
|
||||
]
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
quote = "1.0"
|
||||
serde.workspace = true
|
||||
toml_edit.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,56 +0,0 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ExerciseInfo {
|
||||
name: String,
|
||||
dir: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct InfoFile {
|
||||
exercises: Vec<ExerciseInfo>,
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn include_files(_: TokenStream) -> TokenStream {
|
||||
let info_file = include_str!("../info.toml");
|
||||
let exercises = toml_edit::de::from_str::<InfoFile>(info_file)
|
||||
.expect("Failed to parse `info.toml`")
|
||||
.exercises;
|
||||
|
||||
let exercise_files = exercises
|
||||
.iter()
|
||||
.map(|exercise| format!("../exercises/{}/{}.rs", exercise.dir, exercise.name));
|
||||
let solution_files = exercises
|
||||
.iter()
|
||||
.map(|exercise| format!("../solutions/{}/{}.rs", exercise.dir, exercise.name));
|
||||
|
||||
let mut dirs = Vec::with_capacity(32);
|
||||
let mut dir_inds = vec![0; exercises.len()];
|
||||
|
||||
for (exercise, dir_ind) in exercises.iter().zip(&mut dir_inds) {
|
||||
// The directory is often the last one inserted.
|
||||
if let Some(ind) = dirs.iter().rev().position(|dir| *dir == exercise.dir) {
|
||||
*dir_ind = dirs.len() - 1 - ind;
|
||||
continue;
|
||||
}
|
||||
|
||||
dirs.push(exercise.dir.as_str());
|
||||
*dir_ind = dirs.len() - 1;
|
||||
}
|
||||
|
||||
let readmes = dirs
|
||||
.iter()
|
||||
.map(|dir| format!("../exercises/{dir}/README.md"));
|
||||
|
||||
quote! {
|
||||
EmbeddedFiles {
|
||||
info_file: #info_file,
|
||||
exercise_files: &[#(ExerciseFiles { exercise: include_bytes!(#exercise_files), solution: include_bytes!(#solution_files), dir_ind: #dir_inds }),*],
|
||||
exercise_dirs: &[#(ExerciseDir { name: #dirs, readme: include_bytes!(#readmes) }),*]
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
fn main() {
|
||||
let number = "T-H-R-E-E";
|
||||
println!("Spell a number: {number}");
|
||||
println!("Spell a number: {}", number);
|
||||
|
||||
// Using variable shadowing
|
||||
// https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#shadowing
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
fn bigger(a: i32, b: i32) -> i32 {
|
||||
if a > b { a } else { b }
|
||||
if a > b {
|
||||
a
|
||||
} else {
|
||||
b
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
@ -4,6 +4,8 @@ fn main() {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// TODO: Fix the compiler errors only by reordering the lines in the test.
|
||||
// Don't add, change or remove any line.
|
||||
#[test]
|
||||
fn move_semantics4() {
|
||||
let mut x = Vec::new();
|
||||
|
||||
@ -26,7 +26,6 @@ mod tests {
|
||||
assert_eq!(trim_me("Hello! "), "Hello!");
|
||||
assert_eq!(trim_me(" What's up!"), "What's up!");
|
||||
assert_eq!(trim_me(" Hola! "), "Hola!");
|
||||
assert_eq!(trim_me("Hi!"), "Hi!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -60,11 +60,9 @@ England,Spain,1,0";
|
||||
fn build_scores() {
|
||||
let scores = build_scores_table(RESULTS);
|
||||
|
||||
assert!(
|
||||
["England", "France", "Germany", "Italy", "Poland", "Spain"]
|
||||
.into_iter()
|
||||
.all(|team_name| scores.contains_key(team_name))
|
||||
);
|
||||
assert!(["England", "France", "Germany", "Italy", "Poland", "Spain"]
|
||||
.into_iter()
|
||||
.all(|team_name| scores.contains_key(team_name)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -10,7 +10,7 @@ fn main() {
|
||||
// Solution 1: Matching over the `Option` (not `&Option`) but without moving
|
||||
// out of the `Some` variant.
|
||||
match optional_point {
|
||||
Some(ref p) => println!("Coordinates are {},{}", p.x, p.y),
|
||||
Some(ref p) => println!("Co-ordinates are {},{}", p.x, p.y),
|
||||
// ^^^ added
|
||||
_ => panic!("No match!"),
|
||||
}
|
||||
@ -18,8 +18,7 @@ fn main() {
|
||||
// Solution 2: Matching over a reference (`&Option`) by added `&` before
|
||||
// `optional_point`.
|
||||
match &optional_point {
|
||||
//^ added
|
||||
Some(p) => println!("Coordinates are {},{}", p.x, p.y),
|
||||
Some(p) => println!("Co-ordinates are {},{}", p.x, p.y),
|
||||
_ => panic!("No match!"),
|
||||
}
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
|
||||
use std::num::ParseIntError;
|
||||
|
||||
#[allow(unused_variables, clippy::question_mark)]
|
||||
#[allow(unused_variables)]
|
||||
fn total_cost(item_quantity: &str) -> Result<i32, ParseIntError> {
|
||||
let processing_fee = 1;
|
||||
let cost_per_item = 5;
|
||||
|
||||
@ -29,21 +29,6 @@ impl ParsePosNonzeroError {
|
||||
}
|
||||
}
|
||||
|
||||
// As an alternative solution, implementing the `From` trait allows for the
|
||||
// automatic conversion from a `ParseIntError` into a `ParsePosNonzeroError`
|
||||
// using the `?` operator, without the need to call `map_err`.
|
||||
//
|
||||
// ```
|
||||
// let x: i64 = s.parse()?;
|
||||
// ```
|
||||
//
|
||||
// Traits like `From` will be dealt with in later exercises.
|
||||
impl From<ParseIntError> for ParsePosNonzeroError {
|
||||
fn from(err: ParseIntError) -> Self {
|
||||
ParsePosNonzeroError::ParseInt(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
struct PositiveNonzeroInteger(u64);
|
||||
|
||||
|
||||
@ -1,28 +1,4 @@
|
||||
struct Wrapper<T> {
|
||||
value: T,
|
||||
}
|
||||
|
||||
impl<T> Wrapper<T> {
|
||||
fn new(value: T) -> Self {
|
||||
Wrapper { value }
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// You can optionally experiment here.
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn store_u32_in_wrapper() {
|
||||
assert_eq!(Wrapper::new(42).value, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_str_in_wrapper() {
|
||||
assert_eq!(Wrapper::new("Foo").value, "Foo");
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,32 +1,4 @@
|
||||
// The trait `AppendBar` has only one function which appends "Bar" to any object
|
||||
// implementing this trait.
|
||||
trait AppendBar {
|
||||
fn append_bar(self) -> Self;
|
||||
}
|
||||
|
||||
impl AppendBar for String {
|
||||
fn append_bar(self) -> Self {
|
||||
self + "Bar"
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let s = String::from("Foo");
|
||||
let s = s.append_bar();
|
||||
println!("s: {s}");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn is_foo_bar() {
|
||||
assert_eq!(String::from("Foo").append_bar(), "FooBar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_bar_bar() {
|
||||
assert_eq!(String::from("").append_bar().append_bar(), "BarBar");
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,27 +1,4 @@
|
||||
trait AppendBar {
|
||||
fn append_bar(self) -> Self;
|
||||
}
|
||||
|
||||
impl AppendBar for Vec<String> {
|
||||
fn append_bar(mut self) -> Self {
|
||||
// ^^^ this is important
|
||||
self.push(String::from("Bar"));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// You can optionally experiment here.
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn is_vec_pop_eq_bar() {
|
||||
let mut foo = vec![String::from("Foo")].append_bar();
|
||||
assert_eq!(foo.pop().unwrap(), "Bar");
|
||||
assert_eq!(foo.pop().unwrap(), "Foo");
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,36 +1,4 @@
|
||||
trait Licensed {
|
||||
fn licensing_info(&self) -> String {
|
||||
"Default license".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
struct SomeSoftware {
|
||||
version_number: i32,
|
||||
}
|
||||
|
||||
struct OtherSoftware {
|
||||
version_number: String,
|
||||
}
|
||||
|
||||
impl Licensed for SomeSoftware {}
|
||||
impl Licensed for OtherSoftware {}
|
||||
|
||||
fn main() {
|
||||
// You can optionally experiment here.
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn is_licensing_info_the_same() {
|
||||
let licensing_info = "Default license";
|
||||
let some_software = SomeSoftware { version_number: 1 };
|
||||
let other_software = OtherSoftware {
|
||||
version_number: "v2.0.0".to_string(),
|
||||
};
|
||||
assert_eq!(some_software.licensing_info(), licensing_info);
|
||||
assert_eq!(other_software.licensing_info(), licensing_info);
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,35 +1,4 @@
|
||||
trait Licensed {
|
||||
fn licensing_info(&self) -> String {
|
||||
"Default license".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
struct SomeSoftware;
|
||||
struct OtherSoftware;
|
||||
|
||||
impl Licensed for SomeSoftware {}
|
||||
impl Licensed for OtherSoftware {}
|
||||
|
||||
fn compare_license_types(software1: impl Licensed, software2: impl Licensed) -> bool {
|
||||
// ^^^^^^^^^^^^^ ^^^^^^^^^^^^^
|
||||
software1.licensing_info() == software2.licensing_info()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// You can optionally experiment here.
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn compare_license_information() {
|
||||
assert!(compare_license_types(SomeSoftware, OtherSoftware));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compare_license_information_backwards() {
|
||||
assert!(compare_license_types(OtherSoftware, SomeSoftware));
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,39 +1,4 @@
|
||||
trait SomeTrait {
|
||||
fn some_function(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
trait OtherTrait {
|
||||
fn other_function(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
struct SomeStruct;
|
||||
impl SomeTrait for SomeStruct {}
|
||||
impl OtherTrait for SomeStruct {}
|
||||
|
||||
struct OtherStruct;
|
||||
impl SomeTrait for OtherStruct {}
|
||||
impl OtherTrait for OtherStruct {}
|
||||
|
||||
fn some_func(item: impl SomeTrait + OtherTrait) -> bool {
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
item.some_function() && item.other_function()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// You can optionally experiment here.
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_some_func() {
|
||||
assert!(some_func(SomeStruct));
|
||||
assert!(some_func(OtherStruct));
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,24 +1,4 @@
|
||||
// The Rust compiler needs to know how to check whether supplied references are
|
||||
// valid, so that it can let the programmer know if a reference is at risk of
|
||||
// going out of scope before it is used. Remember, references are borrows and do
|
||||
// not own their own data. What if their owner goes out of scope?
|
||||
|
||||
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
|
||||
// ^^^^ ^^ ^^ ^^
|
||||
if x.len() > y.len() { x } else { y }
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// You can optionally experiment here.
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_longest() {
|
||||
assert_eq!(longest("abcd", "123"), "abcd");
|
||||
assert_eq!(longest("abc", "1234"), "1234");
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,29 +1,4 @@
|
||||
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
|
||||
if x.len() > y.len() { x } else { y }
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let string1 = String::from("long string is long");
|
||||
// Solution1: You can move `strings2` out of the inner block so that it is
|
||||
// not dropped before the print statement.
|
||||
let string2 = String::from("xyz");
|
||||
let result;
|
||||
{
|
||||
result = longest(&string1, &string2);
|
||||
}
|
||||
println!("The longest string is '{result}'");
|
||||
// `string2` dropped at the end of the function.
|
||||
|
||||
// =========================================================================
|
||||
|
||||
let string1 = String::from("long string is long");
|
||||
let result;
|
||||
{
|
||||
let string2 = String::from("xyz");
|
||||
result = longest(&string1, &string2);
|
||||
// Solution2: You can move the print statement into the inner block so
|
||||
// that it is executed before `string2` is dropped.
|
||||
println!("The longest string is '{result}'");
|
||||
// `string2` dropped here (end of the inner scope).
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,18 +1,4 @@
|
||||
// Lifetimes are also needed when structs hold references.
|
||||
|
||||
struct Book<'a> {
|
||||
// ^^^^ added a lifetime annotation
|
||||
author: &'a str,
|
||||
// ^^
|
||||
title: &'a str,
|
||||
// ^^
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let book = Book {
|
||||
author: "George Orwell",
|
||||
title: "1984",
|
||||
};
|
||||
|
||||
println!("{} by {}", book.title, book.author);
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,24 +1,4 @@
|
||||
// Tests are important to ensure that your code does what you think it should
|
||||
// do.
|
||||
|
||||
fn is_even(n: i64) -> bool {
|
||||
n % 2 == 0
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// You can optionally experiment here.
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// When writing unit tests, it is common to import everything from the outer
|
||||
// module (`super`) using a wildcard.
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn you_can_assert() {
|
||||
assert!(is_even(0));
|
||||
assert!(!is_even(-1));
|
||||
// ^ You can assert `false` using the negation operator `!`.
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,22 +1,4 @@
|
||||
// Calculates the power of 2 using a bit shift.
|
||||
// `1 << n` is equivalent to "2 to the power of n".
|
||||
fn power_of_2(n: u8) -> u64 {
|
||||
1 << n
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// You can optionally experiment here.
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn you_can_assert_eq() {
|
||||
assert_eq!(power_of_2(0), 1);
|
||||
assert_eq!(power_of_2(1), 2);
|
||||
assert_eq!(power_of_2(2), 4);
|
||||
assert_eq!(power_of_2(3), 8);
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,45 +1,4 @@
|
||||
struct Rectangle {
|
||||
width: i32,
|
||||
height: i32,
|
||||
}
|
||||
|
||||
impl Rectangle {
|
||||
// Don't change this function.
|
||||
fn new(width: i32, height: i32) -> Self {
|
||||
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 must be positive");
|
||||
}
|
||||
|
||||
Rectangle { width, height }
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// You can optionally experiment here.
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn correct_width_and_height() {
|
||||
let rect = Rectangle::new(10, 20);
|
||||
assert_eq!(rect.width, 10); // Check width
|
||||
assert_eq!(rect.height, 20); // Check height
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic] // Added this attribute to check that the test panics.
|
||||
fn negative_width() {
|
||||
let _rect = Rectangle::new(-10, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic] // Added this attribute to check that the test panics.
|
||||
fn negative_height() {
|
||||
let _rect = Rectangle::new(10, -10);
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,26 +1,4 @@
|
||||
// When performing operations on elements within a collection, iterators are
|
||||
// essential. This module helps you get familiar with the structure of using an
|
||||
// iterator and how to go through elements within an iterable collection.
|
||||
|
||||
fn main() {
|
||||
// You can optionally experiment here.
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn iterators() {
|
||||
let my_fav_fruits = ["banana", "custard apple", "avocado", "peach", "raspberry"];
|
||||
|
||||
// Create an iterator over the array.
|
||||
let mut fav_fruits_iterator = my_fav_fruits.iter();
|
||||
|
||||
assert_eq!(fav_fruits_iterator.next(), Some(&"banana"));
|
||||
assert_eq!(fav_fruits_iterator.next(), Some(&"custard apple"));
|
||||
assert_eq!(fav_fruits_iterator.next(), Some(&"avocado"));
|
||||
assert_eq!(fav_fruits_iterator.next(), Some(&"peach"));
|
||||
assert_eq!(fav_fruits_iterator.next(), Some(&"raspberry"));
|
||||
assert_eq!(fav_fruits_iterator.next(), None);
|
||||
// ^^^^ reached the end
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,56 +1,4 @@
|
||||
// In this exercise, you'll learn some of the unique advantages that iterators
|
||||
// can offer.
|
||||
|
||||
// "hello" -> "Hello"
|
||||
fn capitalize_first(input: &str) -> String {
|
||||
let mut chars = input.chars();
|
||||
match chars.next() {
|
||||
None => String::new(),
|
||||
Some(first) => first.to_uppercase().to_string() + chars.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the `capitalize_first` function to a slice of string slices.
|
||||
// Return a vector of strings.
|
||||
// ["hello", "world"] -> ["Hello", "World"]
|
||||
fn capitalize_words_vector(words: &[&str]) -> Vec<String> {
|
||||
words.iter().map(|word| capitalize_first(word)).collect()
|
||||
}
|
||||
|
||||
// Apply the `capitalize_first` function again to a slice of string
|
||||
// slices. Return a single string.
|
||||
// ["hello", " ", "world"] -> "Hello World"
|
||||
fn capitalize_words_string(words: &[&str]) -> String {
|
||||
words.iter().map(|word| capitalize_first(word)).collect()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// You can optionally experiment here.
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_success() {
|
||||
assert_eq!(capitalize_first("hello"), "Hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty() {
|
||||
assert_eq!(capitalize_first(""), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iterate_string_vec() {
|
||||
let words = vec!["hello", "world"];
|
||||
assert_eq!(capitalize_words_vector(&words), ["Hello", "World"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iterate_into_string() {
|
||||
let words = vec!["hello", " ", "world"];
|
||||
assert_eq!(capitalize_words_string(&words), "Hello World");
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,86 +1,4 @@
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum DivisionError {
|
||||
// Example: 42 / 0
|
||||
DivideByZero,
|
||||
// Only case for `i64`: `i64::MIN / -1` because the result is `i64::MAX + 1`
|
||||
IntegerOverflow,
|
||||
// Example: 5 / 2 = 2.5
|
||||
NotDivisible,
|
||||
}
|
||||
|
||||
fn divide(a: i64, b: i64) -> Result<i64, DivisionError> {
|
||||
if b == 0 {
|
||||
return Err(DivisionError::DivideByZero);
|
||||
}
|
||||
|
||||
if a == i64::MIN && b == -1 {
|
||||
return Err(DivisionError::IntegerOverflow);
|
||||
}
|
||||
|
||||
if a % b != 0 {
|
||||
return Err(DivisionError::NotDivisible);
|
||||
}
|
||||
|
||||
Ok(a / b)
|
||||
}
|
||||
|
||||
fn result_with_list() -> Result<Vec<i64>, DivisionError> {
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
let numbers = [27, 297, 38502, 81];
|
||||
let division_results = numbers.into_iter().map(|n| divide(n, 27));
|
||||
// Collects to the expected return type. Returns the first error in the
|
||||
// division results (if one exists).
|
||||
division_results.collect()
|
||||
}
|
||||
|
||||
fn list_of_results() -> Vec<Result<i64, DivisionError>> {
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
let numbers = [27, 297, 38502, 81];
|
||||
let division_results = numbers.into_iter().map(|n| divide(n, 27));
|
||||
// Collects to the expected return type.
|
||||
division_results.collect()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// You can optionally experiment here.
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_success() {
|
||||
assert_eq!(divide(81, 9), Ok(9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_divide_by_0() {
|
||||
assert_eq!(divide(81, 0), Err(DivisionError::DivideByZero));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_integer_overflow() {
|
||||
assert_eq!(divide(i64::MIN, -1), Err(DivisionError::IntegerOverflow));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_not_divisible() {
|
||||
assert_eq!(divide(81, 6), Err(DivisionError::NotDivisible));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_divide_0_by_something() {
|
||||
assert_eq!(divide(0, 81), Ok(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_result_with_list() {
|
||||
assert_eq!(result_with_list().unwrap(), [1, 11, 1426, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_of_results() {
|
||||
assert_eq!(list_of_results(), [Ok(1), Ok(11), Ok(1426), Ok(3)]);
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,72 +1,4 @@
|
||||
// 3 possible solutions are presented.
|
||||
|
||||
// With `for` loop and a mutable variable.
|
||||
fn factorial_for(num: u64) -> u64 {
|
||||
let mut result = 1;
|
||||
|
||||
for x in 2..=num {
|
||||
result *= x;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
// Equivalent to `factorial_for` but shorter and without a `for` loop and
|
||||
// mutable variables.
|
||||
fn factorial_fold(num: u64) -> u64 {
|
||||
// Case num==0: The iterator 2..=0 is empty
|
||||
// -> The initial value of `fold` is returned which is 1.
|
||||
// Case num==1: The iterator 2..=1 is also empty
|
||||
// -> The initial value 1 is returned.
|
||||
// Case num==2: The iterator 2..=2 contains one element
|
||||
// -> The initial value 1 is multiplied by 2 and the result
|
||||
// is returned.
|
||||
// Case num==3: The iterator 2..=3 contains 2 elements
|
||||
// -> 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)
|
||||
}
|
||||
|
||||
// Equivalent to `factorial_fold` but with a built-in method that is suggested
|
||||
// by Clippy.
|
||||
fn factorial_product(num: u64) -> u64 {
|
||||
(2..=num).product()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// You can optionally experiment here.
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn factorial_of_0() {
|
||||
assert_eq!(factorial_for(0), 1);
|
||||
assert_eq!(factorial_fold(0), 1);
|
||||
assert_eq!(factorial_product(0), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factorial_of_1() {
|
||||
assert_eq!(factorial_for(1), 1);
|
||||
assert_eq!(factorial_fold(1), 1);
|
||||
assert_eq!(factorial_product(1), 1);
|
||||
}
|
||||
#[test]
|
||||
fn factorial_of_2() {
|
||||
assert_eq!(factorial_for(2), 2);
|
||||
assert_eq!(factorial_fold(2), 2);
|
||||
assert_eq!(factorial_product(2), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factorial_of_4() {
|
||||
assert_eq!(factorial_for(4), 24);
|
||||
assert_eq!(factorial_fold(4), 24);
|
||||
assert_eq!(factorial_product(4), 24);
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,168 +1,4 @@
|
||||
// Let's define a simple model to track Rustlings' exercise progress. Progress
|
||||
// will be modelled using a hash map. The name of the exercise is the key and
|
||||
// the progress is the value. Two counting functions were created to count the
|
||||
// number of exercises with a given progress. Recreate this counting
|
||||
// functionality using iterators. Try to not use imperative loops (for/while).
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Progress {
|
||||
None,
|
||||
Some,
|
||||
Complete,
|
||||
}
|
||||
|
||||
fn count_for(map: &HashMap<String, Progress>, value: Progress) -> usize {
|
||||
let mut count = 0;
|
||||
for val in map.values() {
|
||||
if *val == value {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
fn count_iterator(map: &HashMap<String, Progress>, value: Progress) -> usize {
|
||||
// `map` is a hash map with `String` keys and `Progress` values.
|
||||
// map = { "variables1": Complete, "from_str": None, … }
|
||||
map.values().filter(|val| **val == value).count()
|
||||
}
|
||||
|
||||
fn count_collection_for(collection: &[HashMap<String, Progress>], value: Progress) -> usize {
|
||||
let mut count = 0;
|
||||
for map in collection {
|
||||
count += count_for(map, value);
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
fn count_collection_iterator(collection: &[HashMap<String, Progress>], value: Progress) -> usize {
|
||||
// `collection` is a slice of hash maps.
|
||||
// collection = [{ "variables1": Complete, "from_str": None, … },
|
||||
// { "variables2": Complete, … }, … ]
|
||||
collection
|
||||
.iter()
|
||||
.map(|map| count_iterator(map, value))
|
||||
.sum()
|
||||
}
|
||||
|
||||
// Equivalent to `count_collection_iterator` and `count_iterator`, iterating as
|
||||
// if the collection was a single container instead of a container of containers
|
||||
// (and more accurately, a single iterator instead of an iterator of iterators).
|
||||
fn count_collection_iterator_flat(
|
||||
collection: &[HashMap<String, Progress>],
|
||||
value: Progress,
|
||||
) -> usize {
|
||||
// `collection` is a slice of hash maps.
|
||||
// collection = [{ "variables1": Complete, "from_str": None, … },
|
||||
// { "variables2": Complete, … }, … ]
|
||||
collection
|
||||
.iter()
|
||||
.flat_map(HashMap::values) // or just `.flatten()` when wanting the default iterator (`HashMap::iter`)
|
||||
.filter(|val| **val == value)
|
||||
.count()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// You can optionally experiment here.
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use Progress::*;
|
||||
|
||||
fn get_map() -> HashMap<String, Progress> {
|
||||
let mut map = HashMap::new();
|
||||
map.insert(String::from("variables1"), Complete);
|
||||
map.insert(String::from("functions1"), Complete);
|
||||
map.insert(String::from("hashmap1"), Complete);
|
||||
map.insert(String::from("arc1"), Some);
|
||||
map.insert(String::from("as_ref_mut"), None);
|
||||
map.insert(String::from("from_str"), None);
|
||||
|
||||
map
|
||||
}
|
||||
|
||||
fn get_vec_map() -> Vec<HashMap<String, Progress>> {
|
||||
let map = get_map();
|
||||
|
||||
let mut other = HashMap::new();
|
||||
other.insert(String::from("variables2"), Complete);
|
||||
other.insert(String::from("functions2"), Complete);
|
||||
other.insert(String::from("if1"), Complete);
|
||||
other.insert(String::from("from_into"), None);
|
||||
other.insert(String::from("try_from_into"), None);
|
||||
|
||||
vec![map, other]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn count_complete() {
|
||||
let map = get_map();
|
||||
assert_eq!(count_iterator(&map, Complete), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn count_some() {
|
||||
let map = get_map();
|
||||
assert_eq!(count_iterator(&map, Some), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn count_none() {
|
||||
let map = get_map();
|
||||
assert_eq!(count_iterator(&map, None), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn count_complete_equals_for() {
|
||||
let map = get_map();
|
||||
let progress_states = [Complete, Some, None];
|
||||
for progress_state in progress_states {
|
||||
assert_eq!(
|
||||
count_for(&map, progress_state),
|
||||
count_iterator(&map, progress_state),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn count_collection_complete() {
|
||||
let collection = get_vec_map();
|
||||
assert_eq!(count_collection_iterator(&collection, Complete), 6);
|
||||
assert_eq!(count_collection_iterator_flat(&collection, Complete), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn count_collection_some() {
|
||||
let collection = get_vec_map();
|
||||
assert_eq!(count_collection_iterator(&collection, Some), 1);
|
||||
assert_eq!(count_collection_iterator_flat(&collection, Some), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn count_collection_none() {
|
||||
let collection = get_vec_map();
|
||||
assert_eq!(count_collection_iterator(&collection, None), 4);
|
||||
assert_eq!(count_collection_iterator_flat(&collection, None), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn count_collection_equals_for() {
|
||||
let collection = get_vec_map();
|
||||
let progress_states = [Complete, Some, None];
|
||||
|
||||
for progress_state in progress_states {
|
||||
assert_eq!(
|
||||
count_collection_for(&collection, progress_state),
|
||||
count_collection_iterator(&collection, progress_state),
|
||||
);
|
||||
assert_eq!(
|
||||
count_collection_for(&collection, progress_state),
|
||||
count_collection_iterator_flat(&collection, progress_state),
|
||||
);
|
||||
}
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,45 +1,4 @@
|
||||
// In this exercise, we are given a `Vec` of `u32` called `numbers` with values
|
||||
// ranging from 0 to 99. We would like to use this set of numbers within 8
|
||||
// different threads simultaneously. Each thread is going to get the sum of
|
||||
// every eighth value with an offset.
|
||||
//
|
||||
// The first thread (offset 0), will sum 0, 8, 16, …
|
||||
// The second thread (offset 1), will sum 1, 9, 17, …
|
||||
// The third thread (offset 2), will sum 2, 10, 18, …
|
||||
// …
|
||||
// The eighth thread (offset 7), will sum 7, 15, 23, …
|
||||
//
|
||||
// Each thread should own a reference-counting pointer to the vector of
|
||||
// numbers. But `Rc` isn't thread-safe. Therefore, we need to use `Arc`.
|
||||
//
|
||||
// Don't get distracted by how threads are spawned and joined. We will practice
|
||||
// that later in the exercises about threads.
|
||||
|
||||
// Don't change the lines below.
|
||||
#![forbid(unused_imports)]
|
||||
use std::{sync::Arc, thread};
|
||||
|
||||
fn main() {
|
||||
let numbers: Vec<_> = (0..100u32).collect();
|
||||
|
||||
let shared_numbers = Arc::new(numbers);
|
||||
// ^^^^^^^^^^^^^^^^^
|
||||
|
||||
let mut join_handles = Vec::new();
|
||||
|
||||
for offset in 0..8 {
|
||||
let child_numbers = Arc::clone(&shared_numbers);
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
let sum: u32 = child_numbers.iter().filter(|&&n| n % 8 == offset).sum();
|
||||
println!("Sum of offset {offset} is {sum}");
|
||||
});
|
||||
|
||||
join_handles.push(handle);
|
||||
}
|
||||
|
||||
for handle in join_handles.into_iter() {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,47 +1,4 @@
|
||||
// At compile time, Rust needs to know how much space a type takes up. This
|
||||
// becomes problematic for recursive types, where a value can have as part of
|
||||
// itself another value of the same type. To get around the issue, we can use a
|
||||
// `Box` - a smart pointer used to store data on the heap, which also allows us
|
||||
// to wrap a recursive type.
|
||||
//
|
||||
// The recursive type we're implementing in this exercise is the "cons list", a
|
||||
// data structure frequently found in functional programming languages. Each
|
||||
// item in a cons list contains two elements: The value of the current item and
|
||||
// the next item. The last item is a value called `Nil`.
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
enum List {
|
||||
Cons(i32, Box<List>),
|
||||
Nil,
|
||||
}
|
||||
|
||||
fn create_empty_list() -> List {
|
||||
List::Nil
|
||||
}
|
||||
|
||||
fn create_non_empty_list() -> List {
|
||||
List::Cons(42, Box::new(List::Nil))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("This is an empty cons list: {:?}", create_empty_list());
|
||||
println!(
|
||||
"This is a non-empty cons list: {:?}",
|
||||
create_non_empty_list(),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_create_empty_list() {
|
||||
assert_eq!(create_empty_list(), List::Nil);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_non_empty_list() {
|
||||
assert_ne!(create_empty_list(), create_non_empty_list());
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,69 +1,4 @@
|
||||
// This exercise explores the `Cow` (Clone-On-Write) smart pointer. It can
|
||||
// enclose and provide immutable access to borrowed data and clone the data
|
||||
// lazily when mutation or ownership is required. The type is designed to work
|
||||
// with general borrowed data via the `Borrow` trait.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
fn abs_all(input: &mut Cow<[i32]>) {
|
||||
for ind in 0..input.len() {
|
||||
let value = input[ind];
|
||||
if value < 0 {
|
||||
// Clones into a vector if not already owned.
|
||||
input.to_mut()[ind] = -value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// You can optionally experiment here.
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn reference_mutation() {
|
||||
// Clone occurs because `input` needs to be mutated.
|
||||
let vec = vec![-1, 0, 1];
|
||||
let mut input = Cow::from(&vec);
|
||||
abs_all(&mut input);
|
||||
assert!(matches!(input, Cow::Owned(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_no_mutation() {
|
||||
// No clone occurs because `input` doesn't need to be mutated.
|
||||
let vec = vec![0, 1, 2];
|
||||
let mut input = Cow::from(&vec);
|
||||
abs_all(&mut input);
|
||||
assert!(matches!(input, Cow::Borrowed(_)));
|
||||
// ^^^^^^^^^^^^^^^^
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn owned_no_mutation() {
|
||||
// We can also pass `vec` without `&` so `Cow` owns it directly. In this
|
||||
// case, no mutation occurs (all numbers are already absolute) and thus
|
||||
// also no clone. But the result is still owned because it was never
|
||||
// borrowed or mutated.
|
||||
let vec = vec![0, 1, 2];
|
||||
let mut input = Cow::from(vec);
|
||||
abs_all(&mut input);
|
||||
assert!(matches!(input, Cow::Owned(_)));
|
||||
// ^^^^^^^^^^^^^
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn owned_mutation() {
|
||||
// Of course this is also the case if a mutation does occur (not all
|
||||
// numbers are absolute). In this case, the call to `to_mut()` in the
|
||||
// `abs_all` function returns a reference to the same data as before.
|
||||
let vec = vec![-1, 0, 1];
|
||||
let mut input = Cow::from(vec);
|
||||
abs_all(&mut input);
|
||||
assert!(matches!(input, Cow::Owned(_)));
|
||||
// ^^^^^^^^^^^^^
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,102 +1,4 @@
|
||||
// In this exercise, we want to express the concept of multiple owners via the
|
||||
// `Rc<T>` type. This is a model of our solar system - there is a `Sun` type and
|
||||
// multiple `Planet`s. The planets take ownership of the sun, indicating that
|
||||
// they revolve around the sun.
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Sun;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Planet {
|
||||
Mercury(Rc<Sun>),
|
||||
Venus(Rc<Sun>),
|
||||
Earth(Rc<Sun>),
|
||||
Mars(Rc<Sun>),
|
||||
Jupiter(Rc<Sun>),
|
||||
Saturn(Rc<Sun>),
|
||||
Uranus(Rc<Sun>),
|
||||
Neptune(Rc<Sun>),
|
||||
}
|
||||
|
||||
impl Planet {
|
||||
fn details(&self) {
|
||||
println!("Hi from {self:?}!");
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// You can optionally experiment here.
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rc1() {
|
||||
let sun = Rc::new(Sun);
|
||||
println!("reference count = {}", Rc::strong_count(&sun)); // 1 reference
|
||||
|
||||
let mercury = Planet::Mercury(Rc::clone(&sun));
|
||||
println!("reference count = {}", Rc::strong_count(&sun)); // 2 references
|
||||
mercury.details();
|
||||
|
||||
let venus = Planet::Venus(Rc::clone(&sun));
|
||||
println!("reference count = {}", Rc::strong_count(&sun)); // 3 references
|
||||
venus.details();
|
||||
|
||||
let earth = Planet::Earth(Rc::clone(&sun));
|
||||
println!("reference count = {}", Rc::strong_count(&sun)); // 4 references
|
||||
earth.details();
|
||||
|
||||
let mars = Planet::Mars(Rc::clone(&sun));
|
||||
println!("reference count = {}", Rc::strong_count(&sun)); // 5 references
|
||||
mars.details();
|
||||
|
||||
let jupiter = Planet::Jupiter(Rc::clone(&sun));
|
||||
println!("reference count = {}", Rc::strong_count(&sun)); // 6 references
|
||||
jupiter.details();
|
||||
|
||||
let saturn = Planet::Saturn(Rc::clone(&sun));
|
||||
println!("reference count = {}", Rc::strong_count(&sun)); // 7 references
|
||||
saturn.details();
|
||||
|
||||
let uranus = Planet::Uranus(Rc::clone(&sun));
|
||||
println!("reference count = {}", Rc::strong_count(&sun)); // 8 references
|
||||
uranus.details();
|
||||
|
||||
let neptune = Planet::Neptune(Rc::clone(&sun));
|
||||
println!("reference count = {}", Rc::strong_count(&sun)); // 9 references
|
||||
neptune.details();
|
||||
|
||||
assert_eq!(Rc::strong_count(&sun), 9);
|
||||
|
||||
drop(neptune);
|
||||
println!("reference count = {}", Rc::strong_count(&sun)); // 8 references
|
||||
|
||||
drop(uranus);
|
||||
println!("reference count = {}", Rc::strong_count(&sun)); // 7 references
|
||||
|
||||
drop(saturn);
|
||||
println!("reference count = {}", Rc::strong_count(&sun)); // 6 references
|
||||
|
||||
drop(jupiter);
|
||||
println!("reference count = {}", Rc::strong_count(&sun)); // 5 references
|
||||
|
||||
drop(mars);
|
||||
println!("reference count = {}", Rc::strong_count(&sun)); // 4 references
|
||||
|
||||
drop(earth);
|
||||
println!("reference count = {}", Rc::strong_count(&sun)); // 3 references
|
||||
|
||||
drop(venus);
|
||||
println!("reference count = {}", Rc::strong_count(&sun)); // 2 references
|
||||
|
||||
drop(mercury);
|
||||
println!("reference count = {}", Rc::strong_count(&sun)); // 1 reference
|
||||
|
||||
assert_eq!(Rc::strong_count(&sun), 1);
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,37 +1,4 @@
|
||||
// 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.
|
||||
|
||||
use std::{
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
fn main() {
|
||||
let mut handles = Vec::new();
|
||||
for i in 0..10 {
|
||||
let handle = thread::spawn(move || {
|
||||
let start = Instant::now();
|
||||
thread::sleep(Duration::from_millis(250));
|
||||
println!("Thread {i} done");
|
||||
start.elapsed().as_millis()
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
let mut results = Vec::new();
|
||||
for handle in handles {
|
||||
// Collect the results of all threads into the `results` vector.
|
||||
results.push(handle.join().unwrap());
|
||||
}
|
||||
|
||||
if results.len() != 10 {
|
||||
panic!("Oh no! Some thread isn't done yet!");
|
||||
}
|
||||
|
||||
println!();
|
||||
for (i, result) in results.into_iter().enumerate() {
|
||||
println!("Thread {i} took {result}ms");
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,41 +1,4 @@
|
||||
// Building on the last exercise, we want all of the threads to complete their
|
||||
// work. But this time, the spawned threads need to be in charge of updating a
|
||||
// shared value: `JobStatus.jobs_done`
|
||||
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
struct JobStatus {
|
||||
jobs_done: u32,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// `Arc` isn't enough if you want a **mutable** shared state.
|
||||
// We need to wrap the value with a `Mutex`.
|
||||
let status = Arc::new(Mutex::new(JobStatus { jobs_done: 0 }));
|
||||
// ^^^^^^^^^^^ ^
|
||||
|
||||
let mut handles = Vec::new();
|
||||
for _ in 0..10 {
|
||||
let status_shared = Arc::clone(&status);
|
||||
let handle = thread::spawn(move || {
|
||||
thread::sleep(Duration::from_millis(250));
|
||||
|
||||
// Lock before you update a shared value.
|
||||
status_shared.lock().unwrap().jobs_done += 1;
|
||||
// ^^^^^^^^^^^^^^^^
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Waiting for all jobs to complete.
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
println!("Jobs done: {}", status.lock().unwrap().jobs_done);
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,62 +1,4 @@
|
||||
use std::{sync::mpsc, thread, time::Duration};
|
||||
|
||||
struct Queue {
|
||||
first_half: Vec<u32>,
|
||||
second_half: Vec<u32>,
|
||||
}
|
||||
|
||||
impl Queue {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
first_half: vec![1, 2, 3, 4, 5],
|
||||
second_half: vec![6, 7, 8, 9, 10],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_tx(q: Queue, tx: mpsc::Sender<u32>) {
|
||||
// Clone the sender `tx` first.
|
||||
let tx_clone = tx.clone();
|
||||
thread::spawn(move || {
|
||||
for val in q.first_half {
|
||||
println!("Sending {val:?}");
|
||||
// Then use the clone in the first thread. This means that
|
||||
// `tx_clone` is moved to the first thread and `tx` to the second.
|
||||
tx_clone.send(val).unwrap();
|
||||
thread::sleep(Duration::from_millis(250));
|
||||
}
|
||||
});
|
||||
|
||||
thread::spawn(move || {
|
||||
for val in q.second_half {
|
||||
println!("Sending {val:?}");
|
||||
tx.send(val).unwrap();
|
||||
thread::sleep(Duration::from_millis(250));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// You can optionally experiment here.
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn threads3() {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let queue = Queue::new();
|
||||
|
||||
send_tx(queue, tx);
|
||||
|
||||
let mut received = Vec::with_capacity(10);
|
||||
for value in rx {
|
||||
received.push(value);
|
||||
}
|
||||
|
||||
received.sort();
|
||||
assert_eq!(received, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,10 +1,4 @@
|
||||
macro_rules! my_macro {
|
||||
() => {
|
||||
println!("Check out my macro!");
|
||||
};
|
||||
}
|
||||
|
||||
fn main() {
|
||||
my_macro!();
|
||||
// ^
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,10 +1,4 @@
|
||||
// Moved the macro definition to be before its call.
|
||||
macro_rules! my_macro {
|
||||
() => {
|
||||
println!("Check out my macro!");
|
||||
};
|
||||
}
|
||||
|
||||
fn main() {
|
||||
my_macro!();
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,13 +1,4 @@
|
||||
// Added the attribute `macro_use` attribute.
|
||||
#[macro_use]
|
||||
mod macros {
|
||||
macro_rules! my_macro {
|
||||
() => {
|
||||
println!("Check out my macro!");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
my_macro!();
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,15 +1,4 @@
|
||||
// Added semicolons to separate the macro arms.
|
||||
#[rustfmt::skip]
|
||||
macro_rules! my_macro {
|
||||
() => {
|
||||
println!("Check out my macro!");
|
||||
};
|
||||
($val:expr) => {
|
||||
println!("Look at this other macro: {}", $val);
|
||||
};
|
||||
}
|
||||
|
||||
fn main() {
|
||||
my_macro!();
|
||||
my_macro!(7777);
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,17 +1,4 @@
|
||||
// The Clippy tool is a collection of lints to analyze your code so you can
|
||||
// catch common mistakes and improve your Rust code.
|
||||
//
|
||||
// For these exercises, the code will fail to compile when there are Clippy
|
||||
// warnings. Check Clippy's suggestions from the output to solve the exercise.
|
||||
|
||||
use std::f32::consts::PI;
|
||||
|
||||
fn main() {
|
||||
// Use the more accurate `PI` constant.
|
||||
let pi = PI;
|
||||
let radius: f32 = 5.0;
|
||||
|
||||
let area = pi * radius.powi(2);
|
||||
|
||||
println!("The area of a circle with radius {radius:.2} is {area:.5}");
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,10 +1,4 @@
|
||||
fn main() {
|
||||
let mut res = 42;
|
||||
let option = Some(12);
|
||||
// Use `if-let` instead of iteration.
|
||||
if let Some(x) = option {
|
||||
res += x;
|
||||
}
|
||||
|
||||
println!("{res}");
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,31 +1,4 @@
|
||||
use std::mem;
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[allow(unused_variables, unused_assignments)]
|
||||
fn main() {
|
||||
let my_option: Option<&str> = None;
|
||||
// `unwrap` of an `Option` after checking if it is `None` will panic.
|
||||
// Use `if-let` instead.
|
||||
if let Some(value) = my_option {
|
||||
println!("{value}");
|
||||
}
|
||||
|
||||
// A comma was missing.
|
||||
let my_arr = &[
|
||||
-1, -2, -3,
|
||||
-4, -5, -6,
|
||||
];
|
||||
println!("My array! Here it is: {my_arr:?}");
|
||||
|
||||
let mut my_empty_vec = vec![1, 2, 3, 4, 5];
|
||||
// `resize` mutates a vector instead of returning a new one.
|
||||
// `resize(0, …)` clears a vector, so it is better to use `clear`.
|
||||
my_empty_vec.clear();
|
||||
println!("This Vec is empty, see? {my_empty_vec:?}");
|
||||
|
||||
let mut value_a = 45;
|
||||
let mut value_b = 66;
|
||||
// Use `mem::swap` to correctly swap two values.
|
||||
mem::swap(&mut value_a, &mut value_b);
|
||||
println!("value a: {value_a}; value b: {value_b}");
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,60 +1,4 @@
|
||||
// AsRef and AsMut allow for cheap reference-to-reference conversions. Read more
|
||||
// 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
|
||||
// (`.len()` returns the number of bytes in a string).
|
||||
fn byte_counter<T: AsRef<str>>(arg: T) -> usize {
|
||||
arg.as_ref().len()
|
||||
}
|
||||
|
||||
// Obtain the number of characters (not bytes) in the given argument.
|
||||
fn char_counter<T: AsRef<str>>(arg: T) -> usize {
|
||||
arg.as_ref().chars().count()
|
||||
}
|
||||
|
||||
// Squares a number using `as_mut()`.
|
||||
fn num_sq<T: AsMut<u32>>(arg: &mut T) {
|
||||
let arg = arg.as_mut();
|
||||
*arg *= *arg;
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// You can optionally experiment here.
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn different_counts() {
|
||||
let s = "Café au lait";
|
||||
assert_ne!(char_counter(s), byte_counter(s));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_counts() {
|
||||
let s = "Cafe au lait";
|
||||
assert_eq!(char_counter(s), byte_counter(s));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_counts_using_string() {
|
||||
let s = String::from("Café au lait");
|
||||
assert_ne!(char_counter(s.clone()), byte_counter(s));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_counts_using_string() {
|
||||
let s = String::from("Cafe au lait");
|
||||
assert_eq!(char_counter(s.clone()), byte_counter(s));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mut_box() {
|
||||
let mut num: Box<u32> = Box::new(3);
|
||||
num_sq(&mut num);
|
||||
assert_eq!(*num, 9);
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,136 +1,4 @@
|
||||
// The `From` trait is used for value-to-value conversions. If `From` is
|
||||
// implemented, an implementation of `Into` is automatically provided.
|
||||
// You can read more about it in the documentation:
|
||||
// https://doc.rust-lang.org/std/convert/trait.From.html
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Person {
|
||||
name: String,
|
||||
age: u8,
|
||||
}
|
||||
|
||||
// We implement the Default trait to use it as a fallback when the provided
|
||||
// string is not convertible into a `Person` object.
|
||||
impl Default for Person {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: String::from("John"),
|
||||
age: 30,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Person {
|
||||
fn from(s: &str) -> Self {
|
||||
let mut split = s.split(',');
|
||||
let (Some(name), Some(age), None) = (split.next(), split.next(), split.next()) else {
|
||||
// ^^^^ there should be no third element
|
||||
return Self::default();
|
||||
};
|
||||
|
||||
if name.is_empty() {
|
||||
return Self::default();
|
||||
}
|
||||
|
||||
let Ok(age) = age.parse() else {
|
||||
return Self::default();
|
||||
};
|
||||
|
||||
Self {
|
||||
name: name.into(),
|
||||
age,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Use the `from` function.
|
||||
let p1 = Person::from("Mark,20");
|
||||
println!("{p1:?}");
|
||||
|
||||
// Since `From` is implemented for Person, we are able to use `Into`.
|
||||
let p2: Person = "Gerald,70".into();
|
||||
println!("{p2:?}");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default() {
|
||||
let dp = Person::default();
|
||||
assert_eq!(dp.name, "John");
|
||||
assert_eq!(dp.age, 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bad_convert() {
|
||||
let p = Person::from("");
|
||||
assert_eq!(p.name, "John");
|
||||
assert_eq!(p.age, 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_good_convert() {
|
||||
let p = Person::from("Mark,20");
|
||||
assert_eq!(p.name, "Mark");
|
||||
assert_eq!(p.age, 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bad_age() {
|
||||
let p = Person::from("Mark,twenty");
|
||||
assert_eq!(p.name, "John");
|
||||
assert_eq!(p.age, 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_comma_and_age() {
|
||||
let p: Person = Person::from("Mark");
|
||||
assert_eq!(p.name, "John");
|
||||
assert_eq!(p.age, 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_age() {
|
||||
let p: Person = Person::from("Mark,");
|
||||
assert_eq!(p.name, "John");
|
||||
assert_eq!(p.age, 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_name() {
|
||||
let p: Person = Person::from(",1");
|
||||
assert_eq!(p.name, "John");
|
||||
assert_eq!(p.age, 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_name_and_age() {
|
||||
let p: Person = Person::from(",");
|
||||
assert_eq!(p.name, "John");
|
||||
assert_eq!(p.age, 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_name_and_invalid_age() {
|
||||
let p: Person = Person::from(",one");
|
||||
assert_eq!(p.name, "John");
|
||||
assert_eq!(p.age, 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trailing_comma() {
|
||||
let p: Person = Person::from("Mike,32,");
|
||||
assert_eq!(p.name, "John");
|
||||
assert_eq!(p.age, 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trailing_comma_and_some_string() {
|
||||
let p: Person = Person::from("Mike,32,dog");
|
||||
assert_eq!(p.name, "John");
|
||||
assert_eq!(p.age, 30);
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,117 +1,4 @@
|
||||
// This is similar to the previous `from_into` exercise. But this time, we'll
|
||||
// implement `FromStr` and return errors instead of falling back to a default
|
||||
// value. Additionally, upon implementing `FromStr`, you can use the `parse`
|
||||
// method on strings to generate an object of the implementor type. You can read
|
||||
// more about it in the documentation:
|
||||
// https://doc.rust-lang.org/std/str/trait.FromStr.html
|
||||
|
||||
use std::num::ParseIntError;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Person {
|
||||
name: String,
|
||||
age: u8,
|
||||
}
|
||||
|
||||
// We will use this error type for the `FromStr` implementation.
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum ParsePersonError {
|
||||
// Incorrect number of fields
|
||||
BadLen,
|
||||
// Empty name field
|
||||
NoName,
|
||||
// Wrapped error from parse::<u8>()
|
||||
ParseInt(ParseIntError),
|
||||
}
|
||||
|
||||
impl FromStr for Person {
|
||||
type Err = ParsePersonError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut split = s.split(',');
|
||||
let (Some(name), Some(age), None) = (split.next(), split.next(), split.next()) else {
|
||||
// ^^^^ there should be no third element
|
||||
return Err(ParsePersonError::BadLen);
|
||||
};
|
||||
|
||||
if name.is_empty() {
|
||||
return Err(ParsePersonError::NoName);
|
||||
}
|
||||
|
||||
let age = age.parse().map_err(ParsePersonError::ParseInt)?;
|
||||
|
||||
Ok(Self {
|
||||
name: name.into(),
|
||||
age,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let p = "Mark,20".parse::<Person>();
|
||||
println!("{p:?}");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ParsePersonError::*;
|
||||
|
||||
#[test]
|
||||
fn empty_input() {
|
||||
assert_eq!("".parse::<Person>(), Err(BadLen));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn good_input() {
|
||||
let p = "John,32".parse::<Person>();
|
||||
assert!(p.is_ok());
|
||||
let p = p.unwrap();
|
||||
assert_eq!(p.name, "John");
|
||||
assert_eq!(p.age, 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_age() {
|
||||
assert!(matches!("John,".parse::<Person>(), Err(ParseInt(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_age() {
|
||||
assert!(matches!("John,twenty".parse::<Person>(), Err(ParseInt(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_comma_and_age() {
|
||||
assert_eq!("John".parse::<Person>(), Err(BadLen));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_name() {
|
||||
assert_eq!(",1".parse::<Person>(), Err(NoName));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_name_and_age() {
|
||||
assert!(matches!(",".parse::<Person>(), Err(NoName | ParseInt(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_name_and_invalid_age() {
|
||||
assert!(matches!(
|
||||
",one".parse::<Person>(),
|
||||
Err(NoName | ParseInt(_)),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_comma() {
|
||||
assert_eq!("John,32,".parse::<Person>(), Err(BadLen));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_comma_and_some_string() {
|
||||
assert_eq!("John,32,man".parse::<Person>(), Err(BadLen));
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,193 +1,4 @@
|
||||
// `TryFrom` is a simple and safe type conversion that may fail in a controlled
|
||||
// way under some circumstances. Basically, this is the same as `From`. The main
|
||||
// difference is that this should return a `Result` type instead of the target
|
||||
// type itself. You can read more about it in the documentation:
|
||||
// https://doc.rust-lang.org/std/convert/trait.TryFrom.html
|
||||
|
||||
#![allow(clippy::useless_vec)]
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Color {
|
||||
red: u8,
|
||||
green: u8,
|
||||
blue: u8,
|
||||
}
|
||||
|
||||
// We will use this error type for the `TryFrom` conversions.
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum IntoColorError {
|
||||
// Incorrect length of slice
|
||||
BadLen,
|
||||
// Integer conversion error
|
||||
IntConversion,
|
||||
}
|
||||
|
||||
impl TryFrom<(i16, i16, i16)> for Color {
|
||||
type Error = IntoColorError;
|
||||
|
||||
fn try_from(tuple: (i16, i16, i16)) -> Result<Self, Self::Error> {
|
||||
let (Ok(red), Ok(green), Ok(blue)) = (
|
||||
u8::try_from(tuple.0),
|
||||
u8::try_from(tuple.1),
|
||||
u8::try_from(tuple.2),
|
||||
) else {
|
||||
return Err(IntoColorError::IntConversion);
|
||||
};
|
||||
|
||||
Ok(Self { red, green, blue })
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<[i16; 3]> for Color {
|
||||
type Error = IntoColorError;
|
||||
|
||||
fn try_from(arr: [i16; 3]) -> Result<Self, Self::Error> {
|
||||
// Reuse the implementation for a tuple.
|
||||
Self::try_from((arr[0], arr[1], arr[2]))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[i16]> for Color {
|
||||
type Error = IntoColorError;
|
||||
|
||||
fn try_from(slice: &[i16]) -> Result<Self, Self::Error> {
|
||||
// Check the length.
|
||||
if slice.len() != 3 {
|
||||
return Err(IntoColorError::BadLen);
|
||||
}
|
||||
|
||||
// Reuse the implementation for a tuple.
|
||||
Self::try_from((slice[0], slice[1], slice[2]))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Using the `try_from` function.
|
||||
let c1 = Color::try_from((183, 65, 14));
|
||||
println!("{c1:?}");
|
||||
|
||||
// Since `TryFrom` is implemented for `Color`, we can use `TryInto`.
|
||||
let c2: Result<Color, _> = [183, 65, 14].try_into();
|
||||
println!("{c2:?}");
|
||||
|
||||
let v = vec![183, 65, 14];
|
||||
// With slice we should use the `try_from` function
|
||||
let c3 = Color::try_from(&v[..]);
|
||||
println!("{c3:?}");
|
||||
// or put the slice within round brackets and use `try_into`.
|
||||
let c4: Result<Color, _> = (&v[..]).try_into();
|
||||
println!("{c4:?}");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use IntoColorError::*;
|
||||
|
||||
#[test]
|
||||
fn test_tuple_out_of_range_positive() {
|
||||
assert_eq!(Color::try_from((256, 1000, 10000)), Err(IntConversion));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tuple_out_of_range_negative() {
|
||||
assert_eq!(Color::try_from((-1, -10, -256)), Err(IntConversion));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tuple_sum() {
|
||||
assert_eq!(Color::try_from((-1, 255, 255)), Err(IntConversion));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tuple_correct() {
|
||||
let c: Result<Color, _> = (183, 65, 14).try_into();
|
||||
assert!(c.is_ok());
|
||||
assert_eq!(
|
||||
c.unwrap(),
|
||||
Color {
|
||||
red: 183,
|
||||
green: 65,
|
||||
blue: 14,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_array_out_of_range_positive() {
|
||||
let c: Result<Color, _> = [1000, 10000, 256].try_into();
|
||||
assert_eq!(c, Err(IntConversion));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_array_out_of_range_negative() {
|
||||
let c: Result<Color, _> = [-10, -256, -1].try_into();
|
||||
assert_eq!(c, Err(IntConversion));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_array_sum() {
|
||||
let c: Result<Color, _> = [-1, 255, 255].try_into();
|
||||
assert_eq!(c, Err(IntConversion));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_array_correct() {
|
||||
let c: Result<Color, _> = [183, 65, 14].try_into();
|
||||
assert!(c.is_ok());
|
||||
assert_eq!(
|
||||
c.unwrap(),
|
||||
Color {
|
||||
red: 183,
|
||||
green: 65,
|
||||
blue: 14
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slice_out_of_range_positive() {
|
||||
let arr = [10000, 256, 1000];
|
||||
assert_eq!(Color::try_from(&arr[..]), Err(IntConversion));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slice_out_of_range_negative() {
|
||||
let arr = [-256, -1, -10];
|
||||
assert_eq!(Color::try_from(&arr[..]), Err(IntConversion));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slice_sum() {
|
||||
let arr = [-1, 255, 255];
|
||||
assert_eq!(Color::try_from(&arr[..]), Err(IntConversion));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slice_correct() {
|
||||
let v = vec![183, 65, 14];
|
||||
let c: Result<Color, _> = Color::try_from(&v[..]);
|
||||
assert!(c.is_ok());
|
||||
assert_eq!(
|
||||
c.unwrap(),
|
||||
Color {
|
||||
red: 183,
|
||||
green: 65,
|
||||
blue: 14,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slice_excess_length() {
|
||||
let v = vec![0, 0, 0, 0];
|
||||
assert_eq!(Color::try_from(&v[..]), Err(BadLen));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slice_insufficient_length() {
|
||||
let v = vec![0, 0];
|
||||
assert_eq!(Color::try_from(&v[..]), Err(BadLen));
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -1,24 +1,4 @@
|
||||
// Type casting in Rust is done via the usage of the `as` operator.
|
||||
// Note that the `as` operator is not only used when type casting. It also helps
|
||||
// with renaming imports.
|
||||
|
||||
fn average(values: &[f64]) -> f64 {
|
||||
let total = values.iter().sum::<f64>();
|
||||
total / values.len() as f64
|
||||
// ^^^^^^
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let values = [3.5, 0.3, 13.0, 11.7];
|
||||
println!("{}", average(&values));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn returns_proper_type_and_value() {
|
||||
assert_eq!(average(&[3.5, 0.3, 13.0, 11.7]), 7.125);
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
@ -62,8 +62,8 @@ mod tests {
|
||||
// Import `transformer`.
|
||||
use super::my_module::transformer;
|
||||
|
||||
use super::Command;
|
||||
use super::my_module::transformer_iter;
|
||||
use super::Command;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
|
||||
@ -1,65 +1,4 @@
|
||||
// An imaginary magical school has a new report card generation system written
|
||||
// in Rust! Currently, the system only supports creating report cards where the
|
||||
// student's grade is represented numerically (e.g. 1.0 -> 5.5). However, the
|
||||
// school also issues alphabetical grades (A+ -> F-) and needs to be able to
|
||||
// print both types of report card!
|
||||
//
|
||||
// Make the necessary code changes in the struct `ReportCard` and the impl
|
||||
// block to support alphabetical report cards in addition to numerical ones.
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
// Make the struct generic over `T`.
|
||||
struct ReportCard<T> {
|
||||
// ^^^
|
||||
grade: T,
|
||||
// ^
|
||||
student_name: String,
|
||||
student_age: u8,
|
||||
}
|
||||
|
||||
// To be able to print the grade, it has to implement the `Display` trait.
|
||||
impl<T: Display> ReportCard<T> {
|
||||
// ^^^^^^^ require that `T` implements `Display`.
|
||||
fn print(&self) -> String {
|
||||
format!(
|
||||
"{} ({}) - achieved a grade of {}",
|
||||
&self.student_name, &self.student_age, &self.grade,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// You can optionally experiment here.
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn generate_numeric_report_card() {
|
||||
let report_card = ReportCard {
|
||||
grade: 2.1,
|
||||
student_name: "Tom Wriggle".to_string(),
|
||||
student_age: 12,
|
||||
};
|
||||
assert_eq!(
|
||||
report_card.print(),
|
||||
"Tom Wriggle (12) - achieved a grade of 2.1",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_alphabetic_report_card() {
|
||||
let report_card = ReportCard {
|
||||
grade: "A+",
|
||||
student_name: "Gary Plotter".to_string(),
|
||||
student_age: 11,
|
||||
};
|
||||
assert_eq!(
|
||||
report_card.print(),
|
||||
"Gary Plotter (11) - achieved a grade of A+",
|
||||
);
|
||||
}
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
|
||||
650
src/app_state.rs
650
src/app_state.rs
@ -1,650 +0,0 @@
|
||||
use anyhow::{Context, Error, Result, bail};
|
||||
use crossterm::{QueueableCommand, cursor, terminal};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
env,
|
||||
fs::{File, OpenOptions},
|
||||
io::{Read, Seek, StdoutLock, Write},
|
||||
path::{MAIN_SEPARATOR_STR, Path},
|
||||
process::{Command, Stdio},
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering::Relaxed},
|
||||
mpsc,
|
||||
},
|
||||
thread,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
clear_terminal,
|
||||
cmd::CmdRunner,
|
||||
embedded::EMBEDDED_FILES,
|
||||
exercise::{Exercise, RunnableExercise},
|
||||
info_file::ExerciseInfo,
|
||||
term::{self, CheckProgressVisualizer},
|
||||
};
|
||||
|
||||
const STATE_FILE_NAME: &str = ".rustlings-state.txt";
|
||||
const DEFAULT_CHECK_PARALLELISM: usize = 8;
|
||||
|
||||
#[must_use]
|
||||
pub enum ExercisesProgress {
|
||||
// All exercises are done.
|
||||
AllDone,
|
||||
// A new exercise is now pending.
|
||||
NewPending,
|
||||
// The current exercise is still pending.
|
||||
CurrentPending,
|
||||
}
|
||||
|
||||
pub enum StateFileStatus {
|
||||
Read,
|
||||
NotRead,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum CheckProgress {
|
||||
None,
|
||||
Checking,
|
||||
Done,
|
||||
Pending,
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
current_exercise_ind: usize,
|
||||
exercises: Vec<Exercise>,
|
||||
// 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,
|
||||
cmd_runner: CmdRunner,
|
||||
// Running in VS Code.
|
||||
vs_code: bool,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(
|
||||
exercise_infos: Vec<ExerciseInfo>,
|
||||
final_message: String,
|
||||
) -> Result<(Self, StateFileStatus)> {
|
||||
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 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`.
|
||||
// Leaking is not a problem because the `AppState` instance lives until
|
||||
// the end of the program.
|
||||
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 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 below.
|
||||
done: false,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn current_exercise_ind(&self) -> usize {
|
||||
self.current_exercise_ind
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn exercises(&self) -> &[Exercise] {
|
||||
&self.exercises
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn n_done(&self) -> u16 {
|
||||
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 cmd_runner(&self) -> &CmdRunner {
|
||||
&self.cmd_runner
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn vs_code(&self) -> bool {
|
||||
self.vs_code
|
||||
}
|
||||
|
||||
// Write the state file.
|
||||
// The file's format is very simple:
|
||||
// - The first line is a comment.
|
||||
// - The second line is an empty line.
|
||||
// - The third line is the name of the current exercise. It must end with `\n` even if there
|
||||
// are no done exercises.
|
||||
// - The fourth line is an empty line.
|
||||
// - All remaining lines are the names of done exercises.
|
||||
fn write(&mut self) -> Result<()> {
|
||||
self.file_buf.truncate(STATE_FILE_HEADER.len());
|
||||
|
||||
self.file_buf
|
||||
.extend_from_slice(self.current_exercise().name.as_bytes());
|
||||
self.file_buf.push(b'\n');
|
||||
|
||||
for exercise in &self.exercises {
|
||||
if exercise.done {
|
||||
self.file_buf.push(b'\n');
|
||||
self.file_buf.extend_from_slice(exercise.name.as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
pub fn set_current_exercise_ind(&mut self, exercise_ind: usize) -> Result<()> {
|
||||
if exercise_ind == self.current_exercise_ind {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if exercise_ind >= self.exercises.len() {
|
||||
bail!(BAD_INDEX_ERR);
|
||||
}
|
||||
|
||||
self.current_exercise_ind = exercise_ind;
|
||||
|
||||
self.write()
|
||||
}
|
||||
|
||||
pub fn set_current_exercise_by_name(&mut self, name: &str) -> Result<()> {
|
||||
// O(N) is fine since this method is used only once until the program exits.
|
||||
// Building a hashmap would have more overhead.
|
||||
self.current_exercise_ind = self
|
||||
.exercises
|
||||
.iter()
|
||||
.position(|exercise| exercise.name == name)
|
||||
.with_context(|| format!("No exercise found for '{name}'!"))?;
|
||||
|
||||
self.write()
|
||||
}
|
||||
|
||||
// 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 == 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()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Official exercises: Dump the original file from the binary.
|
||||
// Community exercises: Reset the exercise file with `git stash`.
|
||||
fn reset(&self, exercise_ind: usize, path: &str) -> Result<()> {
|
||||
if self.official_exercises {
|
||||
return EMBEDDED_FILES
|
||||
.write_exercise_to_disk(exercise_ind, path)
|
||||
.with_context(|| format!("Failed to reset the exercise {path}"));
|
||||
}
|
||||
|
||||
let output = Command::new("git")
|
||||
.arg("stash")
|
||||
.arg("push")
|
||||
.arg("--")
|
||||
.arg(path)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.output()
|
||||
.with_context(|| format!("Failed to run `git stash push -- {path}`"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
bail!(
|
||||
"`git stash push -- {path}` didn't run successfully: {}",
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reset_current_exercise(&mut self) -> Result<&'static str> {
|
||||
self.set_pending(self.current_exercise_ind)?;
|
||||
let exercise = self.current_exercise();
|
||||
self.reset(self.current_exercise_ind, exercise.path)?;
|
||||
|
||||
Ok(exercise.path)
|
||||
}
|
||||
|
||||
// Reset the exercise by index and return its name.
|
||||
pub fn reset_exercise_by_ind(&mut self, exercise_ind: usize) -> Result<&'static str> {
|
||||
if exercise_ind >= self.exercises.len() {
|
||||
bail!(BAD_INDEX_ERR);
|
||||
}
|
||||
|
||||
self.set_pending(exercise_ind)?;
|
||||
let exercise = &self.exercises[exercise_ind];
|
||||
self.reset(exercise_ind, 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> {
|
||||
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 from the binary and return its path.
|
||||
/// Community exercises: Check if a solution file exists and return its path in that case.
|
||||
pub fn current_solution_path(&self) -> Result<Option<String>> {
|
||||
if cfg!(debug_assertions) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let current_exercise = self.current_exercise();
|
||||
|
||||
if self.official_exercises {
|
||||
EMBEDDED_FILES
|
||||
.write_solution_to_disk(self.current_exercise_ind, current_exercise.name)
|
||||
.map(Some)
|
||||
} else {
|
||||
let sol_path = current_exercise.sol_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<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;
|
||||
self.n_done += 1;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 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! |
|
||||
+-------------------------- ------------------------+
|
||||
\\/\x1b[31m
|
||||
▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒
|
||||
▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒
|
||||
▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒
|
||||
░░▒▒▒▒░░▒▒ ▒▒ ▒▒ ▒▒ ▒▒░░▒▒▒▒
|
||||
▓▓▓▓▓▓▓▓ ▓▓ ▓▓██ ▓▓ ▓▓██ ▓▓ ▓▓▓▓▓▓▓▓
|
||||
▒▒▒▒ ▒▒ ████ ▒▒ ████ ▒▒░░ ▒▒▒▒
|
||||
▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒
|
||||
▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒
|
||||
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
|
||||
▒▒▒▒▒▒▒▒▒▒██▒▒▒▒▒▒██▒▒▒▒▒▒▒▒▒▒
|
||||
▒▒ ▒▒▒▒▒▒▒▒▒▒██████▒▒▒▒▒▒▒▒▒▒ ▒▒
|
||||
▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒
|
||||
▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ▒▒
|
||||
▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒
|
||||
▒▒ ▒▒ ▒▒ ▒▒\x1b[0m
|
||||
|
||||
";
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn dummy_exercise() -> Exercise {
|
||||
Exercise {
|
||||
dir: None,
|
||||
name: "0",
|
||||
path: "exercises/0.rs",
|
||||
canonical_path: None,
|
||||
test: false,
|
||||
strict_clippy: false,
|
||||
hint: "",
|
||||
done: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_pending_exercise() {
|
||||
let mut app_state = AppState {
|
||||
current_exercise_ind: 0,
|
||||
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,
|
||||
cmd_runner: CmdRunner::build().unwrap(),
|
||||
vs_code: false,
|
||||
};
|
||||
|
||||
let mut assert = |done: [bool; 3], expected: [Option<usize>; 3]| {
|
||||
for (exercise, done) in app_state.exercises.iter_mut().zip(done) {
|
||||
exercise.done = done;
|
||||
}
|
||||
for (ind, expected) in expected.into_iter().enumerate() {
|
||||
app_state.current_exercise_ind = ind;
|
||||
assert_eq!(
|
||||
app_state.next_pending_exercise_ind(),
|
||||
expected,
|
||||
"done={done:?}, ind={ind}",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
assert([true, true, true], [None, None, None]);
|
||||
assert([false, false, false], [Some(1), Some(2), Some(0)]);
|
||||
assert([false, true, true], [None, Some(0), Some(0)]);
|
||||
assert([true, false, true], [Some(1), None, Some(1)]);
|
||||
assert([true, true, false], [Some(2), Some(2), None]);
|
||||
assert([true, false, false], [Some(1), Some(2), Some(1)]);
|
||||
assert([false, true, false], [Some(2), Some(2), Some(0)]);
|
||||
assert([false, false, true], [Some(1), Some(0), Some(0)]);
|
||||
}
|
||||
}
|
||||
@ -1,153 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{exercise::RunnableExercise, info_file::ExerciseInfo};
|
||||
|
||||
/// Initial capacity of the bins buffer.
|
||||
pub const BINS_BUFFER_CAPACITY: usize = 1 << 14;
|
||||
|
||||
/// Return the start and end index of the content of the list `bin = […]`.
|
||||
/// bin = [xxxxxxxxxxxxxxxxx]
|
||||
/// |start_ind |
|
||||
/// |end_ind
|
||||
pub fn bins_start_end_ind(cargo_toml: &str) -> Result<(usize, usize)> {
|
||||
let start_ind = cargo_toml
|
||||
.find("bin = [")
|
||||
.context("Failed to find the start of the `bin` list (`bin = [`)")?
|
||||
+ 7;
|
||||
let end_ind = start_ind
|
||||
+ cargo_toml
|
||||
.get(start_ind..)
|
||||
.and_then(|slice| slice.as_bytes().iter().position(|c| *c == b']'))
|
||||
.context("Failed to find the end of the `bin` list (`]`)")?;
|
||||
|
||||
Ok((start_ind, end_ind))
|
||||
}
|
||||
|
||||
/// Generate and append the content of the `bin` list in `Cargo.toml`.
|
||||
/// The `exercise_path_prefix` is the prefix of the `path` field of every list entry.
|
||||
pub fn append_bins(
|
||||
buf: &mut Vec<u8>,
|
||||
exercise_infos: &[ExerciseInfo],
|
||||
exercise_path_prefix: &[u8],
|
||||
) {
|
||||
buf.push(b'\n');
|
||||
for exercise_info in exercise_infos {
|
||||
buf.extend_from_slice(b" { name = \"");
|
||||
buf.extend_from_slice(exercise_info.name.as_bytes());
|
||||
buf.extend_from_slice(b"\", path = \"");
|
||||
buf.extend_from_slice(exercise_path_prefix);
|
||||
buf.extend_from_slice(b"exercises/");
|
||||
if let Some(dir) = &exercise_info.dir {
|
||||
buf.extend_from_slice(dir.as_bytes());
|
||||
buf.push(b'/');
|
||||
}
|
||||
buf.extend_from_slice(exercise_info.name.as_bytes());
|
||||
buf.extend_from_slice(b".rs\" },\n");
|
||||
|
||||
let sol_path = exercise_info.sol_path();
|
||||
if !Path::new(&sol_path).exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
buf.extend_from_slice(b" { name = \"");
|
||||
buf.extend_from_slice(exercise_info.name.as_bytes());
|
||||
buf.extend_from_slice(b"_sol");
|
||||
buf.extend_from_slice(b"\", path = \"");
|
||||
buf.extend_from_slice(exercise_path_prefix);
|
||||
buf.extend_from_slice(b"solutions/");
|
||||
if let Some(dir) = &exercise_info.dir {
|
||||
buf.extend_from_slice(dir.as_bytes());
|
||||
buf.push(b'/');
|
||||
}
|
||||
buf.extend_from_slice(exercise_info.name.as_bytes());
|
||||
buf.extend_from_slice(b".rs\" },\n");
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the `bin` list and leave everything else unchanged.
|
||||
pub fn updated_cargo_toml(
|
||||
exercise_infos: &[ExerciseInfo],
|
||||
current_cargo_toml: &str,
|
||||
exercise_path_prefix: &[u8],
|
||||
) -> Result<Vec<u8>> {
|
||||
let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?;
|
||||
|
||||
let mut updated_cargo_toml = Vec::with_capacity(BINS_BUFFER_CAPACITY);
|
||||
updated_cargo_toml.extend_from_slice(¤t_cargo_toml.as_bytes()[..bins_start_ind]);
|
||||
append_bins(
|
||||
&mut updated_cargo_toml,
|
||||
exercise_infos,
|
||||
exercise_path_prefix,
|
||||
);
|
||||
updated_cargo_toml.extend_from_slice(¤t_cargo_toml.as_bytes()[bins_end_ind..]);
|
||||
|
||||
Ok(updated_cargo_toml)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_bins_start_end_ind() {
|
||||
assert_eq!(bins_start_end_ind("").ok(), None);
|
||||
assert_eq!(bins_start_end_ind("[]").ok(), None);
|
||||
assert_eq!(bins_start_end_ind("bin = [").ok(), None);
|
||||
assert_eq!(bins_start_end_ind("bin = ]").ok(), None);
|
||||
assert_eq!(bins_start_end_ind("bin = []").ok(), Some((7, 7)));
|
||||
assert_eq!(bins_start_end_ind("bin= []").ok(), None);
|
||||
assert_eq!(bins_start_end_ind("bin =[]").ok(), None);
|
||||
assert_eq!(bins_start_end_ind("bin=[]").ok(), None);
|
||||
assert_eq!(bins_start_end_ind("bin = [\nxxx\n]").ok(), Some((7, 12)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bins() {
|
||||
let exercise_infos = [
|
||||
ExerciseInfo {
|
||||
name: String::from("1"),
|
||||
dir: None,
|
||||
test: true,
|
||||
strict_clippy: true,
|
||||
hint: String::new(),
|
||||
skip_check_unsolved: false,
|
||||
},
|
||||
ExerciseInfo {
|
||||
name: String::from("2"),
|
||||
dir: Some(String::from("d")),
|
||||
test: false,
|
||||
strict_clippy: false,
|
||||
hint: String::new(),
|
||||
skip_check_unsolved: false,
|
||||
},
|
||||
];
|
||||
|
||||
let mut buf = Vec::with_capacity(128);
|
||||
append_bins(&mut buf, &exercise_infos, b"");
|
||||
assert_eq!(
|
||||
buf,
|
||||
br#"
|
||||
{ name = "1", path = "exercises/1.rs" },
|
||||
{ name = "2", path = "exercises/d/2.rs" },
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
updated_cargo_toml(
|
||||
&exercise_infos,
|
||||
"abc\n\
|
||||
bin = [xxx]\n\
|
||||
123",
|
||||
b"../"
|
||||
)
|
||||
.unwrap(),
|
||||
br#"abc
|
||||
bin = [
|
||||
{ name = "1", path = "../exercises/1.rs" },
|
||||
{ name = "2", path = "../exercises/d/2.rs" },
|
||||
]
|
||||
123"#,
|
||||
);
|
||||
}
|
||||
}
|
||||
163
src/cmd.rs
163
src/cmd.rs
@ -1,163 +0,0 @@
|
||||
use anyhow::{Context, Result, bail};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
io::{Read, pipe},
|
||||
path::PathBuf,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
/// Run a command with a description for a possible error and append the merged stdout and stderr.
|
||||
/// The boolean in the returned `Result` is true if the command's exit status is success.
|
||||
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 mut handle = if let Some(output) = output {
|
||||
let (mut reader, writer) = pipe().with_context(|| {
|
||||
format!("Failed to create a pipe to run the command `{description}``")
|
||||
})?;
|
||||
|
||||
let writer_clone = writer.try_clone().with_context(|| {
|
||||
format!("Failed to clone the pipe writer for the command `{description}`")
|
||||
})?;
|
||||
|
||||
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}`"))?;
|
||||
|
||||
output.push(b'\n');
|
||||
|
||||
handle
|
||||
} else {
|
||||
cmd.stdout(Stdio::null()).stderr(Stdio::null());
|
||||
spawn(cmd)?
|
||||
};
|
||||
|
||||
handle
|
||||
.wait()
|
||||
.with_context(|| format!("Failed to wait on the command `{description}` to exit"))
|
||||
.map(|status| status.success())
|
||||
}
|
||||
|
||||
// Parses parts of the output of `cargo metadata`.
|
||||
#[derive(Deserialize)]
|
||||
struct CargoMetadata {
|
||||
target_directory: PathBuf,
|
||||
}
|
||||
|
||||
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(subcommand).arg("-q").arg("--bin").arg(bin_name);
|
||||
|
||||
// A hack to make `cargo run` work when developing Rustlings.
|
||||
#[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");
|
||||
}
|
||||
|
||||
CargoSubcommand { cmd, output }
|
||||
}
|
||||
|
||||
/// 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(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::*;
|
||||
|
||||
#[test]
|
||||
fn test_run_cmd() {
|
||||
let mut cmd = Command::new("echo");
|
||||
cmd.arg("Hello");
|
||||
|
||||
let mut output = Vec::with_capacity(8);
|
||||
run_cmd(cmd, "echo …", Some(&mut output)).unwrap();
|
||||
|
||||
assert_eq!(output, b"Hello\n\n");
|
||||
}
|
||||
}
|
||||
46
src/dev.rs
46
src/dev.rs
@ -1,46 +0,0 @@
|
||||
use anyhow::{Context, Result, bail};
|
||||
use clap::Subcommand;
|
||||
use std::path::PathBuf;
|
||||
|
||||
mod check;
|
||||
mod new;
|
||||
mod update;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum DevCommands {
|
||||
/// Create a new project for community exercises
|
||||
New {
|
||||
/// The path to create the project in
|
||||
path: PathBuf,
|
||||
/// Don't try to initialize a Git repository in the project directory
|
||||
#[arg(long)]
|
||||
no_git: bool,
|
||||
},
|
||||
/// Run checks on the exercises
|
||||
Check {
|
||||
/// Require that every exercise has a solution
|
||||
#[arg(short, long)]
|
||||
require_solutions: bool,
|
||||
},
|
||||
/// Update the `Cargo.toml` file for the exercises
|
||||
Update,
|
||||
}
|
||||
|
||||
impl DevCommands {
|
||||
pub fn run(self) -> Result<()> {
|
||||
match self {
|
||||
Self::New { path, no_git } => {
|
||||
if cfg!(debug_assertions) {
|
||||
bail!("Disabled in the debug build");
|
||||
}
|
||||
|
||||
new::new(&path, no_git).context(INIT_ERR)
|
||||
}
|
||||
Self::Check { require_solutions } => check::check(require_solutions),
|
||||
Self::Update => update::update(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const INIT_ERR: &str = "Initialization failed.
|
||||
After resolving the issue, delete the `rustlings` directory (if it was created) and try again";
|
||||
398
src/dev/check.rs
398
src/dev/check.rs
@ -1,398 +0,0 @@
|
||||
use anyhow::{Context, Error, Result, anyhow, bail};
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
collections::HashSet,
|
||||
fs::{self, OpenOptions, read_dir},
|
||||
io::{self, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Stdio},
|
||||
thread,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
CURRENT_FORMAT_VERSION,
|
||||
cargo_toml::{BINS_BUFFER_CAPACITY, append_bins, bins_start_end_ind},
|
||||
cmd::CmdRunner,
|
||||
exercise::{OUTPUT_CAPACITY, RunnableExercise},
|
||||
info_file::{ExerciseInfo, InfoFile},
|
||||
term::ProgressCounter,
|
||||
};
|
||||
|
||||
const MAX_N_EXERCISES: usize = 999;
|
||||
const MAX_EXERCISE_NAME_LEN: usize = 32;
|
||||
|
||||
// Find a char that isn't allowed in the exercise's `name` or `dir`.
|
||||
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.
|
||||
fn check_cargo_toml(
|
||||
exercise_infos: &[ExerciseInfo],
|
||||
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 (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 cfg!(debug_assertions) {
|
||||
bail!(
|
||||
"The file `dev/Cargo.toml` is outdated. Run `cargo dev update` to update it. Then run `cargo run -- dev check` again"
|
||||
);
|
||||
}
|
||||
|
||||
bail!(
|
||||
"The file `Cargo.toml` is outdated. 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<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 {
|
||||
let name = exercise_info.name.as_str();
|
||||
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");
|
||||
}
|
||||
|
||||
if let Some(dir) = &exercise_info.dir {
|
||||
if dir.is_empty() {
|
||||
bail!("The exercise `{name}` has an empty dir name in `info.toml`");
|
||||
}
|
||||
if let Some(c) = forbidden_char(dir) {
|
||||
bail!("Char `{c}` in the exercise dir `{dir}` is not allowed");
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
||||
if !names.insert(name) {
|
||||
bail!("The exercise name `{name}` is duplicated. Exercise names must all be unique");
|
||||
}
|
||||
|
||||
let path = exercise_info.path();
|
||||
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
.open(&path)
|
||||
.with_context(|| format!("Failed to open the file {path}"))?
|
||||
.read_to_string(&mut file_buf)
|
||||
.with_context(|| format!("Failed to read the file {path}"))?;
|
||||
|
||||
if !file_buf.contains("fn main()") {
|
||||
bail!(
|
||||
"The `main` function is missing in the file `{path}`.\n\
|
||||
Create at least an empty `main` function to avoid language server errors"
|
||||
);
|
||||
}
|
||||
|
||||
if !file_buf.contains("// TODO") {
|
||||
bail!(
|
||||
"Didn't find any `// TODO` comment in the file `{path}`.\n\
|
||||
You need to have at least one such comment to guide the user."
|
||||
);
|
||||
}
|
||||
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
||||
file_buf.clear();
|
||||
|
||||
paths.insert(PathBuf::from(path));
|
||||
}
|
||||
|
||||
Ok(paths)
|
||||
}
|
||||
|
||||
// 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: &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()
|
||||
)
|
||||
};
|
||||
|
||||
for entry in read_dir(dir).with_context(|| format!("Failed to open the `{dir}` directory"))? {
|
||||
let entry = entry.with_context(|| format!("Failed to read the `{dir}` directory"))?;
|
||||
|
||||
if entry.file_type().unwrap().is_file() {
|
||||
let path = entry.path();
|
||||
let file_name = path.file_name().unwrap();
|
||||
if file_name == "README.md" {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !allowed_rust_files.contains(&path) {
|
||||
return Err(unexpected_file(&path));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
let dir_path = entry.path();
|
||||
for entry in read_dir(&dir_path)
|
||||
.with_context(|| format!("Failed to open the directory {}", dir_path.display()))?
|
||||
{
|
||||
let entry = entry
|
||||
.with_context(|| format!("Failed to read the directory {}", dir_path.display()))?;
|
||||
let path = entry.path();
|
||||
|
||||
if !entry.file_type().unwrap().is_file() {
|
||||
bail!(
|
||||
"Found `{}` but expected only files. Only one level of exercise nesting is allowed",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
|
||||
let file_name = path.file_name().unwrap();
|
||||
if file_name == "README.md" {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !allowed_rust_files.contains(&path) {
|
||||
return Err(unexpected_file(&path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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")?;
|
||||
|
||||
let handles = info_file
|
||||
.exercises
|
||||
.iter()
|
||||
.filter_map(|exercise_info| {
|
||||
if exercise_info.skip_check_unsolved {
|
||||
return None;
|
||||
}
|
||||
|
||||
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 progress_counter = ProgressCounter::new(&mut stdout, handles.len())?;
|
||||
|
||||
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),
|
||||
}
|
||||
|
||||
progress_counter.increment()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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)\n\
|
||||
Please migrate to the latest format version"
|
||||
),
|
||||
Ordering::Greater => bail!(
|
||||
"`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\n\
|
||||
Try updating the Rustlings program"
|
||||
),
|
||||
Ordering::Equal => (),
|
||||
}
|
||||
|
||||
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)?;
|
||||
|
||||
handle.join().unwrap()
|
||||
}
|
||||
|
||||
enum SolutionCheck {
|
||||
Success { sol_path: String },
|
||||
MissingOptional,
|
||||
RunFailure { output: Vec<u8> },
|
||||
Err(Error),
|
||||
}
|
||||
|
||||
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 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 {
|
||||
return SolutionCheck::Err(anyhow!(
|
||||
"The solution of the exercise {} is missing",
|
||||
exercise_info.name,
|
||||
));
|
||||
}
|
||||
|
||||
return SolutionCheck::MissingOptional;
|
||||
}
|
||||
|
||||
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
|
||||
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")?;
|
||||
|
||||
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("2024")
|
||||
.arg("--color")
|
||||
.arg("always")
|
||||
.stdin(Stdio::null());
|
||||
|
||||
let mut progress_counter = ProgressCounter::new(&mut stdout, handles.len())?;
|
||||
|
||||
for (exercise_info, handle) in info_file.exercises.iter().zip(handles) {
|
||||
let Ok(check_result) = handle.join() else {
|
||||
bail!(
|
||||
"Panic while trying to run the solution of the exercise {}",
|
||||
exercise_info.name,
|
||||
);
|
||||
};
|
||||
|
||||
match check_result {
|
||||
SolutionCheck::Success { sol_path } => {
|
||||
fmt_cmd.arg(&sol_path);
|
||||
sol_paths.insert(PathBuf::from(sol_path));
|
||||
}
|
||||
SolutionCheck::MissingOptional => (),
|
||||
SolutionCheck::RunFailure { output } => {
|
||||
drop(progress_counter);
|
||||
stdout.write_all(&output)?;
|
||||
bail!(
|
||||
"Running the solution of the exercise {} failed with the error above",
|
||||
exercise_info.name,
|
||||
);
|
||||
}
|
||||
SolutionCheck::Err(e) => return Err(e),
|
||||
}
|
||||
|
||||
progress_counter.increment()?;
|
||||
}
|
||||
|
||||
let n_solutions = sol_paths.len();
|
||||
let handle = thread::Builder::new()
|
||||
.spawn(move || check_unexpected_files("solutions", &sol_paths))
|
||||
.context(
|
||||
"Failed to spawn a thread to check for unexpected files in the solutions directory",
|
||||
)?;
|
||||
|
||||
if n_solutions > 0
|
||||
&& !fmt_cmd
|
||||
.status()
|
||||
.context("Failed to run `rustfmt` on all solution files")?
|
||||
.success()
|
||||
{
|
||||
bail!("Some solutions aren't formatted. Run `rustfmt` on them");
|
||||
}
|
||||
|
||||
handle.join().unwrap()
|
||||
}
|
||||
|
||||
pub fn check(require_solutions: bool) -> Result<()> {
|
||||
let info_file = InfoFile::parse()?;
|
||||
|
||||
if info_file.exercises.len() > MAX_N_EXERCISES {
|
||||
bail!("The maximum number of exercises is {MAX_N_EXERCISES}");
|
||||
}
|
||||
|
||||
if cfg!(debug_assertions) {
|
||||
// A hack to make `cargo 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"")?;
|
||||
}
|
||||
|
||||
// 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 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";
|
||||
148
src/dev/new.rs
148
src/dev/new.rs
@ -1,148 +0,0 @@
|
||||
use anyhow::{Context, Result, bail};
|
||||
use std::{
|
||||
env::set_current_dir,
|
||||
fs::{self, create_dir},
|
||||
path::Path,
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use crate::{CURRENT_FORMAT_VERSION, init::RUST_ANALYZER_TOML};
|
||||
|
||||
// Create a directory relative to the current directory and print its path.
|
||||
fn create_rel_dir(dir_name: &str, current_dir: &str) -> Result<()> {
|
||||
create_dir(dir_name)
|
||||
.with_context(|| format!("Failed to create the directory {current_dir}/{dir_name}"))?;
|
||||
println!("Created the directory {current_dir}/{dir_name}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Write a file relative to the current directory and print its path.
|
||||
fn write_rel_file<C>(file_name: &str, current_dir: &str, content: C) -> Result<()>
|
||||
where
|
||||
C: AsRef<[u8]>,
|
||||
{
|
||||
fs::write(file_name, content)
|
||||
.with_context(|| format!("Failed to create the file {current_dir}/{file_name}"))?;
|
||||
// Space to align with `create_rel_dir`.
|
||||
println!("Created the file {current_dir}/{file_name}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn new(path: &Path, no_git: bool) -> Result<()> {
|
||||
let dir_path_str = path.to_string_lossy();
|
||||
|
||||
create_dir(path).with_context(|| format!("Failed to create the directory {dir_path_str}"))?;
|
||||
println!("Created the directory {dir_path_str}");
|
||||
|
||||
set_current_dir(path)
|
||||
.with_context(|| format!("Failed to set {dir_path_str} as the current directory"))?;
|
||||
|
||||
if !no_git
|
||||
&& !Command::new("git")
|
||||
.arg("init")
|
||||
.status()
|
||||
.context("Failed to run `git init`")?
|
||||
.success()
|
||||
{
|
||||
bail!("`git init` didn't run successfully. See the possible error message above");
|
||||
}
|
||||
|
||||
write_rel_file(".gitignore", &dir_path_str, GITIGNORE)?;
|
||||
|
||||
create_rel_dir("exercises", &dir_path_str)?;
|
||||
create_rel_dir("solutions", &dir_path_str)?;
|
||||
|
||||
write_rel_file(
|
||||
"info.toml",
|
||||
&dir_path_str,
|
||||
format!(
|
||||
"{INFO_FILE_BEFORE_FORMAT_VERSION}{CURRENT_FORMAT_VERSION}{INFO_FILE_AFTER_FORMAT_VERSION}"
|
||||
),
|
||||
)?;
|
||||
|
||||
write_rel_file("Cargo.toml", &dir_path_str, CARGO_TOML)?;
|
||||
|
||||
write_rel_file("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",
|
||||
&dir_path_str,
|
||||
crate::init::VS_CODE_EXTENSIONS_JSON,
|
||||
)?;
|
||||
|
||||
println!("\nInitialization done ✓");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub const GITIGNORE: &[u8] = b"Cargo.lock
|
||||
target/
|
||||
.vscode/
|
||||
!.vscode/extensions.json
|
||||
";
|
||||
|
||||
const INFO_FILE_BEFORE_FORMAT_VERSION: &str =
|
||||
"# The format version is an indicator of the compatibility of community exercises with the
|
||||
# Rustlings program.
|
||||
# The format version is not the same as the version of the Rustlings program.
|
||||
# In case Rustlings makes an unavoidable breaking change to the expected format of community
|
||||
# exercises, you would need to raise this version and adapt to the new format.
|
||||
# Otherwise, the newest version of the Rustlings program won't be able to run these exercises.
|
||||
format_version = ";
|
||||
|
||||
const INFO_FILE_AFTER_FORMAT_VERSION: &str = r#"
|
||||
|
||||
# Optional multi-line message to be shown to users when just starting with the exercises.
|
||||
welcome_message = """Welcome to these community Rustlings exercises."""
|
||||
|
||||
# Optional multi-line message to be shown to users after finishing all exercises.
|
||||
final_message = """We hope that you found the exercises helpful :D"""
|
||||
|
||||
# Repeat this section for every exercise.
|
||||
[[exercises]]
|
||||
# Exercise name which is the exercise file name without the `.rs` extension.
|
||||
name = "???"
|
||||
|
||||
# Optional directory name to be provided if you want to organize exercises in directories.
|
||||
# If `dir` is specified, the exercise path is `exercises/DIR/NAME.rs`
|
||||
# Otherwise, the path is `exercises/NAME.rs`
|
||||
# dir = "???"
|
||||
|
||||
# Rustlings expects the exercise to contain tests and run them.
|
||||
# You can optionally disable testing by setting `test` to `false` (the default is `true`).
|
||||
# In that case, the exercise will be considered done when it just successfully compiles.
|
||||
# test = true
|
||||
|
||||
# Rustlings will always run Clippy on exercises.
|
||||
# You can optionally set `strict_clippy` to `true` (the default is `false`) to only consider
|
||||
# the exercise as done when there are no warnings left.
|
||||
# strict_clippy = false
|
||||
|
||||
# A multi-line hint to be shown to users on request.
|
||||
hint = """???"""
|
||||
"#;
|
||||
|
||||
const CARGO_TOML: &[u8] =
|
||||
br#"# Don't edit the `bin` list manually! It is updated by `rustlings dev update`
|
||||
bin = []
|
||||
|
||||
[package]
|
||||
name = "exercises"
|
||||
edition = "2024"
|
||||
# Don't publish the exercises on crates.io!
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
"#;
|
||||
|
||||
const README: &str = "# Rustlings 🦀
|
||||
|
||||
Welcome to these community Rustlings exercises 😃
|
||||
|
||||
First, [install Rustlings using the official instructions](https://github.com/rust-lang/rustlings) ✅
|
||||
|
||||
Then, clone this repository, open a terminal in this directory and run `rustlings` to get started with the exercises 🚀
|
||||
";
|
||||
@ -1,44 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use std::fs;
|
||||
|
||||
use crate::{
|
||||
cargo_toml::updated_cargo_toml,
|
||||
info_file::{ExerciseInfo, InfoFile},
|
||||
};
|
||||
|
||||
// Update the `Cargo.toml` file.
|
||||
fn update_cargo_toml(
|
||||
exercise_infos: &[ExerciseInfo],
|
||||
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, ¤t_cargo_toml, exercise_path_prefix)?;
|
||||
|
||||
fs::write(cargo_toml_path, updated_cargo_toml)
|
||||
.context("Failed to write the `Cargo.toml` file")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update() -> Result<()> {
|
||||
let info_file = InfoFile::parse()?;
|
||||
|
||||
if cfg!(debug_assertions) {
|
||||
// A hack to make `cargo 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 {
|
||||
update_cargo_toml(&info_file.exercises, "Cargo.toml", &[])
|
||||
.context("Failed to update the file `Cargo.toml`")?;
|
||||
|
||||
println!("Updated `Cargo.toml`");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
168
src/embedded.rs
168
src/embedded.rs
@ -1,168 +0,0 @@
|
||||
use anyhow::{Context, Error, Result};
|
||||
use std::{
|
||||
fs::{self, create_dir},
|
||||
io,
|
||||
};
|
||||
|
||||
use crate::info_file::ExerciseInfo;
|
||||
|
||||
/// Contains all embedded files.
|
||||
pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!();
|
||||
|
||||
// Files related to one exercise.
|
||||
struct ExerciseFiles {
|
||||
// The content of the exercise file.
|
||||
exercise: &'static [u8],
|
||||
// The content of the solution file.
|
||||
solution: &'static [u8],
|
||||
// Index of the related `ExerciseDir` in `EmbeddedFiles::exercise_dirs`.
|
||||
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,
|
||||
readme: &'static [u8],
|
||||
}
|
||||
|
||||
impl ExerciseDir {
|
||||
fn init_on_disk(&self) -> Result<()> {
|
||||
// 20 = 10 + 10
|
||||
// exercises/ + /README.md
|
||||
let mut dir_path = String::with_capacity(20 + self.name.len());
|
||||
dir_path.push_str("exercises/");
|
||||
dir_path.push_str(self.name);
|
||||
create_dir_if_not_exists(&dir_path)?;
|
||||
|
||||
let mut readme_path = dir_path;
|
||||
readme_path.push_str("/README.md");
|
||||
|
||||
fs::write(&readme_path, self.readme)
|
||||
.with_context(|| format!("Failed to write the file {readme_path}"))
|
||||
}
|
||||
}
|
||||
|
||||
/// All embedded files.
|
||||
pub struct EmbeddedFiles {
|
||||
/// The content of the `info.toml` file.
|
||||
pub info_file: &'static str,
|
||||
exercise_files: &'static [ExerciseFiles],
|
||||
pub exercise_dirs: &'static [ExerciseDir],
|
||||
}
|
||||
|
||||
impl EmbeddedFiles {
|
||||
/// Dump all the embedded files of the `exercises/` directory.
|
||||
pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> {
|
||||
create_dir("exercises").context("Failed to create the directory `exercises`")?;
|
||||
|
||||
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) {
|
||||
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(())
|
||||
}
|
||||
|
||||
pub fn write_exercise_to_disk(&self, exercise_ind: usize, path: &str) -> Result<()> {
|
||||
let exercise_files = &self.exercise_files[exercise_ind];
|
||||
let dir = &self.exercise_dirs[exercise_files.dir_ind];
|
||||
|
||||
dir.init_on_disk()?;
|
||||
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.
|
||||
pub fn write_solution_to_disk(
|
||||
&self,
|
||||
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 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");
|
||||
|
||||
fs::write(&solution_path, exercise_files.solution)
|
||||
.with_context(|| format!("Failed to write the solution file {solution_path}"))?;
|
||||
|
||||
Ok(solution_path)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ExerciseInfo {
|
||||
dir: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct InfoFile {
|
||||
exercises: Vec<ExerciseInfo>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dirs() {
|
||||
let exercises = toml_edit::de::from_str::<InfoFile>(EMBEDDED_FILES.info_file)
|
||||
.expect("Failed to parse `info.toml`")
|
||||
.exercises;
|
||||
|
||||
assert_eq!(exercises.len(), EMBEDDED_FILES.exercise_files.len());
|
||||
|
||||
for (exercise, exercise_files) in exercises.iter().zip(EMBEDDED_FILES.exercise_files) {
|
||||
assert_eq!(
|
||||
exercise.dir,
|
||||
EMBEDDED_FILES.exercise_dirs[exercise_files.dir_ind].name,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
211
src/exercise.rs
211
src/exercise.rs
@ -1,211 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use crossterm::{
|
||||
QueueableCommand,
|
||||
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
|
||||
};
|
||||
use std::io::{self, StdoutLock, Write};
|
||||
|
||||
use crate::{
|
||||
cmd::CmdRunner,
|
||||
term::{self, CountedWrite, terminal_file_link, write_ansi},
|
||||
};
|
||||
|
||||
/// 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,
|
||||
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');
|
||||
}
|
||||
|
||||
let success = cmd_runner.run_debug_bin(bin_name, output.as_deref_mut())?;
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/// See `info_file::ExerciseInfo`
|
||||
pub struct Exercise {
|
||||
pub dir: Option<&'static str>,
|
||||
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: &'static str,
|
||||
pub done: bool,
|
||||
}
|
||||
|
||||
impl Exercise {
|
||||
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);
|
||||
}
|
||||
|
||||
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<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();
|
||||
}
|
||||
|
||||
let build_success = cmd_runner
|
||||
.cargo("build", bin_name, output.as_deref_mut())
|
||||
.run("cargo build …")?;
|
||||
if !build_success {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
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 {
|
||||
clippy_cmd.args(["--profile", "test"]);
|
||||
}
|
||||
|
||||
let clippy_success = clippy_cmd.run("cargo clippy …")?;
|
||||
let run_success = run_bin(bin_name, output, cmd_runner)?;
|
||||
|
||||
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: 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: Option<&mut Vec<u8>>, cmd_runner: &CmdRunner) -> Result<bool> {
|
||||
let name = self.name();
|
||||
let mut bin_name = String::with_capacity(name.len() + 4);
|
||||
bin_name.push_str(name);
|
||||
bin_name.push_str("_sol");
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
impl RunnableExercise for Exercise {
|
||||
#[inline]
|
||||
fn name(&self) -> &str {
|
||||
self.name
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn dir(&self) -> Option<&str> {
|
||||
self.dir
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn strict_clippy(&self) -> bool {
|
||||
self.strict_clippy
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn test(&self) -> bool {
|
||||
self.test
|
||||
}
|
||||
}
|
||||
119
src/info_file.rs
119
src/info_file.rs
@ -1,119 +0,0 @@
|
||||
use anyhow::{Context, Error, Result, bail};
|
||||
use serde::Deserialize;
|
||||
use std::{fs, io::ErrorKind};
|
||||
|
||||
use crate::{embedded::EMBEDDED_FILES, exercise::RunnableExercise};
|
||||
|
||||
/// Deserialized from the `info.toml` file.
|
||||
#[derive(Deserialize)]
|
||||
pub struct ExerciseInfo {
|
||||
/// Exercise's unique name.
|
||||
pub name: String,
|
||||
/// Exercise's directory name inside the `exercises/` directory.
|
||||
pub dir: Option<String>,
|
||||
/// Run `cargo test` on the exercise.
|
||||
#[serde(default = "default_true")]
|
||||
pub test: bool,
|
||||
/// Deny all Clippy warnings.
|
||||
#[serde(default)]
|
||||
pub strict_clippy: bool,
|
||||
/// The exercise's hint to be shown to the user on request.
|
||||
pub hint: String,
|
||||
/// The exercise is already solved. Ignore it when checking that all exercises are unsolved.
|
||||
#[serde(default)]
|
||||
pub skip_check_unsolved: bool,
|
||||
}
|
||||
#[inline(always)]
|
||||
const fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl ExerciseInfo {
|
||||
/// Path to the exercise file starting with the `exercises/` directory.
|
||||
pub fn path(&self) -> String {
|
||||
let mut path = if let Some(dir) = &self.dir {
|
||||
// 14 = 10 + 1 + 3
|
||||
// exercises/ + / + .rs
|
||||
let mut path = String::with_capacity(14 + dir.len() + self.name.len());
|
||||
path.push_str("exercises/");
|
||||
path.push_str(dir);
|
||||
path.push('/');
|
||||
path
|
||||
} else {
|
||||
// 13 = 10 + 3
|
||||
// exercises/ + .rs
|
||||
let mut path = String::with_capacity(13 + self.name.len());
|
||||
path.push_str("exercises/");
|
||||
path
|
||||
};
|
||||
|
||||
path.push_str(&self.name);
|
||||
path.push_str(".rs");
|
||||
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
impl RunnableExercise for ExerciseInfo {
|
||||
#[inline]
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn dir(&self) -> Option<&str> {
|
||||
self.dir.as_deref()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn strict_clippy(&self) -> bool {
|
||||
self.strict_clippy
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn test(&self) -> bool {
|
||||
self.test
|
||||
}
|
||||
}
|
||||
|
||||
/// The deserialized `info.toml` file.
|
||||
#[derive(Deserialize)]
|
||||
pub struct InfoFile {
|
||||
/// For possible breaking changes in the future for community exercises.
|
||||
pub format_version: u8,
|
||||
/// Shown to users when starting with the exercises.
|
||||
pub welcome_message: Option<String>,
|
||||
/// Shown to users after finishing all exercises.
|
||||
pub final_message: Option<String>,
|
||||
/// List of all exercises.
|
||||
pub exercises: Vec<ExerciseInfo>,
|
||||
}
|
||||
|
||||
impl InfoFile {
|
||||
/// Official exercises: Parse the embedded `info.toml` file.
|
||||
/// Community exercises: Parse the `info.toml` file in the current directory.
|
||||
pub fn parse() -> Result<Self> {
|
||||
// Read a local `info.toml` if it exists.
|
||||
let slf = match fs::read_to_string("info.toml") {
|
||||
Ok(file_content) => toml_edit::de::from_str::<Self>(&file_content)
|
||||
.context("Failed to parse the `info.toml` file")?,
|
||||
Err(e) => {
|
||||
if e.kind() == ErrorKind::NotFound {
|
||||
return toml_edit::de::from_str(EMBEDDED_FILES.info_file)
|
||||
.context("Failed to parse the embedded `info.toml` file");
|
||||
}
|
||||
|
||||
return Err(Error::from(e).context("Failed to read the `info.toml` file"));
|
||||
}
|
||||
};
|
||||
|
||||
if slf.exercises.is_empty() {
|
||||
bail!("{NO_EXERCISES_ERR}");
|
||||
}
|
||||
|
||||
Ok(slf)
|
||||
}
|
||||
}
|
||||
|
||||
const NO_EXERCISES_ERR: &str = "There are no exercises yet!
|
||||
Add at least one exercise before testing.";
|
||||
224
src/init.rs
224
src/init.rs
@ -1,224 +0,0 @@
|
||||
use anyhow::{Context, Result, bail};
|
||||
use crossterm::{
|
||||
QueueableCommand,
|
||||
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
env::set_current_dir,
|
||||
fs::{self, create_dir},
|
||||
io::{self, Write},
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
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<()> {
|
||||
let rustlings_dir = Path::new("rustlings");
|
||||
if rustlings_dir.exists() {
|
||||
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(
|
||||
"Failed to run the command `cargo locate-project …`\n\
|
||||
Did you already install Rust?\n\
|
||||
Try running `cargo --version` to diagnose the problem.",
|
||||
)?;
|
||||
|
||||
if !Command::new("cargo")
|
||||
.arg("clippy")
|
||||
.arg("--version")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.context("Failed to run the command `cargo clippy --version`")?
|
||||
.success()
|
||||
{
|
||||
bail!(
|
||||
"Clippy, the official Rust linter, is missing.\n\
|
||||
Please install it first before initializing Rustlings."
|
||||
)
|
||||
}
|
||||
|
||||
let mut stdout = io::stdout().lock();
|
||||
let mut init_git = true;
|
||||
|
||||
if locate_project_output.status.success() {
|
||||
if Path::new("exercises").exists() && Path::new("solutions").exists() {
|
||||
bail!(IN_INITIALIZED_DIR_ERR);
|
||||
}
|
||||
|
||||
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.\n\
|
||||
Please initialize Rustlings in a different directory"
|
||||
);
|
||||
}
|
||||
|
||||
stdout.write_all(b"This command will create the directory `rustlings/` as a member of this Cargo workspace.\n\
|
||||
Press ENTER to continue ")?;
|
||||
press_enter_prompt(&mut stdout)?;
|
||||
|
||||
// Make sure "rustlings" is added to `workspace.members` by making
|
||||
// Cargo initialize a new project.
|
||||
let status = Command::new("cargo")
|
||||
.arg("new")
|
||||
.arg("-q")
|
||||
.arg("--vcs")
|
||||
.arg("none")
|
||||
.arg("rustlings")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.status()?;
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"Failed to initialize a new Cargo workspace member.\n\
|
||||
Please initialize Rustlings in a different directory"
|
||||
);
|
||||
}
|
||||
|
||||
stdout.write_all(b"The directory `rustlings` has been added to `workspace.members` in the `Cargo.toml` file of this Cargo workspace.\n")?;
|
||||
fs::remove_dir_all("rustlings")
|
||||
.context("Failed to remove the temporary directory `rustlings/`")?;
|
||||
init_git = false;
|
||||
} else {
|
||||
stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\n\
|
||||
Press ENTER to continue ")?;
|
||||
press_enter_prompt(&mut stdout)?;
|
||||
}
|
||||
|
||||
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()?;
|
||||
EMBEDDED_FILES
|
||||
.init_exercises_dir(&info_file.exercises)
|
||||
.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/");
|
||||
dir_path.push_str(dir.name);
|
||||
create_dir(&dir_path)
|
||||
.with_context(|| format!("Failed to create the directory {dir_path}"))?;
|
||||
}
|
||||
for exercise_info in &info_file.exercises {
|
||||
let solution_path = exercise_info.sol_path();
|
||||
fs::write(&solution_path, INIT_SOLUTION_FILE)
|
||||
.with_context(|| format!("Failed to create the file {solution_path}"))?;
|
||||
}
|
||||
|
||||
let current_cargo_toml = include_str!("../dev-Cargo.toml");
|
||||
// Skip the first line (comment).
|
||||
let newline_ind = current_cargo_toml
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.position(|c| *c == b'\n')
|
||||
.context("The embedded `Cargo.toml` is empty or contains only one line")?;
|
||||
let current_cargo_toml = current_cargo_toml
|
||||
.get(newline_ind + 1..)
|
||||
.context("The embedded `Cargo.toml` contains only one line")?;
|
||||
let updated_cargo_toml = updated_cargo_toml(&info_file.exercises, current_cargo_toml, b"")
|
||||
.context("Failed to generate `Cargo.toml`")?;
|
||||
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`")?;
|
||||
|
||||
create_dir(".vscode").context("Failed to create the directory `rustlings/.vscode`")?;
|
||||
fs::write(".vscode/extensions.json", VS_CODE_EXTENSIONS_JSON)
|
||||
.context("Failed to create the file `rustlings/.vscode/extensions.json`")?;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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 INIT_SOLUTION_FILE: &[u8] = b"fn main() {
|
||||
// DON'T EDIT THIS SOLUTION FILE!
|
||||
// It will be automatically filled after you finish the exercise.
|
||||
}
|
||||
";
|
||||
|
||||
pub const RUST_ANALYZER_TOML: &[u8] = br#"check.command = "clippy"
|
||||
check.extraArgs = ["--profile", "test"]
|
||||
cargo.targetDir = true
|
||||
"#;
|
||||
|
||||
const GITIGNORE: &[u8] = b"Cargo.lock
|
||||
target/
|
||||
.vscode/
|
||||
";
|
||||
|
||||
pub const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#;
|
||||
|
||||
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 a different directory.";
|
||||
|
||||
const RUSTLINGS_DIR_ALREADY_EXISTS_ERR: &str =
|
||||
"A directory with the name `rustlings` already exists in the current directory.
|
||||
You probably already initialized Rustlings.
|
||||
Run `cd rustlings`
|
||||
Then run `rustlings` again";
|
||||
|
||||
const POST_INIT_MSG: &[u8] = b"Run `cd rustlings` to go into the generated directory.
|
||||
Then run `rustlings` to get started.
|
||||
";
|
||||
134
src/list.rs
134
src/list.rs
@ -1,134 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use crossterm::{
|
||||
QueueableCommand, cursor,
|
||||
event::{
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, MouseEventKind,
|
||||
},
|
||||
terminal::{
|
||||
DisableLineWrap, EnableLineWrap, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
disable_raw_mode, enable_raw_mode,
|
||||
},
|
||||
};
|
||||
use std::io::{self, StdoutLock, Write};
|
||||
|
||||
use crate::app_state::AppState;
|
||||
|
||||
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
|
||||
.queue(EnterAlternateScreen)?
|
||||
.queue(cursor::Hide)?
|
||||
.queue(DisableLineWrap)?
|
||||
.queue(EnableMouseCapture)?;
|
||||
enable_raw_mode()?;
|
||||
|
||||
let res = handle_list(app_state, &mut stdout);
|
||||
|
||||
// Restore the terminal even if we got an error.
|
||||
stdout
|
||||
.queue(LeaveAlternateScreen)?
|
||||
.queue(cursor::Show)?
|
||||
.queue(EnableLineWrap)?
|
||||
.queue(DisableMouseCapture)?
|
||||
.flush()?;
|
||||
disable_raw_mode()?;
|
||||
|
||||
res
|
||||
}
|
||||
@ -1,104 +0,0 @@
|
||||
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,424 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use crossterm::{
|
||||
QueueableCommand,
|
||||
cursor::{MoveTo, MoveToNextLine},
|
||||
style::{
|
||||
Attribute, Attributes, Color, ResetColor, SetAttribute, SetAttributes, SetForegroundColor,
|
||||
},
|
||||
terminal::{self, BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate},
|
||||
};
|
||||
use std::{
|
||||
fmt::Write as _,
|
||||
io::{self, StdoutLock, Write},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app_state::AppState,
|
||||
exercise::Exercise,
|
||||
term::{CountedWrite, MaxLenWriter, progress_bar},
|
||||
};
|
||||
|
||||
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 {
|
||||
Done,
|
||||
Pending,
|
||||
None,
|
||||
}
|
||||
|
||||
pub struct ListState<'a> {
|
||||
/// Footer message to be displayed if not empty.
|
||||
pub message: String,
|
||||
pub search_query: String,
|
||||
app_state: &'a mut AppState,
|
||||
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> ListState<'a> {
|
||||
pub fn build(app_state: &'a mut AppState, stdout: &mut StdoutLock) -> Result<Self> {
|
||||
stdout.queue(Clear(ClearType::All))?;
|
||||
|
||||
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_with_filter = app_state.exercises().len();
|
||||
let selected = app_state.current_exercise_ind();
|
||||
|
||||
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),
|
||||
search_query: String::new(),
|
||||
app_state,
|
||||
scroll_state,
|
||||
name_col_padding,
|
||||
path_col_padding,
|
||||
filter,
|
||||
// Set by `set_term_size`
|
||||
term_width: 0,
|
||||
term_height: 0,
|
||||
show_footer: true,
|
||||
};
|
||||
|
||||
slf.set_term_size(width, height);
|
||||
slf.draw(stdout)?;
|
||||
|
||||
Ok(slf)
|
||||
}
|
||||
|
||||
pub fn set_term_size(&mut self, width: u16, height: u16) {
|
||||
self.term_width = width;
|
||||
self.term_height = height;
|
||||
|
||||
if height == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let header_height = 1;
|
||||
// 1 progress bar, 2 footer message lines.
|
||||
let footer_height = 3;
|
||||
self.show_footer = height > header_height + footer_height;
|
||||
|
||||
self.scroll_state.set_max_n_rows_to_display(
|
||||
height.saturating_sub(header_height + u16::from(self.show_footer) * footer_height)
|
||||
as usize,
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
self.term_width,
|
||||
)?;
|
||||
next_ln(stdout)?;
|
||||
|
||||
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 ")?;
|
||||
}
|
||||
|
||||
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")?,
|
||||
}
|
||||
|
||||
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)?;
|
||||
}
|
||||
|
||||
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(),
|
||||
};
|
||||
|
||||
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 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),
|
||||
};
|
||||
|
||||
match ind {
|
||||
Some(exercise_ind) => self.scroll_state.set_selected(exercise_ind),
|
||||
None => self.message.push_str(" (not found)"),
|
||||
}
|
||||
}
|
||||
|
||||
// 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 exercise_ind = self.selected_to_exercise_ind(selected)?;
|
||||
self.app_state.set_current_exercise_ind(exercise_ind)?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
217
src/main.rs
217
src/main.rs
@ -1,217 +0,0 @@
|
||||
use anyhow::{Context, Result, bail};
|
||||
use app_state::StateFileStatus;
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::{
|
||||
io::{self, IsTerminal, Write},
|
||||
path::Path,
|
||||
process::ExitCode,
|
||||
};
|
||||
use term::{clear_terminal, press_enter_prompt};
|
||||
|
||||
use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile};
|
||||
|
||||
mod app_state;
|
||||
mod cargo_toml;
|
||||
mod cmd;
|
||||
mod dev;
|
||||
mod embedded;
|
||||
mod exercise;
|
||||
mod info_file;
|
||||
mod init;
|
||||
mod list;
|
||||
mod run;
|
||||
mod term;
|
||||
mod watch;
|
||||
|
||||
const CURRENT_FORMAT_VERSION: u8 = 1;
|
||||
|
||||
/// Rustlings is a collection of small exercises to get you used to writing and reading Rust code
|
||||
#[derive(Parser)]
|
||||
#[command(version)]
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
command: Option<Subcommands>,
|
||||
/// Manually run the current exercise using `r` in the watch mode.
|
||||
/// Only use this if Rustlings fails to detect exercise file changes.
|
||||
#[arg(long)]
|
||||
manual_run: bool,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Subcommands {
|
||||
/// Initialize the official Rustlings exercises
|
||||
Init,
|
||||
/// Run a single exercise. Runs the next pending exercise if the exercise name is not specified
|
||||
Run {
|
||||
/// 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
|
||||
name: String,
|
||||
},
|
||||
/// Show a hint. Shows the hint of the next pending exercise if the exercise name is not specified
|
||||
Hint {
|
||||
/// The name of the exercise
|
||||
name: Option<String>,
|
||||
},
|
||||
/// Commands for developing (community) Rustlings exercises
|
||||
#[command(subcommand)]
|
||||
Dev(DevCommands),
|
||||
}
|
||||
|
||||
fn main() -> Result<ExitCode> {
|
||||
let args = Args::parse();
|
||||
|
||||
if cfg!(not(debug_assertions)) && Path::new("dev/rustlings-repo.txt").exists() {
|
||||
bail!("{OLD_METHOD_ERR}");
|
||||
}
|
||||
|
||||
'priority_cmd: {
|
||||
match args.command {
|
||||
Some(Subcommands::Init) => init::init().context("Initialization failed")?,
|
||||
Some(Subcommands::Dev(dev_command)) => dev_command.run()?,
|
||||
_ => break 'priority_cmd,
|
||||
}
|
||||
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
if !Path::new("exercises").is_dir() {
|
||||
println!("{PRE_INIT_MSG}");
|
||||
return Ok(ExitCode::FAILURE);
|
||||
}
|
||||
|
||||
let info_file = InfoFile::parse()?;
|
||||
|
||||
if info_file.format_version > CURRENT_FORMAT_VERSION {
|
||||
bail!(FORMAT_VERSION_HIGHER_ERR);
|
||||
}
|
||||
|
||||
let (mut app_state, state_file_status) = AppState::new(
|
||||
info_file.exercises,
|
||||
info_file.final_message.unwrap_or_default(),
|
||||
)?;
|
||||
|
||||
// Show the welcome message if the state file doesn't exist yet.
|
||||
if let Some(welcome_message) = info_file.welcome_message {
|
||||
match state_file_status {
|
||||
StateFileStatus::NotRead => {
|
||||
let mut stdout = io::stdout().lock();
|
||||
clear_terminal(&mut stdout)?;
|
||||
|
||||
let welcome_message = welcome_message.trim_ascii();
|
||||
write!(
|
||||
stdout,
|
||||
"{welcome_message}\n\n\
|
||||
Press ENTER to continue "
|
||||
)?;
|
||||
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 => (),
|
||||
}
|
||||
}
|
||||
|
||||
match args.command {
|
||||
None => {
|
||||
if !io::stdout().is_terminal() {
|
||||
bail!("Unsupported or missing terminal/TTY");
|
||||
}
|
||||
|
||||
let notify_exercise_names = if args.manual_run {
|
||||
None
|
||||
} else {
|
||||
// For the notify event handler thread.
|
||||
// Leaking is not a problem because the slice lives until the end of the program.
|
||||
Some(
|
||||
&*app_state
|
||||
.exercises()
|
||||
.iter()
|
||||
.map(|exercise| exercise.name.as_bytes())
|
||||
.collect::<Vec<_>>()
|
||||
.leak(),
|
||||
)
|
||||
};
|
||||
|
||||
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)?;
|
||||
}
|
||||
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)?;
|
||||
let exercise_path = app_state.reset_current_exercise()?;
|
||||
println!("The exercise {exercise_path} has been reset");
|
||||
}
|
||||
Some(Subcommands::Hint { name }) => {
|
||||
if let Some(name) = name {
|
||||
app_state.set_current_exercise_by_name(&name)?;
|
||||
}
|
||||
println!("{}", app_state.current_exercise().hint);
|
||||
}
|
||||
// Handled in an earlier match.
|
||||
Some(Subcommands::Init | Subcommands::Dev(_)) => (),
|
||||
}
|
||||
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
|
||||
const OLD_METHOD_ERR: &str =
|
||||
"You are trying to run Rustlings using the old method before version 6.
|
||||
The new method doesn't include cloning the Rustlings' repository.
|
||||
Please follow the instructions in `README.md`:
|
||||
https://github.com/rust-lang/rustlings#getting-started";
|
||||
|
||||
const FORMAT_VERSION_HIGHER_ERR: &str =
|
||||
"The format version specified in the `info.toml` file is higher than the last one supported.
|
||||
It is possible that you have an outdated version of Rustlings.
|
||||
Try to install the latest Rustlings version first.";
|
||||
|
||||
const PRE_INIT_MSG: &str = r"
|
||||
Welcome to...
|
||||
_ _ _
|
||||
_ __ _ _ ___| |_| (_)_ __ __ _ ___
|
||||
| '__| | | / __| __| | | '_ \ / _` / __|
|
||||
| | | |_| \__ \ |_| | | | | | (_| \__ \
|
||||
|_| \__,_|___/\__|_|_|_| |_|\__, |___/
|
||||
|___/
|
||||
|
||||
The `exercises/` directory couldn't be found in the current directory.
|
||||
If you are just starting with Rustlings, run the command `rustlings init` to initialize it.";
|
||||
60
src/run.rs
60
src/run.rs
@ -1,60 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use crossterm::{
|
||||
QueueableCommand,
|
||||
style::{Color, ResetColor, SetForegroundColor},
|
||||
};
|
||||
use std::{
|
||||
io::{self, Write},
|
||||
process::ExitCode,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app_state::{AppState, ExercisesProgress},
|
||||
exercise::{OUTPUT_CAPACITY, RunnableExercise, solution_link_line},
|
||||
};
|
||||
|
||||
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(Some(&mut output), app_state.cmd_runner())?;
|
||||
|
||||
let mut stdout = io::stdout().lock();
|
||||
stdout.write_all(&output)?;
|
||||
|
||||
if !success {
|
||||
app_state.set_pending(app_state.current_exercise_ind())?;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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()? {
|
||||
stdout.write_all(b"\n")?;
|
||||
solution_link_line(&mut stdout, &solution_path)?;
|
||||
stdout.write_all(b"\n")?;
|
||||
}
|
||||
|
||||
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 => (),
|
||||
}
|
||||
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
310
src/term.rs
310
src/term.rs
@ -1,310 +0,0 @@
|
||||
use crossterm::{
|
||||
Command, QueueableCommand,
|
||||
cursor::MoveTo,
|
||||
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
|
||||
terminal::{Clear, ClearType},
|
||||
};
|
||||
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 struct ProgressCounter<'a, 'lock> {
|
||||
stdout: &'a mut StdoutLock<'lock>,
|
||||
total: usize,
|
||||
counter: usize,
|
||||
}
|
||||
|
||||
impl<'a, 'lock> ProgressCounter<'a, 'lock> {
|
||||
pub fn new(stdout: &'a mut StdoutLock<'lock>, total: usize) -> io::Result<Self> {
|
||||
write!(stdout, "Progress: 0/{total}")?;
|
||||
stdout.flush()?;
|
||||
|
||||
Ok(Self {
|
||||
stdout,
|
||||
total,
|
||||
counter: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn increment(&mut self) -> io::Result<()> {
|
||||
self.counter += 1;
|
||||
write!(self.stdout, "\rProgress: {}/{}", self.counter, self.total)?;
|
||||
self.stdout.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ProgressCounter<'_, '_> {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.stdout.write_all(b"\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
190
src/watch.rs
190
src/watch.rs
@ -1,190 +0,0 @@
|
||||
use anyhow::{Error, Result};
|
||||
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use std::{
|
||||
io::{self, Write},
|
||||
path::Path,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering::Relaxed},
|
||||
mpsc::channel,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
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 { width: u16 },
|
||||
NotifyErr(notify::Error),
|
||||
TerminalEventErr(io::Error),
|
||||
}
|
||||
|
||||
/// Returned by the watch mode to indicate what to do afterwards.
|
||||
#[must_use]
|
||||
enum WatchExit {
|
||||
/// Exit the program.
|
||||
Shutdown,
|
||||
/// Enter the list mode and restart the watch mode afterwards.
|
||||
List,
|
||||
}
|
||||
|
||||
fn run_watch(
|
||||
app_state: &mut AppState,
|
||||
notify_exercise_names: Option<&'static [&'static [u8]]>,
|
||||
) -> Result<WatchExit> {
|
||||
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 _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_follow_symlinks(false)
|
||||
.with_poll_interval(Duration::from_secs(1)),
|
||||
)
|
||||
.inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;
|
||||
|
||||
watcher
|
||||
.watch(Path::new("exercises"), RecursiveMode::Recursive)
|
||||
.inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;
|
||||
|
||||
Some(watcher)
|
||||
} else {
|
||||
manual_run = true;
|
||||
None
|
||||
};
|
||||
|
||||
let mut watch_state = WatchState::build(app_state, watch_event_sender, manual_run)?;
|
||||
let mut stdout = io::stdout().lock();
|
||||
|
||||
watch_state.run_current_exercise(&mut stdout)?;
|
||||
|
||||
while let Ok(event) = watch_event_receiver.recv() {
|
||||
match event {
|
||||
WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise(&mut stdout)? {
|
||||
ExercisesProgress::AllDone => break,
|
||||
ExercisesProgress::NewPending => watch_state.run_current_exercise(&mut stdout)?,
|
||||
ExercisesProgress::CurrentPending => (),
|
||||
},
|
||||
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) => {
|
||||
stdout.write_all(QUIT_MSG)?;
|
||||
break;
|
||||
}
|
||||
WatchEvent::FileChange { exercise_ind } => {
|
||||
watch_state.handle_file_change(exercise_ind, &mut stdout)?;
|
||||
}
|
||||
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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 in this directory.
|
||||
";
|
||||
|
||||
const NOTIFY_ERR: &str = "
|
||||
The automatic detection of exercise file changes failed :(
|
||||
Please try running `rustlings` again.
|
||||
|
||||
If you keep getting this error, run `rustlings --manual-run` to deactivate the file watcher.
|
||||
You need to manually trigger running the current exercise using `r` then.
|
||||
";
|
||||
@ -1,132 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use notify::{
|
||||
Event, EventKind,
|
||||
event::{AccessKind, AccessMode, MetadataKind, ModifyKind, RenameMode},
|
||||
};
|
||||
use std::{
|
||||
sync::{
|
||||
atomic::Ordering::Relaxed,
|
||||
mpsc::{RecvTimeoutError, Sender, SyncSender, sync_channel},
|
||||
},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use super::{EXERCISE_RUNNING, WatchEvent};
|
||||
|
||||
const DEBOUNCE_DURATION: Duration = Duration::from_millis(200);
|
||||
|
||||
pub struct NotifyEventHandler {
|
||||
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 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")?;
|
||||
|
||||
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,299 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use crossterm::{
|
||||
QueueableCommand,
|
||||
style::{
|
||||
Attribute, Attributes, Color, ResetColor, SetAttribute, SetAttributes, SetForegroundColor,
|
||||
},
|
||||
terminal,
|
||||
};
|
||||
use std::{
|
||||
io::{self, Read, StdoutLock, Write},
|
||||
sync::mpsc::{Sender, SyncSender, sync_channel},
|
||||
thread,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app_state::{AppState, ExercisesProgress},
|
||||
clear_terminal,
|
||||
exercise::{OUTPUT_CAPACITY, RunnableExercise, solution_link_line},
|
||||
term::progress_bar,
|
||||
};
|
||||
|
||||
use super::{InputPauseGuard, WatchEvent, terminal_event::terminal_event_handler};
|
||||
|
||||
const HEADING_ATTRIBUTES: Attributes = Attributes::none()
|
||||
.with(Attribute::Bold)
|
||||
.with(Attribute::Underlined);
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
enum DoneStatus {
|
||||
DoneWithSolution(String),
|
||||
DoneWithoutSolution,
|
||||
Pending,
|
||||
}
|
||||
|
||||
pub struct WatchState<'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 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;
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
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(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()? {
|
||||
DoneStatus::DoneWithSolution(solution_path)
|
||||
} else {
|
||||
DoneStatus::DoneWithoutSolution
|
||||
};
|
||||
} else {
|
||||
self.app_state
|
||||
.set_pending(self.app_state.current_exercise_ind())?;
|
||||
|
||||
self.done_status = DoneStatus::Pending;
|
||||
}
|
||||
|
||||
self.render(stdout)?;
|
||||
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.terminal_event_unpause_sender.send(())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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,73 +0,0 @@
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
|
||||
use std::sync::{
|
||||
atomic::Ordering::Relaxed,
|
||||
mpsc::{Receiver, Sender},
|
||||
};
|
||||
|
||||
use super::{EXERCISE_RUNNING, WatchEvent};
|
||||
|
||||
pub enum InputEvent {
|
||||
Next,
|
||||
Run,
|
||||
Hint,
|
||||
List,
|
||||
CheckAll,
|
||||
Reset,
|
||||
Quit,
|
||||
}
|
||||
|
||||
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 EXERCISE_RUNNING.load(Relaxed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let input_event = match key.code {
|
||||
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;
|
||||
}
|
||||
|
||||
// Pause input until quitting the confirmation prompt.
|
||||
if unpause_receiver.recv().is_err() {
|
||||
return;
|
||||
};
|
||||
|
||||
continue;
|
||||
}
|
||||
KeyCode::Char('q') => break WatchEvent::Input(InputEvent::Quit),
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
if sender.send(WatchEvent::Input(input_event)).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Ok(Event::Resize(width, _)) => {
|
||||
if sender.send(WatchEvent::TerminalResize { width }).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Ok(Event::FocusGained | Event::FocusLost | Event::Mouse(_)) => continue,
|
||||
Err(e) => break WatchEvent::TerminalEventErr(e),
|
||||
}
|
||||
};
|
||||
|
||||
let _ = sender.send(last_watch_event);
|
||||
}
|
||||
@ -1,182 +0,0 @@
|
||||
use std::{
|
||||
env::{self, consts::EXE_SUFFIX},
|
||||
process::{Command, Stdio},
|
||||
str::from_utf8,
|
||||
};
|
||||
|
||||
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_compilation_success() {
|
||||
Cmd::default()
|
||||
.current_dir("tests/test_exercises")
|
||||
.args(&["run", "compilation_success"])
|
||||
.success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_compilation_failure() {
|
||||
Cmd::default()
|
||||
.current_dir("tests/test_exercises")
|
||||
.args(&["run", "compilation_failure"])
|
||||
.fail();
|
||||
}
|
||||
|
||||
#[test]
|
||||
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_test_failure() {
|
||||
Cmd::default()
|
||||
.current_dir("tests/test_exercises")
|
||||
.args(&["run", "test_failure"])
|
||||
.fail();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_exercise_not_in_info() {
|
||||
Cmd::default()
|
||||
.current_dir("tests/test_exercises")
|
||||
.args(&["run", "not_in_info"])
|
||||
.fail();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_without_exercise_name() {
|
||||
Cmd::default().args(&["reset"]).fail();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hint() {
|
||||
Cmd::default()
|
||||
.current_dir("tests/test_exercises")
|
||||
.args(&["hint", "test_failure"])
|
||||
.output(FullStdout("The answer to everything: 42\n"))
|
||||
.success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init() {
|
||||
let test_dir = tempfile::TempDir::new().unwrap();
|
||||
let test_dir = test_dir.path().to_str().unwrap();
|
||||
|
||||
Cmd::default().current_dir(test_dir).fail();
|
||||
|
||||
Cmd::default()
|
||||
.current_dir(test_dir)
|
||||
.args(&["init"])
|
||||
.success();
|
||||
|
||||
// Running `init` after a successful initialization.
|
||||
Cmd::default()
|
||||
.current_dir(test_dir)
|
||||
.args(&["init"])
|
||||
.output(PartialStderr("`cd rustlings`"))
|
||||
.fail();
|
||||
|
||||
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();
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
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 = "2024"
|
||||
publish = false
|
||||
@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
let
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
fn main() {}
|
||||
@ -1 +0,0 @@
|
||||
fn main() {}
|
||||
@ -1,9 +0,0 @@
|
||||
fn main() {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn fails() {
|
||||
assert!(false);
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
fn main() {
|
||||
println!("Output from `main` function");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn passes() {}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
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"
|
||||
7
website/.gitignore
vendored
7
website/.gitignore
vendored
@ -1,7 +0,0 @@
|
||||
/node_modules/
|
||||
/package-lock.json
|
||||
|
||||
/public/
|
||||
|
||||
/static/main.css
|
||||
/static/processed_images/
|
||||
@ -1,41 +0,0 @@
|
||||
base_url = "https://rustlings.rust-lang.org"
|
||||
title = "Rustlings"
|
||||
description = "Small exercises to get you used to reading and writing Rust code!"
|
||||
|
||||
compile_sass = false
|
||||
build_search_index = false
|
||||
|
||||
[markdown]
|
||||
highlight_code = true
|
||||
highlight_theme = "dracula"
|
||||
|
||||
insert_anchor_links = "heading"
|
||||
|
||||
[extra]
|
||||
logo_path = "images/happy_ferris.svg"
|
||||
|
||||
[[extra.menu_items]]
|
||||
name = "Rustlings"
|
||||
url = "@/_index.md"
|
||||
[[extra.menu_items]]
|
||||
name = "Setup"
|
||||
url = "@/setup/index.md"
|
||||
[[extra.menu_items]]
|
||||
name = "Usage"
|
||||
url = "@/usage/index.md"
|
||||
[[extra.menu_items]]
|
||||
name = "Community Exercises"
|
||||
url = "@/community-exercises/index.md"
|
||||
[[extra.menu_items]]
|
||||
name = "Q&A"
|
||||
url = "https://github.com/rust-lang/rustlings/discussions/categories/q-a?discussions_q="
|
||||
|
||||
[[extra.footer_items]]
|
||||
name = "Repository"
|
||||
url = "https://github.com/rust-lang/rustlings"
|
||||
[[extra.footer_items]]
|
||||
name = "Changelog"
|
||||
url = "https://github.com/rust-lang/rustlings/blob/main/CHANGELOG.md"
|
||||
[[extra.footer_items]]
|
||||
name = "MIT License"
|
||||
url = "https://github.com/rust-lang/rustlings/blob/main/LICENSE"
|
||||
@ -1,21 +0,0 @@
|
||||
+++
|
||||
+++
|
||||
|
||||
Small exercises to get you used to reading and writing [Rust](https://www.rust-lang.org) code - _Recommended in parallel to reading [the official Rust book](https://doc.rust-lang.org/book) 📚️_
|
||||
|
||||
<script src="https://asciinema.org/a/719805.js" id="asciicast-719805" async="true"></script>
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# Installation
|
||||
cargo install rustlings
|
||||
# Initialization
|
||||
rustlings init
|
||||
# Moving into new directory
|
||||
cd rustlings
|
||||
# Starting Rustlings
|
||||
rustlings
|
||||
```
|
||||
|
||||
Visit the [**setup**](@/setup/index.md) page for more details 🧰
|
||||
@ -1,73 +0,0 @@
|
||||
+++
|
||||
title = "Community Exercises"
|
||||
+++
|
||||
|
||||
## List of Community Exercises
|
||||
|
||||
- 🇯🇵 [Japanese Rustlings](https://github.com/sotanengel/rustlings-jp):A Japanese translation of the Rustlings exercises.
|
||||
- 🇨🇳 [Simplified Chinese Rustlings](https://github.com/SandmeyerX/rustlings-zh-cn): A simplified Chinese translation of the Rustlings exercises.
|
||||
|
||||
> You can use the same `rustlings` program that you installed with `cargo install rustlings` to run community exercises.
|
||||
|
||||
## Creating Community Exercises
|
||||
|
||||
Rustling's support for community exercises allows you to create your own exercises to focus on some specific topic.
|
||||
You could also offer a translation of the original Rustlings exercises as community exercises.
|
||||
|
||||
### Getting Started
|
||||
|
||||
To create community exercises, install Rustlings and run `rustlings dev new PROJECT_NAME`.
|
||||
This command will, similar to `cargo new PROJECT_NAME`, create the template directory `PROJECT_NAME` with all what you need to get started.
|
||||
|
||||
_Read the comments_ in the generated `info.toml` file to understand its format.
|
||||
It allows you to set a custom welcome and final message and specify the metadata of every exercise.
|
||||
|
||||
### Creating an Exercise
|
||||
|
||||
Here is an example of the metadata of one exercise:
|
||||
|
||||
```toml
|
||||
[[exercises]]
|
||||
name = "intro1"
|
||||
hint = """
|
||||
To finish this exercise, you need to …
|
||||
These links might help you …"""
|
||||
```
|
||||
|
||||
After entering this in `info.toml`, create the file `intro1.rs` in the `exercises/` directory.
|
||||
The exercise needs to contain a `main` function, but it can be empty.
|
||||
Adding tests is recommended.
|
||||
Look at the official Rustlings exercises for inspiration.
|
||||
|
||||
You can optionally add a solution file `intro1.rs` to the `solutions/` directory.
|
||||
|
||||
Now, run `rustlings dev check`.
|
||||
It will tell you about any issues with your exercises.
|
||||
For example, it will tell you to run `rustlings dev update` to update the `Cargo.toml` file to include the new exercise `intro1`.
|
||||
|
||||
`rustlings dev check` will also run your solutions (if you have any) to make sure that they run successfully.
|
||||
|
||||
That's it!
|
||||
You finished your first exercise 🎉
|
||||
|
||||
### Cargo.toml
|
||||
|
||||
Except of the `bin` list, you can modify the `Cargo.toml` file as you want.
|
||||
|
||||
> The `bin` list is automatically updated by running `rustlings dev update`
|
||||
|
||||
- You can add dependencies in the `[dependencies]` table.
|
||||
- You might want to [configure some lints](https://doc.rust-lang.org/cargo/reference/manifest.html#the-lints-section) for all exercises. You can do so in the `[lints.rust]` and `[lints.clippy]` tables.
|
||||
|
||||
### Publishing
|
||||
|
||||
Now, add more exercises and publish them as a Git repository.
|
||||
|
||||
Users just have to clone that repository and run `rustlings` in it to start working on your exercises (just like the official ones).
|
||||
|
||||
One difference to the official exercises is that the solution files will not be hidden until the user finishes an exercise.
|
||||
But you can trust your users to not open the solution too early 😉
|
||||
|
||||
### Sharing
|
||||
|
||||
After publishing your community exercises, open an issue or a pull request in the [official Rustlings repository](https://github.com/rust-lang/rustlings) to add your project to the [list of community exercises](#list-of-community-exercises) 😃
|
||||
@ -1,78 +0,0 @@
|
||||
+++
|
||||
title = "Setup"
|
||||
+++
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## Installing Rust
|
||||
|
||||
Before installing Rustlings, you must have the **latest version of Rust** installed.
|
||||
Visit [www.rust-lang.org/tools/install](https://www.rust-lang.org/tools/install) for further instructions.
|
||||
This will also install _Cargo_, Rust's package/project manager.
|
||||
|
||||
> 🐧 If you are on **Linux**, make sure you have `gcc` installed (_for a linker_).
|
||||
>
|
||||
> Debian: `sudo apt install gcc`\
|
||||
> Fedora: `sudo dnf install gcc`
|
||||
|
||||
> 🍎 If you are on **MacOS**, make sure you have _Xcode and its developer tools_ installed: `xcode-select --install`
|
||||
|
||||
## Installing Rustlings
|
||||
|
||||
The following command will download and compile Rustlings:
|
||||
|
||||
```bash
|
||||
cargo install rustlings
|
||||
```
|
||||
|
||||
{% details(summary="If the installation fails…") %}
|
||||
|
||||
- Make sure you have the latest Rust version by running `rustup update`
|
||||
- Try adding the `--locked` flag: `cargo install rustlings --locked`
|
||||
- Otherwise, please [report the issue](https://github.com/rust-lang/rustlings/issues/new)
|
||||
|
||||
{% end %}
|
||||
|
||||
## Initialization
|
||||
|
||||
After installing Rustlings, run the following command to initialize the `rustlings/` directory:
|
||||
|
||||
```bash
|
||||
rustlings init
|
||||
```
|
||||
|
||||
{% details(summary="If the command <code>rustlings</code> can't be found…") %}
|
||||
|
||||
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.
|
||||
|
||||
- Either add `~/.cargo/bin` manually to `PATH`
|
||||
- Or uninstall Rust from the package manager and [install it using the official way with `rustup`](https://www.rust-lang.org/tools/install)
|
||||
|
||||
{% end %}
|
||||
|
||||
Now, go into the newly initialized directory and launch Rustlings for further instructions on getting started with the exercises:
|
||||
|
||||
```bash
|
||||
cd rustlings/
|
||||
rustlings
|
||||
```
|
||||
|
||||
## Working environment
|
||||
|
||||
### Editor
|
||||
|
||||
Our general recommendation is [VS Code](https://code.visualstudio.com/) with the [rust-analyzer plugin](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer).
|
||||
But any editor that supports [rust-analyzer](https://rust-analyzer.github.io/) should be enough for working on the exercises.
|
||||
|
||||
### Terminal
|
||||
|
||||
While working with Rustlings, please use a modern terminal for the best user experience.
|
||||
The default terminal on Linux and Mac should be sufficient.
|
||||
On Windows, we recommend the [Windows Terminal](https://aka.ms/terminal).
|
||||
|
||||
## Usage
|
||||
|
||||
After being done with the setup, visit the [**usage**](@/usage/index.md) page for some info about using Rustlings 🚀
|
||||
@ -1,55 +0,0 @@
|
||||
+++
|
||||
title = "Usage"
|
||||
+++
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## Doing exercises
|
||||
|
||||
The exercises are sorted by topic and can be found in the subdirectory `exercises/<topic>`.
|
||||
For every topic, there is an additional `README.md` file with some resources to get you started on the topic.
|
||||
We highly recommend that you have a look at them before you start 📚️
|
||||
|
||||
Most exercises contain an error that keeps them from compiling, and it's up to you to fix it!
|
||||
Some exercises contain tests that need to pass for the exercise to be done ✅
|
||||
|
||||
Search for `TODO` and `todo!()` to find out what you need to change.
|
||||
Ask for hints by entering `h` in the _watch mode_ 💡
|
||||
|
||||
## Watch Mode
|
||||
|
||||
After the [initialization](@/setup/index.md#initialization), Rustlings can be launched by simply running the command `rustlings`.
|
||||
|
||||
This will start the _watch mode_ which walks you through the exercises in a predefined order (what we think is best for newcomers).
|
||||
It will rerun the current exercise automatically every time you change the exercise's file in the `exercises/` directory.
|
||||
|
||||
{% details(summary="If detecting file changes in the <code>exercises/</code> directory fails…") %}
|
||||
|
||||
You can add the **`--manual-run`** flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` in the watch mode.
|
||||
|
||||
Please [report the issue](https://github.com/rust-lang/rustlings/issues/new) with some information about your operating system and whether you run Rustlings in a container or a virtual machine (e.g. WSL).
|
||||
|
||||
{% end %}
|
||||
|
||||
## Exercise List
|
||||
|
||||
In the [watch mode](#watch-mode) (after launching `rustlings`), you can enter `l` to open the interactive exercise list.
|
||||
|
||||
The list allows you to…
|
||||
|
||||
- See the status of all exercises (done or pending)
|
||||
- `c`: Continue at another exercise (temporarily skip some exercises or go back to a previous one)
|
||||
- `r`: Reset status and file of 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_ discussions](https://github.com/rust-lang/rustlings/discussions/categories/q-a?discussions_q=) if your question isn't answered there 💡
|
||||
|
||||
## 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.
|
||||
|
||||
> If you want to create your own Rustlings exercises, visit the [**community exercises**](@/community-exercises/index.md) page 🏗️
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user