diff options
author | Martin Geisler <mgeisler@google.com> | 2024-03-19 12:19:30 +0100 |
---|---|---|
committer | Martin Geisler <mgeisler@google.com> | 2024-03-26 18:02:13 +0100 |
commit | 9f6ccd869b3a86f9243d5b225a183fb84bc183e0 (patch) | |
tree | 71caddf2114036698ed360a6999b394aa058de9e | |
parent | aa31e376b2032cee14c07c43ebcbcb352d35c6dc (diff) | |
download | debug_tree-9f6ccd869b3a86f9243d5b225a183fb84bc183e0.tar.gz |
Import 'debug_tree' crateupstream
Request Document: go/android-rust-importing-crates
For CL Reviewers: go/android3p#cl-review
For Build Team: go/ab-third-party-imports
Bug: http://b/328410074
Test: m libdebug_tree
Change-Id: I4283f42b85c79fe280d2eebe2be8f8283b6abe64
Change-Id: I537f1f12b44c56a83aa973b440271b19ca2bb40d
34 files changed, 2918 insertions, 0 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json new file mode 100644 index 0000000..27bd387 --- /dev/null +++ b/.cargo_vcs_info.json @@ -0,0 +1,5 @@ +{ + "git": { + "sha1": "5b709de2d8872102b20b566c408d31d0662d7a9f" + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d4fe7bd --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/target +**/*.rs.bk +.idea +*.lock +test_out + +.DS_Store diff --git a/.md-inc.toml b/.md-inc.toml new file mode 100644 index 0000000..57b0690 --- /dev/null +++ b/.md-inc.toml @@ -0,0 +1,2 @@ +base_dir = "examples" +files = ["README.md"]
\ No newline at end of file diff --git a/Android.bp b/Android.bp new file mode 100644 index 0000000..ed34a1a --- /dev/null +++ b/Android.bp @@ -0,0 +1,40 @@ +// This file is generated by cargo_embargo. +// Do not modify this file as changes will be overridden on upgrade. + +// TODO: Add license. +rust_test { + name: "debug_tree_test_src_lib", + host_supported: true, + crate_name: "debug_tree", + cargo_env_compat: true, + cargo_pkg_version: "0.4.0", + srcs: ["src/lib.rs"], + test_suites: ["general-tests"], + auto_gen_config: true, + test_options: { + unit_test: true, + }, + edition: "2018", + rustlibs: [ + "libfutures", + "libonce_cell", + "libtokio", + ], +} + +rust_library { + name: "libdebug_tree", + host_supported: true, + crate_name: "debug_tree", + cargo_env_compat: true, + cargo_pkg_version: "0.4.0", + srcs: ["src/lib.rs"], + edition: "2018", + rustlibs: ["libonce_cell"], + apex_available: [ + "//apex_available:platform", + "//apex_available:anyapex", + ], + product_available: true, + vendor_available: true, +} diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d6c6559 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,33 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies +# +# If you believe there's an error in this file please file an +# issue against the rust-lang/cargo repository. If you're +# editing this file be aware that the upstream Cargo.toml +# will likely look very different (and much more reasonable) + +[package] +edition = "2018" +name = "debug_tree" +version = "0.4.0" +authors = ["Marty Papamanolis <marty@mindpipess.com>"] +description = "Build a tree one element at a time and output it as a pretty string." +readme = "README.md" +license = "MIT" +repository = "https://github.com/martypapa/debug-tree" + +[lib] +name = "debug_tree" +crate-type = ["lib"] +[dependencies.once_cell] +version = "1" +[dev-dependencies.futures] +version = "0.3.4" + +[dev-dependencies.tokio] +version = "0.2.9" +features = ["macros", "fs"] @@ -0,0 +1,19 @@ +MIT License + +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. diff --git a/METADATA b/METADATA new file mode 100644 index 0000000..6537ff9 --- /dev/null +++ b/METADATA @@ -0,0 +1,20 @@ +name: "debug_tree" +description: "Build a tree one element at a time and output it as a pretty string." +third_party { + identifier { + type: "crates.io" + value: "debug_tree" + } + identifier { + type: "Archive" + value: "https://static.crates.io/crates/debug_tree/debug_tree-0.4.0.crate" + primary_source: true + } + version: "0.4.0" + license_type: NOTICE + last_upgrade_date { + year: 2024 + month: 3 + day: 21 + } +} diff --git a/MODULE_LICENSE_MIT b/MODULE_LICENSE_MIT new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/MODULE_LICENSE_MIT @@ -0,0 +1,2 @@ +# Bug component: 688011 +include platform/prebuilts/rust:main:/OWNERS diff --git a/README.md b/README.md new file mode 100644 index 0000000..84511aa --- /dev/null +++ b/README.md @@ -0,0 +1,284 @@ +# Debug Tree + +This library allows you to build a tree one element at a time and output it as a pretty string. + +The tree can easily be output to a `String`, `stdout` or a file. + +This is particularly convenient for generating clean output from nested and recursive functions. + +* [Recursive Fibonacci Example](#recursive-fibonacci-example) +* [Overview](#overview) +* [More Examples](#more-examples) + * [Multiple Tagged Trees](#multiple-tagged-trees) + * [Nested Functions](#nested-functions) + * [Panic](#panics) + * [Without Macros](#without-macros) + + +## Recursive Fibonacci Example + +Using the `add_branch!()` macro at the start of the `factors()` function, you can generate an entire call tree, with minimal effort. + +<!--{ fibonacci.rs | code: rust }--> +```rust +use debug_tree::*; + +fn factors(x: usize) { + add_branch!("{}", x); // <~ THE MAGIC LINE + for i in 1..x { + if x % i == 0 { + factors(i); + } + } +} + +fn main() { + // output to file at the end of this block + defer_write!("examples/out/fibonacci.txt"); + add_branch!("A Fibonacci Tree"); + factors(6); + add_leaf!("That's All Folks!"); +} +``` +<!--{ end }--> + +<!--{ out/fibonacci.txt | code }--> +``` +A Fibonacci Tree +├╼ 6 +│ ├╼ 1 +│ ├╼ 2 +│ │ └╼ 1 +│ └╼ 3 +│ └╼ 1 +└╼ That's All Folks! +``` +<!--{ end }--> + +## Overview + +- Add a branch + - `add_branch!("Hello, {}", "World")` + - The branch will exit at the end of the current block + +- Add a leaf + - `add_leaf!("I am a {}", "leaf")` + - Added to the current scoped branch + +- Print a tree, or write it to file at the end of a block + - `defer_print!()` + - `defer_write!("filename.txt")` + - The tree will be empty after these calls + - To prevent clearing, use `defer_peek_print!` and `defer_peek_write!` + + +- Handle multiple trees using named trees + - `add_branch_to!("A", "I'm a branch on tree 'A'")` + - `add_leaf_to!("A", "I'm a leaf on tree 'A'")` + - `defer_print!("A")` + - `defer_write!("A", "filename.txt")` + +- Get a named tree + - `tree("TREE_NAME")` + +- Retrieve the pretty-string from a tree + - `tree("TREE_NAME").string()` + + +- Usage across threads + - `default_tree()` is local to each thread + - Named trees are shared between threads + +## More Examples + +### Multiple Tagged Trees + +If you need multiple, separated trees you can use a name tag. + +<!--{ multiple_trees.rs | code: rust }--> +```rust +use debug_tree::*; + +fn populate(tree_name: &str, n_children: usize) { + add_branch_to!(tree_name, "{} TREE", tree_name); + for _ in 0..n_children { + populate(tree_name, n_children / 2); + } +} +fn main() { + // Override tree config (just for "B") + let b_tree = tree("B"); + b_tree.set_config_override( + TreeConfig::new() + .indent(4) + .symbols(TreeSymbols::with_rounded().leaf("> ")), + ); + defer_write!(b_tree, "examples/out/multiple_trees_B.txt"); + defer_write!("A", "examples/out/multiple_trees_A.txt"); + + populate("A", 2); + populate("B", 3); +} +``` +<!--{ end }--> +<!--{ out/multiple_trees_A.txt | code }--> +``` +A TREE +├╼ A TREE +│ └╼ A TREE +└╼ A TREE + └╼ A TREE +``` +<!--{ end }--> +<!--{ out/multiple_trees_B.txt | code }--> +``` +B TREE +├──> B TREE +│ ╰──> B TREE +├──> B TREE +│ ╰──> B TREE +╰──> B TREE + ╰──> B TREE +``` +<!--{ end }--> + +### Nested Functions + +Branches also make nested function calls a lot easier to follow. + +<!--{ nested.rs | code: rust }--> +```rust +use debug_tree::*; +fn a() { + add_branch!("a"); + b(); + c(); +} +fn b() { + add_branch!("b"); + c(); +} +fn c() { + add_branch!("c"); + add_leaf!("Nothing to see here"); +} + +fn main() { + defer_write!("examples/out/nested.txt"); + a(); +} +``` +<!--{ end }--> +<!--{ out/nested.txt | code }--> +``` +a +├╼ b +│ └╼ c +│ └╼ Nothing to see here +└╼ c + └╼ Nothing to see here +``` +<!--{ end }--> + +### Line Breaks + +Newlines in multi-line strings are automatically indented. + +<!--{ multi_line.rs | code: rust }--> +```rust +use debug_tree::*; +fn main() { + // output to file at the end of this block + defer_write!("examples/out/multi_line.txt"); + add_branch!("1"); + add_leaf!("1.1\nAnother line...\n... and one more line"); + add_leaf!("1.2"); +} +``` +<!--{ end }--> + +<!--{ out/multi_line.txt | code }--> +``` +1 +├╼ 1.1 +│ Another line... +│ ... and one more line +└╼ 1.2 +``` +<!--{ end }--> + +### Panics +Even if there is a panic, the tree is not lost! +The `defer_` functions were introduced to allow the tree +to be printed our written to file in the case of a `panic!` or early return. + +<!--{ panic.rs | code: rust }--> +```rust +use debug_tree::*; + +fn i_will_panic() { + add_branch!("Here are my last words"); + add_leaf!("Stay calm, and try not to panic"); + panic!("I told you so...") +} + +fn main() { + // output to file at the end of this block + defer_write!("examples/out/panic.txt"); + // print at the end of this block + { + add_branch!("By using the 'defer_' functions"); + add_branch!("Output will still be generated"); + add_branch!("Otherwise you might lose your valuable tree!"); + } + add_branch!("Now for something crazy..."); + i_will_panic(); +} +``` +<!--{ end }--> + +<!--{ out/panic.txt | code }--> +``` +By using the 'defer_' functions +└╼ Output will still be generated + └╼ Otherwise you might lose your valuable tree! +Now for something crazy... +└╼ Here are my last words + └╼ Stay calm, and try not to panic +``` +<!--{ end }--> + + +### Without Macros + +If you prefer not using macros, you can construct `TreeBuilder`s manually. + +<!--{ no_macros.rs | code: rust }--> +```rust +use debug_tree::TreeBuilder; + +fn main() { + // Make a new tree. + let tree = TreeBuilder::new(); + + // Add a scoped branch. The next item added will belong to the branch. + let mut branch = tree.add_branch("1 Branch"); + + // Add a leaf to the current branch + tree.add_leaf("1.1 Child"); + + // Leave scope early + branch.release(); + tree.add_leaf("2 Sibling"); + // output to file + tree.write("examples/out/no_macros.txt").ok(); // Write and flush. +} +``` +<!--{ end }--> +<!--{ out/no_macros.txt | code }--> +``` +1 Branch +└╼ 1.1 Child +2 Sibling +``` +<!--{ end }-->
\ No newline at end of file diff --git a/cargo_embargo.json b/cargo_embargo.json new file mode 100644 index 0000000..c8842d1 --- /dev/null +++ b/cargo_embargo.json @@ -0,0 +1,4 @@ +{ + "run_cargo": false, + "tests": true +} diff --git a/doc/build/LICENSE.adoc b/doc/build/LICENSE.adoc new file mode 100644 index 0000000..1d67901 --- /dev/null +++ b/doc/build/LICENSE.adoc @@ -0,0 +1,19 @@ +Copyright (C) 2014-2019 The Asciidoctor Project + +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.
\ No newline at end of file diff --git a/doc/build/asciidoc-coalescer.rb b/doc/build/asciidoc-coalescer.rb new file mode 100644 index 0000000..a4f9968 --- /dev/null +++ b/doc/build/asciidoc-coalescer.rb @@ -0,0 +1,66 @@ +#!/usr/bin/env ruby + +# This script coalesces the AsciiDoc content from a document master into a +# single output file. It does so by resolving all preprocessor directives in +# the document, and in any files which are included. The resolving of include +# directives is likely of most interest to users of this script. +# +# This script works by using Asciidoctor's PreprocessorReader to read and +# resolve all the lines in the specified input file. The script then writes the +# result to the output. +# +# The script only recognizes attributes passed in as options or those defined +# in the document header. It does not currently process attributes defined in +# other, arbitrary locations within the document. +# +# You can find a similar extension written against AsciidoctorJ here: +# https://github.com/hibernate/hibernate-asciidoctor-extensions/blob/master/src/main/java/org/hibernate/infra/asciidoctor/extensions/savepreprocessed/SavePreprocessedOutputPreprocessor.java + +# TODO +# - add cli option to write attributes passed to cli to header of document +# - escape all preprocessor directives after lines are processed (these are preprocessor directives that were escaped in the input) +# - wrap in a custom converter so it can be used as an extension + +require 'asciidoctor' +require 'optparse' + +options = { attributes: [], output: '-' } +OptionParser.new do |opts| + opts.banner = 'Usage: ruby asciidoc-coalescer.rb [OPTIONS] FILE' + opts.on('-a', '--attribute key[=value]', 'A document attribute to set in the form of key[=value]') do |a| + options[:attributes] << a + end + opts.on('-o', '--output FILE', 'Write output to FILE instead of stdout.') do |o| + options[:output] = o + end +end.parse! + +unless (source_file = ARGV.shift) + warn 'Please specify an AsciiDoc source file to coalesce.' + exit 1 +end + +unless (output_file = options[:output]) == '-' + if (output_file = File.expand_path output_file) == (File.expand_path source_file) + warn 'Source and output cannot be the same file.' + exit 1 + end +end + +# NOTE first, resolve attributes defined at the end of the document header +# QUESTION can we do this in a single load? +doc = Asciidoctor.load_file source_file, safe: :unsafe, header_only: true, attributes: options[:attributes] +# NOTE quick and dirty way to get the attributes set or unset by the document header +header_attr_names = (doc.instance_variable_get :@attributes_modified).to_a +header_attr_names.each {|k| doc.attributes[%(#{k}!)] = '' unless doc.attr? k } + +doc = Asciidoctor.load_file source_file, safe: :unsafe, parse: false, attributes: doc.attributes +# FIXME also escape ifdef, ifndef, ifeval and endif directives +# FIXME do this more carefully by reading line by line; if input differs by output by leading backslash, restore original line +lines = doc.reader.read.gsub(/^include::(?=.*\[\]$)/m, '\\include::') + +if output_file == '-' + puts lines +else + File.open(output_file, 'w') {|f| f.write lines } +end
\ No newline at end of file diff --git a/doc/readme_template.adoc b/doc/readme_template.adoc new file mode 100644 index 0000000..ec7cabb --- /dev/null +++ b/doc/readme_template.adoc @@ -0,0 +1,127 @@ +:examples: ../examples/ + += Debug Tree + +This library allows you to build a tree one element at a time and output it as a pretty string. + +The tree can easily be output to a `String`, `stdout` or a file. + +This is particularly convenient for generating clean output from nested and recursive functions. + + +:toc: + +== Recursive Fibonacci Example + +Using the `add_branch!()` macro at the start of the `factors()` function, you can generate an entire call tree, with minimal effort. + +[source,rust] +---- +include::{examples}fibonacci.rs[] +---- + +---- +include::{examples}out/fibonacci.txt[] +---- +== Overview + +* Add a branch +- `add_branch!("Hello, {}", "World")` +- The branch will exit at the end of the current block + +* Add a leaf +- `add_leaf!("I am a {}", "leaf")` +- Added to the current scoped branch + +* Print a tree, or write it to file at the end of a block +- `defer_print!()` +- `defer_write!("filename.txt")` +- The tree will be empty after these calls +- To prevent clearing, use `defer_peek_print!` and `defer_peek_write!` + +* Get the trees pretty-string +- + +* Handle multiple trees using named trees +- `add_branch_to!("A", "I'm a branch on tree 'A'")` +- `add_leaf_to!("A", "I'm a leaf on tree 'A'")` +- `defer_print!("A")` +- `defer_write!("A", "filename.txt")` + +* Get a named tree +- `tree("TREE_NAME")` + +* Retrieve the pretty-string from a tree +- `tree("TREE_NAME").string()` + + +* Usage across threads +- `default_tree()` is local to each thread +- Named trees are shared between threads + +== More Examples + +=== Multiple Tagged Trees + +If you need multiple, separated trees you can use a name tag. + +[source,rust] +---- +include::{examples}multiple_trees.rs[] +---- +---- +include::{examples}out/multiple_trees_A.txt[] +---- +---- +include::{examples}out/multiple_trees_B.txt[] +---- + +=== Nested Functions + +Branches also make nested function calls a lot easier to follow. + +[source,rust] +---- +include::{examples}nested.rs[] +---- +---- +include::{examples}out/nested.txt[] +---- + +=== Line Breaks + +Newlines in multi-line strings are automatically indented. + +[source,rust] +---- +include::{examples}multi_line.rs[] +---- +---- +include::{examples}out/multi_line.txt[] +---- + +=== Panics +Even if there is a panic, the tree is not lost! +The `defer_` functions were introduced to allow the tree +to be printed our written to file in the case of a `panic!` or early return. + +[source,rust] +---- +include::{examples}panic.rs[] +---- +---- +include::{examples}out/panic.txt[] +---- + + +=== Without Macros + +If you prefer not using macros, you can construct `TreeBuilder`s manually. + +[source,rust] +---- +include::{examples}no_macros.rs[] +---- +---- +include::{examples}out/no_macros.txt[] +---- diff --git a/examples/fibonacci.rs b/examples/fibonacci.rs new file mode 100644 index 0000000..1095592 --- /dev/null +++ b/examples/fibonacci.rs @@ -0,0 +1,18 @@ +use debug_tree::*; + +fn factors(x: usize) { + add_branch!("{}", x); // <~ THE MAGIC LINE + for i in 1..x { + if x % i == 0 { + factors(i); + } + } +} + +fn main() { + // output to file at the end of this block + defer_write!("examples/out/fibonacci.txt"); + add_branch!("A Fibonacci Tree"); + factors(6); + add_leaf!("That's All Folks!"); +} diff --git a/examples/multi_line.rs b/examples/multi_line.rs new file mode 100644 index 0000000..fbe9c6c --- /dev/null +++ b/examples/multi_line.rs @@ -0,0 +1,8 @@ +use debug_tree::*; +fn main() { + // output to file at the end of this block + defer_write!("examples/out/multi_line.txt"); + add_branch!("1"); + add_leaf!("1.1\nAnother line...\n... and one more line"); + add_leaf!("1.2"); +} diff --git a/examples/multiple_trees.rs b/examples/multiple_trees.rs new file mode 100644 index 0000000..01a4941 --- /dev/null +++ b/examples/multiple_trees.rs @@ -0,0 +1,22 @@ +use debug_tree::*; + +fn populate(tree_name: &str, n_children: usize) { + add_branch_to!(tree_name, "{} TREE", tree_name); + for _ in 0..n_children { + populate(tree_name, n_children / 2); + } +} +fn main() { + // Override tree config (just for "B") + let b_tree = tree("B"); + b_tree.set_config_override( + TreeConfig::new() + .indent(4) + .symbols(TreeSymbols::with_rounded().leaf("> ")), + ); + defer_write!(b_tree, "examples/out/multiple_trees_B.txt"); + defer_write!("A", "examples/out/multiple_trees_A.txt"); + + populate("A", 2); + populate("B", 3); +} diff --git a/examples/nested.rs b/examples/nested.rs new file mode 100644 index 0000000..5315be5 --- /dev/null +++ b/examples/nested.rs @@ -0,0 +1,19 @@ +use debug_tree::*; +fn a() { + add_branch!("a"); + b(); + c(); +} +fn b() { + add_branch!("b"); + c(); +} +fn c() { + add_branch!("c"); + add_leaf!("Nothing to see here"); +} + +fn main() { + defer_write!("examples/out/nested.txt"); + a(); +} diff --git a/examples/no_macros.rs b/examples/no_macros.rs new file mode 100644 index 0000000..1891bf4 --- /dev/null +++ b/examples/no_macros.rs @@ -0,0 +1,18 @@ +use debug_tree::TreeBuilder; + +fn main() { + // Make a new tree. + let tree = TreeBuilder::new(); + + // Add a scoped branch. The next item added will belong to the branch. + let mut branch = tree.add_branch("1 Branch"); + + // Add a leaf to the current branch + tree.add_leaf("1.1 Child"); + + // Leave scope early + branch.release(); + tree.add_leaf("2 Sibling"); + // output to file + tree.write("examples/out/no_macros.txt").ok(); // Write and flush. +} diff --git a/examples/out/fibonacci.txt b/examples/out/fibonacci.txt new file mode 100644 index 0000000..40725b8 --- /dev/null +++ b/examples/out/fibonacci.txt @@ -0,0 +1,8 @@ +A Fibonacci Tree +├╼ 6 +│ ├╼ 1 +│ ├╼ 2 +│ │ └╼ 1 +│ └╼ 3 +│ └╼ 1 +└╼ That's All Folks!
\ No newline at end of file diff --git a/examples/out/multi_line.txt b/examples/out/multi_line.txt new file mode 100644 index 0000000..93e17ff --- /dev/null +++ b/examples/out/multi_line.txt @@ -0,0 +1,5 @@ +1 +├╼ 1.1 +│ Another line... +│ ... and one more line +└╼ 1.2
\ No newline at end of file diff --git a/examples/out/multiple_trees_A.txt b/examples/out/multiple_trees_A.txt new file mode 100644 index 0000000..d4b76d4 --- /dev/null +++ b/examples/out/multiple_trees_A.txt @@ -0,0 +1,5 @@ +A TREE +├╼ A TREE +│ └╼ A TREE +└╼ A TREE + └╼ A TREE
\ No newline at end of file diff --git a/examples/out/multiple_trees_B.txt b/examples/out/multiple_trees_B.txt new file mode 100644 index 0000000..b2cce80 --- /dev/null +++ b/examples/out/multiple_trees_B.txt @@ -0,0 +1,7 @@ +B TREE +├──> B TREE +│ ╰──> B TREE +├──> B TREE +│ ╰──> B TREE +╰──> B TREE + ╰──> B TREE
\ No newline at end of file diff --git a/examples/out/nested.txt b/examples/out/nested.txt new file mode 100644 index 0000000..f629559 --- /dev/null +++ b/examples/out/nested.txt @@ -0,0 +1,6 @@ +a +├╼ b +│ └╼ c +│ └╼ Nothing to see here +└╼ c + └╼ Nothing to see here
\ No newline at end of file diff --git a/examples/out/no_macros.txt b/examples/out/no_macros.txt new file mode 100644 index 0000000..9ee6b47 --- /dev/null +++ b/examples/out/no_macros.txt @@ -0,0 +1,3 @@ +1 Branch +└╼ 1.1 Child +2 Sibling
\ No newline at end of file diff --git a/examples/out/panic.txt b/examples/out/panic.txt new file mode 100644 index 0000000..3e5b29e --- /dev/null +++ b/examples/out/panic.txt @@ -0,0 +1,6 @@ +By using the 'defer_' functions +└╼ Output will still be generated + └╼ Otherwise you might lose your valuable tree! +Now for something crazy... +└╼ Here are my last words + └╼ Stay calm, and try not to panic
\ No newline at end of file diff --git a/examples/panic.rs b/examples/panic.rs new file mode 100644 index 0000000..e1a9563 --- /dev/null +++ b/examples/panic.rs @@ -0,0 +1,20 @@ +use debug_tree::*; + +fn i_will_panic() { + add_branch!("Here are my last words"); + add_leaf!("Stay calm, and try not to panic"); + panic!("I told you so...") +} + +fn main() { + // output to file at the end of this block + defer_write!("examples/out/panic.txt"); + // print at the end of this block + { + add_branch!("By using the 'defer_' functions"); + add_branch!("Output will still be generated"); + add_branch!("Otherwise you might lose your valuable tree!"); + } + add_branch!("Now for something crazy..."); + i_will_panic(); +} diff --git a/src/default.rs b/src/default.rs new file mode 100644 index 0000000..75efbb8 --- /dev/null +++ b/src/default.rs @@ -0,0 +1,167 @@ +use crate::TreeBuilder; + +/// Returns the default tree for the current thread +/// +/// # Example +/// +/// ``` +/// use debug_tree::default_tree; +/// default_tree().add_leaf("A new leaf"); +/// assert_eq!("A new leaf", default_tree().peek_string()); +/// ``` +pub fn default_tree() -> TreeBuilder { + thread_local! { + static DEFAULT_BUILDER: TreeBuilder = TreeBuilder::new(); + } + DEFAULT_BUILDER.with(|f| f.clone()) +} + +/// Adds a leaf to the default tree with the given text and formatting arguments +/// +/// # Arguments +/// * `text...` - Formatted text arguments, as per `format!(...)`. +/// +/// # Example +/// +/// ``` +/// #[macro_use] +/// use debug_tree::{default_tree, add_leaf}; +/// fn main() { +/// add_leaf!("A {} leaf", "new"); +/// assert_eq!("A new leaf", &default_tree().peek_string()); +/// } +/// ``` +#[macro_export] +macro_rules! add_leaf { + ($($arg:tt)*) => { + if $crate::default::default_tree().is_enabled() { + $crate::default::default_tree().add_leaf(&format!($($arg)*)) + } + }; + } + +/// Adds the value as a leaf to the default tree. +/// +/// Returns the given `value` argument. +/// +/// # Arguments +/// * `value` - An expression that implements the `Display` trait. +/// +/// # Example +/// +/// ``` +/// #[macro_use] +/// use debug_tree::{default_tree, add_leaf_value}; +/// fn main() { +/// let value = add_leaf_value!(10); +/// assert_eq!("10", &default_tree().string()); +/// assert_eq!(10, value); +/// } +/// ``` +#[macro_export] +macro_rules! add_leaf_value { + ($value:expr) => {{ + let v = $value; + if $crate::default::default_tree().is_enabled() { + $crate::default::default_tree().add_leaf(&format!("{}", &v)); + } + v + }}; +} + +/// Adds a scoped branch to the default tree with the given text and formatting arguments +/// The branch will be exited at the end of the current block. +/// +/// # Arguments +/// * `text...` - Formatted text arguments, as per `format!(...)`. +/// +/// # Example +/// +/// ``` +/// #[macro_use] +/// use debug_tree::{default_tree, add_branch, add_leaf}; +/// fn main() { +/// { +/// add_branch!("New {}", "Branch"); // _branch enters scope +/// // tree is now pointed inside new branch. +/// add_leaf!("Child of {}", "Branch"); +/// // Block ends, so tree exits the current branch. +/// } +/// add_leaf!("Sibling of {}", "Branch"); +/// assert_eq!("\ +/// New Branch +/// └╼ Child of Branch +/// Sibling of Branch" , &default_tree().string()); +/// } +/// ``` +#[macro_export] +macro_rules! add_branch { + () => { + let _debug_tree_branch = if $crate::default::default_tree().is_enabled() { + $crate::default::default_tree().enter_scoped() + } else { + $crate::scoped_branch::ScopedBranch::none() + }; + }; + ($($arg:tt)*) => { + let _debug_tree_branch = if $crate::default::default_tree().is_enabled() { + $crate::default::default_tree().add_branch(&format!($($arg)*)) + } else { + $crate::scoped_branch::ScopedBranch::none() + }; + }; + +} + +#[cfg(test)] +mod test { + use crate::default_tree; + use crate::*; + + #[test] + fn unnamed_branch() { + add_leaf!("1"); + add_branch!(); + add_leaf!("1.1"); + { + add_branch!(); + add_leaf!("1.1.1"); + } + add_leaf!("1.2"); + default_tree().peek_print(); + assert_eq!( + "\ +1 +├╼ 1.1 +│ └╼ 1.1.1 +└╼ 1.2", + default_tree().string() + ); + } + #[test] + fn named_branch() { + add_branch!("11"); + { + add_branch!("11.1"); + add_leaf!("11.1.1"); + } + add_leaf!("11.2"); + default_tree().peek_print(); + assert_eq!( + "\ +11 +├╼ 11.1 +│ └╼ 11.1.1 +└╼ 11.2", + default_tree().string() + ); + } + + #[test] + fn leaf_with_value() { + let value = add_leaf_value!(10); + default_tree().peek_print(); + assert_eq!("10", default_tree().string()); + assert_eq!(10, value); + } +} diff --git a/src/defer.rs b/src/defer.rs new file mode 100644 index 0000000..2158a1d --- /dev/null +++ b/src/defer.rs @@ -0,0 +1,46 @@ +use crate::TreeBuilder; + +/// A deferred function called with an argument, `TreeBuilder` +pub struct DeferredFn<F: Fn(TreeBuilder) -> ()> { + tree: Option<TreeBuilder>, + action: Option<F>, +} + +impl<F> DeferredFn<F> +where + F: Fn(TreeBuilder) -> (), +{ + /// Create a new deferred function based on `tree` + pub fn new(tree: TreeBuilder, action: F) -> Self { + DeferredFn { + tree: Some(tree), + action: Some(action), + } + } + /// Create an empty deferred function + /// This does nothing when scope ends + pub fn none() -> Self { + DeferredFn { + tree: None, + action: None, + } + } + + /// Disables the deferred function + /// This prevents the function from executing when the scope ends + pub fn cancel(&mut self) { + self.tree = None; + self.action = None; + } +} + +impl<F> Drop for DeferredFn<F> +where + F: Fn(TreeBuilder) -> (), +{ + fn drop(&mut self) { + if let (Some(x), Some(action)) = (&self.tree, &self.action) { + action(x.clone()); + } + } +} diff --git a/src/internal.rs b/src/internal.rs new file mode 100644 index 0000000..d8bf0a9 --- /dev/null +++ b/src/internal.rs @@ -0,0 +1,281 @@ +use crate::tree_config::{tree_config, TreeConfig}; +use std::cmp::max; +use std::sync::{Arc, Mutex}; + +/// Tree that holds `text` for the current leaf and a list of `children` that are the branches. +#[derive(Debug)] +pub struct Tree { + pub text: Option<String>, + pub children: Vec<Tree>, +} + +/// Position of the element relative to its siblings +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Position { + Inside, + First, + Last, + Only, +} + +impl Tree { + /// Create a new tree with some optional text. + pub fn new(text: Option<&str>) -> Tree { + Tree { + text: text.map(|x| x.to_string()), + children: Vec::new(), + } + } + + /// Navigate to the branch at the given `path` relative to this tree. + /// If a valid branch is found by following the path, it is returned. + pub fn at_mut(&mut self, path: &[usize]) -> Option<&mut Tree> { + match path.first() { + Some(&i) => match self.children.get_mut(i) { + Some(x) => x.at_mut(&path[1..]), + _ => None, + }, + _ => Some(self), + } + } + + /// "Render" this tree as a list of `String`s. + /// Each string represents a line in the tree. + /// `does_continue` is a bool for each column indicating whether the tree continues. + pub fn lines( + &self, + does_continue: &Vec<bool>, + index: usize, + pool_size: usize, + config: &TreeConfig, + ) -> Vec<String> { + let does_continue = if config.show_first_level && does_continue.is_empty() { + vec![true] + } else { + does_continue.clone() + }; + let position = match index { + _ if pool_size == 1 => Position::Only, + _ if (index + 1) == pool_size => Position::Last, + 0 => Position::First, + _ => Position::Inside, + }; + let mut next_continue = does_continue.clone(); + next_continue.push(match position { + Position::Inside | Position::First => true, + Position::Last | Position::Only => false, + }); + + let mut txt = String::new(); + let pad: String; + if does_continue.len() > 1 { + for &i in &does_continue[2..] { + txt.push_str(&format!( + "{}{:indent$}", + if i { config.symbols.continued } else { " " }, + "", + indent = max(config.indent, 1) - 1 + )); + } + pad = txt.clone(); + let branch_size = max(config.indent, 2usize) - 2; + let branch = match config.symbols.branch.len() { + 0 => "-".repeat(branch_size), + 1 => config.symbols.branch.repeat(branch_size), + _n => config + .symbols + .branch + .repeat(branch_size) + .chars() + .take(branch_size) + .collect::<String>(), + }; + + let is_multiline = self + .text + .as_ref() + .map(|x| x.contains("\n")) + .unwrap_or(false); + + let first_leaf = match (is_multiline, config.symbols.multiline_first) { + (true, Some(x)) => x, + _ => config.symbols.leaf, + }; + txt.push_str(&format!( + "{}{}{}", + match position { + Position::Only => config.symbols.join_only, + Position::First => config.symbols.join_first, + Position::Last => config.symbols.join_last, + Position::Inside => config.symbols.join_inner, + }, + branch, + first_leaf, + )); + + let s = match &self.text { + Some(x) => match is_multiline { + true => format!( + "{}", + x.replace( + "\n", + &format!( + "\n{}{}{}{}", + &pad, + match position { + Position::Only | Position::Last => + " ".repeat(config.symbols.continued.chars().count()), + _ => config.symbols.continued.to_string(), + }, + " ".repeat(branch_size), + match &config.symbols.multiline_continued { + Some(multi) => multi.to_string(), + _ => " ".repeat(first_leaf.chars().count()), + } + ), + ) + ), + false => x.clone(), + }, + _ => String::new(), + }; + txt.push_str(&s); + } else { + if let Some(x) = &self.text { + txt.push_str(&x); + } + } + let mut ret = vec![txt]; + for (index, x) in self.children.iter().enumerate() { + for line in x.lines(&next_continue, index, self.children.len(), config) { + ret.push(line); + } + } + ret + } +} + +/// Holds the current state of the tree, including the path to the branch. +/// Multiple trees may point to the same data. +#[derive(Debug, Clone)] +pub(crate) struct TreeBuilderBase { + data: Arc<Mutex<Tree>>, + path: Vec<usize>, + dive_count: usize, + config: Option<TreeConfig>, + is_enabled: bool, +} + +impl TreeBuilderBase { + /// Create a new state + pub fn new() -> TreeBuilderBase { + TreeBuilderBase { + data: Arc::new(Mutex::new(Tree::new(None))), + path: vec![], + dive_count: 1, + config: None, + is_enabled: true, + } + } + + pub fn set_enabled(&mut self, enabled: bool) { + self.is_enabled = enabled; + } + pub fn is_enabled(&self) -> bool { + self.is_enabled + } + + pub fn add_leaf(&mut self, text: &str) { + let &dive_count = &self.dive_count; + if dive_count > 0 { + for i in 0..dive_count { + let mut n = 0; + if let Some(x) = self.data.lock().unwrap().at_mut(&self.path) { + x.children.push(Tree::new(if i == max(1, dive_count) - 1 { + Some(&text) + } else { + None + })); + n = x.children.len() - 1; + } + self.path.push(n); + } + self.dive_count = 0; + } else { + if let Some(x) = self + .data + .lock() + .unwrap() + .at_mut(&self.path[..max(1, self.path.len()) - 1]) + { + x.children.push(Tree::new(Some(&text))); + let n = match self.path.last() { + Some(&x) => x + 1, + _ => 0, + }; + self.path.last_mut().map(|x| *x = n); + } + } + } + + pub fn set_config_override(&mut self, config: Option<TreeConfig>) { + self.config = config; + } + + pub fn config_override(&self) -> &Option<TreeConfig> { + &self.config + } + pub fn config_override_mut(&mut self) -> &mut Option<TreeConfig> { + &mut self.config + } + + pub fn enter(&mut self) { + self.dive_count += 1; + } + + /// Try stepping up to the parent tree branch. + /// Returns false if already at the top branch. + pub fn exit(&mut self) -> bool { + if self.dive_count > 0 { + self.dive_count -= 1; + true + } else { + if self.path.len() > 1 { + self.path.pop(); + true + } else { + false + } + } + } + + pub fn depth(&self) -> usize { + max(1, self.path.len() + self.dive_count) - 1 + } + + pub fn peek_print(&self) { + println!("{}", self.peek_string()); + } + + pub fn print(&mut self) { + self.peek_print(); + self.clear(); + } + pub fn clear(&mut self) { + *self = Self::new(); + } + + pub fn string(&mut self) -> String { + let s = self.peek_string(); + self.clear(); + s + } + + pub fn peek_string(&self) -> String { + let config = self + .config_override() + .clone() + .unwrap_or_else(|| tree_config().clone()); + (&self.data.lock().unwrap().lines(&vec![], 0, 1, &config)[1..]).join("\n") + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..3da09a2 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,805 @@ +use std::sync::{Arc, Mutex}; + +#[macro_use] +pub mod default; +mod internal; +pub mod scoped_branch; + +pub mod defer; +mod test; +pub mod tree_config; + +pub use default::default_tree; +use once_cell::sync::Lazy; +use scoped_branch::ScopedBranch; +use std::collections::BTreeMap; +use std::fs::File; +use std::io::Write; + +pub use crate::tree_config::*; + +/// Reference wrapper for `TreeBuilderBase` +#[derive(Debug, Clone)] +pub struct TreeBuilder(Arc<Mutex<internal::TreeBuilderBase>>); + +impl TreeBuilder { + /// Returns a new `TreeBuilder` with an empty `Tree`. + /// + /// # Example + /// + /// ``` + /// use debug_tree::TreeBuilder; + /// let tree = TreeBuilder::new(); + /// ``` + pub fn new() -> TreeBuilder { + TreeBuilder { + 0: Arc::new(Mutex::new(internal::TreeBuilderBase::new())), + } + } + + /// Set the configuration override for displaying trees + /// + /// # Example + /// + /// ``` + /// use debug_tree::{TreeBuilder, add_branch_to, add_leaf_to, TreeSymbols, TreeConfig}; + /// let tree = TreeBuilder::new(); + /// { + /// add_branch_to!(tree, "1"); + /// { + /// add_branch_to!(tree, "1.1"); + /// add_leaf_to!(tree, "1.1.1"); + /// add_leaf_to!(tree, "1.1.2"); + /// } + /// add_leaf_to!(tree, "1.2"); + /// } + /// add_leaf_to!(tree, "2"); + /// tree.set_config_override(TreeConfig::new() + /// .show_first_level() + /// .symbols(TreeSymbols::with_rounded())); + /// tree.peek_print(); + /// assert_eq!("\ + /// ├╼ 1 + /// │ ├╼ 1.1 + /// │ │ ├╼ 1.1.1 + /// │ │ ╰╼ 1.1.2 + /// │ ╰╼ 1.2 + /// ╰╼ 2" , &tree.string()); + /// ``` + pub fn set_config_override(&self, config: TreeConfig) { + let mut lock = self.0.lock().unwrap(); + lock.set_config_override(Some(config)) + } + + /// Remove the configuration override + /// The default configuration will be used instead + pub fn remove_config_override(&self) { + self.0.lock().unwrap().set_config_override(None); + } + + /// Update the configuration override for displaying trees + /// If an override doesn't yet exist, it is created. + /// + /// # Example + /// + /// ``` + /// use debug_tree::{TreeBuilder, add_branch_to, add_leaf_to, TreeSymbols}; + /// let tree = TreeBuilder::new(); + /// { + /// add_branch_to!(tree, "1"); + /// { + /// add_branch_to!(tree, "1.1"); + /// add_leaf_to!(tree, "1.1.1"); + /// add_leaf_to!(tree, "1.1.2"); + /// } + /// add_leaf_to!(tree, "1.2"); + /// } + /// add_leaf_to!(tree, "2"); + /// tree.update_config_override(|x|{ + /// x.indent = 3; + /// x.symbols = TreeSymbols::with_rounded(); + /// x.show_first_level = true; + /// }); + /// tree.peek_print(); + /// assert_eq!("\ + /// ├─╼ 1 + /// │ ├─╼ 1.1 + /// │ │ ├─╼ 1.1.1 + /// │ │ ╰─╼ 1.1.2 + /// │ ╰─╼ 1.2 + /// ╰─╼ 2" , &tree.string()); + /// ``` + pub fn update_config_override<F: Fn(&mut TreeConfig)>(&self, update: F) { + let mut lock = self.0.lock().unwrap(); + match lock.config_override_mut() { + Some(x) => update(x), + None => { + let mut x = TreeConfig::default(); + update(&mut x); + lock.set_config_override(Some(x)); + } + } + } + + /// Returns the optional configuration override. + pub fn get_config_override(&self) -> Option<TreeConfig> { + let lock = self.0.lock().unwrap(); + lock.config_override().clone() + } + + /// Returns whether a configuration override is set. + pub fn has_config_override(&self) -> bool { + let lock = self.0.lock().unwrap(); + lock.config_override().is_some() + } + + /// Adds a new branch with text, `text` and returns a `ScopedBranch`. + /// When the returned `ScopedBranch` goes out of scope, (likely the end of the current block), + /// or if its `release()` method is called, the tree will step back out of the added branch. + /// + /// # Arguments + /// * `text` - A string slice to use as the newly added branch's text. + /// + /// # Examples + /// + /// Exiting branch when end of scope is reached. + /// ``` + /// use debug_tree::TreeBuilder; + /// let tree = TreeBuilder::new(); + /// { + /// let _branch = tree.add_branch("Branch"); // _branch enters scope + /// // tree is now pointed inside new branch. + /// tree.add_leaf("Child of Branch"); + /// // _branch leaves scope, tree moves up to parent branch. + /// } + /// tree.add_leaf("Sibling of Branch"); + /// assert_eq!("\ + /// Branch + /// └╼ Child of Branch + /// Sibling of Branch" , &tree.string()); + /// ``` + /// + /// Using `release()` before out of scope. + /// ``` + /// use debug_tree::TreeBuilder; + /// let tree = TreeBuilder::new(); + /// { + /// let mut branch = tree.add_branch("Branch"); // branch enters scope + /// // tree is now pointed inside new branch. + /// tree.add_leaf("Child of Branch"); + /// branch.release(); + /// tree.add_leaf("Sibling of Branch"); + /// // branch leaves scope, but no effect because its `release()` method has already been called + /// } + /// assert_eq!("\ + /// Branch + /// └╼ Child of Branch + /// Sibling of Branch", &tree.string()); + /// ``` + pub fn add_branch(&self, text: &str) -> ScopedBranch { + self.add_leaf(text); + ScopedBranch::new(self.clone()) + } + + /// Adds a new branch with text, `text` and returns a `ScopedBranch`. + /// When the returned `ScopedBranch` goes out of scope, (likely the end of the current block), + /// or if its `release()` method is called, the tree tree will step back out of the added branch. + /// + /// # Arguments + /// * `text` - A string slice to use as the newly added branch's text. + /// + /// # Examples + /// + /// Stepping out of branch when end of scope is reached. + /// ``` + /// use debug_tree::TreeBuilder; + /// let tree = TreeBuilder::new(); + /// { + /// tree.add_leaf("Branch"); + /// let _branch = tree.enter_scoped(); // _branch enters scope + /// // tree is now pointed inside new branch. + /// tree.add_leaf("Child of Branch"); + /// // _branch leaves scope, tree moves up to parent branch. + /// } + /// tree.add_leaf("Sibling of Branch"); + /// assert_eq!("\ + /// Branch + /// └╼ Child of Branch + /// Sibling of Branch", &tree.string()); + /// ``` + /// + /// Using `release()` before out of scope. + /// ``` + /// use debug_tree::TreeBuilder; + /// let tree = TreeBuilder::new(); + /// { + /// tree.add_leaf("Branch"); + /// let mut branch = tree.enter_scoped(); // branch enters scope + /// // tree is now pointed inside new branch. + /// tree.add_leaf("Child of Branch"); + /// branch.release(); + /// tree.add_leaf("Sibling of Branch"); + /// // branch leaves scope, but no effect because its `release()` method has already been called + /// } + /// assert_eq!("\ + /// Branch + /// └╼ Child of Branch + /// Sibling of Branch", &tree.string()); + /// ``` + pub fn enter_scoped(&self) -> ScopedBranch { + if self.is_enabled() { + ScopedBranch::new(self.clone()) + } else { + ScopedBranch::none() + } + } + + /// Adds a leaf to current branch with the given text, `text`. + /// + /// # Arguments + /// * `text` - A string slice to use as the newly added leaf's text. + /// + /// # Example + /// + /// ``` + /// use debug_tree::TreeBuilder; + /// let tree = TreeBuilder::new(); + /// tree.add_leaf("New leaf"); + /// ``` + pub fn add_leaf(&self, text: &str) { + let mut x = self.0.lock().unwrap(); + if x.is_enabled() { + x.add_leaf(&text); + } + } + + /// Steps into a new child branch. + /// Stepping out of the branch requires calling `exit()`. + /// + /// # Example + /// ``` + /// use debug_tree::TreeBuilder; + /// let tree = TreeBuilder::new(); + /// tree.add_leaf("Branch"); + /// tree.enter(); + /// tree.add_leaf("Child of Branch"); + /// assert_eq!("\ + /// Branch + /// └╼ Child of Branch", &tree.string()); + /// ``` + pub fn enter(&self) { + let mut x = self.0.lock().unwrap(); + if x.is_enabled() { + x.enter(); + } + } + + /// Exits the current branch, to the parent branch. + /// If no parent branch exists, no action is taken + /// + /// # Example + /// + /// ``` + /// use debug_tree::TreeBuilder; + /// let tree = TreeBuilder::new(); + /// tree.add_leaf("Branch"); + /// tree.enter(); + /// tree.add_leaf("Child of Branch"); + /// tree.exit(); + /// tree.add_leaf("Sibling of Branch"); + /// assert_eq!("\ + /// Branch + /// └╼ Child of Branch + /// Sibling of Branch", &tree.string()); + /// ``` + pub fn exit(&self) -> bool { + let mut x = self.0.lock().unwrap(); + if x.is_enabled() { + x.exit() + } else { + false + } + } + + /// Returns the depth of the current branch + /// The initial depth when no branches have been adeed is 0. + /// + /// # Example + /// + /// ``` + /// use debug_tree::TreeBuilder; + /// let tree = TreeBuilder::new(); + /// assert_eq!(0, tree.depth()); + /// let _b = tree.add_branch("Branch"); + /// assert_eq!(1, tree.depth()); + /// let _b = tree.add_branch("Child branch"); + /// assert_eq!(2, tree.depth()); + /// ``` + pub fn depth(&self) -> usize { + self.0.lock().unwrap().depth() + } + + /// Prints the tree without clearing. + /// + /// # Example + /// + /// ``` + /// use debug_tree::TreeBuilder; + /// let tree = TreeBuilder::new(); + /// tree.add_leaf("Leaf"); + /// tree.peek_print(); + /// // Leaf + /// tree.peek_print(); + /// // Leaf + /// // Leaf 2 + /// ``` + pub fn peek_print(&self) { + self.0.lock().unwrap().peek_print(); + } + + /// Prints the tree and then clears it. + /// + /// # Example + /// + /// ``` + /// use debug_tree::TreeBuilder; + /// let tree = TreeBuilder::new(); + /// tree.add_leaf("Leaf"); + /// tree.print(); + /// // Leaf + /// tree.add_leaf("Leaf 2"); + /// tree.print(); + /// // Leaf 2 + /// ``` + pub fn print(&self) { + self.0.lock().unwrap().print(); + } + + /// Returns the tree as a string without clearing the tree. + /// + /// # Example + /// + /// ``` + /// use debug_tree::TreeBuilder; + /// let tree = TreeBuilder::new(); + /// tree.add_leaf("Leaf"); + /// assert_eq!("Leaf", tree.peek_string()); + /// tree.add_leaf("Leaf 2"); + /// assert_eq!("Leaf\nLeaf 2", tree.peek_string()); + /// ``` + pub fn peek_string(&self) -> String { + self.0.lock().unwrap().peek_string() + } + + /// Returns the tree as a string and clears the tree. + /// + /// # Example + /// + /// ``` + /// use debug_tree::TreeBuilder; + /// let tree = TreeBuilder::new(); + /// tree.add_leaf("Leaf"); + /// assert_eq!("Leaf", tree.string()); + /// tree.add_leaf("Leaf 2"); + /// assert_eq!("Leaf 2", tree.string()); + /// ``` + pub fn string(&self) -> String { + self.0.lock().unwrap().string() + } + + /// Writes the tree to file without clearing. + /// + /// # Example + /// + /// ``` + /// use debug_tree::TreeBuilder; + /// use std::fs::{read_to_string, create_dir}; + /// use std::io::Read; + /// let tree = TreeBuilder::new(); + /// create_dir("test_out").ok(); + /// tree.add_leaf("Leaf"); + /// assert_eq!(tree.peek_string(), "Leaf"); + /// tree.peek_write("test_out/peek_write.txt"); + /// assert_eq!(read_to_string("test_out/peek_write.txt").unwrap(), "Leaf"); + /// assert_eq!(tree.peek_string(), "Leaf"); + /// ``` + pub fn peek_write(&self, path: &str) -> std::io::Result<()> { + let mut file = File::create(path)?; + file.write_all(self.peek_string().as_bytes()) + } + + /// Writes the tree to file without clearing. + /// + /// # Example + /// + /// ``` + /// use debug_tree::TreeBuilder; + /// use std::io::Read; + /// use std::fs::{read_to_string, create_dir}; + /// let tree = TreeBuilder::new(); + /// create_dir("test_out").ok(); + /// tree.add_leaf("Leaf"); + /// assert_eq!(tree.peek_string(), "Leaf"); + /// tree.write("test_out/write.txt"); + /// assert_eq!(read_to_string("test_out/write.txt").unwrap(), "Leaf"); + /// assert_eq!(tree.peek_string(), ""); + /// ``` + pub fn write(&self, path: &str) -> std::io::Result<()> { + let mut file = File::create(path)?; + file.write_all(self.string().as_bytes()) + } + + /// Clears the tree. + /// + /// # Example + /// + /// ``` + /// use debug_tree::TreeBuilder; + /// let tree = TreeBuilder::new(); + /// tree.add_leaf("Leaf"); + /// assert_eq!("Leaf", tree.peek_string()); + /// tree.clear(); + /// assert_eq!("", tree.peek_string()); + /// ``` + pub fn clear(&self) { + self.0.lock().unwrap().clear() + } + + /// Sets the enabled state of the tree. + /// + /// If not enabled, the tree will not be modified by adding leaves or branches. + /// Additionally, if called using the `add_`... macros, arguments will not be processed. + /// This is particularly useful for suppressing output in production, with very little overhead. + /// + /// # Example + /// ``` + /// #[macro_use] + /// use debug_tree::{TreeBuilder, add_leaf_to}; + /// let mut tree = TreeBuilder::new(); + /// tree.add_leaf("Leaf 1"); + /// tree.set_enabled(false); + /// add_leaf_to!(tree, "Leaf 2"); + /// tree.set_enabled(true); + /// add_leaf_to!(tree, "Leaf 3"); + /// assert_eq!("Leaf 1\nLeaf 3", tree.peek_string()); + /// ``` + pub fn set_enabled(&self, enabled: bool) { + self.0.lock().unwrap().set_enabled(enabled); + } + + /// Returns the enabled state of the tree. + /// + /// # Example + /// ``` + /// use debug_tree::TreeBuilder; + /// let mut tree = TreeBuilder::new(); + /// assert_eq!(true, tree.is_enabled()); + /// tree.set_enabled(false); + /// assert_eq!(false, tree.is_enabled()); + /// ``` + pub fn is_enabled(&self) -> bool { + self.0.lock().unwrap().is_enabled() + } +} + +pub trait AsTree { + fn as_tree(&self) -> TreeBuilder; + fn is_tree_enabled(&self) -> bool { + self.as_tree().is_enabled() + } +} + +impl AsTree for TreeBuilder { + fn as_tree(&self) -> TreeBuilder { + self.clone() + } +} + +pub(crate) fn get_or_add_tree<T: AsRef<str>>(name: T) -> TreeBuilder { + let mut map = TREE_MAP.lock().unwrap(); + match map.get(name.as_ref()) { + Some(x) => x.clone(), + _ => { + let val = TreeBuilder::new(); + map.insert(name.as_ref().to_string(), val.clone()); + val + } + } +} + +pub(crate) fn get_tree<T: AsRef<str>>(name: T) -> Option<TreeBuilder> { + TREE_MAP.lock().unwrap().get(name.as_ref()).cloned() +} + +type TreeMap = BTreeMap<String, TreeBuilder>; + +static TREE_MAP: Lazy<Arc<Mutex<TreeMap>>> = + Lazy::new(|| -> Arc<Mutex<TreeMap>> { Arc::new(Mutex::new(TreeMap::new())) }); + +/// Sets the enabled state of the tree. +/// +/// # Arguments +/// * `name` - The tree name +/// * `enabled` - The enabled state +/// +pub fn set_enabled<T: AsRef<str>>(name: T, enabled: bool) { + let mut map = TREE_MAP.lock().unwrap(); + match map.get_mut(name.as_ref()) { + Some(x) => x.set_enabled(enabled), + _ => { + let tree = TreeBuilder::new(); + tree.set_enabled(enabled); + map.insert(name.as_ref().to_string(), tree); + } + } +} + +impl<T: AsRef<str>> AsTree for T { + fn as_tree(&self) -> TreeBuilder { + get_or_add_tree(self) + } + /// Check if the named tree is enabled and exists + /// This does not create a new tree if non-existent + /// + /// # Arguments + /// * `tree_name` - The tree name + /// + fn is_tree_enabled(&self) -> bool { + get_tree(self).map(|x| x.is_enabled()).unwrap_or(false) + } +} + +/// Returns the tree +/// If there is no tree then one is created and then returned. +pub fn tree<T: AsTree>(tree: T) -> TreeBuilder { + tree.as_tree() +} + +/// Returns the tree named `name` +/// If there is no tree named `name` then one is created and then returned. +pub fn is_tree_enabled<T: AsTree>(tree: &T) -> bool { + tree.is_tree_enabled() +} + +/// Calls [clear](TreeBuilder::clear) for the tree named `name` +/// If there is no tree named `name` then one is created +pub fn clear<T: AsRef<str>>(name: T) { + name.as_tree().clear(); +} + +/// Returns [string](TreeBuilder::string) for the tree named `name` +/// If there is no tree named `name` then one is created +pub fn string<T: AsRef<str>>(name: T) -> String { + name.as_tree().string() +} + +/// Returns [peek_string](TreeBuilder::peek_string) for the tree named `name` +/// If there is no tree named `name` then one is created +pub fn peek_string<T: AsRef<str>>(name: T) -> String { + name.as_tree().peek_string() +} + +/// Calls [print](TreeBuilder::print) for the tree named `name` +/// If there is no tree named `name` then one is created +pub fn print<T: AsRef<str>>(name: T) { + name.as_tree().print(); +} + +/// Calls [peek_print](TreeBuilder::peek_print) for the tree named `name` +/// If there is no tree named `name` then one is created +pub fn peek_print<T: AsRef<str>>(name: T) { + name.as_tree().peek_print(); +} + +/// Calls [write](TreeBuilder::write) for the tree named `name` +/// If there is no tree named `name` then one is created +pub fn write<T: AsRef<str>, P: AsRef<str>>(name: T, path: P) -> std::io::Result<()> { + name.as_tree().write(path.as_ref()) +} + +/// Calls [peek_print](TreeBuilder::peek_print) for the tree named `name` +/// If there is no tree named `name` then one is created +pub fn peek_write<T: AsRef<str>, P: AsRef<str>>(name: T, path: P) -> std::io::Result<()> { + name.as_tree().peek_write(path.as_ref()) +} + +/// Adds a leaf to given tree with the given text and formatting arguments +/// +/// # Arguments +/// * `tree` - The tree that the leaf should be added to +/// * `text...` - Formatted text arguments, as per `format!(...)`. +/// +/// # Example +/// +/// ``` +/// #[macro_use] +/// use debug_tree::{TreeBuilder, add_leaf_to}; +/// fn main() { +/// let tree = TreeBuilder::new(); +/// add_leaf_to!(tree, "A {} leaf", "new"); +/// assert_eq!("A new leaf", &tree.peek_string()); +/// } +/// ``` +#[macro_export] +macro_rules! add_leaf_to { + ($tree:expr, $($arg:tt)*) => (if $crate::is_tree_enabled(&$tree) { + use $crate::AsTree; + $tree.as_tree().add_leaf(&format!($($arg)*)) + }); +} + +/// Adds a leaf to given tree with the given `value` argument +/// +/// # Arguments +/// * `tree` - The tree that the leaf should be added to +/// * `value` - An expression that implements the `Display` trait. +/// +/// # Example +/// +/// ``` +/// #[macro_use] +/// use debug_tree::{TreeBuilder, add_leaf_value_to}; +/// fn main() { +/// let tree = TreeBuilder::new(); +/// let value = add_leaf_value_to!(tree, 5 * 4 * 3 * 2); +/// assert_eq!(120, value); +/// assert_eq!("120", &tree.peek_string()); +/// } +/// ``` +#[macro_export] +macro_rules! add_leaf_value_to { + ($tree:expr, $value:expr) => {{ + let v = $value; + if $crate::is_tree_enabled(&$tree) { + use $crate::AsTree; + $tree.as_tree().add_leaf(&format!("{}", &v)); + } + v + }}; +} + +/// Adds a scoped branch to given tree with the given text and formatting arguments +/// The branch will be exited at the end of the current block. +/// +/// # Arguments +/// * `tree` - The tree that the leaf should be added to +/// * `text...` - Formatted text arguments, as per `format!(...)`. +/// +/// # Example +/// +/// ``` +/// #[macro_use] +/// use debug_tree::{TreeBuilder, add_branch_to, add_leaf_to}; +/// fn main() { +/// let tree = TreeBuilder::new(); +/// { +/// add_branch_to!(tree, "New {}", "Branch"); // _branch enters scope +/// // tree is now pointed inside new branch. +/// add_leaf_to!(tree, "Child of {}", "Branch"); +/// // Block ends, so tree exits the current branch. +/// } +/// add_leaf_to!(tree, "Sibling of {}", "Branch"); +/// assert_eq!("\ +/// New Branch +/// └╼ Child of Branch +/// Sibling of Branch" , &tree.string()); +/// } +/// ``` +#[macro_export] +macro_rules! add_branch_to { + ($tree:expr) => { + let _debug_tree_branch = if $crate::is_tree_enabled(&$tree) { + use $crate::AsTree; + $tree.as_tree().enter_scoped() + } else { + $crate::scoped_branch::ScopedBranch::none() + }; + }; + ($tree:expr, $($arg:tt)*) => { + let _debug_tree_branch = if $crate::is_tree_enabled(&$tree) { + use $crate::AsTree; + $tree.as_tree().add_branch(&format!($($arg)*)) + } else { + $crate::scoped_branch::ScopedBranch::none() + }; + }; +} + +/// Calls `function` with argument, `tree`, at the end of the current scope +/// The function will only be executed if the tree is enabled when this macro is called +#[macro_export] +macro_rules! defer { + ($function:expr) => { + let _debug_tree_defer = { + use $crate::AsTree; + if $crate::default::default_tree().is_enabled() { + use $crate::AsTree; + $crate::defer::DeferredFn::new($crate::default::default_tree(), $function) + } else { + $crate::defer::DeferredFn::none() + } + }; + }; + ($tree:expr, $function:expr) => { + let _debug_tree_defer = { + use $crate::AsTree; + if $tree.as_tree().is_enabled() { + $crate::defer::DeferredFn::new($tree.as_tree(), $function) + } else { + $crate::defer::DeferredFn::none() + } + }; + }; +} + +/// Calls [print](TreeBuilder::print) on `tree` at the end of the current scope. +/// The function will only be executed if the tree is enabled when this macro is called +#[macro_export] +macro_rules! defer_print { + () => { + $crate::defer!(|x| { + x.print(); + }) + }; + ($tree:expr) => { + $crate::defer!($tree, |x| { + x.print(); + }) + }; +} + +/// Calls [peek_print](TreeBuilder::peek_print) on `tree` at the end of the current scope. +/// The function will only be executed if the tree is enabled when this macro is called +#[macro_export] +macro_rules! defer_peek_print { + () => { + $crate::defer!(|x| { + x.peek_print(); + }) + }; + ($tree:expr) => { + $crate::defer!($tree, |x| { + x.peek_print(); + }) + }; +} + +/// Calls [write](TreeBuilder::write) on `tree` at the end of the current scope. +/// The function will only be executed if the tree is enabled when this macro is called +#[macro_export] +macro_rules! defer_write { + ($tree:expr, $path:expr) => { + $crate::defer!($tree, |x| { + if let Err(err) = x.write($path) { + eprintln!("error during `defer_write`: {}", err); + } + }) + }; + ($path:expr) => { + $crate::defer!(|x| { + if let Err(err) = x.write($path) { + eprintln!("error during `defer_write`: {}", err); + } + }) + }; +} + +/// Calls [peek_write](TreeBuilder::peek_write) on `tree` at the end of the current scope. +/// The function will only be executed if the tree is enabled when this macro is called +#[macro_export] +macro_rules! defer_peek_write { + ($tree:expr, $path:expr) => { + $crate::defer!($tree, |x| { + if let Err(err) = x.peek_write($path) { + eprintln!("error during `defer_peek_write`: {}", err); + } + }) + }; + ($path:expr) => { + $crate::defer!(|x| { + if let Err(err) = x.peek_write($path) { + eprintln!("error during `defer_peek_write`: {}", err); + } + }) + }; +} diff --git a/src/scoped_branch.rs b/src/scoped_branch.rs new file mode 100644 index 0000000..63aadb4 --- /dev/null +++ b/src/scoped_branch.rs @@ -0,0 +1,26 @@ +use crate::TreeBuilder; + +pub struct ScopedBranch { + state: Option<TreeBuilder>, +} + +impl ScopedBranch { + pub fn new(state: TreeBuilder) -> ScopedBranch { + state.enter(); + ScopedBranch { state: Some(state) } + } + pub fn none() -> ScopedBranch { + ScopedBranch { state: None } + } + pub fn release(&mut self) { + if let Some(x) = &self.state { + x.exit(); + } + self.state = None; + } +} +impl Drop for ScopedBranch { + fn drop(&mut self) { + self.release(); + } +} diff --git a/src/test.rs b/src/test.rs new file mode 100644 index 0000000..40887ce --- /dev/null +++ b/src/test.rs @@ -0,0 +1,596 @@ +#[cfg(test)] +mod test { + use crate::*; + use futures::future::join5; + use std::cmp::{max, min}; + use std::fs::{create_dir, read_to_string, remove_file}; + + #[test] + fn test_branch() { + let d: TreeBuilder = TreeBuilder::new(); + d.add_leaf("1"); + { + let _l = d.enter_scoped(); + d.add_leaf("1.1"); + d.add_leaf("1.2"); + } + d.add_leaf("2"); + d.add_leaf("3"); + let _l = d.enter_scoped(); + d.add_leaf("3.1"); + d.add_leaf("3.2"); + d.peek_print(); + assert_eq!( + "\ +1 +├╼ 1.1 +└╼ 1.2 +2 +3 +├╼ 3.1 +└╼ 3.2", + d.string() + ); + } + + #[test] + fn test_branch2() { + let d = TreeBuilder::new(); + d.add_leaf("1"); + { + let _scope = d.enter_scoped(); + d.add_leaf("1.1"); + { + let _scope = d.enter_scoped(); + d.add_leaf("1.1.1"); + } + } + + d.add_leaf("2"); + d.enter(); + d.add_leaf("2.1"); + d.enter(); + d.add_leaf("2.1.1"); + d.peek_print(); + assert_eq!( + "\ +1 +└╼ 1.1 + └╼ 1.1.1 +2 +└╼ 2.1 + └╼ 2.1.1", + d.string() + ); + } + + #[test] + fn simple() { + let d = TreeBuilder::new(); + d.add_leaf("Hi"); + assert_eq!("Hi", d.string()); + } + + #[test] + fn depth() { + let d = TreeBuilder::new(); + assert_eq!(0, d.depth()); + d.add_leaf("Hi"); + assert_eq!(0, d.depth()); + let _b = d.add_branch("Hi"); + assert_eq!(1, d.depth()); + d.add_leaf("Hi"); + assert_eq!(1, d.depth()); + } + + #[test] + fn indent() { + let d = TreeBuilder::new(); + d.add_leaf("1"); + add_branch_to!(d); + d.add_leaf("1.1"); + { + add_branch_to!(d); + d.add_leaf("1.1.1"); + } + d.set_config_override(TreeConfig::new().indent(4)); + d.peek_print(); + assert_eq!( + "\ +1 +└──╼ 1.1 + └──╼ 1.1.1", + d.string() + ); + } + + #[test] + fn macros() { + let d = TreeBuilder::new(); + add_leaf_to!(d, "1"); + { + add_branch_to!(d); + add_leaf_to!(d, "1.1") + } + d.peek_print(); + assert_eq!( + "\ +1 +└╼ 1.1", + d.string() + ); + } + + #[test] + fn macros_with_fn() { + let d = TreeBuilder::new(); + let tree = || d.clone(); + add_leaf_to!(tree(), "1"); + { + add_branch_to!(tree()); + add_leaf_to!(tree(), "1.1") + } + tree().peek_print(); + assert_eq!( + "\ +1 +└╼ 1.1", + d.string() + ); + } + + #[test] + fn leaf_with_value() { + let d = TreeBuilder::new(); + let value = add_leaf_value_to!(d, 1); + d.peek_print(); + assert_eq!("1", d.string()); + assert_eq!(1, value); + } + + #[test] + fn macros2() { + let d = TreeBuilder::new(); + add_branch_to!(d, "1"); + add_leaf_to!(d, "1.1"); + d.peek_print(); + assert_eq!( + "\ +1 +└╼ 1.1", + d.string() + ); + } + + #[test] + fn mid() { + let d = TreeBuilder::new(); + d.add_leaf(&format!("{}{}", "1", "0")); + d.enter(); + d.add_leaf("10.1"); + d.add_leaf("10.2"); + d.enter(); + d.add_leaf("10.1.1"); + d.add_leaf("10.1.2\nNext line"); + d.exit(); + d.add_leaf(&format!("10.3")); + d.peek_print(); + assert_eq!( + "\ +10 +├╼ 10.1 +├╼ 10.2 +│ ├╼ 10.1.1 +│ └╼ 10.1.2 +│ Next line +└╼ 10.3", + d.string() + ); + } + + fn factors(x: usize) { + add_branch!("{}", x); + for i in 1..x { + if x % i == 0 { + factors(i); + } + } + } + + #[test] + fn recursive() { + factors(6); + default_tree().peek_print(); + assert_eq!( + "\ +6 +├╼ 1 +├╼ 2 +│ └╼ 1 +└╼ 3 + └╼ 1", + default_tree().string() + ); + } + + fn a() { + add_branch!("a"); + b(); + c(); + } + + fn b() { + add_branch!("b"); + c(); + } + + fn c() { + add_branch!("c"); + add_leaf!("Nothing to see here"); + } + + #[test] + fn nested() { + a(); + default_tree().peek_print(); + assert_eq!( + "\ +a +├╼ b +│ └╼ c +│ └╼ Nothing to see here +└╼ c + └╼ Nothing to see here", + default_tree().string() + ); + } + + #[test] + fn disabled_output() { + let tree = TreeBuilder::new(); + tree.set_enabled(false); + add_leaf_to!(tree, "Leaf"); + tree.add_leaf("Leaf"); + + add_branch_to!(tree, "Branch"); + tree.add_branch("Branch"); + assert_eq!("", tree.string()); + } + + #[test] + fn enabled_output() { + let tree = TreeBuilder::new(); + tree.set_enabled(false); + add_branch_to!(tree, "Ignored branch"); + add_leaf_to!(tree, "Ignored leaf"); + tree.set_enabled(true); + add_leaf_to!(tree, "Leaf"); + tree.add_leaf("Leaf"); + + add_branch_to!(tree, "Branch"); + tree.add_branch("Branch"); + assert_eq!( + "Leaf +Leaf +Branch +└╼ Branch", + tree.string() + ); + } + + #[test] + fn tree_by_name() { + clear("A"); + let b = tree("B"); + b.clear(); + { + add_branch_to!("A", "1"); + add_branch_to!(b, "3"); + add_leaf_to!("A", "1.1"); + add_leaf_to!("B", "3.1"); + } + add_leaf_to!("A", "2"); + peek_print("A"); + b.peek_print(); + assert_eq!( + "1 +└╼ 1.1 +2", + string("A") + ); + assert_eq!( + "3 +└╼ 3.1", + b.string() + ); + } + + #[test] + fn tree_by_name_disabled() { + let d = tree("D"); + d.clear(); + d.set_enabled(true); + clear("C"); + set_enabled("C", false); + { + add_branch_to!("C", "1"); + set_enabled("C", true); + add_branch_to!(d, "3"); + add_leaf_to!("C", "1.1"); + d.set_enabled(false); + add_leaf_to!("D", "3.1"); + } + add_leaf_to!("C", "2"); + peek_print("C"); + d.peek_print(); + assert_eq!( + "1.1 +2", + string("C") + ); + assert_eq!("3", d.string()); + } + + #[test] + fn defer_write() { + let tree = TreeBuilder::new(); + { + create_dir("test_out").ok(); + remove_file("test_out/defer_write.txt").ok(); + File::create("test_out/defer_write.txt").unwrap(); + defer_write!(tree, "test_out/defer_write.txt"); + tree.add_leaf("Branch"); + assert_eq!(read_to_string("test_out/defer_write.txt").unwrap(), ""); + assert_eq!(tree.peek_string(), "Branch"); + } + assert_eq!(tree.peek_string(), ""); + assert_eq!( + read_to_string("test_out/defer_write.txt").unwrap(), + "Branch" + ); + } + + #[test] + fn defer_peek_write() { + let tree = TreeBuilder::new(); + { + create_dir("test_out").ok(); + remove_file("test_out/defer_peek_write.txt").ok(); + File::create("test_out/defer_peek_write.txt").unwrap(); + defer_peek_write!(tree, "test_out/defer_peek_write.txt"); + tree.add_leaf("Branch"); + assert_eq!(read_to_string("test_out/defer_peek_write.txt").unwrap(), ""); + assert_eq!(tree.peek_string(), "Branch"); + } + assert_eq!(tree.peek_string(), "Branch"); + assert_eq!( + read_to_string("test_out/defer_peek_write.txt").unwrap(), + "Branch" + ); + } + + #[test] + #[should_panic] + #[allow(unreachable_code)] + fn defer_peek_write_panic() { + let tree = TreeBuilder::new(); + { + create_dir("test_out").ok(); + remove_file("test_out/defer_peek_write_panic.txt").ok(); + File::create("test_out/defer_peek_write_panic.txt").unwrap(); + defer_peek_write!(tree, "test_out/defer_peek_write_panic.txt"); + tree.add_leaf("This should be the only line in this file"); + assert_eq!(read_to_string("test_out/defer_peek_write.txt").unwrap(), ""); + assert_eq!( + tree.peek_string(), + "This should be the only line in this file" + ); + panic!(); + tree.add_leaf("This line should not exist"); + } + } + + fn example_tree() -> TreeBuilder { + let tree = TreeBuilder::new(); + { + add_branch_to!(tree, "1"); + { + add_branch_to!(tree, "1.1"); + add_leaf_to!(tree, "1.1.1"); + add_leaf_to!(tree, "1.1.2\nWith two\nextra lines"); + add_leaf_to!(tree, "1.1.3"); + } + add_branch_to!(tree, "1.2"); + add_leaf_to!(tree, "1.2.1"); + } + { + add_branch_to!(tree, "2"); + add_leaf_to!(tree, "2.1"); + add_leaf_to!(tree, "2.2"); + } + add_leaf_to!(tree, "3"); + tree + } + + #[test] + fn format_output() { + let tree = example_tree(); + tree.set_config_override( + TreeConfig::new() + .indent(8) + .symbols(TreeSymbols { + continued: "| |", + join_first: "|A|", + join_last: "|Z|", + join_inner: "|N|", + join_only: "|O|", + branch: "123456[NOT SHOWN]", + leaf: ")}>", + multiline_first: Some(")}MULTI>"), + multiline_continued: Some(".. CONTINUED: "), + }) + .show_first_level(), + ); + tree.peek_print(); + assert_eq!( + tree.string(), + "\ +|A|123456)}>1 +| | |A|123456)}>1.1 +| | | | |A|123456)}>1.1.1 +| | | | |N|123456)}MULTI>1.1.2 +| | | | | | .. CONTINUED: With two +| | | | | | .. CONTINUED: extra lines +| | | | |Z|123456)}>1.1.3 +| | |Z|123456)}>1.2 +| | |O|123456)}>1.2.1 +|N|123456)}>2 +| | |A|123456)}>2.1 +| | |Z|123456)}>2.2 +|Z|123456)}>3" + ); + } + + #[test] + fn format_output_thick() { + let tree = example_tree(); + tree.set_config_override( + TreeConfig::new() + .symbols(TreeSymbols::with_thick()) + .indent(4) + .show_first_level(), + ); + tree.peek_print(); + assert_eq!( + tree.string(), + "\ +┣━━╼ 1 +┃ ┣━━╼ 1.1 +┃ ┃ ┣━━╼ 1.1.1 +┃ ┃ ┣━━╼ 1.1.2 +┃ ┃ ┃ With two +┃ ┃ ┃ extra lines +┃ ┃ ┗━━╼ 1.1.3 +┃ ┗━━╼ 1.2 +┃ ┗━━╼ 1.2.1 +┣━━╼ 2 +┃ ┣━━╼ 2.1 +┃ ┗━━╼ 2.2 +┗━━╼ 3" + ); + } + + #[test] + fn format_output_pipes() { + let tree = example_tree(); + tree.set_config_override( + TreeConfig::new() + .symbols(TreeSymbols::with_pipes()) + .indent(3) + .show_first_level(), + ); + tree.peek_print(); + assert_eq!( + tree.string(), + "\ +╠═╼ 1 +║ ╠═╼ 1.1 +║ ║ ╠═╼ 1.1.1 +║ ║ ╠═╼ 1.1.2 +║ ║ ║ With two +║ ║ ║ extra lines +║ ║ ╚═╼ 1.1.3 +║ ╚═╼ 1.2 +║ ╚═╼ 1.2.1 +╠═╼ 2 +║ ╠═╼ 2.1 +║ ╚═╼ 2.2 +╚═╼ 3" + ); + } + + #[test] + fn format_output_dashed() { + let tree = example_tree(); + tree.set_config_override( + TreeConfig::new() + .symbols(TreeSymbols::with_dashed().multiline_continued(" > ")) + .indent(4) + .show_first_level(), + ); + tree.peek_print(); + assert_eq!( + tree.string(), + "\ +┊╌╌- 1 +┊ ┊╌╌- 1.1 +┊ ┊ ┊╌╌- 1.1.1 +┊ ┊ ┊╌╌- 1.1.2 +┊ ┊ ┊ > With two +┊ ┊ ┊ > extra lines +┊ ┊ '╌╌- 1.1.3 +┊ '╌╌- 1.2 +┊ '╌╌- 1.2.1 +┊╌╌- 2 +┊ ┊╌╌- 2.1 +┊ '╌╌- 2.2 +'╌╌- 3" + ); + } + + #[test] + fn format_output_rounded() { + let tree = example_tree(); + tree.set_config_override( + TreeConfig::new() + .symbols(TreeSymbols::with_rounded()) + .indent(4), + ); + tree.peek_print(); + assert_eq!( + tree.string(), + "\ +1 +├──╼ 1.1 +│ ├──╼ 1.1.1 +│ ├──╼ 1.1.2 +│ │ With two +│ │ extra lines +│ ╰──╼ 1.1.3 +╰──╼ 1.2 + ╰──╼ 1.2.1 +2 +├──╼ 2.1 +╰──╼ 2.2 +3" + ); + } + + async fn wait_a_bit(tree: TreeBuilder, index: usize) { + tree.print(); + add_branch_to!(tree, "inside async branch {}", index); + tree.print(); + add_leaf_to!(tree, "inside async leaf {}", index); + tree.print(); + } + + #[tokio::test] + async fn async_barrier() { + let tree = TreeBuilder::new(); + defer_peek_print!(tree); + add_branch_to!(tree, "root"); + add_leaf_to!(tree, "before async"); + + let x2 = wait_a_bit(tree.clone(), 4); + let x1 = wait_a_bit(tree.clone(), 5); + let x3 = wait_a_bit(tree.clone(), 3); + let x4 = wait_a_bit(tree.clone(), 2); + let x5 = wait_a_bit(tree.clone(), 1); + + add_leaf_to!(tree, "before join async"); + + join5(x1, x2, x3, x4, x5).await; + add_leaf_to!(tree, "after join async"); + assert_eq!(tree.peek_string(), "after join async"); + } +} diff --git a/src/tree_config.rs b/src/tree_config.rs new file mode 100644 index 0000000..ec534a3 --- /dev/null +++ b/src/tree_config.rs @@ -0,0 +1,224 @@ +use once_cell::sync::Lazy; +use std::sync::{Arc, Mutex}; + +#[derive(Debug, Clone)] +pub struct TreeSymbols { + /// A vertical base of the tree (│) + pub continued: &'static str, + + /// Symbol for joining the first branch in a group (├) + pub join_first: &'static str, + + /// Symbol for joining the last branch in a group (└) + pub join_last: &'static str, + + /// Symbol for joining a branch that is not first or last in its group (├) + pub join_inner: &'static str, + + /// Symbol for joining a branch if it is the only one in its group (├) + pub join_only: &'static str, + + /// A repeated branch token (─) + pub branch: &'static str, + + /// End of a leaf (╼) + pub leaf: &'static str, + + pub multiline_first: Option<&'static str>, + pub multiline_continued: Option<&'static str>, +} + +#[derive(Debug, Clone)] +pub struct TreeConfig { + pub symbols: TreeSymbols, + + /// Aside from the first branch, `indent` is equal to the number of spaces a child branch is + /// shifted from its parent. + pub indent: usize, + + pub show_first_level: bool, +} +impl TreeSymbols { + pub fn new() -> Self { + Self { + continued: "│", + join_first: "├", + join_inner: "├", + join_last: "└", + join_only: "└", + branch: "─", + leaf: "╼ ", + multiline_first: None, + multiline_continued: None, + } + } + pub fn with_pipes() -> Self { + Self { + continued: "║", + join_first: "╠", + join_inner: "╠", + join_last: "╚", + join_only: "╚", + branch: "═", + leaf: "╼ ", + multiline_first: None, + multiline_continued: None, + } + } + pub fn with_thick() -> Self { + Self { + continued: "┃", + join_first: "┣", + join_inner: "┣", + join_last: "┗", + join_only: "┗", + branch: "━", + leaf: "╼ ", + multiline_first: None, + multiline_continued: None, + } + } + pub fn with_rounded() -> Self { + Self { + continued: "│", + join_first: "├", + join_inner: "├", + join_last: "╰", + join_only: "╰", + branch: "─", + leaf: "╼ ", + multiline_first: None, + multiline_continued: None, + } + } + pub fn with_dashed() -> Self { + Self { + continued: "┊", + join_first: "┊", + join_inner: "┊", + join_last: "'", + join_only: "'", + branch: "╌", + leaf: "- ", + multiline_first: None, + multiline_continued: None, + } + } + + pub fn continued(mut self, sym: &'static str) -> Self { + self.continued = sym; + self + } + pub fn join_first(mut self, sym: &'static str) -> Self { + self.join_first = sym; + self + } + pub fn join_inner(mut self, sym: &'static str) -> Self { + self.join_inner = sym; + self + } + pub fn join_last(mut self, sym: &'static str) -> Self { + self.join_last = sym; + self + } + pub fn join_only(mut self, sym: &'static str) -> Self { + self.join_only = sym; + self + } + + pub fn branch(mut self, sym: &'static str) -> Self { + self.branch = sym; + self + } + pub fn leaf(mut self, sym: &'static str) -> Self { + self.leaf = sym; + self + } + pub fn multiline_first(mut self, sym: &'static str) -> Self { + self.multiline_first = Some(sym); + self + } + pub fn multiline_continued(mut self, sym: &'static str) -> Self { + self.multiline_continued = Some(sym); + self + } +} + +impl TreeConfig { + pub fn new() -> Self { + Self { + symbols: TreeSymbols::new(), + indent: 2, + show_first_level: false, + } + } + pub fn with_symbols(symbols: TreeSymbols) -> Self { + Self { + symbols, + indent: 2, + show_first_level: false, + } + } + pub fn indent(mut self, x: usize) -> Self { + self.indent = x; + self + } + pub fn show_first_level(mut self) -> Self { + self.show_first_level = true; + self + } + pub fn hide_first_level(mut self) -> Self { + self.show_first_level = false; + self + } + pub fn symbols(mut self, x: TreeSymbols) -> Self { + self.symbols = x; + self + } +} + +impl Default for TreeSymbols { + fn default() -> Self { + tree_config_symbols() + } +} +impl Default for TreeConfig { + fn default() -> Self { + tree_config() + } +} + +static DEFAULT_CONFIG: Lazy<Arc<Mutex<TreeConfig>>> = + Lazy::new(|| -> Arc<Mutex<TreeConfig>> { Arc::new(Mutex::new(TreeConfig::new())) }); + +/// Set the default tree config +pub fn set_tree_config(x: TreeConfig) { + *DEFAULT_CONFIG.lock().unwrap() = x; +} + +/// The default tree config +pub fn tree_config() -> TreeConfig { + DEFAULT_CONFIG.lock().unwrap().clone() +} + +/// Set the default tree symbols config +pub fn set_tree_config_symbols(x: TreeSymbols) { + DEFAULT_CONFIG.lock().unwrap().symbols = x; +} + +/// The default tree symbols config +pub fn tree_config_symbols() -> TreeSymbols { + DEFAULT_CONFIG.lock().unwrap().symbols.clone() +} + +/// The default tree symbols config +pub fn update_tree_config<F: FnMut(&mut TreeConfig)>(mut update: F) { + let mut x = DEFAULT_CONFIG.lock().unwrap(); + update(&mut x); +} + +/// The default tree symbols config +pub fn update_tree_config_symbols<F: FnMut(&mut TreeSymbols)>(mut update: F) { + let mut x = DEFAULT_CONFIG.lock().unwrap(); + update(&mut x.symbols); +} |