diff --git a/Cargo.lock b/Cargo.lock index 1c77477..949af84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -247,6 +247,14 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" name = "countdown" version = "0.1.0" +[[package]] +name = "declaring-variables" +version = "0.1.0" +dependencies = [ + "syn", + "syntest", +] + [[package]] name = "determine-number-characteristics" version = "0.1.0" @@ -304,6 +312,10 @@ version = "0.1.0" name = "find-the-first-palindrome" version = "0.1.0" +[[package]] +name = "finite-state-automaton" +version = "0.1.0" + [[package]] name = "fizz-buzz" version = "0.1.0" @@ -684,6 +696,10 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" name = "mathematical-operations" version = "0.1.0" +[[package]] +name = "maze-solver" +version = "0.1.0" + [[package]] name = "median-and-mode" version = "0.1.0" @@ -885,9 +901,9 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] @@ -1144,9 +1160,9 @@ version = "0.1.0" [[package]] name = "syn" -version = "2.0.60" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", @@ -1159,6 +1175,14 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "syntest" +version = "0.1.0" +dependencies = [ + "anyhow", + "syn", +] + [[package]] name = "system-configuration" version = "0.5.1" diff --git a/README.MD b/README.md similarity index 100% rename from README.MD rename to README.md diff --git a/challenges/Cargo.toml b/challenges/Cargo.toml index 81e35ee..5251b45 100644 --- a/challenges/Cargo.toml +++ b/challenges/Cargo.toml @@ -9,5 +9,6 @@ serde = { version = "1.0.200", features = ["derive"] } serde_json = "1.0.116" toml = "0.8.12" -[lib] +[[test]] +name = "tests" path = "tests.rs" diff --git a/challenges/challenges.json b/challenges/challenges.json index 6576431..ad6bfc8 100644 --- a/challenges/challenges.json +++ b/challenges/challenges.json @@ -11,6 +11,18 @@ "created_at": "2024-04-20T00:00:00Z", "updated_at": "2024-04-20T00:00:00Z" }, + { + "id": 22, + "title": "Declaring Variables", + "slug": "declaring-variables", + "short_description": "Learn to declare immutable variables in Rust and understand their usage.", + "language": "RUST", + "difficulty": "BEGINNER", + "track": "RUST_BASICS", + "tags": ["variables", "immutability"], + "created_at": "2024-06-07T00:00:00Z", + "updated_at": "2024-06-07T00:00:00Z" + }, { "id": 2, "title": "Character counting string", @@ -231,5 +243,29 @@ "tags": ["control flow", "palindrome"], "created_at": "2024-06-05T00:00:00Z", "updated_at": "2024-06-05T00:00:00Z" + }, + { + "id": 20, + "title": "Finite State Automaton", + "slug": "finite-state-automaton", + "short_description": "Implement a finite state automaton (FSA) to recognize a specific pattern in a sequence of characters.", + "language": "RUST", + "difficulty": "HARD", + "track": "CONTROL_FLOW", + "tags": ["pattern recognition", "automata theory", "control flow"], + "created_at": "2024-06-07T00:00:00Z", + "updated_at": "2024-06-07T00:00:00Z" + }, + { + "id": 21, + "title": "Maze Solver", + "slug": "maze-solver", + "short_description": "Implement a maze solver using control flow in Rust to navigate through a maze represented as a grid.", + "language": "RUST", + "difficulty": "ADVANCED", + "track": "CONTROL_FLOW", + "tags": ["control flow", "algorithms", "path-finding"], + "created_at": "2024-06-07T00:00:00Z", + "updated_at": "2024-06-07T00:00:00Z" } ] diff --git a/challenges/declaring-variables/Cargo.toml b/challenges/declaring-variables/Cargo.toml new file mode 100644 index 0000000..8a853af --- /dev/null +++ b/challenges/declaring-variables/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "declaring-variables" +version = "0.1.0" +edition = "2021" + +[dependencies] + +[dev-dependencies] +syntest = { path = "../../crates/syntest" } +syn = "2.0.66" diff --git a/challenges/declaring-variables/description.md b/challenges/declaring-variables/description.md new file mode 100644 index 0000000..343a5e5 --- /dev/null +++ b/challenges/declaring-variables/description.md @@ -0,0 +1,30 @@ +## Declaring variables challenge + +Declaring variables in programming is a fundamental concept that allows you to **store and manipulate data**. Variables are used to store values that can be accessed and modified throughout the program. + +In Rust, variables are declared using the `let` keyword followed by the variable name and an optional type annotation. + +In Rust, variables are **immutable by default**. This means that once a value is bound to a variable, it cannot be changed. This is a key feature of Rust that helps ensure safety and prevent bugs by avoiding unexpected changes to values. + +## Your task + +In this challenge, you will declare and use immutable variables in Rust. You will be given a function where you need to **declare variables** and use them to perform specific operations. The goal is to get comfortable with the concept of **immutability and understand how to use immutable variables effectively.** + +Your task is to define two immutable variables inside the function using the `let` keyword: + +- `width` with a value of `10` +- `height` with a value of `5` + +Then, calculate the area of a rectangle using the formula `area = width * height` and return the calculated area. + +## Requirements + +- Declare two immutable variables, `width` and `height`, and assign them values. +- Calculate the area of the rectangle using the formula `width * height`. +- Return the calculated area. + +## Hints + +- Use the `let` keyword to declare variables. +- Remember that variables declared with `let` are immutable by default. +- Use multiplication `*` to calculate the area. diff --git a/challenges/declaring-variables/src/lib.rs b/challenges/declaring-variables/src/lib.rs new file mode 100644 index 0000000..a1c34cc --- /dev/null +++ b/challenges/declaring-variables/src/lib.rs @@ -0,0 +1,6 @@ +pub fn calculate_area() -> u32 { + let width = 10; + let height = 5; + + width * height +} diff --git a/challenges/declaring-variables/src/starter.rs b/challenges/declaring-variables/src/starter.rs new file mode 100644 index 0000000..ab8dd5a --- /dev/null +++ b/challenges/declaring-variables/src/starter.rs @@ -0,0 +1,6 @@ +pub fn calculate_area() -> u32 { + // TODO: Implement the function here + // Declare width and height as immutable variables + // Calculate the area of the rectangle + // Return the calculated area +} diff --git a/challenges/declaring-variables/tests/tests.rs b/challenges/declaring-variables/tests/tests.rs new file mode 100644 index 0000000..78809a3 --- /dev/null +++ b/challenges/declaring-variables/tests/tests.rs @@ -0,0 +1,32 @@ +#[cfg(test)] +mod tests { + use declaring_variables::*; + use syntest::{Syntest, Value}; + + #[test] + fn test_calculate_area() { + assert_eq!(calculate_area(), 50); + } + + #[test] + fn test_variables() { + let syntest = Syntest::from("./src/lib.rs"); + + // Expect the 2 variables to exist + let variables_to_exist = ["height", "width"]; + variables_to_exist.iter().for_each(|&v| { + let var = syntest + .var_details("calculate_area", v) + .expect(&format!("Variable {} was not declared", v)); + + assert!(var.is_used(), "Variable {v} was not used"); + assert_eq!(var.name(), v); + + if v == "width" { + assert_eq!(var.value(), Value::Int(10), "Width should be 10"); + } else if v == "height" { + assert_eq!(var.value(), Value::Int(5), "Height should be 5"); + } + }); + } +} diff --git a/challenges/finite-state-automaton/Cargo.toml b/challenges/finite-state-automaton/Cargo.toml new file mode 100644 index 0000000..00e7544 --- /dev/null +++ b/challenges/finite-state-automaton/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "finite-state-automaton" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/challenges/finite-state-automaton/description.md b/challenges/finite-state-automaton/description.md new file mode 100644 index 0000000..2f1c3c0 --- /dev/null +++ b/challenges/finite-state-automaton/description.md @@ -0,0 +1,48 @@ +In this challenge, you will implement a **finite state automaton (FSA)** to recognize a specific pattern in a sequence of characters. + +## What is a Finite State Automaton? + +A finite state automaton is a mathematical model of computation used to design both computer programs and sequential logic circuits. It is a powerful tool for **pattern matching as it consists of a finite number of states and transitions between these states based on input symbols**. + +Finite state automatons are widely used in **text processing, lexical analysis**, and many other areas where **pattern recognition** is essential. + +## Your Task + +You need to create an FSA that can recognize the pattern **"ab\*c"**, where: + +- **'a'** is followed by zero or more **'b'**s and then followed by **'c'**. + +You will implement a function `recognize_pattern` that takes a string slice as input and returns a boolean indicating whether the input string matches the pattern. + +## Requirements + +- Implement the finite state automaton using Rust's **enums** and the `match` statement. +- The function should handle **empty strings** and any invalid input gracefully. +- Your **FSA** should consist of states and transitions implemented as Rust enums and functions. +- You must handle state transitions explicitly using the `match` statement. + +## Example + +```rust +let result = recognize_pattern("abbbc"); +assert_eq!(result, true); + +let result = recognize_pattern("ac"); +assert_eq!(result, true); + +let result = recognize_pattern("abbbd"); +assert_eq!(result, false); + +let result = recognize_pattern(""); +assert_eq!(result, false); +``` + +> Did You Know? +> +> **Finite state automatons** have a wide range of applications outside computer science as well. For example, they are used in the design of digital circuits. In digital circuit design, an FSA can be used to create sequential circuits such as counters and communication protocol controllers. FSAs are also used in the field of linguistics to model the morphology of languages and in robotics to control the behavior of autonomous robots. + +## Hints + +- Think about how you can represent states and transitions using Rust enums. +- Carefully plan the transitions between states based on the input characters. +- Make sure to handle edge cases, such as an empty input string or invalid characters. diff --git a/challenges/finite-state-automaton/src/lib.rs b/challenges/finite-state-automaton/src/lib.rs new file mode 100644 index 0000000..8976ec1 --- /dev/null +++ b/challenges/finite-state-automaton/src/lib.rs @@ -0,0 +1,29 @@ +#[derive(Debug)] +enum State { + Start, + A, + B, + C, + Invalid, +} + +pub fn recognize_pattern(input: &str) -> bool { + let mut state = State::Start; + + for ch in input.chars() { + state = match (state, ch) { + (State::Start, 'a') => State::A, + (State::A, 'b') => State::B, + (State::A, 'c') => State::C, + (State::B, 'b') => State::B, + (State::B, 'c') => State::C, + _ => State::Invalid, + }; + + if let State::Invalid = state { + return false; + } + } + + matches!(state, State::C) +} diff --git a/challenges/finite-state-automaton/src/starter.rs b/challenges/finite-state-automaton/src/starter.rs new file mode 100644 index 0000000..d33d8fd --- /dev/null +++ b/challenges/finite-state-automaton/src/starter.rs @@ -0,0 +1,5 @@ +pub fn recognize_pattern(input: &str) -> bool { + // Implement your finite state automaton here + + false +} diff --git a/challenges/finite-state-automaton/tests/tests.rs b/challenges/finite-state-automaton/tests/tests.rs new file mode 100644 index 0000000..8cb03e1 --- /dev/null +++ b/challenges/finite-state-automaton/tests/tests.rs @@ -0,0 +1,49 @@ +#[cfg(test)] +mod tests { + use finite_state_automaton::*; + + #[test] + fn test_recognize_pattern_valid() { + assert_eq!(recognize_pattern("abbbc"), true); + assert_eq!(recognize_pattern("ac"), true); + assert_eq!(recognize_pattern("ab"), false); + assert_eq!(recognize_pattern("abbc"), true); + assert_eq!(recognize_pattern("abbbbbc"), true); + } + + #[test] + fn test_recognize_pattern_invalid() { + assert_eq!(recognize_pattern("abbbd"), false); + assert_eq!(recognize_pattern(""), false); + assert_eq!(recognize_pattern("a"), false); + assert_eq!(recognize_pattern("abbd"), false); + assert_eq!(recognize_pattern("abbbcc"), false); + } + + #[test] + fn test_recognize_pattern_edge_cases() { + assert_eq!(recognize_pattern("abbbbbc"), true); + assert_eq!(recognize_pattern("a"), false); + assert_eq!(recognize_pattern("abc"), true); + assert_eq!(recognize_pattern("abbc"), true); + assert_eq!(recognize_pattern("ab"), false); + } + + #[test] + fn test_recognize_pattern_long_input() { + let long_input_valid = "a".to_owned() + "b".repeat(434).as_str() + "c"; + let long_input_invalid = "a".to_owned() + "b".repeat(333).as_str() + "d"; + assert_eq!(recognize_pattern(&long_input_valid), true); + assert_eq!(recognize_pattern(&long_input_invalid), false); + assert_eq!(recognize_pattern(&(long_input_valid.clone() + "c")), false); + } + + #[test] + fn test_recognize_pattern_random_cases() { + assert_eq!(recognize_pattern("abbbbbbbbbc"), true); + assert_eq!(recognize_pattern("abbbbbbbbd"), false); + assert_eq!(recognize_pattern("aabbcc"), false); + assert_eq!(recognize_pattern("abbbc"), true); + assert_eq!(recognize_pattern("acc"), false); + } +} diff --git a/challenges/maze-solver/Cargo.toml b/challenges/maze-solver/Cargo.toml new file mode 100644 index 0000000..f09953d --- /dev/null +++ b/challenges/maze-solver/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "maze-solver" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/challenges/maze-solver/description.md b/challenges/maze-solver/description.md new file mode 100644 index 0000000..48788e5 --- /dev/null +++ b/challenges/maze-solver/description.md @@ -0,0 +1,72 @@ +In this challenge, you will implement a **maze solver in Rust**. A maze can be represented as a **grid where each cell is either an open path or a wall**. The goal is to navigate from a starting point to an ending point using the shortest path possible. This problem will test your understanding of control flow in Rust, including loops, conditionals, and possibly recursion. + +## Your Task + +Your task is to write a function `solve_maze` that takes a **2D vector of characters representing the maze and two tuples representing the start and end points.** The maze will be given as a grid of characters where `'S'` represents the start, `'E'` represents the end, `'.'` represents an open path, and `'#'` represents a wall. + +**The function should return a vector of tuples** representing the path from start to end if a path exists, or an empty vector if no path exists. + +## Constraints + +- The maze will always contain exactly one `'S'` and one `'E'`. +- You can only move **up, down, left, or right**. +- Implement the function using appropriate control flow constructs **(loops, conditionals, and/or recursion)**. +- Ensure your solution **handles edge cases**, such as no available path or mazes with various sizes. +- If the maze is a dead-end, return an empty vector. +- If the maze has multiple solutions, return the shortest path. + +## Example + +```rust +let maze = vec![ + vec!['S', '.', '#', '#', '#'], + vec!['#', '.', '#', '.', '.'], + vec!['#', '.', '.', '.', '#'], + vec!['#', '#', '#', '.', '#'], + vec!['#', '#', '#', 'E', '#'] +]; +let start = (0, 0); +let end = (4, 3); + +let path = solve_maze(maze, start, end); +assert_eq!( + path, + vec![ + (0, 0), // starting point + (0, 1), // right + (1, 1), // down + (2, 1), // down + (2, 2), // right + (2, 3), // right + (3, 3), // down + (4, 3) // down + ] +); +``` + +In the example above, we start from `'S'` at `(0, 0)`, the first move would be going to the right `(0, 1)`, then down `(1, 1)`, and so on until we reach the end at `(4, 3)`. + +> Did You Know? +> +> Maze solving algorithms are not just for games. They are used in robotics for **pathfinding**, in computer networks for routing, and in various optimization problems. Algorithms such as Depth-First Search (DFS), Breadth-First Search (BFS), and A\* are commonly used to solve these problems efficiently. + +## Hints + +1. **Collections**: + + - `VecDeque`: A double-ended queue from the `std::collections` module, which is useful for implementing a queue for BFS. Methods like `push_back` and `pop_front` will be helpful. + +2. **Indexing**: + + - Use `usize` for indices and be cautious with arithmetic operations to avoid overflow. The `wrapping_add` method can help with safe arithmetic. + +3. **2D Vector Initialization**: + + - Initialize 2D vectors for `visited` and `path` tracking. Use nested `vec!` macros for creating the initial structure. + +4. **Backtracking Path**: + + - Once the end point is reached, backtrack using the `path` vector to reconstruct the path from end to start. Collect these coordinates in a vector and reverse it to get the path from start to end. + +5. **Boundary Checks**: + - Ensure the new coordinates are within the maze boundaries and check if the cell is a wall (`'#'`) or already visited. diff --git a/challenges/maze-solver/src/lib.rs b/challenges/maze-solver/src/lib.rs new file mode 100644 index 0000000..93c45b1 --- /dev/null +++ b/challenges/maze-solver/src/lib.rs @@ -0,0 +1,40 @@ +pub fn solve_maze( + maze: Vec>, + start: (usize, usize), + end: (usize, usize), +) -> Vec<(usize, usize)> { + use std::collections::VecDeque; + + let directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]; + let mut queue = VecDeque::new(); + let mut visited = vec![vec![false; maze[0].len()]; maze.len()]; + let mut path = vec![vec![None; maze[0].len()]; maze.len()]; + + queue.push_back(start); + visited[start.0][start.1] = true; + + while let Some((x, y)) = queue.pop_front() { + if (x, y) == end { + let mut p = vec![]; + let mut current = Some((x, y)); + while let Some(c) = current { + p.push(c); + current = path[c.0][c.1]; + } + p.reverse(); + return p; + } + + for &(dx, dy) in &directions { + let nx = x.wrapping_add(dx as usize); + let ny = y.wrapping_add(dy as usize); + if nx < maze.len() && ny < maze[0].len() && maze[nx][ny] != '#' && !visited[nx][ny] { + queue.push_back((nx, ny)); + visited[nx][ny] = true; + path[nx][ny] = Some((x, y)); + } + } + } + + vec![] +} diff --git a/challenges/maze-solver/src/starter.rs b/challenges/maze-solver/src/starter.rs new file mode 100644 index 0000000..1ddf262 --- /dev/null +++ b/challenges/maze-solver/src/starter.rs @@ -0,0 +1,7 @@ +pub fn solve_maze( + maze: Vec>, + start: (usize, usize), + end: (usize, usize), +) -> Vec<(usize, usize)> { + // Your code here +} diff --git a/challenges/maze-solver/tests/tests.rs b/challenges/maze-solver/tests/tests.rs new file mode 100644 index 0000000..25c222e --- /dev/null +++ b/challenges/maze-solver/tests/tests.rs @@ -0,0 +1,151 @@ +#[cfg(test)] +mod tests { + use maze_solver::*; + + #[test] + fn test_solve_maze() { + let maze = vec![ + vec!['S', '.', '#', '#', '#'], + vec!['#', '.', '#', '.', '.'], + vec!['#', '.', '.', '.', '#'], + vec!['#', '#', '#', '.', '#'], + vec!['#', '#', '#', 'E', '#'], + ]; + let start = (0, 0); + let end = (4, 3); + + let path = solve_maze(maze, start, end); + assert_eq!( + path, + vec![ + (0, 0), + (0, 1), + (1, 1), + (2, 1), + (2, 2), + (2, 3), + (3, 3), + (4, 3) + ] + ); + } + + #[test] + fn test_solve_maze_2() { + let maze = vec![ + vec!['S', '.', '.', '#', '#'], + vec!['#', '#', '.', '#', '.'], + vec!['#', '.', '.', '.', '.'], + vec!['#', '#', '#', '#', '.'], + vec!['#', '#', '#', 'E', '.'], + ]; + let start = (0, 0); + let end = (4, 3); + + let path = solve_maze(maze, start, end); + assert_eq!( + path, + vec![ + (0, 0), + (0, 1), + (0, 2), + (1, 2), + (2, 2), + (2, 3), + (2, 4), + (3, 4), + (4, 4), + (4, 3) + ] + ); + } + + #[test] + fn test_solve_maze_3() { + let maze = vec![ + vec!['S', '.', '#', '#', '#'], + vec!['#', '.', '.', '.', '.'], + vec!['#', '#', '#', '#', '.'], + vec!['#', 'E', '.', '.', '.'], + vec!['#', '#', '#', '#', '#'], + ]; + let start = (0, 0); + let end = (3, 1); + + let path = solve_maze(maze, start, end); + assert_eq!( + path, + vec![ + (0, 0), + (0, 1), + (1, 1), + (1, 2), + (1, 3), + (1, 4), + (2, 4), + (3, 4), + (3, 3), + (3, 2), + (3, 1) + ] + ); + } + + #[test] + fn test_solve_maze_4() { + let maze = vec![ + vec!['S', '.', '.', '#', 'E'], + vec!['#', '#', '.', '#', '#'], + vec!['#', '.', '.', '.', '#'], + vec!['#', '#', '#', '.', '#'], + vec!['#', '#', '#', '#', '#'], + ]; + let start = (0, 0); + let end = (0, 4); + + let path = solve_maze(maze, start, end); + assert_eq!(path, vec![]); + } + + #[test] + fn test_multiple_solutions() { + let maze = vec![ + vec!['S', '.', '.', '.', '.'], + vec!['.', '#', '#', '#', '.'], + vec!['.', '#', '#', '#', '.'], + vec!['.', '#', '#', '#', '.'], + vec!['.', 'E', '.', '.', '.'], + ]; + let start = (0, 0); + let end = (4, 1); + + let path = solve_maze(maze, start, end); + assert_eq!(path, vec![(0, 0), (1, 0), (2, 0), (3, 0), (4, 0), (4, 1)]); + } + + #[test] + fn test_no_path() { + let maze = vec![ + vec!['S', '#', '#', '#', '#'], + vec!['#', '#', '#', '#', '#'], + vec!['#', '#', '#', '#', '#'], + vec!['#', '#', '#', '#', '#'], + vec!['#', '#', '#', 'E', '#'], + ]; + let start = (0, 0); + let end = (4, 3); + + let path = solve_maze(maze, start, end); + assert_eq!(path, vec![]); + } + + #[test] + fn test_edge_case() { + let maze = vec![vec!['S', '.', '.', '.', 'E']]; + let start = (0, 0); + let end = (0, 4); + + let path = solve_maze(maze, start, end); + assert_eq!(path, vec![(0, 0), (0, 1), (0, 2), (0, 3), (0, 4)]); + } +} diff --git a/challenges/tests.rs b/challenges/tests.rs index 0f3688e..edfe97f 100644 --- a/challenges/tests.rs +++ b/challenges/tests.rs @@ -1,4 +1,6 @@ +use chrono::NaiveDateTime; use serde::Deserialize; +use std::{fs, path::PathBuf}; #[derive(Deserialize)] enum Difficulty { @@ -16,166 +18,160 @@ enum Track { CONTROL_FLOW, } -#[cfg(test)] -mod tests { - use super::*; - use chrono::NaiveDateTime; - use std::{fs, path::PathBuf}; - - #[derive(Deserialize)] - struct Package { - pub name: String, - } - - #[derive(Deserialize)] - struct CargoToml { - pub package: Package, - } +#[derive(Deserialize)] +struct Package { + pub name: String, +} - #[derive(Deserialize)] - #[allow(unused)] - struct Challenge { - pub id: u32, - pub title: String, - pub slug: String, - pub short_description: String, - pub language: String, - pub difficulty: Difficulty, - pub track: Track, - pub tags: Vec, - pub created_at: String, - pub updated_at: String, - } +#[derive(Deserialize)] +struct CargoToml { + pub package: Package, +} - fn challenges_json() -> Result, std::io::Error> { - let challenges_str = fs::read_to_string("challenges.json")?; - let challenges = serde_json::from_str::>(&challenges_str)?; - Ok(challenges) - } +#[derive(Deserialize)] +#[allow(unused)] +struct Challenge { + pub id: u32, + pub title: String, + pub slug: String, + pub short_description: String, + pub language: String, + pub difficulty: Difficulty, + pub track: Track, + pub tags: Vec, + pub created_at: String, + pub updated_at: String, +} - fn challenges_dir_list() -> Result, std::io::Error> { - let mut entries = fs::read_dir("../challenges")?; - let mut dirs = vec![]; - let ignored_dirs = ["src/", "target/"]; +fn challenges_json() -> Result, std::io::Error> { + let challenges_str = fs::read_to_string("challenges.json")?; + let challenges = serde_json::from_str::>(&challenges_str)?; + Ok(challenges) +} - while let Some(entry) = entries.next() { - let entry = entry?; - let path = entry.path(); - if path.is_dir() { - let abs_path = path.canonicalize().unwrap(); +fn challenges_dir_list() -> Result, std::io::Error> { + let mut entries = fs::read_dir("../challenges")?; + let mut dirs = vec![]; + let ignored_dirs = ["src/", "target/"]; - if ignored_dirs.iter().any(|&dir| abs_path.ends_with(dir)) { - continue; - } + while let Some(entry) = entries.next() { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + let abs_path = path.canonicalize().unwrap(); - dirs.push(abs_path); + if ignored_dirs.iter().any(|&dir| abs_path.ends_with(dir)) { + continue; } - } - Ok(dirs) + dirs.push(abs_path); + } } - #[test] - fn test_read_challenges() { - let challenges = challenges_json().expect("Failed to read challenges"); - assert!(!challenges.is_empty(), "Expected some challenges"); - } + Ok(dirs) +} - #[test] - fn test_dirs() { - let challenges = challenges_dir_list().expect("Failed to read challenges directories"); - for challenge_dir in challenges { - let dir_name = challenge_dir.file_name().unwrap().to_str().unwrap(); - - let description_md = challenge_dir.join("description.md"); - let cargo_toml = challenge_dir.join("Cargo.toml"); - let src_lib = challenge_dir.join("src/lib.rs"); - let src_starter = challenge_dir.join("src/starter.rs"); - - // let src_tests = challenge_dir.join("src/tests.rs"); - let tests_exist = challenge_dir.join("src/tests.rs").exists() - || challenge_dir.join("tests/tests.rs").exists(); - - assert!( - description_md.exists(), - "Missing description.md in {}", - dir_name - ); - assert!(cargo_toml.exists(), "Missing Cargo.toml in {}", dir_name); - assert!(src_lib.exists(), "Missing src/lib.rs in {}", dir_name); - assert!(tests_exist, "Missing tests, you should have either src/tests.rs or tests/tests.rs available in {}", dir_name); - assert!( - src_starter.exists(), - "Missing src/starter.rs in {}", - dir_name - ); - - let cargo_content = fs::read_to_string(&cargo_toml).unwrap(); - let cargo: CargoToml = toml::from_str(&cargo_content).unwrap(); - let package_name = cargo.package.name; - - assert_eq!( - package_name, dir_name, - "Cargo.toml package name does not match directory name {}", - dir_name - ); - - let challenges = challenges_json().expect("Failed to read challenges"); - let challenge = challenges.iter().find(|c| c.slug == dir_name); - - assert!( - challenge.is_some(), - "Missing entry in challenges.json for {}", - dir_name - ); - } - } +#[test] +fn test_read_challenges() { + let challenges = challenges_json().expect("Failed to read challenges"); + assert!(!challenges.is_empty(), "Expected some challenges"); +} + +#[test] +fn test_dirs() { + let challenges = challenges_dir_list().expect("Failed to read challenges directories"); + for challenge_dir in challenges { + let dir_name = challenge_dir.file_name().unwrap().to_str().unwrap(); + + let description_md = challenge_dir.join("description.md"); + let cargo_toml = challenge_dir.join("Cargo.toml"); + let src_lib = challenge_dir.join("src/lib.rs"); + let src_starter = challenge_dir.join("src/starter.rs"); + let tests = challenge_dir.join("tests/tests.rs"); + + assert!( + description_md.exists(), + "Missing description.md in {}", + dir_name + ); + assert!(cargo_toml.exists(), "Missing Cargo.toml in {}", dir_name); + assert!(src_lib.exists(), "Missing src/lib.rs in {}", dir_name); + assert!( + tests.exists(), + "Missing tests, you should have either src/tests.rs or tests/tests.rs available in {}", + dir_name + ); + assert!( + src_starter.exists(), + "Missing src/starter.rs in {}", + dir_name + ); + + let cargo_content = fs::read_to_string(&cargo_toml).unwrap(); + let cargo: CargoToml = toml::from_str(&cargo_content).unwrap(); + let package_name = cargo.package.name; + + assert_eq!( + package_name, dir_name, + "Cargo.toml package name does not match directory name {}", + dir_name + ); - #[test] - fn test_duplicate_ids() { let challenges = challenges_json().expect("Failed to read challenges"); - let mut ids = vec![]; + let challenge = challenges.iter().find(|c| c.slug == dir_name); - for challenge in challenges { - if ids.contains(&challenge.id) { - panic!("Duplicate id found: {}", challenge.slug); - } + assert!( + challenge.is_some(), + "Missing entry in challenges.json for {}", + dir_name + ); + } +} + +#[test] +fn test_duplicate_ids() { + let challenges = challenges_json().expect("Failed to read challenges"); + let mut ids = vec![]; - ids.push(challenge.id); + for challenge in challenges { + if ids.contains(&challenge.id) { + panic!("Duplicate id found: {}", challenge.slug); } - } - #[test] - fn test_duplicate_slugs() { - let challenges = challenges_json().expect("Failed to read challenges"); - let mut slugs = vec![]; + ids.push(challenge.id); + } +} - for challenge in challenges { - if slugs.contains(&challenge.slug) { - panic!("Duplicate slug found: {}", challenge.slug); - } +#[test] +fn test_duplicate_slugs() { + let challenges = challenges_json().expect("Failed to read challenges"); + let mut slugs = vec![]; - slugs.push(challenge.slug); + for challenge in challenges { + if slugs.contains(&challenge.slug) { + panic!("Duplicate slug found: {}", challenge.slug); } + + slugs.push(challenge.slug); } +} - #[test] - fn test_date_validity() { - let challenges: Vec = challenges_json().expect("Failed to read challenges"); +#[test] +fn test_date_validity() { + let challenges: Vec = challenges_json().expect("Failed to read challenges"); - for challenge in challenges { - let created_at: NaiveDateTime = - NaiveDateTime::parse_from_str(&challenge.created_at, "%Y-%m-%dT%H:%M:%S%Z") - .expect("Failed to parse created_at date"); + for challenge in challenges { + let created_at: NaiveDateTime = + NaiveDateTime::parse_from_str(&challenge.created_at, "%Y-%m-%dT%H:%M:%S%Z") + .expect("Failed to parse created_at date"); - let updated_at: NaiveDateTime = - NaiveDateTime::parse_from_str(&challenge.updated_at, "%Y-%m-%dT%H:%M:%S%Z") - .expect("Failed to parse updated_at date"); + let updated_at: NaiveDateTime = + NaiveDateTime::parse_from_str(&challenge.updated_at, "%Y-%m-%dT%H:%M:%S%Z") + .expect("Failed to parse updated_at date"); - assert!( - created_at <= updated_at, - "created_at date should be before updated_at date" - ); - } + assert!( + created_at <= updated_at, + "created_at date should be before updated_at date" + ); } } diff --git a/crates/cli/src/download.rs b/crates/cli/src/download.rs index 88512a3..3f693a3 100644 --- a/crates/cli/src/download.rs +++ b/crates/cli/src/download.rs @@ -98,8 +98,8 @@ mod tests { fs::create_dir_all("temp/test_downloads_challenge").ok(); env::set_current_dir("temp/test_downloads_challenge").ok(); - for challenge in CHALLENGES { - get_challenge(challenge) + let test_challenge = |challenge: String| async move { + get_challenge(&challenge) .await .expect("Failed to download challenge"); @@ -111,15 +111,21 @@ mod tests { ]; for file in paths_to_exist.iter() { - let path = format!("{}/{}", challenge, file); - - assert!(Path::new(&path).exists()); + let path = Path::new(&challenge).join(file); + assert!(path.exists(), "File does not exist: {:?}", path); // all files shouldn't have the content "404: Not Found" let contents = fs::read_to_string(&path).unwrap(); assert!(!contents.contains("404: Not Found")); } - } + }; + + let handles = CHALLENGES + .iter() + .map(|c| tokio::spawn(test_challenge(c.to_string()))) + .collect::>(); + + futures::future::join_all(handles).await; } } diff --git a/crates/syntest/Cargo.toml b/crates/syntest/Cargo.toml new file mode 100644 index 0000000..5df0413 --- /dev/null +++ b/crates/syntest/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "syntest" +description = "A library to test Rust syntax" +repository = "https://github.com/dcodesdev/rustfinity.com/tree/main/crates/syntest" +license = "MIT" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.86" +syn = { version = "2.0.66", features = ["full", "extra-traits"] } diff --git a/crates/syntest/README.md b/crates/syntest/README.md new file mode 100644 index 0000000..c47234c --- /dev/null +++ b/crates/syntest/README.md @@ -0,0 +1,23 @@ +# Syntest + +Syntest is a small crate that is used to test the rustfinity challenges not just by output and behavior but also by the syntax of the code. + +## Usage + +```rust +use syntest::Syntest; + +#[test] +fn test_syntax() { + // opens the file and reads the code + let syntest = Syntest::from("./src/main.rs"); + + // gets all the local variables inside a function + let variables = syntest.variables("main"); + + // Checks if all of the variables are used + variables.iter().for_each(|variable| { + assert!(variable.is_used()); + }); +} +``` diff --git a/crates/syntest/src/lib.rs b/crates/syntest/src/lib.rs new file mode 100644 index 0000000..b80913e --- /dev/null +++ b/crates/syntest/src/lib.rs @@ -0,0 +1,5 @@ +mod syntest; +mod var; + +pub use syntest::Syntest; +pub use var::{LocalVariable, Value}; diff --git a/crates/syntest/src/syntest.rs b/crates/syntest/src/syntest.rs new file mode 100644 index 0000000..e6c7264 --- /dev/null +++ b/crates/syntest/src/syntest.rs @@ -0,0 +1,530 @@ +use std::fs; +use std::path::PathBuf; +use syn::{ + parse_file, punctuated::Punctuated, token::PathSep, Expr, Item, ItemFn, Lit, Pat, PathSegment, + Stmt, +}; + +use crate::var::LocalVariable; + +pub struct Syntest { + pub file: syn::File, +} + +impl Syntest { + pub fn new(path: PathBuf) -> anyhow::Result { + Ok(Self { + file: parse_file(&fs::read_to_string(&path)?)?, + }) + } + + pub fn from_code(code: &str) -> anyhow::Result { + Ok(Self { + file: parse_file(code)?, + }) + } + + pub fn expr(&self, mut handler: F) + where + F: FnMut(&Expr), + { + self.file.items.iter().for_each(|item| { + let mut ran = false; + if let Item::Fn(f) = item { + f.block.stmts.iter().for_each(|stmt| { + if let Stmt::Expr(expr, _) = stmt { + handler(expr); + ran = true; + } + }) + } + + if !ran { + panic!("No expression found"); + } + }) + } + + pub fn func(&self, fn_name: &str, mut handler: F) + where + F: FnMut(&ItemFn), + { + let mut ran = false; + self.file.items.iter().for_each(|item| { + if let Item::Fn(f) = item { + if f.sig.ident == fn_name { + handler(f); + ran = true; + } + } + }); + + if !ran { + panic!("Function {} not found", fn_name); + } + } + + pub fn func_stmts(&self, fn_name: &str, mut handler: F) + where + F: FnMut(&ItemFn, &Stmt), + { + let mut ran = false; + self.func(fn_name, |f| { + f.block.stmts.iter().for_each(|stmt| { + handler(f, stmt); + ran = true; + }) + }); + + if !ran { + panic!("Function {} not found", fn_name); + } + } + + /// Finds all variables defined in a function block + /// and checks if they are used or not + pub fn variables(&self, fn_name: &str) -> Vec { + let mut variables = Vec::new(); + + self.func_stmts(fn_name, |_, stmt| match stmt { + Stmt::Local(local) => { + let used = false; + let mut local_value = None; + + if let Pat::Ident(ident) = &local.pat { + if let Some(init) = &local.init { + match *init.expr.clone() { + Expr::Lit(expr_lit) => match expr_lit.lit { + Lit::Str(lit_str) => { + local_value = Some(LocalVariable::Str { + name: ident.ident.to_string(), + value: lit_str.value(), + used, + }); + } + Lit::Int(lit_int) => { + local_value = Some(LocalVariable::Int { + name: ident.ident.to_string(), + value: lit_int.base10_parse().unwrap(), + used, + }); + } + Lit::Float(lit_float) => { + local_value = Some(LocalVariable::Float { + name: ident.ident.to_string(), + value: lit_float.base10_parse().unwrap(), + used, + }); + } + Lit::Char(lit_char) => { + local_value = Some(LocalVariable::Char { + name: ident.ident.to_string(), + value: lit_char.value(), + used, + }); + } + Lit::Bool(lit_bool) => { + local_value = Some(LocalVariable::Bool { + name: ident.ident.to_string(), + value: lit_bool.value(), + used, + }); + } + _ => {} + }, + Expr::Closure(_) => { + local_value = Some(LocalVariable::Closure { + name: ident.ident.to_string(), + used, + }); + } + Expr::Binary(expr_binary) => { + self.match_expr(&expr_binary.left, &mut variables); + self.match_expr(&expr_binary.right, &mut variables); + + local_value = Some(LocalVariable::Other { + name: ident.ident.to_string(), + used, + }) + } + Expr::Path(expr_path) => { + self.create_var_from_segments( + &mut variables, + &expr_path.path.segments, + ); + } + _ => { + local_value = Some(LocalVariable::Other { + name: ident.ident.to_string(), + used, + }); + } + } + } + } + + if let Some(local_value) = local_value { + variables.push(local_value); + } + } + Stmt::Expr(expr, _) => self.match_expr(expr, &mut variables), + _ => {} + }); + + variables + } + + fn match_expr(&self, expr: &Expr, variables: &mut Vec) { + match expr { + Expr::Binary(binary_expr) => { + self.match_expr(&binary_expr.left, variables); + self.match_expr(&binary_expr.right, variables); + } + Expr::Path(path_expr) => { + self.match_segments(variables, &path_expr.path.segments); + } + Expr::Return(return_expr) => { + if let Some(expr) = return_expr.expr.clone() { + self.match_expr(&expr, variables) + } + } + _ => {} + } + } + + fn create_var_from_segments( + &self, + variables: &mut Vec, + segments: &Punctuated, + ) { + segments.iter().for_each(|segment| { + variables.push(LocalVariable::Other { + name: segment.ident.to_string(), + used: false, + }) + }); + } + + fn match_segments( + &self, + variables: &mut Vec, + segments: &Punctuated, + ) { + let mut check_segment = |path_segment: &PathSegment| { + let mut found = false; + variables.iter_mut().for_each(|variable| match variable { + LocalVariable::Str { name, used, .. } => { + if name == &path_segment.ident.to_string() { + *used = true; + found = true; + } + } + LocalVariable::Int { name, used, .. } => { + if name == &path_segment.ident.to_string() { + *used = true; + found = true; + } + } + LocalVariable::Float { name, used, .. } => { + if name == &path_segment.ident.to_string() { + *used = true; + found = true; + } + } + LocalVariable::Char { name, used, .. } => { + if name == &path_segment.ident.to_string() { + *used = true; + found = true; + } + } + LocalVariable::Bool { name, used, .. } => { + if name == &path_segment.ident.to_string() { + *used = true; + found = true; + } + } + LocalVariable::Closure { name, used, .. } => { + if name == &path_segment.ident.to_string() { + *used = true; + found = true; + } + } + LocalVariable::Other { name, used, .. } => { + if name == &path_segment.ident.to_string() { + *used = true; + found = true; + } + } + }); + + if !found { + variables.push(LocalVariable::Other { + name: path_segment.ident.to_string(), + used: true, + }); + } + }; + + segments.iter().for_each(&mut check_segment); + } + + pub fn var_details(&self, fn_name: &str, var_name: &str) -> Option { + let vars = self.variables(fn_name); + + vars.into_iter().find(|var| var.name() == var_name) + } +} + +impl From<&str> for Syntest { + fn from(path: &str) -> Self { + Syntest::new(PathBuf::from(path)).unwrap() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_local_value_str() { + let content: &str = r#" + pub fn test_fn() { + let my_local_str = "local"; + } + "#; + let vars = Syntest::from_code(content).unwrap().variables("test_fn"); + + vars.iter().for_each(|var| match var { + LocalVariable::Str { name, value, used } => { + assert_eq!(name, "my_local_str"); + assert_eq!(value, "local"); + assert_eq!(*used, false); + } + _ => { + panic!("Invalid variable type") + } + }) + } + + #[test] + fn test_multiple_values() { + let content = r#" + pub fn test_fn() { + let my_local_int = 42; + let another_local_int = 10; + let local_str = "string"; + + let re_assigned = my_local_int + another_local_int; + } + "#; + + let vars = Syntest::from_code(content).unwrap().variables("test_fn"); + + vars.iter().for_each(|var| match var { + LocalVariable::Int { name, value, used } => match name.as_str() { + "my_local_int" => { + assert_eq!(*value, 42); + assert_eq!(*used, true); + } + "another_local_int" => { + assert_eq!(*value, 10); + assert_eq!(*used, true); + } + _ => { + panic!("Invalid variable name") + } + }, + LocalVariable::Str { name, value, used } => { + assert_eq!(name, "local_str"); + assert_eq!(value, "string"); + assert_eq!(*used, false); + } + LocalVariable::Other { name, used } => { + assert_eq!(name, "re_assigned"); + assert_eq!(*used, false); + } + _ => { + panic!("Invalid variable type") + } + }) + } + + #[test] + fn test_return_tail() { + let content = r#" + pub fn test_fn() { + let my_local_int = 42; + let another_local_int = 10; + let local_str = "string"; + + let re_assigned = my_local_int + another_local_int; + + re_assigned + } + "#; + + let vars = Syntest::from_code(content).unwrap().variables("test_fn"); + + let mut asserted = false; + + vars.iter().for_each(|var| match var { + LocalVariable::Other { name, used } => { + assert_eq!(name, "re_assigned"); + assert_eq!(*used, true); + asserted = true; + } + _ => {} + }); + + if !asserted { + panic!("No return value found"); + } + } + + #[test] + fn test_return_keyword() { + let content = r#" + pub fn test_fn() { + let my_local_int = 42; + let another_local_int = 10; + let local_str = "string"; + + let re_assigned = my_local_int + another_local_int; + + return re_assigned; + } + "#; + + let vars = Syntest::from_code(content).unwrap().variables("test_fn"); + + let mut asserted = false; + + vars.iter().for_each(|var| match var { + LocalVariable::Other { name, used } => { + assert_eq!(name, "re_assigned"); + assert_eq!(*used, true); + asserted = true; + } + _ => {} + }); + + if !asserted { + panic!("No return value found"); + } + } + + #[test] + #[should_panic(expected = "Function non_existent_fn not found")] + fn test_get_non_existent_function() { + let content = r#" + pub fn test_fn() { + let my_local_bool = true; + } + "#; + + Syntest::from_code(content) + .unwrap() + .variables("non_existent_fn"); + } + + #[test] + fn test_var_details() { + let content = r#" + pub fn test_fn() { + let my_local_int = 42; + let another_local_int = 10; + let local_str = "string"; + + let re_assigned = my_local_int + another_local_int; + + return re_assigned; + } + "#; + + let syntest = Syntest::from_code(content).unwrap(); + + let vars = [ + "my_local_int", + "another_local_int", + "local_str", + "re_assigned", + ]; + + vars.iter().for_each(|&var| { + let details = syntest.var_details("test_fn", var).unwrap(); + + if var == "local_str" { + assert_eq!(details.is_used(), false); + } else { + assert_eq!(details.is_used(), true); + } + + assert_eq!(details.name(), var); + }) + } + + #[test] + fn test_multiple_math_ops() { + let content = r#" + pub fn test_fn() { + let my_local_int = 42; + let another_local_int = 10; + let local_str = "string"; + + let re_assigned = my_local_int + another_local_int; + let re_assigned2 = re_assigned * 2; + let re_assigned3 = re_assigned2 / 2; + let re_assigned4 = re_assigned3 - 2; + + return re_assigned4; + } + "#; + + let syntest = Syntest::from_code(content).unwrap(); + + let vars = [ + "my_local_int", + "another_local_int", + "local_str", + "re_assigned", + "re_assigned2", + "re_assigned3", + "re_assigned4", + ]; + + vars.iter().for_each(|&var| { + let details = syntest.var_details("test_fn", var).unwrap(); + + if var == "local_str" { + assert_eq!(details.is_used(), false); + } else { + assert_eq!(details.is_used(), true); + } + + assert_eq!(details.name(), var, "Variable name mismatch"); + }) + } + + #[test] + fn test_multi_op_one_line() { + let content = r#" + pub fn test_fn() { + let my_local_int = 42; + let another_local_int = 10; + + let re_assigned = my_local_int + another_local_int * 2 / 2 - 2; + + return re_assigned; + } + "#; + let syntest = Syntest::from_code(content).unwrap(); + let vars = ["my_local_int", "another_local_int", "re_assigned"]; + + vars.iter().for_each(|&var| { + let details = syntest.var_details("test_fn", var).unwrap(); + + assert!(details.is_used(), "Variable {} not used", var); + assert_eq!(details.name(), var, "Variable name mismatch"); + }) + } +} diff --git a/crates/syntest/src/var.rs b/crates/syntest/src/var.rs new file mode 100644 index 0000000..16f3d38 --- /dev/null +++ b/crates/syntest/src/var.rs @@ -0,0 +1,93 @@ +use std::fmt::Display; + +#[derive(Debug, PartialEq)] +pub enum LocalVariable { + Str { + name: String, + value: String, + used: bool, + }, + Int { + name: String, + value: usize, + used: bool, + }, + Float { + name: String, + value: f64, + used: bool, + }, + Char { + name: String, + value: char, + used: bool, + }, + Bool { + name: String, + value: bool, + used: bool, + }, + Closure { + name: String, + used: bool, + }, + Other { + name: String, + used: bool, + }, +} + +#[derive(Debug, PartialEq)] +pub enum Value { + Str(String), + Int(usize), + Float(f64), + Char(char), + Bool(bool), + Closure, + Other, +} + +impl LocalVariable { + pub fn is_used(&self) -> bool { + match self { + LocalVariable::Str { used, .. } => *used, + LocalVariable::Int { used, .. } => *used, + LocalVariable::Float { used, .. } => *used, + LocalVariable::Char { used, .. } => *used, + LocalVariable::Bool { used, .. } => *used, + LocalVariable::Closure { used, .. } => *used, + LocalVariable::Other { used, .. } => *used, + } + } + + pub fn name(&self) -> &str { + match self { + LocalVariable::Str { name, .. } => name, + LocalVariable::Int { name, .. } => name, + LocalVariable::Float { name, .. } => name, + LocalVariable::Char { name, .. } => name, + LocalVariable::Bool { name, .. } => name, + LocalVariable::Closure { name, .. } => name, + LocalVariable::Other { name, .. } => name, + } + } + + pub fn value(&self) -> Value { + match self { + LocalVariable::Str { value, .. } => Value::Str(value.clone()), + LocalVariable::Int { value, .. } => Value::Int(*value), + LocalVariable::Float { value, .. } => Value::Float(*value), + LocalVariable::Char { value, .. } => Value::Char(*value), + LocalVariable::Bool { value, .. } => Value::Bool(*value), + LocalVariable::Closure { .. } => Value::Closure, + LocalVariable::Other { .. } => Value::Other, + } + } +} + +impl Display for LocalVariable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name()) + } +}