diff --git a/.github/release-body.md b/.github/release-body.md index 2c5997b..6e8fc56 100644 --- a/.github/release-body.md +++ b/.github/release-body.md @@ -1,26 +1,23 @@ -### 2024-01-05 +### 2024-01-18 ### Chores -+ .devcontainer updated, [2313618eb1493ce41d70847b888c32b65fdc40ea], [5af6b8bcd31c3c38ff5a5799c76dc1cbe1167763], [9b0b6b10c3a0c1d5095490cfd3cda18d252f38f5] -+ alpine version bump, [061de032dad935c56c6caab419ecb5c9bbac4c7e] -+ dependencies updated, [0890991ff1a239fe2d556a0c4eac6ae05beb9b50], [0a7b266b2a358a4788ae877ca8a97f08eac4eef2], [333621f1a7321c1fdf73fd35dd7f3ab165a9dc64], [3e51889cd8a552b1da463ae6a40d5de6eec188f5], [a179bb6f6a7e076269fa830f56c0d4a31cf8488a] -+ file formatting, [eb5e74ae67d815bf49f241d2baf319e41cf9adf8] -+ Rust 1.75.0 linting, [81be75f27fd32a59ebff57e44c5022ff862df84b] ++ dependencies updated, [53b4bafbe53312fe41608ddf33e865d474222aaa], [58ef151600e362048a607c8ae61a5edfe80ab1dd], [b6fd35022a99ec0e982ddb154b0450d49c4840e9], [0438c108bdd9815d7eae1b89c47c4e6438f358d6] ++ files formatted, [1806165c3e266876b2d1806f7b662d09705f3aad] ++ create_release.sh check for unused lint, [d0b27211928f93f8455e1ee5a6a6485c6a21d382] ### Docs -+ screenshot updated, [0231d1bdcda304300d289243a95044ab3bdce85c] -+ comment typo, [0ad1ec9d85d6f0cac743b4421d0ad03432c9d717] += Readme updated, screenshot added, [7561a93415c1e1f596b15edba95e7b32a939cd90], [4069e5572f81cb689dbb9f735db919e4636cdccc] ### Features -+ re-arrange columns, container name is now the first column, added a ContainerName & ContainerImage struct via `string_wrapper` macro, closes #32, [e936bb4b78980d0e34a1ef5e9f6f82a9ed0ddc7f] ++ Ports section added, closes #21, [65a1afcb0605604ede350a5630c775f94ebb74ee], [7a096a65c40924021fe643fe0aa1067095832df9] ### Fixes -+ Docker Commands hidden, [4301e4709f99fc23ee438bf345b0dc698a05dc4e] -+ .gitattributes, [1234ea53897b2ed6ada0eb18cd81b8783a5dc5f5] ++ sort arrow now on left of header, [40ddcb727d2c1758d6dd26a58507b85b219f51e2] ### Refactors -+ GitHub workflow action improved, [04b66af2b60c96cfbece0b13109e30b08ef35cc4] -+ sort_containers, [ccf8b55a7495982f72b4fb3af6e11a9bd7465216] -+ string_wrapper .get() return `&str`, [a722731c6a77e00d1fb13967b51400aa34e72213] ++ rename string_wrapper > unit_struct, [27cf53e41f8b379f606c1c27620ee08e79bac57e] + +### Tests ++ Finally have tests, currently for layout and associated methods, at the moment running the tests will not interfere with any running Docker containers, [4bcf77db776a36e0a8151ecfbda722a66c4ba46c] see CHANGELOG.md for more details diff --git a/.github/screenshot_01.png b/.github/screenshot_01.png index 27a7932..383a567 100644 Binary files a/.github/screenshot_01.png and b/.github/screenshot_01.png differ diff --git a/CHANGELOG.md b/CHANGELOG.md index cfeb98b..94a81a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +# v0.6.0 +### 2024-01-18 + +### Chores ++ dependencies updated, [53b4bafb](https://github.com/mrjackwills/oxker/commit/53b4bafbe53312fe41608ddf33e865d474222aaa), [58ef1516](https://github.com/mrjackwills/oxker/commit/58ef151600e362048a607c8ae61a5edfe80ab1dd), [b6fd3502](https://github.com/mrjackwills/oxker/commit/b6fd35022a99ec0e982ddb154b0450d49c4840e9), [0438c108](https://github.com/mrjackwills/oxker/commit/0438c108bdd9815d7eae1b89c47c4e6438f358d6) ++ files formatted, [1806165c](https://github.com/mrjackwills/oxker/commit/1806165c3e266876b2d1806f7b662d09705f3aad) ++ create_release.sh check for unused lint, [d0b27211](https://github.com/mrjackwills/oxker/commit/d0b27211928f93f8455e1ee5a6a6485c6a21d382) + +### Docs += Readme updated, screenshot added, [7561a934](https://github.com/mrjackwills/oxker/commit/7561a93415c1e1f596b15edba95e7b32a939cd90), [4069e557](https://github.com/mrjackwills/oxker/commit/4069e5572f81cb689dbb9f735db919e4636cdccc) + +### Features ++ Ports section added, closes [#21](https://github.com/mrjackwills/oxker/issues/21), [65a1afcb](https://github.com/mrjackwills/oxker/commit/65a1afcb0605604ede350a5630c775f94ebb74ee), [7a096a65](https://github.com/mrjackwills/oxker/commit/7a096a65c40924021fe643fe0aa1067095832df9) + +### Fixes ++ sort arrow now on left of header, [40ddcb72](https://github.com/mrjackwills/oxker/commit/40ddcb727d2c1758d6dd26a58507b85b219f51e2) + +### Refactors ++ rename string_wrapper > unit_struct, [27cf53e4](https://github.com/mrjackwills/oxker/commit/27cf53e41f8b379f606c1c27620ee08e79bac57e) + +### Tests ++ Finally have tests, currently for layout and associated methods, at the moment running the tests will not interfere with any running Docker containers, [4bcf77db](https://github.com/mrjackwills/oxker/commit/4bcf77db776a36e0a8151ecfbda722a66c4ba46c) + # v0.5.0 ### 2024-01-05 diff --git a/Cargo.lock b/Cargo.lock index ac77486..c67ea51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,9 +52,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.5" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" dependencies = [ "anstyle", "anstyle-parse", @@ -127,9 +127,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.5" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "bitflags" @@ -139,9 +139,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" [[package]] name = "bollard" @@ -237,9 +237,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.13" +version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52bdc885e4cacc7f7c9eedc1ef6da641603180c783c41a15c264944deeaab642" +checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" dependencies = [ "clap_builder", "clap_derive", @@ -247,9 +247,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.12" +version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" +checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" dependencies = [ "anstream", "anstyle", @@ -295,7 +295,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "crossterm_winapi", "libc", "mio", @@ -426,9 +426,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", @@ -443,9 +443,9 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "h2" -version = "0.3.22" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" dependencies = [ "bytes", "fnv", @@ -484,9 +484,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" [[package]] name = "hex" @@ -643,9 +643,9 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" -version = "0.3.66" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" dependencies = [ "wasm-bindgen", ] @@ -658,9 +658,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.151" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "libredox" @@ -668,7 +668,7 @@ version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "libc", "redox_syscall", ] @@ -783,7 +783,7 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "oxker" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "bollard", @@ -881,9 +881,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.75" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "907a61bd0f64c2f29cd1cf1dc34d05176426a3f504a78010f08416ddb7b13708" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] @@ -933,7 +933,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5659e52e4ba6e07b2dad9f1158f578ef84a73762625ddb51536019f34d180eb" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "cassowary", "crossterm", "indoc", @@ -992,18 +992,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.194" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.194" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", @@ -1110,9 +1110,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.2" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +checksum = "3b187f0231d56fe41bfb12034819dd2bf336422a5866de41bc3fec4b2e3883e8" [[package]] name = "socket2" @@ -1382,9 +1382,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" @@ -1469,9 +1469,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1479,9 +1479,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" dependencies = [ "bumpalo", "log", @@ -1494,9 +1494,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1504,9 +1504,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", @@ -1517,9 +1517,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" [[package]] name = "winapi" diff --git a/Cargo.toml b/Cargo.toml index 1f5d3fd..5bcbd65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxker" -version = "0.5.0" +version = "0.6.0" edition = "2021" authors = ["Jack Wills "] description = "A simple tui to view & control docker containers" @@ -17,7 +17,7 @@ unsafe_code = "forbid" [lints.clippy] expect_used = "warn" nursery = "warn" -pedantic ="warn" +pedantic = "warn" todo = "warn" unused_async = "warn" unwrap_used = "warn" @@ -29,15 +29,15 @@ similar_names = "allow" anyhow = "1.0" bollard = "0.15" cansi = "2.2" -clap = { version = "4.4", features = ["derive", "unicode", "color"] } +clap = { version = "4.4", features = ["color", "derive", "unicode"] } crossterm = "0.27" futures-util = "0.3" -parking_lot = { version= "0.12" } +parking_lot = { version = "0.12" } tokio = { version = "1.35", features = ["full"] } tracing = "0.1" tracing-subscriber = "0.3" ratatui = "0.25" -uuid = { version = "1.6", features = ["v4", "fast-rng"] } +uuid = { version = "1.6", features = ["fast-rng", "v4"] } directories = "5.0" [dev-dependencies] @@ -46,6 +46,5 @@ directories = "5.0" lto = true codegen-units = 1 panic = 'abort' -strip=true +strip = true debug = false - diff --git a/README.md b/README.md index e1d47d5..806a60f 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,8 @@ see CHANGELOG.md for more details" > .github/release-body.md + echo -e "${RELEASE_BODY_ADDITION}\n\nsee CHANGELOG.md for more details" >.github/release-body.md # Add subheading with release version and date of release - echo -e "# ${NEW_TAG_WITH_V}\n${DATE_SUBHEADING}${CHANGELOG_ADDITION}$(cat CHANGELOG.md)" > CHANGELOG.md + echo -e "# ${NEW_TAG_WITH_V}\n${DATE_SUBHEADING}${CHANGELOG_ADDITION}$(cat CHANGELOG.md)" >CHANGELOG.md # Update changelog to add links to commits [hex:8](url_with_full_commit) # "[aaaaaaaaaabbbbbbbbbbccccccccccddddddddd]" -> "[aaaaaaaa](https:/www.../commit/aaaaaaaaaabbbbbbbbbbccccccccccddddddddd)" @@ -111,20 +107,19 @@ update_release_body_and_changelog () { } # update version in cargo.toml, to match selected current version -update_version_number_in_files () { +update_version_number_in_files() { sed -i "s|^version = .*|version = \"${MAJOR}.${MINOR}.${PATCH}\"|" Cargo.toml } # Work out the current version, based on git tags # create new semver version based on user input # Set MAJOR MINOR PATCH -check_tag () { +check_tag() { LATEST_TAG=$(git describe --tags "$(git rev-list --tags --max-count=1)") echo -e "\nCurrent tag: ${PURPLE}${LATEST_TAG}${RESET}\n" echo -e "${YELLOW}Choose new tag version:${RESET}\n" - if [[ $LATEST_TAG =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$ ]] - then - IFS="." read -r MAJOR MINOR PATCH <<< "${LATEST_TAG:1}" + if [[ $LATEST_TAG =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$ ]]; then + IFS="." read -r MAJOR MINOR PATCH <<<"${LATEST_TAG:1}" else MAJOR="0" MINOR="0" @@ -134,51 +129,53 @@ check_tag () { OP_MINOR="minor___v$(update_minor)" OP_PATCH="patch___v$(update_patch)" OPTIONS=("$OP_MAJOR" "$OP_MINOR" "$OP_PATCH") - select choice in "${OPTIONS[@]}" - do + select choice in "${OPTIONS[@]}"; do case $choice in - "$OP_MAJOR" ) - MAJOR=$((MAJOR + 1)) - MINOR=0 - PATCH=0 - break;; - "$OP_MINOR") - MINOR=$((MINOR + 1)) - PATCH=0 - break;; - "$OP_PATCH") - PATCH=$((PATCH + 1)) - break;; - *) - error_close "invalid option $REPLY" + "$OP_MAJOR") + MAJOR=$((MAJOR + 1)) + MINOR=0 + PATCH=0 + break + ;; + "$OP_MINOR") + MINOR=$((MINOR + 1)) + PATCH=0 + break + ;; + "$OP_PATCH") + PATCH=$((PATCH + 1)) + break + ;; + *) + error_close "invalid option $REPLY" + ;; esac done } # ask continue, or quit -ask_continue () { +ask_continue() { ask_yn "continue" - if [[ ! "$(user_input)" =~ ^y$ ]] - then + if [[ ! "$(user_input)" =~ ^y$ ]]; then exit fi } # run all tests -cargo_test () { +cargo_test() { cargo test -- --test-threads=1 ask_continue } # Simulate publishing to crates.io -cargo_publish () { +cargo_publish() { cargo publish --dry-run ask_continue } # Build all releases that GitHub workflow would # This will download GB's of docker images -cargo_build () { +cargo_build() { cargo install cross cargo_clean echo -e "${YELLOW}cross build --target x86_64-unknown-linux-musl --release${RESET}" @@ -199,9 +196,8 @@ cargo_build () { cargo_clean } - # $1 text to colourise -release_continue () { +release_continue() { echo -e "\n${PURPLE}$1${RESET}" ask_continue } @@ -212,14 +208,26 @@ cargo_clean() { cargo clean } # Check repository for typos -check_typos () { +check_typos() { echo -e "\n${PURPLE}check typos${RESET}" typos ask_continue } +# Make sure the unused lint isn't used +check_allow_unused() { + matches_any=$(find . -type d \( -name .git -o -name target \) -prune -o -type f -exec grep -lE '^#!\[allow\(unused\)\]$' {} +) + matches_cargo=$(grep "^unused = \"allow\"" ./Cargo.toml) + if [ -n "$matches_any" ]; then + error_close "\"#[allow(unused)]\" in ${matches_any}" + elif [ -n "$matches_cargo" ]; then + error_close "\"unused = \"allow\"\" in Cargo.toml" + fi +} + # Full flow to create a new release release_flow() { + check_allow_unused check_typos check_git @@ -231,20 +239,20 @@ release_flow() { cd "${CWD}" || error_close "Can't find ${CWD}" check_tag - + NEW_TAG_WITH_V="v${MAJOR}.${MINOR}.${PATCH}" printf "\nnew tag chosen: %s\n\n" "${NEW_TAG_WITH_V}" RELEASE_BRANCH=release-$NEW_TAG_WITH_V echo -e ask_changelog_update - + release_continue "checkout ${RELEASE_BRANCH}" git checkout -b "$RELEASE_BRANCH" release_continue "update_version_number_in_files" update_version_number_in_files - + echo -e "\ncargo fmt" cargo fmt echo -e "\n${PURPLE}cargo check${RESET}\n" @@ -283,7 +291,6 @@ release_flow() { git branch -d "$RELEASE_BRANCH" } - main() { cmd=(dialog --backtitle "Choose option" --radiolist "choose" 14 80 16) options=( @@ -297,24 +304,27 @@ main() { if [ $exitStatus -ne 0 ]; then exit fi - for choice in $choices - do + for choice in $choices; do case $choice in - 0) - exit;; - 1) - cargo_test - main - break;; - 2) - release_flow - break;; - 3) - cargo_build - main - break;; + 0) + exit + ;; + 1) + cargo_test + main + break + ;; + 2) + release_flow + break + ;; + 3) + cargo_build + main + break + ;; esac done } -main \ No newline at end of file +main diff --git a/docker-compose.yml b/docker-compose.yml index b2547f0..442e95e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,8 @@ services: ipc: private restart: always shm_size: 256MB + ports: + - "127.0.0.1:4040:4040" networks: - oxker-example-net deploy: diff --git a/src/app_data/container_state.rs b/src/app_data/container_state.rs index 247d6ff..53976d6 100644 --- a/src/app_data/container_state.rs +++ b/src/app_data/container_state.rs @@ -4,6 +4,7 @@ use std::{ fmt, }; +use bollard::service::Port; use ratatui::{ style::Color, widgets::{ListItem, ListState}, @@ -49,7 +50,7 @@ impl PartialOrd for ContainerId { /// TODO - use string_wrapper for ContainerId? /// ContainerName and ContainerImage are simple structs, used so can implement custom fmt functions to them -macro_rules! string_wrapper { +macro_rules! unit_struct { ($name:ident) => { #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct $name(String); @@ -60,6 +61,13 @@ macro_rules! string_wrapper { } } + #[cfg(test)] + impl From<&str> for $name { + fn from(value: &str) -> Self { + Self(value.to_owned()) + } + } + impl$name { pub fn get(&self) -> &str { self.0.as_str() @@ -90,10 +98,51 @@ macro_rules! string_wrapper { }; } -string_wrapper!(ContainerName); -string_wrapper!(ContainerImage); +unit_struct!(ContainerName); +unit_struct!(ContainerImage); -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ContainerPorts { + pub ip: Option, + pub private: u16, + pub public: Option, +} + +impl From<&Port> for ContainerPorts { + fn from(value: &Port) -> Self { + Self { + ip: value.ip.clone(), + private: value.private_port, + public: value.public_port, + } + } +} + +impl ContainerPorts { + pub fn len_ip(&self) -> usize { + self.ip.as_ref().unwrap_or(&String::new()).chars().count() + } + pub fn len_private(&self) -> usize { + format!("{}", self.private).chars().count() + } + pub fn len_public(&self) -> usize { + format!("{}", self.public.unwrap_or_default()) + .chars() + .count() + } + + pub fn print(&self) -> (String, String, String) { + ( + self.ip + .as_ref() + .map_or(String::new(), std::borrow::ToOwned::to_owned), + format!("{}", self.private), + self.public.map_or(String::new(), |s| s.to_string()), + ) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] pub struct StatefulList { pub state: ListState, pub items: Vec, @@ -154,7 +203,7 @@ impl StatefulList { .state .selected() .map_or(0, |value| if len > 0 { value + 1 } else { value }); - format!("{c}/{}", self.items.len()) + format!(" {c}/{}", self.items.len()) } } } @@ -234,13 +283,13 @@ impl fmt::Display for State { } /// Items for the container control list -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DockerControls { Pause, Restart, Start, Stop, - Unpause, + Resume, Delete, } @@ -252,7 +301,7 @@ impl DockerControls { Self::Start => Color::Green, Self::Stop => Color::Red, Self::Delete => Color::Gray, - Self::Unpause => Color::Blue, + Self::Resume => Color::Blue, } } @@ -260,7 +309,7 @@ impl DockerControls { pub fn gen_vec(state: State) -> Vec { match state { State::Dead | State::Exited => vec![Self::Start, Self::Restart, Self::Delete], - State::Paused => vec![Self::Unpause, Self::Stop, Self::Delete], + State::Paused => vec![Self::Resume, Self::Stop, Self::Delete], State::Restarting => vec![Self::Stop, Self::Delete], State::Running => vec![Self::Pause, Self::Restart, Self::Stop, Self::Delete], _ => vec![Self::Delete], @@ -276,7 +325,7 @@ impl fmt::Display for DockerControls { Self::Restart => "restart", Self::Start => "start", Self::Stop => "stop", - Self::Unpause => "resume", + Self::Resume => "resume", }; write!(f, "{disp}") } @@ -416,7 +465,7 @@ impl fmt::Display for LogsTz { /// Store the logs alongside a HashSet, each log *should* generate a unique timestamp, /// so if we store the timestamp separately in a HashSet, we can then check if we should insert a log line into the /// stateful list dependent on whethere the timestamp is in the HashSet or not -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Logs { logs: StatefulList>, tz: HashSet, @@ -475,23 +524,25 @@ impl Logs { } /// Info for each container -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ContainerItem { - pub created: u64, pub cpu_stats: VecDeque, + pub created: u64, pub docker_controls: StatefulList, pub id: ContainerId, pub image: ContainerImage, + pub is_oxker: bool, pub last_updated: u64, pub logs: Logs, pub mem_limit: ByteStats, pub mem_stats: VecDeque, pub name: ContainerName, + // todo remove option, can be empty vec + pub ports: Vec, pub rx: ByteStats, pub state: State, pub status: String, pub tx: ByteStats, - pub is_oxker: bool, } /// Basic display information, for when running in debug mode @@ -509,6 +560,7 @@ impl fmt::Display for ContainerItem { } impl ContainerItem { + #[allow(clippy::too_many_arguments)] /// Create a new container item pub fn new( created: u64, @@ -516,14 +568,16 @@ impl ContainerItem { image: String, is_oxker: bool, name: String, + ports: Vec, state: State, status: String, ) -> Self { let mut docker_controls = StatefulList::new(DockerControls::gen_vec(state)); docker_controls.start(); + Self { - created, cpu_stats: VecDeque::with_capacity(60), + created, docker_controls, id, image: image.into(), @@ -533,6 +587,7 @@ impl ContainerItem { mem_limit: ByteStats::default(), mem_stats: VecDeque::with_capacity(60), name: name.into(), + ports, rx: ByteStats::default(), state, status, @@ -594,7 +649,7 @@ impl ContainerItem { } /// Container information panel headings + widths, for nice pretty formatting -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Columns { pub name: (Header, u8), pub state: (Header, u8), @@ -623,3 +678,98 @@ impl Columns { } } } + +#[cfg(test)] +mod tests { + use ratatui::widgets::ListItem; + + use crate::{ + app_data::{ContainerImage, Logs}, + ui::log_sanitizer, + }; + + use super::{ByteStats, ContainerName, CpuStats, LogsTz}; + + #[test] + // Display CpuStats as a string + fn test_container_state_cpustats_to_string() { + let test = |f: f64, s: &str| { + assert_eq!(CpuStats::new(f).to_string(), s); + }; + + test(0.0, "00.00%"); + test(1.5, "01.50%"); + test(15.15, "15.15%"); + test(150.15, "150.15%"); + } + + #[test] + // Display bytestats as a string, convert into correct data unit (Kb, MB, GB) + fn test_container_state_bytestats_to_string() { + let test = |u: u64, s: &str| { + assert_eq!(ByteStats::new(u).to_string(), s); + }; + + test(0, "0.00 kB"); + test(150, "0.15 kB"); + test(1500, "1.50 kB"); + test(150_000, "150.00 kB"); + test(1_500_000, "1.50 MB"); + test(15_000_000, "15.00 MB"); + test(150_000_000, "150.00 MB"); + test(1_500_000_000, "1.50 GB"); + test(15_000_000_000, "15.00 GB"); + test(150_000_000_000, "150.00 GB"); + } + + #[test] + /// ContainerName as string truncated correctly + fn test_container_state_container_name_to_string() { + let result = ContainerName::from("name_01"); + assert_eq!(result.to_string(), "name_01"); + + let result = ContainerName::from("name_01_name_01_name_01_name_01_"); + assert_eq!(result.to_string(), "name_01_name_01_name_01_name_…"); + + let result = result.get(); + assert_eq!(result, "name_01_name_01_name_01_name_01_"); + } + + #[test] + /// ContainerImage as string truncated correctly + fn test_container_state_container_image() { + let result = ContainerImage::from("name_01"); + assert_eq!(result.to_string(), "name_01"); + + let result = ContainerImage::from("name_01_name_01_name_01_name_01_"); + assert_eq!(result.to_string(), "name_01_name_01_name_01_name_…"); + + let result = result.get(); + assert_eq!(result, "name_01_name_01_name_01_name_01_"); + } + + #[test] + /// Logs can only contain 1 entry per LogzTz + fn test_container_state_logz() { + let input = "2023-01-14T19:13:30.783138328Z Lorem ipsum dolor sit amet"; + let tz = LogsTz::from(input); + let mut logs = Logs::default(); + let line = log_sanitizer::remove_ansi(input); + + logs.insert(ListItem::new(line.clone()), tz.clone()); + logs.insert(ListItem::new(line.clone()), tz.clone()); + logs.insert(ListItem::new(line), tz); + + assert_eq!(logs.logs.items.len(), 1); + + let input = "2023-01-15T19:13:30.783138328Z Lorem ipsum dolor sit amet"; + let tz = LogsTz::from(input); + let line = log_sanitizer::remove_ansi(input); + + logs.insert(ListItem::new(line.clone()), tz.clone()); + logs.insert(ListItem::new(line.clone()), tz.clone()); + logs.insert(ListItem::new(line), tz); + + assert_eq!(logs.logs.items.len(), 2); + } +} diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index a9fcadc..557de6b 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -17,27 +17,6 @@ use crate::{ }; pub use container_state::*; -#[cfg(not(debug_assertions))] -/// Global app_state, stored in an Arc -#[derive(Debug, Clone)] -pub struct AppData { - containers: StatefulList, - error: Option, - sorted_by: Option<(Header, SortedOrder)>, - pub args: CliArgs, -} - -#[cfg(debug_assertions)] -/// Global app_state, stored in an Arc -#[derive(Debug, Clone)] -pub struct AppData { - containers: StatefulList, - error: Option, - sorted_by: Option<(Header, SortedOrder)>, - debug_string: String, - pub args: CliArgs, -} - #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum SortedOrder { Asc, @@ -75,18 +54,47 @@ impl fmt::Display for Header { } } +/// Global app_state, stored in an Arc +#[derive(Debug, Clone)] +#[cfg(not(test))] +pub struct AppData { + containers: StatefulList, + error: Option, + sorted_by: Option<(Header, SortedOrder)>, + pub args: CliArgs, +} + +#[derive(Debug, Clone)] +#[cfg(test)] +pub struct AppData { + pub containers: StatefulList, + pub error: Option, + pub sorted_by: Option<(Header, SortedOrder)>, + pub args: CliArgs, +} + impl AppData { - #[cfg(debug_assertions)] - pub fn get_debug_string(&self) -> &str { - &self.debug_string + /// Generate a default app_state + pub fn default(args: CliArgs) -> Self { + Self { + args, + containers: StatefulList::new(vec![]), + error: None, + sorted_by: None, + } } - #[cfg(debug_assertions)] - #[allow(unused)] - pub fn push_debug_string(&mut self, x: &str) { - self.debug_string.push_str(x); + /// Current time as unix timestamp + #[allow(clippy::expect_used)] + fn get_systemtime() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("In our known reality, this error should never occur") + .as_secs() } + /// Container sort related methods + /// Change the sorted order, also set the selected container state to match new order fn set_sorted(&mut self, x: Option<(Header, SortedOrder)>) { self.sorted_by = x; @@ -99,40 +107,6 @@ impl AppData { })); } - /// Current time as unix timestamp - #[allow(clippy::expect_used)] - fn get_systemtime() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("In our known reality, this error should never occur") - .as_secs() - } - - /// Generate a default app_state - #[cfg(not(debug_assertions))] - pub fn default(args: CliArgs) -> Self { - Self { - args, - containers: StatefulList::new(vec![]), - error: None, - sorted_by: None, - } - } - - /// Generate a default app_state - #[cfg(debug_assertions)] - pub fn default(args: CliArgs) -> Self { - Self { - args, - containers: StatefulList::new(vec![]), - error: None, - sorted_by: None, - debug_string: String::new(), - } - } - - /// Container sort related methods - /// Remove the sorted header & order, and sort by default - created datetime pub fn reset_sorted(&mut self) { self.set_sorted(None); @@ -237,6 +211,11 @@ impl AppData { self.containers.items.len() } + /// Get all the ContainerItems + pub const fn get_container_items(&self) -> &Vec { + &self.containers.items + } + /// Get title for containers section pub fn container_title(&self) -> String { self.containers.get_state_title() @@ -262,9 +241,9 @@ impl AppData { self.containers.previous(); } - /// Get Container items - pub const fn get_container_items(&self) -> &Vec { - &self.containers.items + /// Get ListState of containers + pub fn get_container_state(&mut self) -> &mut ListState { + &mut self.containers.state } /// Get Option of the current selected container @@ -275,6 +254,51 @@ impl AppData { .and_then(|i| self.containers.items.get(i)) } + /// Find the longest port when it's transformed into a string, defaults are header lens (ip, private, public) + pub fn get_longest_port(&self) -> (usize, usize, usize) { + let mut longest_ip = 5; + let mut longest_private = 10; + let mut longest_public = 9; + + for item in &self.containers.items { + // if let Some(ports) = item.ports.as_ref() { + longest_ip = longest_ip.max( + item.ports + .iter() + .map(ContainerPorts::len_ip) + .max() + .unwrap_or(3), + ); + longest_private = longest_private.max( + item.ports + .iter() + .map(ContainerPorts::len_private) + .max() + .unwrap_or(8), + ); + longest_public = longest_public.max( + item.ports + .iter() + .map(ContainerPorts::len_public) + .max() + .unwrap_or(6), + ); + } + // } + + (longest_ip, longest_private, longest_public) + // ) + } + /// Get Option of the current selected container's ports, sorted by private port + pub fn get_selected_ports(&mut self) -> Option<(Vec, State)> { + if let Some(item) = self.get_mut_selected_container() { + let mut ports = item.ports.clone(); + ports.sort_by(|a, b| a.private.cmp(&b.private)); + return Some((ports, item.state)); + } + None + } + /// Get mutable Option of the current selected container fn get_mut_selected_container(&mut self) -> Option<&mut ContainerItem> { self.containers @@ -283,16 +307,37 @@ impl AppData { .and_then(|i| self.containers.items.get_mut(i)) } - /// Get ListState of containers - pub fn get_container_state(&mut self) -> &mut ListState { - &mut self.containers.state + /// return a mutable container by given id + fn get_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> { + self.containers.items.iter_mut().find(|i| &i.id == id) } + /// Get the ContainerName of by ID + pub fn get_container_name_by_id(&mut self, id: &ContainerId) -> Option { + self.containers + .items + .iter_mut() + .find(|i| &i.id == id) + .map(|i| i.name.clone()) + } + + /// Find the id of the currently selected container. + /// If any containers on system, will always return a ContainerId + /// Only returns None when no containers found. + pub fn get_selected_container_id(&self) -> Option { + self.get_selected_container().map(|i| i.id.clone()) + } + + /// Get the Id and State for the currently selected container - used by the exec check method + pub fn get_selected_container_id_state_name(&self) -> Option<(ContainerId, State, String)> { + self.get_selected_container() + .map(|i| (i.id.clone(), i.state, i.name.get().to_owned())) + } /// Selected DockerCommand methods /// Get the current selected docker command /// So know which command to execute - pub fn selected_docker_command(&self) -> Option { + pub fn selected_docker_controls(&self) -> Option { self.get_selected_container().and_then(|i| { i.docker_controls.state.selected().and_then(|x| { i.docker_controls @@ -302,6 +347,35 @@ impl AppData { }) }) } + + /// Change selected choice of docker commands of selected container + pub fn docker_controls_next(&mut self) { + if let Some(i) = self.get_mut_selected_container() { + i.docker_controls.next(); + } + } + + /// Change selected choice of docker commands of selected container + pub fn docker_controls_previous(&mut self) { + if let Some(i) = self.get_mut_selected_container() { + i.docker_controls.previous(); + } + } + + /// Change selected choice of docker commands of selected container + pub fn docker_controls_start(&mut self) { + if let Some(i) = self.get_mut_selected_container() { + i.docker_controls.start(); + } + } + + /// Change selected choice of docker commands of selected container + pub fn docker_controls_end(&mut self) { + if let Some(i) = self.get_mut_selected_container() { + i.docker_controls.end(); + } + } + /// Get mutable Option of the currently selected container DockerControls state pub fn get_control_state(&mut self) -> Option<&mut ListState> { self.get_mut_selected_container() @@ -314,34 +388,6 @@ impl AppData { .map(|i| &mut i.docker_controls.items) } - /// Change selected choice of docker commands of selected container - pub fn docker_command_next(&mut self) { - if let Some(i) = self.get_mut_selected_container() { - i.docker_controls.next(); - } - } - - /// Change selected choice of docker commands of selected container - pub fn docker_command_previous(&mut self) { - if let Some(i) = self.get_mut_selected_container() { - i.docker_controls.previous(); - } - } - - /// Change selected choice of docker commands of selected container - pub fn docker_command_start(&mut self) { - if let Some(i) = self.get_mut_selected_container() { - i.docker_controls.start(); - } - } - - /// Change selected choice of docker commands of selected container - pub fn docker_command_end(&mut self) { - if let Some(i) = self.get_mut_selected_container() { - i.docker_controls.end(); - } - } - /// Logs related methods /// Get the title for log panel for selected container, will be either @@ -349,16 +395,16 @@ impl AppData { /// 2) "logs - container_name" when no logs found, again 32 chars max /// 3) "" no container currently selected - aka no containers on system pub fn get_log_title(&self) -> String { - self.get_selected_container().map_or_else(String::new, |y| { - let logs_len = y.logs.get_state_title(); - // let mut name = y.name.clone(); - // name.truncate(32); - if logs_len.is_empty() { - format!("- {} ", y.name) - } else { - format!("{logs_len} - {}", y.name.get()) - } - }) + self.get_selected_container() + .map_or_else(String::new, |ci| { + let logs_len = ci.logs.get_state_title(); + let prefix = if logs_len.is_empty() { + String::from(" ") + } else { + format!("{logs_len} ") + }; + format!("{}- {}", prefix, ci.name.get()) + }) } /// select next selected log line @@ -389,19 +435,6 @@ impl AppData { } } - /// Chart data related methods - - /// Get mutable Option of the currently selected container chart data - pub fn get_chart_data(&mut self) -> Option<(CpuTuple, MemTuple)> { - self.containers - .state - .selected() - .and_then(|i| self.containers.items.get_mut(i)) - .map(|i| i.get_chart_data()) - } - - /// Logs related methods - /// Get mutable Vec of current containers logs pub fn get_logs(&mut self) -> Vec> { self.containers @@ -420,6 +453,17 @@ impl AppData { .map(|i| i.logs.state()) } + /// Chart data related methods + + /// Get mutable Option of the currently selected container chart data + pub fn get_chart_data(&mut self) -> Option<(CpuTuple, MemTuple)> { + self.containers + .state + .selected() + .and_then(|i| self.containers.items.get_mut(i)) + .map(|i| i.get_chart_data()) + } + /// Error related methods /// return single app_state error @@ -445,6 +489,12 @@ impl AppData { self.get_selected_container().map_or(false, |i| i.is_oxker) } + /// Check if selected container is oxker and also that oxker is being run in a container + pub fn is_oxker_in_container(&self) -> bool { + self.get_selected_container() + .map_or(false, |i| i.is_oxker && self.args.in_container) + } + /// Find the widths for the strings in the containers panel. /// So can display nicely and evenly pub fn get_width(&self) -> Columns { @@ -485,36 +535,9 @@ impl AppData { /// Update related methods - /// return a mutable container by given id - fn get_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> { - self.containers.items.iter_mut().find(|i| &i.id == id) - } - - /// Get the ContainerName of by ID - pub fn get_container_name_by_id(&mut self, id: &ContainerId) -> Option { - self.containers - .items - .iter_mut() - .find(|i| &i.id == id) - .map(|i| i.name.clone()) - } - - /// Find the id of the currently selected container. - /// If any containers on system, will always return a ContainerId - /// Only returns None when no containers found. - pub fn get_selected_container_id(&self) -> Option { - self.get_selected_container().map(|i| i.id.clone()) - } - - /// Get the Id and State for the currently selected container - used by the exec check method - pub fn get_selected_container_id_state_name(&self) -> Option<(ContainerId, State, String)> { - self.get_selected_container() - .map(|i| (i.id.clone(), i.state, i.name.get().to_owned())) - } - /// Update container mem, cpu, & network stats, in single function so only need to call .lock() once /// Will also, if a sort is set, sort the containers - pub fn update_stats( + pub fn update_stats_by_id( &mut self, id: &ContainerId, cpu_stat: Option, @@ -593,6 +616,10 @@ impl AppData { }) }); + let ports = i.ports.as_ref().map_or(vec![], |i| { + i.iter().map(ContainerPorts::from).collect::>() + }); + let id = ContainerId::from(id.as_str()); let is_oxker = i @@ -633,13 +660,17 @@ impl AppData { }; item.state = state; }; + + item.ports = ports; + if item.image.get() != image { item.image.set(image); }; } else { // container not known, so make new ContainerItem and push into containers Vec - let container = - ContainerItem::new(created, id, image, is_oxker, name, state, status); + let container = ContainerItem::new( + created, id, image, is_oxker, name, ports, state, status, + ); self.containers.items.push(container); } } @@ -660,7 +691,6 @@ impl AppData { for mut i in logs { let tz = LogsTz::from(i.as_str()); - // Strip the timestamp if `-t` flag set if !timestamp { i = i.replace(&tz.to_string(), ""); } @@ -685,3 +715,1148 @@ impl AppData { } } } + +#[cfg(test)] +#[allow(clippy::unwrap_used, clippy::many_single_char_names)] +mod tests { + + use super::*; + use crate::tests::{gen_appdata, gen_container_summary, gen_containers}; + use std::collections::VecDeque; + + // ******* // + // Sort by // + // ******* // + + #[test] + /// Sort by header: name + fn test_app_data_set_sort_by_header_name() { + let (_ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_items(); + assert_eq!(result, &containers); + + // descending + app_data.set_sorted(Some((Header::Name, SortedOrder::Desc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("3")); + assert_eq!(b.id, ContainerId::from("2")); + assert_eq!(c.id, ContainerId::from("1")); + + // ascending + app_data.set_sorted(Some((Header::Name, SortedOrder::Asc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("1")); + assert_eq!(b.id, ContainerId::from("2")); + assert_eq!(c.id, ContainerId::from("3")); + } + + #[test] + /// Sort by header: state + fn test_app_data_set_sort_by_header_state() { + let (_ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_items(); + assert_eq!(result, &containers); + + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) { + i.state = State::Exited; + } + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) { + i.state = State::Running; + } + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) { + i.state = State::Paused; + } + + // descending + app_data.set_sorted(Some((Header::State, SortedOrder::Desc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("1")); + assert_eq!(b.id, ContainerId::from("3")); + assert_eq!(c.id, ContainerId::from("2")); + + // ascending + app_data.set_sorted(Some((Header::State, SortedOrder::Asc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("2")); + assert_eq!(b.id, ContainerId::from("3")); + assert_eq!(c.id, ContainerId::from("1")); + } + + #[test] + /// Sort by header: status + fn test_app_data_set_sort_by_header_status() { + let (_ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_items(); + assert_eq!(result, &containers); + + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) { + i.status = "Exited (0) 10 minutes ago".to_owned(); + } + + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) { + i.status = "Up 2 hours (Paused)".to_owned(); + } + + // Sort by status + // descending + app_data.set_sorted(Some((Header::Status, SortedOrder::Desc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("3")); + assert_eq!(b.id, ContainerId::from("1")); + assert_eq!(c.id, ContainerId::from("2")); + + // ascending + app_data.set_sorted(Some((Header::Status, SortedOrder::Asc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("2")); + assert_eq!(b.id, ContainerId::from("1")); + assert_eq!(c.id, ContainerId::from("3")); + } + + #[test] + /// Sort by header: cpu + fn test_app_data_set_sort_by_header_cpu() { + let (_ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_items(); + assert_eq!(result, &containers); + + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) { + i.cpu_stats = VecDeque::from([CpuStats::new(10.1)]); + } + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) { + i.cpu_stats = VecDeque::from([CpuStats::new(8.1)]); + } + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) { + i.cpu_stats = VecDeque::from([CpuStats::new(20.3)]); + } + + // descending + app_data.set_sorted(Some((Header::Cpu, SortedOrder::Desc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("3")); + assert_eq!(b.id, ContainerId::from("1")); + assert_eq!(c.id, ContainerId::from("2")); + + // ascending + app_data.set_sorted(Some((Header::Cpu, SortedOrder::Asc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("2")); + assert_eq!(b.id, ContainerId::from("1")); + assert_eq!(c.id, ContainerId::from("3")); + } + + #[test] + /// Sort by header: memory + fn test_app_data_set_sort_by_header_mem() { + let (_ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_items(); + assert_eq!(result, &containers); + + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) { + i.mem_stats = VecDeque::from([ByteStats::new(40)]); + } + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) { + i.mem_stats = VecDeque::from([ByteStats::new(80)]); + } + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) { + i.mem_stats = VecDeque::from([ByteStats::new(2)]); + } + + // descending + app_data.set_sorted(Some((Header::Memory, SortedOrder::Desc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("2")); + assert_eq!(b.id, ContainerId::from("1")); + assert_eq!(c.id, ContainerId::from("3")); + + // ascending + app_data.set_sorted(Some((Header::Memory, SortedOrder::Asc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("3")); + assert_eq!(b.id, ContainerId::from("1")); + assert_eq!(c.id, ContainerId::from("2")); + } + + #[test] + /// Sort by header: id + fn test_app_data_set_sort_by_header_id() { + let (_ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_items(); + assert_eq!(result, &containers); + + // descending + app_data.set_sorted(Some((Header::Id, SortedOrder::Desc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("3")); + assert_eq!(b.id, ContainerId::from("2")); + assert_eq!(c.id, ContainerId::from("1")); + + // ascending + app_data.set_sorted(Some((Header::Id, SortedOrder::Asc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("1")); + assert_eq!(b.id, ContainerId::from("2")); + assert_eq!(c.id, ContainerId::from("3")); + } + + #[test] + /// Sort by header: image + fn test_app_data_set_sort_by_header_image() { + let (_ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_items(); + assert_eq!(result, &containers); + + // descending + app_data.set_sorted(Some((Header::Image, SortedOrder::Desc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("3")); + assert_eq!(b.id, ContainerId::from("2")); + assert_eq!(c.id, ContainerId::from("1")); + + // ascending + app_data.set_sorted(Some((Header::Image, SortedOrder::Asc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("1")); + assert_eq!(b.id, ContainerId::from("2")); + assert_eq!(c.id, ContainerId::from("3")); + } + + #[test] + /// Sort by header: rx + fn test_app_data_set_sort_by_header_rx() { + let (_ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_items(); + assert_eq!(result, &containers); + + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) { + i.rx = ByteStats::new(40); + } + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) { + i.rx = ByteStats::new(80); + } + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) { + i.rx = ByteStats::new(2); + } + + // descending + app_data.set_sorted(Some((Header::Rx, SortedOrder::Desc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("2")); + assert_eq!(b.id, ContainerId::from("1")); + assert_eq!(c.id, ContainerId::from("3")); + + // ascending + app_data.set_sorted(Some((Header::Rx, SortedOrder::Asc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("3")); + assert_eq!(b.id, ContainerId::from("1")); + assert_eq!(c.id, ContainerId::from("2")); + } + + #[test] + /// Sort by header: tx + fn test_app_data_set_sort_by_header_tx() { + let (_ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_items(); + assert_eq!(result, &containers); + + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) { + i.rx = ByteStats::new(400); + } + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) { + i.rx = ByteStats::new(80); + } + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) { + i.rx = ByteStats::new(83); + } + + // descending + app_data.set_sorted(Some((Header::Rx, SortedOrder::Desc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("1")); + assert_eq!(b.id, ContainerId::from("3")); + assert_eq!(c.id, ContainerId::from("2")); + + // ascending + app_data.set_sorted(Some((Header::Rx, SortedOrder::Asc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("2")); + assert_eq!(b.id, ContainerId::from("3")); + assert_eq!(c.id, ContainerId::from("1")); + } + + #[test] + /// Sort by header when selected headers match + fn test_app_data_set_sort_by_header_match() { + let (_ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_items(); + assert_eq!(result, &containers); + + // descending + app_data.set_sorted(Some((Header::Rx, SortedOrder::Desc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("3")); + assert_eq!(b.id, ContainerId::from("2")); + assert_eq!(c.id, ContainerId::from("1")); + + // ascending + app_data.set_sorted(Some((Header::Rx, SortedOrder::Asc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("1")); + assert_eq!(b.id, ContainerId::from("2")); + assert_eq!(c.id, ContainerId::from("3")); + } + + #[test] + /// reset sorted + fn test_app_data_reset_sorted() { + let (_ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_items(); + assert_eq!(result, &containers); + + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) { + i.rx = ByteStats::new(400); + } + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) { + i.rx = ByteStats::new(80); + } + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) { + i.rx = ByteStats::new(83); + } + + app_data.set_sorted(Some((Header::Rx, SortedOrder::Asc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("2")); + assert_eq!(b.id, ContainerId::from("3")); + assert_eq!(c.id, ContainerId::from("1")); + + app_data.set_sorted(None); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("1")); + assert_eq!(b.id, ContainerId::from("2")); + assert_eq!(c.id, ContainerId::from("3")); + } + + // **************** // + // Container state // + // **************** // + + #[test] + /// Get len of current containers vec + fn test_app_data_get_container_len() { + let (_ids, containers) = gen_containers(); + let app_data = gen_appdata(&containers); + assert_eq!(app_data.get_container_len(), 3); + } + + #[test] + /// Select the first container + fn test_app_data_containers_start() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + + // No container selected + let result = app_data.get_container_state(); + assert_eq!(result.selected(), None); + assert_eq!(result.offset(), 0); + + // First container selected + app_data.containers_start(); + let result = app_data.get_container_state(); + assert_eq!(result.selected(), Some(0)); + assert_eq!(result.offset(), 0); + + let result = app_data.get_selected_container_id(); + assert_eq!(result, Some(ContainerId::from("1"))); + let result = app_data.get_selected_container_id_state_name(); + assert_eq!( + result, + Some(( + ContainerId::from("1"), + State::Running, + "container_1".to_owned() + )) + ); + + // Calling previous when at start has no effect + app_data.containers_previous(); + let result = app_data.get_selected_container_id(); + assert_eq!(result, Some(ContainerId::from("1"))); + let result = app_data.get_selected_container_id_state_name(); + assert_eq!( + result, + Some(( + ContainerId::from("1"), + State::Running, + "container_1".to_owned() + )) + ); + } + + #[test] + /// advance container list state by one + /// get get_selected_container_id() & get_selected_container_id_state_name() return valid Some data + fn test_app_data_containers_next() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + + // Advance list state by 1 + app_data.containers_start(); + app_data.containers_next(); + + let result = app_data.get_container_state(); + assert_eq!(result.selected(), Some(1)); + assert_eq!(result.offset(), 0); + + let result = app_data.get_selected_container_id(); + assert_eq!(result, Some(ContainerId::from("2"))); + let result = app_data.get_selected_container_id_state_name(); + assert_eq!( + result, + Some(( + ContainerId::from("2"), + State::Running, + "container_2".to_owned() + )) + ); + } + + #[test] + /// advance container list state to the end + /// get get_selected_container_id() & get_selected_container_id_state_name() return valid Some data + fn test_app_data_containers_end() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + + app_data.containers_end(); + let result = app_data.get_container_state(); + assert_eq!(result.selected(), Some(2)); + assert_eq!(result.offset(), 0); + + let result = app_data.get_selected_container_id(); + assert_eq!(result, Some(ContainerId::from("3"))); + let result = app_data.get_selected_container_id_state_name(); + assert_eq!( + result, + Some(( + ContainerId::from("3"), + State::Running, + "container_3".to_owned() + )) + ); + + // Calling previous when at end has no effect + app_data.containers_next(); + let result = app_data.get_selected_container_id(); + assert_eq!(result, Some(ContainerId::from("3"))); + let result = app_data.get_selected_container_id_state_name(); + assert_eq!( + result, + Some(( + ContainerId::from("3"), + State::Running, + "container_3".to_owned() + )) + ); + } + + #[test] + /// go to previous container + fn test_app_data_containers_prev() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + + app_data.containers_end(); + app_data.containers_previous(); + let result = app_data.get_container_state(); + assert_eq!(result.selected(), Some(1)); + assert_eq!(result.offset(), 0); + } + + #[test] + // Get the currently selected container + fn test_app_data_get_selected_container() { + let (_ids, mut containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_selected_container(); + assert_eq!(result, None); + + app_data.containers.start(); + app_data.containers.next(); + + let result = app_data.get_selected_container(); + assert_eq!(result, Some(&containers[1])); + + // As above, but now as mut + let result = app_data.get_mut_selected_container(); + assert_eq!(result, Some(&mut containers[1])); + } + + #[test] + // Get mut container by id + fn test_app_data_get_container_by_id() { + let (_ids, mut containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_by_id(&ContainerId::from("2")); + assert_eq!(result, Some(&mut containers[1])); + } + + #[test] + // Get just the containers name by id + fn test_app_data_get_container_name_by_id() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_name_by_id(&ContainerId::from("2")); + assert_eq!(result, Some(ContainerName::from("container_2"))); + } + + #[test] + // Get the id of the currently selected container + fn test_app_data_get_selected_container_id() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + app_data.containers_end(); + + let result = app_data.get_selected_container_id(); + assert_eq!(result, Some(ContainerId::from("3"))); + } + + #[test] + fn test_app_data_get_selected_container_id_state_name() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + app_data.containers_end(); + + let result = app_data.get_selected_container_id_state_name(); + assert_eq!( + result, + Some(( + ContainerId::from("3"), + State::Running, + "container_3".to_owned() + )) + ); + } + + // ************** // + // DockerControls // + // ************** // + + #[test] + /// Docker commands returned correctly + fn test_app_data_selected_docker_command() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + + // No commands when no container selected + let result = app_data.selected_docker_controls(); + assert!(result.is_none()); + + // Correct commands returned + app_data.containers_start(); + app_data.docker_controls_start(); + + let result = app_data.selected_docker_controls(); + assert_eq!(result, Some(DockerControls::Pause)); + } + + #[test] + /// Docker command next works + fn test_app_data_selected_docker_command_next() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + app_data.containers_start(); + app_data.docker_controls_start(); + app_data.docker_controls_next(); + + let result = app_data.selected_docker_controls(); + assert_eq!(result, Some(DockerControls::Restart)); + } + + #[test] + /// Dockercommand end works, and next has no effect when at end + fn test_app_data_selected_docker_command_end() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + app_data.containers_start(); + app_data.docker_controls_end(); + + let result = app_data.selected_docker_controls(); + assert_eq!(result, Some(DockerControls::Delete)); + + // Next has no effect when at end + app_data.docker_controls_next(); + let result = app_data.selected_docker_controls(); + assert_eq!(result, Some(DockerControls::Delete)); + } + + #[test] + /// Docker commands previous works, and has no effect when at start + fn test_app_data_selected_docker_command_previous() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + app_data.containers_start(); + app_data.docker_controls_end(); + app_data.docker_controls_previous(); + + let result = app_data.selected_docker_controls(); + assert_eq!(result, Some(DockerControls::Stop)); + + // previous has no effect when at start + app_data.docker_controls_start(); + app_data.docker_controls_previous(); + let result = app_data.selected_docker_controls(); + assert_eq!(result, Some(DockerControls::Pause)); + } + + #[test] + /// DockerCommands get correct controls dependant on container state + fn test_app_data_get_control_items() { + let test_state = |state: State, expected: &mut Vec| { + let gen_item_state = |state: State| { + ContainerItem::new( + 1, + ContainerId::from("1"), + "image_1".to_owned(), + false, + "container_1".to_owned(), + vec![], + state, + "Up 1 hour".to_owned(), + ) + }; + let mut app_data = gen_appdata(&vec![gen_item_state(state)]); + app_data.containers_start(); + app_data.docker_controls_start(); + + let result = app_data.get_control_items(); + assert_eq!(result, Some(expected)); + }; + + test_state( + State::Dead, + &mut vec![ + DockerControls::Start, + DockerControls::Restart, + DockerControls::Delete, + ], + ); + test_state( + State::Exited, + &mut vec![ + DockerControls::Start, + DockerControls::Restart, + DockerControls::Delete, + ], + ); + test_state( + State::Paused, + &mut vec![ + DockerControls::Resume, + DockerControls::Stop, + DockerControls::Delete, + ], + ); + test_state(State::Removing, &mut vec![DockerControls::Delete]); + test_state( + State::Restarting, + &mut vec![DockerControls::Stop, DockerControls::Delete], + ); + test_state( + State::Running, + &mut vec![ + DockerControls::Pause, + DockerControls::Restart, + DockerControls::Stop, + DockerControls::Delete, + ], + ); + test_state(State::Unknown, &mut vec![DockerControls::Delete]); + } + + // **** // + // Logs // + // **** // + + #[test] + /// log title string generated correctly + fn test_app_data_get_log_title() { + let (ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + // No container selected select + let result = app_data.get_log_title(); + assert_eq!(result, ""); + + // No logs + app_data.containers.start(); + let result = app_data.get_log_title(); + assert_eq!(result, " - container_1"); + + // On last line of logs + let logs = (1..=3).map(|i| format!("{i}")).collect::>(); + app_data.update_log_by_id(logs, &ids[0]); + let result = app_data.get_log_title(); + assert_eq!(result, " 3/3 - container_1"); + + // Change log state to no longer be at the end + app_data.log_previous(); + let result = app_data.get_log_title(); + assert_eq!(result, " 2/3 - container_1"); + } + + #[test] + /// log title string generated correctly after container change + fn test_app_data_get_log_title_after_container_change() { + let (ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + + // No container selected select + let result = app_data.get_log_title(); + assert_eq!(result, ""); + + app_data.containers_start(); + + let result = app_data.get_log_title(); + assert_eq!(result, " - container_1"); + + // change container + app_data.containers_next(); + let result = app_data.get_log_title(); + assert_eq!(result, " - container_2"); + + // On last line of logs + let logs = (1..=3).map(|i| format!("{i}")).collect::>(); + app_data.update_log_by_id(logs, &ids[1]); + let result = app_data.get_log_title(); + assert_eq!(result, " 3/3 - container_2"); + + // Change log state to no longer be at the end + app_data.log_previous(); + let result = app_data.get_log_title(); + assert_eq!(result, " 2/3 - container_2"); + } + + #[test] + /// update logs by id works + fn test_app_data_update_log_by_id() { + let (ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + + // No container selected select + let result = app_data.get_log_title(); + assert_eq!(result, ""); + + app_data.containers_start(); + let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>(); + + app_data.update_log_by_id(logs, &ids[0]); + // app_data.log_start(); + + let result = app_data.get_log_state(); + assert!(result.is_some()); + assert_eq!(result.as_ref().unwrap().selected(), Some(2)); + assert_eq!(result.unwrap().offset(), 0); + + let result = app_data.get_logs(); + assert_eq!(result.len(), 3); + + let result = app_data.get_log_title(); + assert_eq!(result, " 3/3 - container_1"); + } + + #[test] + /// logs state reset to start + fn test_app_data_logs_start() { + let (ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>(); + app_data.containers_start(); + app_data.update_log_by_id(logs, &ids[0]); + + app_data.log_start(); + + let result = app_data.get_log_state(); + assert!(result.is_some()); + assert_eq!(result.as_ref().unwrap().selected(), Some(0)); + assert_eq!(result.unwrap().offset(), 0); + + let result = app_data.get_log_title(); + assert_eq!(result, " 1/3 - container_1"); + } + + #[test] + /// logs state end goes to the end of the logs list + fn test_app_data_logs_end() { + let (ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>(); + app_data.containers_start(); + app_data.update_log_by_id(logs, &ids[0]); + + app_data.log_start(); + + let result = app_data.get_log_state(); + assert!(result.is_some()); + assert_eq!(result.as_ref().unwrap().selected(), Some(0)); + assert_eq!(result.unwrap().offset(), 0); + + let result = app_data.get_log_title(); + assert_eq!(result, " 1/3 - container_1"); + + app_data.log_end(); + let result = app_data.get_log_state(); + assert!(result.is_some()); + assert_eq!(result.as_ref().unwrap().selected(), Some(2)); + assert_eq!(result.unwrap().offset(), 0); + + let result = app_data.get_log_title(); + assert_eq!(result, " 3/3 - container_1"); + } + + #[test] + /// logs state next works + /// At end has no effect + fn test_app_data_logs_next() { + let (ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>(); + app_data.containers_start(); + app_data.update_log_by_id(logs, &ids[0]); + + app_data.log_start(); + + let result = app_data.get_log_state(); + assert!(result.is_some()); + assert_eq!(result.as_ref().unwrap().selected(), Some(0)); + assert_eq!(result.unwrap().offset(), 0); + + let result = app_data.get_log_title(); + assert_eq!(result, " 1/3 - container_1"); + + app_data.log_next(); + + let result = app_data.get_log_state(); + assert!(result.is_some()); + assert_eq!(result.as_ref().unwrap().selected(), Some(1)); + assert_eq!(result.unwrap().offset(), 0); + + let result = app_data.get_log_title(); + assert_eq!(result, " 2/3 - container_1"); + + app_data.log_next(); + let result = app_data.get_log_state(); + assert!(result.is_some()); + assert_eq!(result.as_ref().unwrap().selected(), Some(2)); + assert_eq!(result.unwrap().offset(), 0); + + let result = app_data.get_log_title(); + assert_eq!(result, " 3/3 - container_1"); + app_data.log_next(); + + let result = app_data.get_log_state(); + assert!(result.is_some()); + assert_eq!(result.as_ref().unwrap().selected(), Some(2)); + assert_eq!(result.unwrap().offset(), 0); + + let result = app_data.get_log_title(); + assert_eq!(result, " 3/3 - container_1"); + } + + #[test] + /// logs state previous works + /// previous at start has no effect + fn test_app_data_logs_previous() { + let (ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>(); + app_data.containers_start(); + app_data.update_log_by_id(logs, &ids[0]); + + app_data.log_end(); + + let result = app_data.get_log_state(); + assert!(result.is_some()); + assert_eq!(result.as_ref().unwrap().selected(), Some(2)); + assert_eq!(result.unwrap().offset(), 0); + + let result = app_data.get_log_title(); + assert_eq!(result, " 3/3 - container_1"); + + app_data.log_previous(); + + let result = app_data.get_log_state(); + assert!(result.is_some()); + assert_eq!(result.as_ref().unwrap().selected(), Some(1)); + assert_eq!(result.unwrap().offset(), 0); + let result = app_data.get_log_title(); + assert_eq!(result, " 2/3 - container_1"); + + app_data.log_previous(); + let result = app_data.get_log_state(); + assert!(result.is_some()); + assert_eq!(result.as_ref().unwrap().selected(), Some(0)); + assert_eq!(result.unwrap().offset(), 0); + let result = app_data.get_log_title(); + assert_eq!(result, " 1/3 - container_1"); + + app_data.log_previous(); + let result = app_data.get_log_state(); + assert!(result.is_some()); + assert_eq!(result.as_ref().unwrap().selected(), Some(0)); + assert_eq!(result.unwrap().offset(), 0); + let result = app_data.get_log_title(); + assert_eq!(result, " 1/3 - container_1"); + } + + // ********** // + // Chart data // + // ********** // + + #[test] + /// Chart data returned correctly + fn test_app_data_get_chart_data() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_chart_data(); + assert!(result.is_none()); + + app_data.containers_start(); + + if let Some(item) = app_data.get_container_by_id(&ContainerId::from("1")) { + item.cpu_stats = VecDeque::from([CpuStats::new(1.1), CpuStats::new(1.2)]); + item.mem_stats = VecDeque::from([ByteStats::new(1), ByteStats::new(2)]); + } + + let result = app_data.get_chart_data(); + assert_eq!( + result, + Some(( + ( + vec![(0.0, 1.1), (1.0, 1.2)], + CpuStats::new(1.2), + State::Running + ), + ( + vec![(0.0, 1.0), (1.0, 2.0)], + ByteStats::new(2), + State::Running + ) + )) + ); + } + + // ************* // + // Header Widths // + // ************* // + + #[test] + /// Header widths return correctly + fn test_app_data_get_width() { + let (_ids, containers) = gen_containers(); + let app_data = gen_appdata(&containers); + + let result = app_data.get_width(); + let expected = Columns { + name: (Header::Name, 11), + state: (Header::State, 11), + status: (Header::Status, 16), + cpu: (Header::Cpu, 7), + mem: (Header::Memory, 7, 7), + id: (Header::Id, 8), + image: (Header::Image, 7), + net_rx: (Header::Rx, 7), + net_tx: (Header::Tx, 7), + }; + assert_eq!(result, expected); + } + + // ***** // + // Ports // + // ***** // + + #[test] + /// Returns selected containers ports ordered by private ip + fn test_app_data_get_selected_ports() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + + app_data.containers.items[0].ports.push(ContainerPorts { + ip: None, + private: 10, + public: Some(1), + }); + app_data.containers.items[0].ports.push(ContainerPorts { + ip: None, + private: 11, + public: Some(3), + }); + app_data.containers.items[0].ports.push(ContainerPorts { + ip: None, + private: 4, + public: Some(2), + }); + + // No containers selected + let result = app_data.get_selected_ports(); + assert!(result.is_none()); + + // Selected container & ports + app_data.containers_start(); + let result = app_data.get_selected_ports(); + + assert_eq!( + result, + Some(( + vec![ + ContainerPorts { + ip: None, + private: 4, + public: Some(2) + }, + ContainerPorts { + ip: None, + private: 10, + public: Some(1) + }, + ContainerPorts { + ip: None, + private: 11, + public: Some(3) + }, + ContainerPorts { + ip: None, + private: 8001, + public: None + } + ], + State::Running + )) + ); + + // Selected container & no ports + app_data.containers_start(); + app_data.containers.items[0].ports = vec![]; + let result = app_data.get_selected_ports(); + + assert_eq!(result, Some((vec![], State::Running))); + } + + // ************** // + // Update mtehods // + // ************** // + + #[test] + /// Update stats functioning + fn test_app_data_update_stats() { + let (ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_items(); + assert_eq!(result[0], containers[0]); + + app_data.update_stats_by_id(&ids[0], Some(10.0), Some(10), 10, 10, 10); + + let result = app_data.get_container_items(); + assert_ne!(result[0], containers[0]); + assert_eq!(result[0].cpu_stats, VecDeque::from([CpuStats::new(10.0)])); + assert_eq!(result[0].mem_stats, VecDeque::from([ByteStats::new(10)])); + assert_eq!(result[0].mem_limit, ByteStats::new(10)); + assert_eq!(result[0].rx, ByteStats::new(10)); + assert_eq!(result[0].tx, ByteStats::new(10)); + } + + #[test] + /// Update stats functioning + fn test_app_data_update_containers() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + let result_pre = app_data.get_container_items().clone(); + let mut input = vec![ + gen_container_summary(1, "paused"), + gen_container_summary(2, "dead"), + ]; + + app_data.update_containers(&mut input); + let result_post = app_data.get_container_items(); + assert_ne!(&result_pre, result_post); + assert_eq!(result_post[0].state, State::Paused); + assert_eq!(result_post[1].state, State::Dead); + } + + #[test] + /// Update logs don't work if container is_oxker: true + fn test_app_data_update_log_by_id_is_oxker() { + let (ids, mut containers) = gen_containers(); + containers[0].is_oxker = true; + let mut app_data = gen_appdata(&containers); + let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>(); + + app_data.update_log_by_id(logs, &ids[0]); + app_data.log_start(); + + let result = app_data.get_log_state(); + assert!(result.is_none()); + } +} diff --git a/src/docker_data/message.rs b/src/docker_data/message.rs index e1066c1..866aada 100644 --- a/src/docker_data/message.rs +++ b/src/docker_data/message.rs @@ -14,6 +14,6 @@ pub enum DockerMessage { Restart(ContainerId), Start(ContainerId), Stop(ContainerId), - Unpause(ContainerId), + Resume(ContainerId), Update, } diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs index 8964ebf..be8f1f2 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -71,6 +71,7 @@ pub struct DockerData { impl DockerData { /// Use docker stats to calculate current cpu usage #[allow(clippy::cast_precision_loss)] + // FIX: this can overflow fn calculate_usage(stats: &Stats) -> f64 { let mut cpu_percentage = 0.0; let previous_cpu = stats.precpu_stats.cpu_usage.total_usage; @@ -150,7 +151,7 @@ impl DockerData { app_data .lock() - .update_stats(&id, cpu_stats, mem_stat, mem_limit, rx, tx); + .update_stats_by_id(&id, cpu_stats, mem_stat, mem_limit, rx, tx); } } spawns.lock().remove(&spawn_id); @@ -162,7 +163,6 @@ impl DockerData { /// Update all stats, spawn each container into own tokio::spawn thread fn update_all_container_stats(&mut self, all_ids: &[(State, ContainerId)]) { for (state, id) in all_ids { - // let init = self.init.as_ref().map_or_else(|| None, |x| Some((Arc::clone(x), all_ids.len()))); let docker = Arc::clone(&self.docker); let app_data = Arc::clone(&self.app_data); let spawns = Arc::clone(&self.spawns); @@ -387,11 +387,11 @@ impl DockerData { }); self.update_everything().await; } - DockerMessage::Unpause(id) => { + DockerMessage::Resume(id) => { tokio::spawn(async move { let handle = GuiState::start_loading_animation(&gui_state, uuid); if docker.unpause_container(id.get()).await.is_err() { - Self::set_error(&app_data, DockerControls::Unpause, &gui_state); + Self::set_error(&app_data, DockerControls::Resume, &gui_state); } gui_state.lock().stop_loading_animation(&handle, uuid); }); @@ -436,7 +436,7 @@ impl DockerData { } /// Send an update message every x ms, where x is the args.docker_interval - fn croner(args: &CliArgs, docker_tx: Sender) { + fn scheduler(args: &CliArgs, docker_tx: Sender) { let update_duration = std::time::Duration::from_millis(u64::from(args.docker_interval)); let mut now = std::time::Instant::now(); tokio::spawn(async move { @@ -472,10 +472,152 @@ impl DockerData { spawns: Arc::new(Mutex::new(HashMap::new())), }; inner.initialise_container_data().await; - Self::croner(&args, docker_tx); + Self::scheduler(&args, docker_tx); inner.message_handler().await; } } } // tests, use redis-test container, check logs exists, and selector of logs, and that it increases, and matches end, when you run restart on the docker containers +#[cfg(test)] +mod tests { + use bollard::container::{ + BlkioStats, CPUStats, CPUUsage, MemoryStats, PidsStats, StorageStats, ThrottlingData, + }; + + use super::*; + + #[allow(clippy::too_many_lines)] + fn gen_stats(x: u64, y: u64) -> Stats { + Stats { + read: String::new(), + preread: String::new(), + num_procs: 0, + pids_stats: PidsStats { + current: None, + limit: None, + }, + network: None, + networks: None, + memory_stats: MemoryStats { + stats: None, + max_usage: None, + usage: None, + failcnt: None, + limit: None, + commit: None, + commit_peak: None, + commitbytes: None, + commitpeakbytes: None, + privateworkingset: None, + }, + blkio_stats: BlkioStats { + io_service_bytes_recursive: None, + io_serviced_recursive: None, + io_queue_recursive: None, + io_service_time_recursive: None, + io_wait_time_recursive: None, + io_merged_recursive: None, + io_time_recursive: None, + sectors_recursive: None, + }, + cpu_stats: CPUStats { + cpu_usage: CPUUsage { + percpu_usage: Some(vec![ + 291_593_800, + 182_192_900, + 195_048_700, + 23_032_300, + 132_928_700, + 235_555_600, + 120_225_700, + 175_752_000, + 213_060_300, + 95_321_600, + 226_821_000, + 0, + 109_151_300, + 0, + 86_240_200, + 1_884_400, + 59_077_300, + 23_224_900, + 95_386_300, + 144_987_400, + ]), + total_usage: 250_000_000, + usage_in_usermode: 1_020_000_000, + usage_in_kernelmode: 1_030_000_000, + }, + system_cpu_usage: Some(x), + online_cpus: Some(1), + throttling_data: ThrottlingData { + periods: 0, + throttled_periods: 0, + throttled_time: 0, + }, + }, + precpu_stats: CPUStats { + cpu_usage: CPUUsage { + percpu_usage: Some(vec![ + 291_593_800, + 182_192_900, + 195_048_700, + 23_032_300, + 132_928_700, + 235_555_600, + 120_225_700, + 175_752_000, + 213_060_300, + 95_321_600, + 226_821_000, + 0, + 109_151_300, + 0, + 86_240_200, + 1_884_400, + 59_077_300, + 23_224_900, + 93_831_100, + 144_987_400, + ]), + total_usage: 200_000_000, + usage_in_usermode: 1_020_000_000, + usage_in_kernelmode: 1_020_000_000, + }, + system_cpu_usage: Some(y), + online_cpus: Some(1), + throttling_data: ThrottlingData { + periods: 0, + throttled_periods: 0, + throttled_time: 0, + }, + }, + storage_stats: StorageStats { + read_count_normalized: None, + read_size_bytes: None, + write_count_normalized: None, + write_size_bytes: None, + }, + name: "/container_1".to_owned(), + id: "1".to_owned(), + } + } + + #[test] + #[allow(clippy::float_cmp)] + /// Test the stats calculator, had to cheat here to get round input/outputs + fn test_calculate_usage_no_previous_cpu() { + let stats = gen_stats(1_000_000_000, 900_000_000); + let result = DockerData::calculate_usage(&stats); + assert_eq!(result, 50.0); + + let stats = gen_stats(1_000_000_000, 800_000_000); + let result = DockerData::calculate_usage(&stats); + assert_eq!(result, 25.0); + + let stats = gen_stats(1_000_000_000, 750_000_000); + let result = DockerData::calculate_usage(&stats); + assert_eq!(result, 20.00); + } +} diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index 1f7fad0..8f9cc36 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -268,11 +268,11 @@ impl InputHandler { // This isn't great, just means you can't send docker commands before full initialization of the program let panel = self.gui_state.lock().get_selected_panel(); if panel == SelectablePanel::Commands { - let option_command = self.app_data.lock().selected_docker_command(); + let option_command = self.app_data.lock().selected_docker_controls(); if let Some(command) = option_command { // Poor way of disallowing commands to be sent to a containerised okxer - if self.app_data.lock().is_oxker() { + if self.app_data.lock().is_oxker_in_container() { return; }; let option_id = self.app_data.lock().get_selected_container_id(); @@ -286,8 +286,8 @@ impl InputHandler { DockerControls::Pause => { self.docker_tx.send(DockerMessage::Pause(id)).await.ok() } - DockerControls::Unpause => { - self.docker_tx.send(DockerMessage::Unpause(id)).await.ok() + DockerControls::Resume => { + self.docker_tx.send(DockerMessage::Resume(id)).await.ok() } DockerControls::Start => { self.docker_tx.send(DockerMessage::Start(id)).await.ok() @@ -337,7 +337,7 @@ impl InputHandler { match selected_panel { SelectablePanel::Containers => locked_data.containers_start(), SelectablePanel::Logs => locked_data.log_start(), - SelectablePanel::Commands => locked_data.docker_command_start(), + SelectablePanel::Commands => locked_data.docker_controls_start(), } } @@ -348,7 +348,7 @@ impl InputHandler { match selected_panel { SelectablePanel::Containers => locked_data.containers_end(), SelectablePanel::Logs => locked_data.log_end(), - SelectablePanel::Commands => locked_data.docker_command_end(), + SelectablePanel::Commands => locked_data.docker_controls_end(), } } @@ -481,7 +481,7 @@ impl InputHandler { match selected_panel { SelectablePanel::Containers => locked_data.containers_next(), SelectablePanel::Logs => locked_data.log_next(), - SelectablePanel::Commands => locked_data.docker_command_next(), + SelectablePanel::Commands => locked_data.docker_controls_next(), }; } @@ -492,7 +492,7 @@ impl InputHandler { match selected_panel { SelectablePanel::Containers => locked_data.containers_previous(), SelectablePanel::Logs => locked_data.log_previous(), - SelectablePanel::Commands => locked_data.docker_command_previous(), + SelectablePanel::Commands => locked_data.docker_controls_previous(), } } } diff --git a/src/main.rs b/src/main.rs index d976325..7eb6851 100644 --- a/src/main.rs +++ b/src/main.rs @@ -164,3 +164,92 @@ async fn main() { } } } + +#[cfg(test)] +#[allow(clippy::unwrap_used, clippy::many_single_char_names, unused)] +mod tests { + use bollard::service::{ContainerSummary, Port}; + + use crate::{ + app_data::{AppData, ContainerId, ContainerItem, ContainerPorts, State, StatefulList}, + parse_args::CliArgs, + }; + + pub const fn gen_args() -> CliArgs { + CliArgs { + color: false, + docker_interval: 1000, + gui: true, + host: None, + in_container: false, + save_dir: None, + raw: false, + show_self: false, + timestamp: false, + use_cli: false, + } + } + + pub fn gen_item(id: &ContainerId, index: usize) -> ContainerItem { + ContainerItem::new( + u64::try_from(index).unwrap(), + id.clone(), + format!("image_{index}"), + false, + format!("container_{index}"), + vec![ContainerPorts { + ip: None, + private: u16::try_from(index).unwrap_or(1) + 8000, + public: None, + }], + State::Running, + format!("Up {index} hour"), + ) + } + + pub fn gen_appdata(containers: &[ContainerItem]) -> AppData { + AppData { + containers: StatefulList::new(containers.to_vec()), + error: None, + sorted_by: None, + args: gen_args(), + } + } + + pub fn gen_containers() -> (Vec, Vec) { + let ids = (1..=3) + .map(|i| ContainerId::from(format!("{i}").as_str())) + .collect::>(); + let containers = ids + .iter() + .enumerate() + .map(|(index, id)| gen_item(id, index + 1)) + .collect::>(); + (ids, containers) + } + + pub fn gen_container_summary(index: usize, state: &str) -> ContainerSummary { + ContainerSummary { + id: Some(format!("{index}")), + names: Some(vec![format!("container_{}", index)]), + image: Some(format!("image_{index}")), + image_id: Some(format!("{index}")), + command: None, + created: Some(i64::try_from(index).unwrap()), + ports: Some(vec![Port { + ip: None, + private_port: u16::try_from(index).unwrap_or(1) + 8000, + public_port: None, + typ: None, + }]), + size_rw: None, + size_root_fs: None, + labels: None, + state: Some(state.to_owned()), + status: Some(format!("Up {index} hour")), + host_config: None, + network_settings: None, + mounts: None, + } + } +} diff --git a/src/ui/color_match.rs b/src/ui/color_match.rs index b35112c..a1171f2 100644 --- a/src/ui/color_match.rs +++ b/src/ui/color_match.rs @@ -72,3 +72,79 @@ pub mod log_sanitizer { } } } + +#[cfg(test)] +mod tests { + use ratatui::{ + style::{Color, Style}, + text::{Line, Span}, + }; + + use super::log_sanitizer; + + // This spells out "oxker", with each char having a foreground and background colour + const INPUT: &str = "\x1b[31;47mo\x1b[32;40mx\x1b[33;41mk\x1b[34;42me\x1b[35;43mr\x1b[0m"; + + #[test] + /// Return test raw, as in show escape codes + fn color_match_raw() { + let result = log_sanitizer::raw(INPUT); + let expected = vec![Line { + spans: [Span { + content: std::borrow::Cow::Borrowed( + "\x1b[31;47mo\x1b[32;40mx\x1b[33;41mk\x1b[34;42me\x1b[35;43mr\x1b[0m", + ), + style: Style::default(), + }] + .to_vec(), + alignment: None, + }]; + assert_eq!(result, expected); + } + + #[test] + // Use the escape codes to colorize the text + fn color_match_colorize() { + let result = log_sanitizer::colorize_logs(INPUT); + let expected = vec![Line { + spans: vec![ + Span { + content: std::borrow::Cow::Borrowed("o"), + style: Style::default().fg(Color::Red).bg(Color::White), + }, + Span { + content: std::borrow::Cow::Borrowed("x"), + style: Style::default().fg(Color::Green).bg(Color::Black), + }, + Span { + content: std::borrow::Cow::Borrowed("k"), + style: Style::default().fg(Color::Yellow).bg(Color::Red), + }, + Span { + content: std::borrow::Cow::Borrowed("e"), + style: Style::default().fg(Color::Blue).bg(Color::Green), + }, + Span { + content: std::borrow::Cow::Borrowed("r"), + style: Style::default().fg(Color::Magenta).bg(Color::Yellow), + }, + ], + alignment: None, + }]; + assert_eq!(result, expected); + } + + #[test] + // Remove all escape ansi codes from given input + fn color_match_remove_ansi() { + let result = log_sanitizer::remove_ansi(INPUT); + let expected = vec![Line { + spans: vec![Span { + content: std::borrow::Cow::Borrowed("oxker"), + style: Style::default(), + }], + alignment: None, + }]; + assert_eq!(result, expected); + } +} diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 8bbc176..2dacdf4 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -1,7 +1,7 @@ use parking_lot::Mutex; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, + style::{Color, Modifier, Style, Stylize}, symbols, text::{Line, Span}, widgets::{ @@ -66,10 +66,10 @@ fn generate_block<'a>( .update_region_map(Region::Panel(panel), area); let mut title = match panel { SelectablePanel::Containers => { - format!("{} {}", panel.title(), app_data.lock().container_title()) + format!("{}{}", panel.title(), app_data.lock().container_title()) } SelectablePanel::Logs => { - format!("{} {}", panel.title(), app_data.lock().get_log_title()) + format!("{}{}", panel.title(), app_data.lock().get_log_title()) } SelectablePanel::Commands => String::new(), }; @@ -95,6 +95,7 @@ pub fn commands( gui_state: &Arc>, ) { let block = || generate_block(app_data, area, fd, gui_state, SelectablePanel::Commands); + // let block = block(); let items = app_data.lock().get_control_items().map_or(vec![], |i| { i.iter() .map(|c| { @@ -115,6 +116,7 @@ pub fn commands( if let Some(i) = app_data.lock().get_control_state() { f.render_stateful_widget(items, area, i); } else { + let block = || generate_block(app_data, area, fd, gui_state, SelectablePanel::Commands); let paragraph = Paragraph::new("") .block(block()) .alignment(Alignment::Center); @@ -207,7 +209,6 @@ pub fn containers( f: &mut Frame, fd: &FrameData, gui_state: &Arc>, - widths: &Columns, ) { let block = generate_block(app_data, area, fd, gui_state, SelectablePanel::Containers); @@ -215,7 +216,7 @@ pub fn containers( .lock() .get_container_items() .iter() - .map(|i| ListItem::new(format_containers(i, widths))) + .map(|i| ListItem::new(format_containers(i, &fd.columns))) .collect::>(); if items.is_empty() { @@ -268,6 +269,65 @@ pub fn logs( } } +// Display the ports in a formatted list +pub fn ports( + f: &mut Frame, + area: Rect, + app_data: &Arc>, + max_lens: (usize, usize, usize), +) { + if let Some(ports) = app_data.lock().get_selected_ports() { + let block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .title_alignment(Alignment::Center) + .title(Span::styled( + " ports ", + Style::default() + .fg(ports.1.get_color()) + .add_modifier(Modifier::BOLD), + )); + + let (ip, private, public) = max_lens; + + if ports.0.is_empty() { + let text = match ports.1 { + State::Running | State::Paused | State::Restarting => "no ports", + _ => "", + }; + let paragraph = Paragraph::new(Span::from(text).add_modifier(Modifier::BOLD)) + .alignment(Alignment::Center) + .block(block); + f.render_widget(paragraph, area); + } else { + let mut output = vec![Line::from( + Span::from(format!( + "{:>ip$}{:>private$}{:>public$}", + "ip", "private", "public" + )) + .fg(Color::Yellow), + )]; + for (index, item) in ports.0.iter().enumerate() { + let fg = if index % 2 == 0 { + Color::White + } else { + Color::Magenta + }; + let strings = item.print(); + + let line = vec![ + Span::from(format!("{:>ip$}", strings.0)).fg(fg), + Span::from(format!("{:>private$}", strings.1)).fg(fg), + Span::from(format!("{:>public$}", strings.2)).fg(fg), + ]; + output.push(Line::from(line)); + } + let paragraph = Paragraph::new(output).block(block); + f.render_widget(paragraph, area); + } + } +} + /// Draw the cpu + mem charts pub fn chart(f: &mut Frame, area: Rect, app_data: &Arc>) { if let Some((cpu, mem)) = app_data.lock().get_chart_data() { @@ -306,10 +366,7 @@ fn make_chart<'a, T: Stats + Display>( current: &'a T, max: &'a T, ) -> Chart<'a> { - let title_color = match state { - State::Running => Color::Green, - _ => state.get_color(), - }; + let title_color = state.get_color(); let label_color = match state { State::Running => ORANGE, _ => state.get_color(), @@ -364,22 +421,22 @@ pub fn heading_bar( // Generate a block for the header, if the header is currently being used to sort a column, then highlight it white let header_block = |x: &Header| { let mut color = Color::Black; - let mut suffix = ""; - let mut suffix_margin = 0; + let mut prefix = ""; + let mut prefix_margin = 0; if let Some((a, b)) = data.sorted_by.as_ref() { if x == a { match b { - SortedOrder::Asc => suffix = " ▲", - SortedOrder::Desc => suffix = " ▼", + SortedOrder::Asc => prefix = "▲ ", + SortedOrder::Desc => prefix = "▼ ", } - suffix_margin = 2; + prefix_margin = 2; color = Color::White; }; }; ( Block::default().style(Style::default().bg(Color::Magenta).fg(color)), - suffix, - suffix_margin, + prefix, + prefix_margin, ) }; @@ -390,30 +447,26 @@ pub fn heading_bar( // Yes this is a mess, needs documenting correctly let text = match header { Header::State => format!( - " {:>width$}{ic}", - header, - ic = block.1, - width = width - block.2, + " {x:>width$}", + x = format!("{ic}{header}", ic = block.1), + width = width ), Header::Name => format!( - " {:>width$}{ic}", - header, - ic = block.1, - width = width - block.2, + " {x:>width$}", + x = format!("{ic}{header}", ic = block.1), + width = width ), Header::Status => format!( - "{} {:>width$}{ic}", + "{} {x:>width$}", MARGIN, - header, - ic = block.1, - width = width - block.2 + x = format!("{ic}{header}", ic = block.1), + width = width ), _ => format!( - "{}{:>width$}{ic}", + "{}{x:>width$}", MARGIN, - header, - ic = block.1, - width = width - block.2 + x = format!("{ic}{header}", ic = block.1), + width = width ), }; let count = u16::try_from(text.chars().count()).unwrap_or_default(); @@ -738,7 +791,7 @@ pub fn help_box(f: &mut Frame) { .title(title) .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(Color::Black)); + .border_style(Style::default().fg(Color::Black).bg(Color::Magenta)); // Order is important here f.render_widget(Clear, area); @@ -763,7 +816,10 @@ pub fn delete_confirm(f: &mut Frame, gui_state: &Arc>, name: &Co Span::from("Are you sure you want to delete container: "), Span::styled( name.to_string(), - Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), + Style::default() + .fg(Color::Red) + .bg(Color::White) + .add_modifier(Modifier::BOLD), ), ]); @@ -780,19 +836,22 @@ pub fn delete_confirm(f: &mut Frame, gui_state: &Arc>, name: &Co Block::default() .border_type(BorderType::Rounded) .borders(Borders::ALL) + .style(Style::default().bg(Color::White)) }; let yes_para = Paragraph::new(yes_text) .alignment(Alignment::Center) .block(button_block()); + // Need to add some padding for the borders - let yes_chars = u16::try_from(yes_text.chars().count() + 2).unwrap_or(9); + let _yes_chars = u16::try_from(yes_text.chars().count() + 2).unwrap_or(9); let no_para = Paragraph::new(no_text) .alignment(Alignment::Center) .block(button_block()); + // Need to add some padding for the borders - let no_chars = u16::try_from(no_text.chars().count() + 2).unwrap_or(8); + // let no_chars = u16::try_from(no_text.chars().count() + 2).unwrap_or(8); let area = popup( lines, @@ -815,23 +874,15 @@ pub fn delete_confirm(f: &mut Frame, gui_state: &Arc>, name: &Co ) .split(area); - // Should maybe have a differenet button_space IF the f.width() is within 2 chars of no_chars + yes_chars? - let button_spacing = (max_line_width - no_chars - yes_chars) / 3; - - let button_spacing = if (button_spacing + max_line_width) > f.size().width { - 1 - } else { - button_spacing - }; let split_buttons = Layout::default() .direction(Direction::Horizontal) .constraints( [ - Constraint::Max(button_spacing), - Constraint::Min(no_chars), - Constraint::Max(button_spacing), - Constraint::Min(yes_chars), - Constraint::Max(button_spacing), + Constraint::Percentage(10), + Constraint::Percentage(35), + Constraint::Percentage(10), + Constraint::Percentage(35), + Constraint::Percentage(10), ] .as_ref(), ) @@ -893,6 +944,8 @@ pub fn error(f: &mut Frame, error: AppError, seconds: Option) { .alignment(Alignment::Center); let area = popup(lines, max_line_width, f.size(), BoxLocation::MiddleCentre); + + // let (paragraph, area) = gen_error(f, error, seconds); f.render_widget(Clear, area); f.render_widget(paragraph, area); } @@ -952,24 +1005,2051 @@ fn popup(text_lines: usize, text_width: usize, r: Rect, box_location: BoxLocatio .split(popup_layout[indexes.0])[indexes.1] } -#[cfg(debug_assertions)] -// Single row at the top of the screen for debugging -pub fn debug_bar(area: Rect, f: &mut Frame, debug_string: &str) { - let block = Block::default().style(Style::default().bg(Color::Red)); - let paragraph = Paragraph::new(debug_string) - .style(Style::default().fg(Color::White)) - .block(block); - f.render_widget(paragraph, area); +#[cfg(test)] +#[allow(clippy::unwrap_used, clippy::many_single_char_names)] +mod tests { + + use std::{ops::RangeInclusive, sync::Arc}; + + use parking_lot::Mutex; + use ratatui::{ + backend::TestBackend, + layout::Rect, + style::{Color, Modifier}, + Terminal, + }; + use uuid::Uuid; + + use crate::{ + app_data::{ + AppData, ContainerId, ContainerImage, ContainerName, ContainerPorts, Header, + SortedOrder, State, StatefulList, + }, + app_error::AppError, + tests::{gen_appdata, gen_container_summary, gen_containers}, + ui::{draw_frame, GuiState}, + }; + + use super::{FrameData, ORANGE, VERSION}; + + struct TuiTestSetup { + app_data: Arc>, + gui_state: Arc>, + fd: FrameData, + area: Rect, + terminal: Terminal, + ids: Vec, + } + + const BORDER_CHARS: [&str; 6] = ["╭", "╮", "─", "│", "╰", "╯"]; + + /// Generate state to be used in *most* gui tests + fn test_setup(w: u16, h: u16, control_start: bool, container_start: bool) -> TuiTestSetup { + let backend = TestBackend::new(w, h); + let terminal = Terminal::new(backend).unwrap(); + + let (ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + if control_start { + app_data.docker_controls_start(); + } + if container_start { + app_data.containers_start(); + } + + let gui_state = GuiState::default(); + + let app_data = Arc::new(Mutex::new(app_data)); + let gui_state = Arc::new(Mutex::new(gui_state)); + + let fd = FrameData::from((app_data.lock(), gui_state.lock())); + let area = Rect::new(0, 0, w, h); + TuiTestSetup { + app_data, + gui_state, + fd, + area, + terminal, + ids, + } + } + + /// Insert some logs into the first container + fn insert_logs(setup: &TuiTestSetup) { + let logs = (1..=3).map(|i| format!("{i} line {i}")).collect::>(); + setup.app_data.lock().update_log_by_id(logs, &setup.ids[0]); + } + + // ******************** // + // DockerControls panel // + // ******************** // + + #[test] + // Test that when DockerCommands are available, they are drawn correctly, dependant on container state + fn test_draw_blocks_commands_none() { + let (w, h) = (12, 6); + let mut setup = test_setup(w, h, false, false); + + setup + .terminal + .draw(|f| { + super::commands(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state); + }) + .unwrap(); + + let expected = [ + "╭──────────╮", + "│ │", + "│ │", + "│ │", + "│ │", + "╰──────────╯", + ]; + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + assert_eq!(result_cell.fg, Color::Reset); + } + } + } + + #[test] + // Test that when DockerCommands are available, they are drawn correctly, dependant on container state + fn test_draw_blocks_commands_some() { + let (w, h) = (12, 6); + let mut setup = test_setup(w, h, true, true); + + setup + .terminal + .draw(|f| { + super::commands(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state); + }) + .unwrap(); + + let expected = [ + "╭──────────╮", + "│▶ pause │", + "│ restart │", + "│ stop │", + "│ delete │", + "╰──────────╯", + ]; + let result = &setup.terminal.backend().buffer().content; + + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + + // Check the text color is correct + match index { + // pause + 15..=19 => { + assert_eq!(result_cell.fg, Color::Yellow); + } + // restart + 27..=33 => { + assert_eq!(result_cell.fg, Color::Magenta); + } + // stop + 39..=42 => { + assert_eq!(result_cell.fg, Color::Red); + } + // delete + 51..=56 => { + assert_eq!(result_cell.fg, Color::Gray); + } + // no text + _ => { + assert_eq!(result_cell.fg, Color::Reset); + } + } + if result_cell.symbol().starts_with('▶') { + assert_eq!(result_cell.fg, Color::Reset); + } + } + } + + // Change the controls state + setup + .app_data + .lock() + .update_containers(&mut vec![gen_container_summary(1, "paused")]); + setup.app_data.lock().docker_controls_next(); + + let expected = [ + "╭──────────╮", + "│ resume │", + "│▶ stop │", + "│ delete │", + "│ │", + "╰──────────╯", + ]; + + setup + .terminal + .draw(|f| { + super::commands(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + + // Chceck the text color is correct + match index { + // resume + 15..=20 => { + assert_eq!(result_cell.fg, Color::Blue); + } + // stop + 27..=30 => { + assert_eq!(result_cell.fg, Color::Red); + } + // delete + 39..=44 => { + assert_eq!(result_cell.fg, Color::Gray); + } + // no text + _ => { + assert_eq!(result_cell.fg, Color::Reset); + } + } + if result_cell.symbol().starts_with('▶') { + assert_eq!(result_cell.fg, Color::Reset); + } + } + } + } + + #[test] + // When control panel is selected, the border is blue, if not then white, selected text is highlighted + fn test_draw_blocks_commands_panel_selected_color() { + let (w, h) = (12, 6); + let mut setup = test_setup(w, h, true, true); + let expected = [ + "╭──────────╮", + "│▶ pause │", + "│ restart │", + "│ stop │", + "│ delete │", + "╰──────────╯", + ]; + + // Unselected, has a grey border + setup + .terminal + .draw(|f| { + super::commands(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + if BORDER_CHARS.contains(&result_cell.symbol()) { + assert_eq!(result_cell.fg, Color::Reset); + } + } + } + + // Control panel now selected, should have a blue border + setup.gui_state.lock().next_panel(); + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + setup + .terminal + .draw(|f| { + super::commands(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + if BORDER_CHARS.contains(&result_cell.symbol()) { + assert_eq!(result_cell.fg, Color::LightCyan); + } + // Make sure that the selected line has bold text + match index { + // pause + 13..=22 => { + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + _ => { + assert!(result_cell.modifier.is_empty()); + } + } + } + } + } + + // *********************** // + // Container summary panel // + // *********************** // + + // Check that the correct solor is applied to the state/status/cpu/memory section + fn check_expected(expected: [&str; 6], w: u16, _h: u16, setup: &TuiTestSetup, color: Color) { + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + if (145..=207).contains(&index) { + assert_eq!(result_cell.fg, color); + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + } + } + } + + #[test] + // No containers, panel unselected, then selected, border color changes correctly + fn test_draw_blocks_containers_none() { + let (w, h) = (25, 6); + let mut setup = test_setup(w, h, true, true); + setup.app_data.lock().containers = StatefulList::new(vec![]); + + let expected = [ + "╭ Containers ───────────╮", + "│ no containers running │", + "│ │", + "│ │", + "│ │", + "╰───────────────────────╯", + ]; + + setup.gui_state.lock().next_panel(); + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + setup + .terminal + .draw(|f| { + super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + assert_eq!(result_cell.fg, Color::Reset); + } + } + + setup.gui_state.lock().previous_panel(); + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + setup + .terminal + .draw(|f| { + super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + if BORDER_CHARS.contains(&result_cell.symbol()) { + assert_eq!(result_cell.fg, Color::LightCyan); + } + } + } + } + + #[test] + // Containers panel drawn, selected line is bold, border is blue + fn test_draw_blocks_containers_some() { + let (w, h) = (130, 6); + let mut setup = test_setup(w, h, true, true); + + let expected = [ + "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│⚪ container_1 ✓ running Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│ │", + "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + ]; + + setup + .terminal + .draw(|f| { + super::containers(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + // result matches expected + assert_eq!(result_cell.symbol(), expected_char.to_string()); + + // Selected container is bold + match index { + 131 | 133..=258 => assert_eq!(result_cell.modifier, Modifier::BOLD), + _ => { + assert!(result_cell.modifier.is_empty()); + } + } + + // Border is blue + if BORDER_CHARS.contains(&result_cell.symbol()) { + assert_eq!(result_cell.fg, Color::LightCyan); + } + } + } + + // Change selected panel, border is now no longer blue + setup.gui_state.lock().next_panel(); + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + setup + .terminal + .draw(|f| { + super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + + // Border is gray + if BORDER_CHARS.contains(&result_cell.symbol()) { + assert_eq!(result_cell.fg, Color::Reset); + } + } + } + } + + #[test] + /// ALl columns on all rows are coloured correctly + fn test_draw_blocks_containers_colors() { + let (w, h) = (130, 6); + let mut setup = test_setup(w, h, true, true); + + let expected = [ + "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│⚪ container_1 ✓ running Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│ │", + "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + ]; + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + setup + .terminal + .draw(|f| { + super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let index_blue = [ + 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 208, 209, 210, 211, 212, 213, + 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, + ]; + let index_blue = index_blue + .iter() + .flat_map(|&x| vec![x, x + 130, x + 260]) + .collect::>(); + let index_green = [ + 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, + 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, + 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, + 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, + ]; + let index_green = index_green + .iter() + .flat_map(|&x| vec![x, x + 130, x + 260]) + .collect::>(); + + let index_rx = [229, 230, 231, 232, 233, 234, 235, 236, 237, 238]; + let index_rx = index_rx + .iter() + .flat_map(|&x| vec![x, x + 130, x + 260]) + .collect::>(); + + let index_tx = [239, 240, 241, 242, 243, 244, 245, 246, 247, 248]; + let index_tx = index_tx + .iter() + .flat_map(|&x| vec![x, x + 130, x + 260]) + .collect::>(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + + let result_cell = &result[index]; + assert_eq!(result_cell.symbol(), expected_char.to_string()); + + match index { + _x if index_blue.contains(&index) => { + assert_eq!(result_cell.fg, Color::Blue); + } + _x if index_green.contains(&index) => { + assert_eq!(result_cell.fg, Color::Green); + } + _x if index_rx.contains(&index) => { + assert_eq!(result_cell.fg, Color::Rgb(255, 233, 193)); + } + _x if index_tx.contains(&index) => { + assert_eq!(result_cell.fg, Color::Rgb(205, 140, 140)); + } + (0..=130) | (259..=260) | (389..=390) | (519..=520) | (649..=779) => { + assert_eq!(result_cell.fg, Color::LightCyan); + } + _ => { + assert_eq!(result_cell.fg, Color::Reset); + } + } + } + } + } + + #[test] + /// When long container/image name, it is truncated correctly + fn test_draw_blocks_containers_long_name_image() { + let (w, h) = (170, 6); + let mut setup = test_setup(w, h, true, true); + setup.app_data.lock().containers.items[0].name = + ContainerName::from("a_long_container_name_for_the_purposes_of_this_test"); + setup.app_data.lock().containers.items[0].image = + ContainerImage::from("a_long_image_name_for_the_purposes_of_this_test"); + + let expected = [ + "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│⚪ a_long_container_name_for_the… ॥ paused Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 a_long_image_name_for_the_pur… 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│ │", + "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + ]; + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + setup.app_data.lock().containers.items[0].state = State::Paused; + + setup + .terminal + .draw(|f| { + super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + } + } + + // THis char: … + } + + #[test] + /// When container is paused, correct colors displayed + fn test_draw_blocks_containers_paused() { + let (w, h) = (130, 6); + let mut setup = test_setup(w, h, true, true); + + let expected = [ + "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│⚪ container_1 ॥ paused Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│ │", + "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + ]; + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + setup.app_data.lock().containers.items[0].state = State::Paused; + + setup + .terminal + .draw(|f| { + super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + check_expected(expected, w, h, &setup, Color::Yellow); + } + + #[test] + /// When container is dead, correct colors displayed + fn test_draw_blocks_containers_dead() { + let (w, h) = (130, 6); + let mut setup = test_setup(w, h, true, true); + + let expected = [ + "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│⚪ container_1 ✖ dead Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│ │", + "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + ]; + setup.app_data.lock().containers.items[0].state = State::Dead; + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + setup + .terminal + .draw(|f| { + super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + check_expected(expected, w, h, &setup, Color::Red); + } + + #[test] + /// When container is exited, correct colors displayed + fn test_draw_blocks_containers_exited() { + let (w, h) = (130, 6); + let mut setup = test_setup(w, h, true, true); + + let expected = [ + "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│⚪ container_1 ✖ exited Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│ │", + "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + ]; + setup.app_data.lock().containers.items[0].state = State::Exited; + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + setup + .terminal + .draw(|f| { + super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + check_expected(expected, w, h, &setup, Color::Red); + } + #[test] + /// When container is paused, correct colors displayed + fn test_draw_blocks_containers_removing() { + let (w, h) = (130, 6); + let mut setup = test_setup(w, h, true, true); + + let expected = [ + "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│⚪ container_1 removing Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│ │", + "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + ]; + setup.app_data.lock().containers.items[0].state = State::Removing; + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + setup + .terminal + .draw(|f| { + super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + check_expected(expected, w, h, &setup, Color::LightRed); + } + #[test] + /// When container state is restarting, correct colors displayed + fn test_draw_blocks_containers_restarting() { + let (w, h) = (130, 6); + let mut setup = test_setup(w, h, true, true); + + let expected = [ + "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│⚪ container_1 ↻ restarting Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│ │", + "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + ]; + setup.app_data.lock().containers.items[0].state = State::Restarting; + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + setup + .terminal + .draw(|f| { + super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + check_expected(expected, w, h, &setup, Color::LightGreen); + } + #[test] + /// When container state is unknown, correct colors displayed + fn test_draw_blocks_containers_unknown() { + let (w, h) = (130, 6); + let mut setup = test_setup(w, h, true, true); + + let expected = [ + "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│⚪ container_1 ? unknown Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│ │", + "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + ]; + setup.app_data.lock().containers.items[0].state = State::Unknown; + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + setup + .terminal + .draw(|f| { + super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + check_expected(expected, w, h, &setup, Color::Red); + } + // ********** // + // Logs panel // + // ********** // + + #[test] + // No logs, panel unselected, then selected, border color changes correctly + fn test_draw_blocks_logs_none() { + let (w, h) = (25, 6); + let mut setup = test_setup(w, h, true, true); + setup.app_data.lock().containers = StatefulList::new(vec![]); + + let expected = [ + "╭ Logs ─────────────────╮", + "│ no logs found │", + "│ │", + "│ │", + "│ │", + "╰───────────────────────╯", + ]; + + let _fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + setup + .terminal + .draw(|f| { + super::logs(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + assert_eq!(result_cell.fg, Color::Reset); + } + } + + setup.gui_state.lock().next_panel(); + setup.gui_state.lock().next_panel(); + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + // When selected, has a blue border + setup + .terminal + .draw(|f| { + super::logs(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + if BORDER_CHARS.contains(&result_cell.symbol()) { + assert_eq!(result_cell.fg, Color::LightCyan); + } + } + } + } + + #[test] + // Parsing logs, spinner visible, and then animates by one frame + fn test_draw_blocks_logs_parsing() { + let (w, h) = (25, 6); + let mut setup = test_setup(w, h, true, true); + let uuid = Uuid::new_v4(); + setup.gui_state.lock().next_loading(uuid); + + let expected = [ + "╭ Logs - container_1 ───╮", + "│ parsing logs ⠙ │", + "│ │", + "│ │", + "│ │", + "╰───────────────────────╯", + ]; + + let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + fd.init = true; + + setup + .terminal + .draw(|f| { + super::logs(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let test = |terminal: &Terminal, expected: [&str; 6]| { + let result = &terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + assert_eq!(result_cell.fg, Color::Reset); + } + } + }; + + test(&setup.terminal, expected); + + // animation moved by one frame + setup.gui_state.lock().next_loading(uuid); + + let expected = [ + "╭ Logs - container_1 ───╮", + "│ parsing logs ⠹ │", + "│ │", + "│ │", + "│ │", + "╰───────────────────────╯", + ]; + + let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + fd.init = true; + setup + .terminal + .draw(|f| { + super::logs(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + test(&setup.terminal, expected); + } + + #[test] + // Logs correct displayed, changing log state also draws correctly + fn test_draw_blocks_logs_some() { + let (w, h) = (25, 6); + let mut setup = test_setup(w, h, true, true); + + insert_logs(&setup); + + let test = |terminal: &Terminal, + expected: [&str; 6], + range: RangeInclusive| { + let result = &terminal.backend().buffer().content; + + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + assert_eq!(result_cell.fg, Color::Reset); + + if range.contains(&index) { + assert_eq!(result_cell.modifier, Modifier::BOLD); + } else { + assert!(result_cell.modifier.is_empty()); + } + } + } + }; + + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + setup + .terminal + .draw(|f| { + super::logs(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + let expected = [ + "╭ Logs 3/3 - container_1╮", + "│ line 1 │", + "│ line 2 │", + "│▶ line 3 │", + "│ │", + "╰───────────────────────╯", + ]; + test(&setup.terminal, expected, 76..=98); + + // Change selected log line + setup.app_data.lock().log_previous(); + let _fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + setup + .terminal + .draw(|f| { + super::logs(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state); + }) + .unwrap(); + + let expected = [ + "╭ Logs 2/3 - container_1╮", + "│ line 1 │", + "│▶ line 2 │", + "│ line 3 │", + "│ │", + "╰───────────────────────╯", + ]; + test(&setup.terminal, expected, 51..=73); + } + + #[test] + // Full (long) name displayed in logs border + fn test_draw_blocks_logs_long_name() { + let (w, h) = (80, 6); + let mut setup = test_setup(w, h, true, true); + setup.app_data.lock().containers.items[0].name = + ContainerName::from("a_long_container_name_for_the_purposes_of_this_test"); + setup.app_data.lock().containers.items[0].image = + ContainerImage::from("a_long_image_name_for_the_purposes_of_this_test"); + + insert_logs(&setup); + + let expected = [ + "╭ Logs 3/3 - a_long_container_name_for_the_purposes_of_this_test ──────────────╮", + "│ line 1 │", + "│ line 2 │", + "│▶ line 3 │", + "│ │", + "╰──────────────────────────────────────────────────────────────────────────────╯", + ]; + + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + setup + .terminal + .draw(|f| { + super::logs(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + } + } + } + + // ************ // + // Charts panel // + // ************ // + + const EXPECTED: [&str; 10] = [ + "╭───────────── cpu 03.00% ─────────────╮╭────────── memory 30.00 kB ───────────╮", + "│10.00%│ • ││100.00 kB│ •• │", + "│ │ •• ││ │ •• │", + "│ │ ••• ││ │ • • │", + "│ │ • • ││ │ • • │", + "│ │ • •• ││ │•• •• │", + "│ │• • ││ │• • │", + "│ │• • ││ │• • │", + "│ │ ││ │ │", + "╰──────────────────────────────────────╯╰──────────────────────────────────────╯", + ]; + const MEMORY_INDEX: [usize; 16] = [ + 134, 135, 214, 215, 293, 295, 372, 375, 451, 452, 455, 456, 531, 535, 611, 615, + ]; + + const CPU_INDEX: [usize; 15] = [ + 92, 171, 172, 250, 251, 252, 330, 332, 409, 413, 414, 488, 493, 568, 573, + ]; + + #[allow(clippy::cast_precision_loss)] + // Add fixed data to the cpu & mem vecdeques, that match the above data + fn insert_chart_data(setup: &TuiTestSetup) { + for i in 1..=10 { + setup.app_data.lock().update_stats_by_id( + &setup.ids[0], + Some(i as f64), + Some(i * 10000), + i * 10000, + i, + i, + ); + } + for i in 1..=3 { + setup.app_data.lock().update_stats_by_id( + &setup.ids[0], + Some(i as f64), + Some(i * 10000), + i * 10000, + i, + i, + ); + } + } + #[test] + // When status is Running, but not data, charts drawn without dots etc + fn test_draw_blocks_charts_running_none() { + let (w, h) = (80, 10); + let mut setup = test_setup(w, h, true, true); + + setup + .terminal + .draw(|f| { + super::chart(f, setup.area, &setup.app_data); + }) + .unwrap(); + + let expected = [ + "╭───────────── cpu 00.00% ─────────────╮╭─────────── memory 0.00 kB ───────────╮", + "│00.00%│ ││0.00 kB│ │", + "│ │ ││ │ │", + "│ │ ││ │ │", + "│ │ ││ │ │", + "│ │ ││ │ │", + "│ │ ││ │ │", + "│ │ ││ │ │", + "│ │ ││ │ │", + "╰──────────────────────────────────────╯╰──────────────────────────────────────╯", + ]; + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + + match index { + // chart tiles - cpu 03.00% && memory 30.00 kB - are green + 14..=25 | 52..=67 => { + assert_eq!(result_cell.fg, Color::Green); + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + // Cpu & Memory max are orange and bold + 81..=86 | 121..=127 => { + assert_eq!(result_cell.fg, ORANGE); + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + // All others + _ => { + assert_eq!(result_cell.fg, Color::Reset); + assert!(result_cell.modifier.is_empty()); + } + } + } + } + } + + #[test] + // When status is Running, charts correctly drawn + fn test_draw_blocks_charts_running_some() { + let (w, h) = (80, 10); + let mut setup = test_setup(w, h, true, true); + + insert_chart_data(&setup); + + setup + .terminal + .draw(|f| { + super::chart(f, setup.area, &setup.app_data); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in EXPECTED.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + match index { + // chart tiles - cpu 03.00% && memory 30.00 kB - are green + 14..=25 | 51..=67 => { + assert_eq!(result_cell.fg, Color::Green); + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + // Cpu & Memory max are orange and bold + 81..=86 | 121..=129 => { + assert_eq!(result_cell.fg, ORANGE); + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + // cpu dots are magenta + _x if CPU_INDEX.contains(&index) => { + assert_eq!(result_cell.fg, Color::Magenta); + assert!(result_cell.modifier.is_empty()); + } + // memory dots are cyan + _x if MEMORY_INDEX.contains(&index) => { + assert_eq!(result_cell.fg, Color::Cyan); + assert!(result_cell.modifier.is_empty()); + } + // All others + _ => { + assert_eq!(result_cell.fg, Color::Reset); + assert!(result_cell.modifier.is_empty()); + } + } + } + } + } + + #[test] + // Whens status paused, some text is now Yellow + fn test_draw_blocks_charts_paused() { + let (w, h) = (80, 10); + let mut setup = test_setup(w, h, true, true); + + insert_chart_data(&setup); + setup.app_data.lock().containers.items[0].state = State::Paused; + + setup + .terminal + .draw(|f| { + super::chart(f, setup.area, &setup.app_data); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in EXPECTED.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + match index { + // Titles and y axis are yellow + 14..=25 | 51..=67 | 81..=86 | 121..=129 => { + assert_eq!(result_cell.fg, Color::Yellow); + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + _x if CPU_INDEX.contains(&index) => { + assert_eq!(result_cell.fg, Color::Magenta); + assert!(result_cell.modifier.is_empty()); + } + // memory dots are cyan + _x if MEMORY_INDEX.contains(&index) => { + assert_eq!(result_cell.fg, Color::Cyan); + assert!(result_cell.modifier.is_empty()); + } + // All others + _ => { + assert_eq!(result_cell.fg, Color::Reset); + assert!(result_cell.modifier.is_empty()); + } + } + } + } + } + + #[test] + // When dead, text is read + fn test_draw_blocks_charts_dead() { + let (w, h) = (80, 10); + let mut setup = test_setup(w, h, true, true); + insert_chart_data(&setup); + setup.app_data.lock().containers.items[0].state = State::Dead; + + setup + .terminal + .draw(|f| { + super::chart(f, setup.area, &setup.app_data); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in EXPECTED.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + match index { + // Titles and y axis are red + 14..=25 | 51..=67 | 81..=86 | 121..=129 => { + assert_eq!(result_cell.fg, Color::Red); + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + // cpu dots are magenta + _x if CPU_INDEX.contains(&index) => { + assert_eq!(result_cell.fg, Color::Magenta); + assert!(result_cell.modifier.is_empty()); + } + // memory dots are cyan + _x if MEMORY_INDEX.contains(&index) => { + assert_eq!(result_cell.fg, Color::Cyan); + assert!(result_cell.modifier.is_empty()); + } + // All others + _ => { + assert_eq!(result_cell.fg, Color::Reset); + assert!(result_cell.modifier.is_empty()); + } + } + } + } + } + + // ******* // + // Headers // + // ******* // + + #[test] + /// Heading back only has show/exit help when no containers, correctly coloured + fn test_draw_blocks_headers_no_containers() { + let (w, h) = (140, 1); + let mut setup = test_setup(w, h, true, true); + setup.app_data.lock().containers = StatefulList::new(vec![]); + + let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + let expected = " ( h ) show help "; + + setup + .terminal + .draw(|f| { + super::heading_bar(setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (index, expected_char) in expected.chars().enumerate() { + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!(result_cell.fg, Color::White); + } + + fd.help_visible = true; + let expected = " ( h ) exit help "; + setup + .terminal + .draw(|f| { + super::heading_bar(setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (index, expected_char) in expected.chars().enumerate() { + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!(result_cell.fg, Color::Black); + } + } + + #[test] + /// Show all headings when containers present, colors valid + fn test_draw_blocks_headers_some_containers() { + let (w, h) = (140, 1); + let mut setup = test_setup(w, h, true, true); + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + let expected = " name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "; + setup + .terminal + .draw(|f| { + super::heading_bar(setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (index, expected_char) in expected.chars().enumerate() { + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!( + result_cell.fg, + match index { + (2..=122) => Color::Black, + _ => Color::White, + } + ); + } + } + + #[test] + /// Test all combination of headers & sort by + fn test_draw_blocks_headers_sort_containers() { + let (w, h) = (140, 1); + let mut setup = test_setup(w, h, true, true); + let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + let mut test = |expected: &str, range: RangeInclusive, x: (Header, SortedOrder)| { + fd.sorted_by = Some(x); + + setup + .terminal + .draw(|f| { + super::heading_bar(setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (index, expected_char) in expected.chars().enumerate() { + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!( + result_cell.fg, + match index { + 0 | 1 => Color::White, + // given range | help section + x if range.contains(&x) || (123..=139).contains(&x) => Color::White, + _ => Color::Black, + } + ); + } + }; + + // Name + test(" ▲ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 1..=14, (Header::Name, SortedOrder::Asc)); + test(" ▼ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 1..=14, (Header::Name, SortedOrder::Desc)); + + // state + test(" name ▲ state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 15..=26, (Header::State, SortedOrder::Asc)); + test(" name ▼ state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 15..=26, (Header::State, SortedOrder::Desc)); + + // status + test(" name state ▲ status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 27..=47, (Header::Status, SortedOrder::Asc)); + test(" name state ▼ status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 27..=47, (Header::Status, SortedOrder::Desc)); + + // cpu + test(" name state status ▲ cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 48..=57, (Header::Cpu, SortedOrder::Asc)); + test(" name state status ▼ cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 48..=57, (Header::Cpu, SortedOrder::Desc)); + + // mem + test(" name state status cpu ▲ memory/limit id image ↓ rx ↑ tx ( h ) show help ", 58..=77, (Header::Memory, SortedOrder::Asc)); + test(" name state status cpu ▼ memory/limit id image ↓ rx ↑ tx ( h ) show help ", 58..=77, (Header::Memory, SortedOrder::Desc)); + + // id + test(" name state status cpu memory/limit ▲ id image ↓ rx ↑ tx ( h ) show help ", 78..=88, (Header::Id, SortedOrder::Asc)); + test(" name state status cpu memory/limit ▼ id image ↓ rx ↑ tx ( h ) show help ", 78..=88, (Header::Id, SortedOrder::Desc)); + + // image + test(" name state status cpu memory/limit id ▲ image ↓ rx ↑ tx ( h ) show help ", 89..=98, (Header::Image, SortedOrder::Asc)); + test(" name state status cpu memory/limit id ▼ image ↓ rx ↑ tx ( h ) show help ", 89..=98, (Header::Image, SortedOrder::Desc)); + + // rx + test(" name state status cpu memory/limit id image ▲ ↓ rx ↑ tx ( h ) show help ", 99..=108, (Header::Rx, SortedOrder::Asc)); + test(" name state status cpu memory/limit id image ▼ ↓ rx ↑ tx ( h ) show help ", 99..=108, (Header::Rx, SortedOrder::Desc)); + + // tx + test(" name state status cpu memory/limit id image ↓ rx ▲ ↑ tx ( h ) show help ", 109..=122, (Header::Tx, SortedOrder::Asc)); + test(" name state status cpu memory/limit id image ↓ rx ▼ ↑ tx ( h ) show help ", 109..=122, (Header::Tx, SortedOrder::Desc)); + } + + #[test] + /// Show animation + fn test_draw_blocks_headers_animation() { + let (w, h) = (140, 1); + let mut setup = test_setup(w, h, true, true); + let uuid = Uuid::new_v4(); + setup.gui_state.lock().next_loading(uuid); + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + setup + .terminal + .draw(|f| { + super::heading_bar(setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let expected = " ⠙ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "; + + let result = &setup.terminal.backend().buffer().content; + for (index, expected_char) in expected.chars().enumerate() { + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!( + result_cell.fg, + match index { + (2..=122) => Color::Black, + _ => Color::White, + } + ); + } + } + + // ********** // + // Help popup // + // ********** // + #[test] + // This will cause issues once the version has more than the current 5 chars (0.5.0) + // Help popup is drawn correctly + fn test_draw_blocks_help() { + let (w, h) = (87, 30); + let mut setup = test_setup(w, h, true, true); + + setup + .terminal + .draw(|f| { + super::help_box(f); + }) + .unwrap(); + + let expected = [ + " ".to_owned(), + format!(" ╭ {VERSION} ────────────────────────────────────────────────────────────────────────────╮ "), + " │ │ ".to_owned(), + " │ 88 │ ".to_owned(), + " │ 88 │ ".to_owned(), + " │ 88 │ ".to_owned(), + " │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │ ".to_owned(), + r#" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 │ "#.to_owned(), + r#" │ 8b d8 )888( 8888[ 8PP""""""" 88 │ "#.to_owned(), + r#" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ "#.to_owned(), + r#" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ "#.to_owned(), + " │ │ ".to_owned(), + " │ A simple tui to view & control docker containers │ ".to_owned(), + " │ │ ".to_owned(), + " │ ( tab ) or ( shift+tab ) change panels │ ".to_owned(), + " │ ( ↑ ↓ ) or ( j k ) or ( PgUp PgDown ) or ( Home End ) change selected line │ ".to_owned(), + " │ ( enter ) send docker container command │ ".to_owned(), + " │ ( e ) exec into a container │ ".to_owned(), + " │ ( h ) toggle this help information │ ".to_owned(), + " │ ( s ) save logs to file │ ".to_owned(), + " │ ( m ) toggle mouse capture - if disabled, text on screen can be selected & copied │ ".to_owned(), + " │ ( 0 ) stop sort │ ".to_owned(), + " │ ( 1 - 9 ) sort by header - or click header │ ".to_owned(), + " │ ( q ) quit at any time │ ".to_owned(), + " │ │ ".to_owned(), + " │ currently an early work in progress, all and any input appreciated │ ".to_owned(), + " │ https://github.com/mrjackwills/oxker │ ".to_owned(), + " │ │ ".to_owned(), + " ╰───────────────────────────────────────────────────────────────────────────────────╯ ".to_owned(), + " ".to_owned(), + ]; + + for (row_index, row) in expected.iter().enumerate() { + let mut bracket_key = vec![]; + let mut push_bracket_key = false; + + let result = &setup.terminal.backend().buffer().content; + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + let result_str = result_cell.symbol(); + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + + // First and last row, and first char and last char in each row, is empty + if row_index == 0 + || row_index == usize::from(h - 1) + || char_index == 0 + || char_index == usize::from(w - 1) + { + assert_eq!(result_cell.fg, Color::Reset); + assert_eq!(result_cell.bg, Color::Reset); + // Borders + } else if BORDER_CHARS.contains(&result_str) { + assert_eq!(result_cell.fg, Color::Black); + assert_eq!(result_cell.bg, Color::Magenta); + // everything else has a magenta background + } else { + assert_eq!(result_cell.bg, Color::Magenta); + } + + // check that ( [key] ) is white + if result_str == "(" { + push_bracket_key = true; + bracket_key.push(result_cell); + } + if push_bracket_key { + bracket_key.push(result_cell); + if result_str == ")" { + push_bracket_key = false; + for i in &bracket_key { + assert_eq!(i.fg, Color::White); + } + bracket_key.clear(); + } + } + // TODO should really be testing every color of every str here + } + } + } + + // ************ // + // Delete popup // + // ************ // + + #[test] + // Delete container popup is drawn correctly + fn test_draw_blocks_delete() { + let (w, h) = (82, 10); + let mut setup = test_setup(w, h, true, true); + + let expected = [ + " ", + " ╭──────────────────────── Confirm Delete ────────────────────────╮ ", + " │ │ ", + " │ Are you sure you want to delete container: container_1 │ ", + " │ │ ", + " │ ╭─────────────────────╮ ╭─────────────────────╮ │ ", + " │ │ (N)o │ │ (Y)es │ │ ", + " │ ╰─────────────────────╯ ╰─────────────────────╯ │ ", + " ╰────────────────────────────────────────────────────────────────╯ ", + " ", + ]; + + setup + .terminal + .draw(|f| { + super::delete_confirm(f, &setup.gui_state, &ContainerName::from("container_1")); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + + if row_index == 0 + || row_index == usize::from(h - 1) + || char_index < 8 + || char_index > usize::from(w - 9) + { + assert_eq!(result_cell.fg, Color::Reset); + assert_eq!(result_cell.bg, Color::Reset); + } else { + assert_eq!(result_cell.bg, Color::White); + } + + // Borders are black + if BORDER_CHARS.contains(&result_cell.symbol()) { + assert_eq!(result_cell.fg, Color::Black); + // Container name is red + } else if row_index == 3 && (57..=67).contains(&char_index) { + assert_eq!(result_cell.fg, Color::Red); + // All other text is black + } else if !row_index == 0 + && !row_index == usize::from(h - 1) + && !char_index < 8 + && !char_index > usize::from(w - 9) + { + assert_eq!(result_cell.fg, Color::Black); + } + } + } + } + + // ***** // + // popup // + // ***** // + + #[test] + /// Info box drawn in bottom right + fn test_draw_blocks_info() { + let (w, h) = (45, 9); + let mut setup = test_setup(w, h, true, true); + + let expected = [ + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " test ", + " ", + ]; + + setup + .terminal + .draw(|f| { + super::info(f, "test", std::time::Instant::now(), &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(expected_char.to_string(), result_cell.symbol()); + + let (fg, bg) = if row_index >= 6 && char_index >= 32 { + (Color::White, Color::Blue) + } else { + (Color::Reset, Color::Reset) + }; + + assert_eq!(result_cell.fg, fg); + assert_eq!(result_cell.bg, bg); + } + } + } + + // *********** // + // Error popup // + // *********** // + + #[test] + // Test that the error popup is centered, red background, white border, white text, and displays the correct text + fn test_draw_blocks_docker_connect_error() { + let (w, h) = (46, 9); + let mut setup = test_setup(w, h, true, true); + + setup + .terminal + .draw(|f| { + super::error(f, AppError::DockerConnect, Some(4)); + }) + .unwrap(); + + let expected = vec![ + " ".to_owned(), + " ╭───────────────── Error ──────────────────╮ ".to_owned(), + " │ │ ".to_owned(), + " │ Unable to access docker daemon │ ".to_owned(), + " │ │ ".to_owned(), + format!(" │ oxker::v{VERSION} closing in 04 seconds │ "), + " │ │ ".to_owned(), + " ╰──────────────────────────────────────────╯ ".to_owned(), + " ".to_owned(), + ]; + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + + if (1..=usize::from(h) - 2).contains(&row_index) + && (1..=usize::from(w) - 2).contains(&char_index) + { + assert_eq!(result_cell.bg, Color::Red); + } + if result_cell + .symbol() + .chars() + .next() + .unwrap() + .is_alphanumeric() + { + assert_eq!(result_cell.fg, Color::White); + } + } + } + } + + #[test] + // Test that the clearable error popup is centered, red background, white border, white text, and displays the correct text + fn test_draw_blocks_clearable_error() { + let (w, h) = (39, 10); + let mut setup = test_setup(w, h, true, true); + + setup + .terminal + .draw(|f| { + super::error(f, AppError::DockerExec, Some(4)); + }) + .unwrap(); + + let expected = [ + " ", + " ╭────────────── Error ──────────────╮ ", + " │ │ ", + " │ Unable to exec into container │ ", + " │ │ ", + " │ ( c ) clear error │ ", + " │ ( q ) quit oxker │ ", + " │ │ ", + " ╰───────────────────────────────────╯ ", + " ", + ]; + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + if (1..=usize::from(h) - 2).contains(&row_index) + && (1..=usize::from(w) - 2).contains(&char_index) + { + assert_eq!(result_cell.bg, Color::Red); + } + if result_cell + .symbol() + .chars() + .next() + .unwrap() + .is_alphanumeric() + || ["(", ")"].contains(&result_cell.symbol()) + { + assert_eq!(result_cell.fg, Color::White); + } + } + } + } + + #[test] + // Port section when container has no ports + fn test_draw_blocks_ports_no_ports() { + let (w, h) = (30, 8); + let mut setup = test_setup(w, h, true, true); + setup.app_data.lock().containers.items[0].ports = vec![]; + + let max_lens = setup.app_data.lock().get_longest_port(); + setup + .terminal + .draw(|f| { + super::ports(f, setup.area, &setup.app_data, max_lens); + }) + .unwrap(); + + let expected = [ + "╭────────── ports ───────────╮", + "│ no ports │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "╰────────────────────────────╯", + ]; + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(expected_char.to_string(), result_cell.symbol()); + if row_index == 0 && !BORDER_CHARS.contains(&result_cell.symbol()) { + assert_eq!(result_cell.fg, Color::Green); + assert_eq!(result_cell.modifier, Modifier::BOLD); + } else { + assert_eq!(result_cell.fg, Color::Reset); + } + } + } + + // when state is "State::Running | State::Paused | State::Restarting, won't show "no ports" + setup.app_data.lock().containers.items[0].state = State::Dead; + let max_lens = setup.app_data.lock().get_longest_port(); + setup + .terminal + .draw(|f| { + super::ports(f, setup.area, &setup.app_data, max_lens); + }) + .unwrap(); + + let expected = [ + "╭────────── ports ───────────╮", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "╰────────────────────────────╯", + ]; + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(expected_char.to_string(), result_cell.symbol()); + if row_index == 0 && !BORDER_CHARS.contains(&result_cell.symbol()) { + assert_eq!(result_cell.fg, Color::Red); + assert_eq!(result_cell.modifier, Modifier::BOLD); + } else { + assert_eq!(result_cell.fg, Color::Reset); + } + } + } + } + + #[test] + // Port section when container has multiple ports + fn test_draw_blocks_ports_multiple_ports() { + let (w, h) = (32, 8); + let mut setup = test_setup(w, h, true, true); + setup.app_data.lock().containers.items[0] + .ports + .push(ContainerPorts { + ip: None, + private: 8002, + public: None, + }); + setup.app_data.lock().containers.items[0] + .ports + .push(ContainerPorts { + ip: Some("127.0.0.1".to_owned()), + private: 8003, + public: Some(8003), + }); + + let max_lens = setup.app_data.lock().get_longest_port(); + + setup + .terminal + .draw(|f| { + super::ports(f, setup.area, &setup.app_data, max_lens); + }) + .unwrap(); + + let expected = [ + "╭─────────── ports ────────────╮", + "│ ip private public │", + "│ 8001 │", + "│ 8002 │", + "│127.0.0.1 8003 8003 │", + "│ │", + "│ │", + "╰──────────────────────────────╯", + ]; + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(expected_char.to_string(), result_cell.symbol()); + + let result_cell_as_char = result_cell + .symbol() + .chars() + .next() + .unwrap() + .is_ascii_alphanumeric(); + if row_index == 0 && result_cell_as_char { + assert_eq!(result_cell.fg, Color::Green); + } + if row_index == 1 && result_cell_as_char { + assert_eq!(result_cell.fg, Color::Yellow); + } + if row_index == 2 && result_cell_as_char { + assert_eq!(result_cell.fg, Color::White); + } + if row_index == 3 && result_cell_as_char { + assert_eq!(result_cell.fg, Color::Magenta); + } + if row_index == 4 && result_cell_as_char { + assert_eq!(result_cell.fg, Color::White); + } + } + } + } + + #[test] + // Port section title color correct dependant on state + fn test_draw_blocks_ports_container_state() { + let (w, h) = (32, 8); + let mut setup = test_setup(w, h, true, true); + let max_lens = setup.app_data.lock().get_longest_port(); + + setup.app_data.lock().containers.items[0].state = State::Paused; + setup + .terminal + .draw(|f| { + super::ports(f, setup.area, &setup.app_data, max_lens); + }) + .unwrap(); + + let expected = [ + "╭─────────── ports ────────────╮", + "│ ip private public │", + "│ 8001 │", + "│ │", + "│ │", + "│ │", + "│ │", + "╰──────────────────────────────╯", + ]; + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(expected_char.to_string(), result_cell.symbol()); + + if row_index == 0 + && result_cell + .symbol() + .chars() + .next() + .unwrap() + .is_ascii_alphanumeric() + { + assert_eq!(result_cell.fg, Color::Yellow); + } + } + } + + setup.app_data.lock().containers.items[0].state = State::Dead; + setup + .terminal + .draw(|f| { + super::ports(f, setup.area, &setup.app_data, max_lens); + }) + .unwrap(); + + let expected = [ + "╭─────────── ports ────────────╮", + "│ ip private public │", + "│ 8001 │", + "│ │", + "│ │", + "│ │", + "│ │", + "╰──────────────────────────────╯", + ]; + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(expected_char.to_string(), result_cell.symbol()); + + if row_index == 0 + && result_cell + .symbol() + .chars() + .next() + .unwrap() + .is_ascii_alphanumeric() + { + assert_eq!(result_cell.fg, Color::Red); + } + } + } + } + + // *************** // + // The whole layout // + // **************** // + #[test] + // Check that the whole layout is drawn correctly + fn test_draw_blocks_whole_layout() { + let (w, h) = (160, 30); + let mut setup = test_setup(w, h, true, true); + + insert_chart_data(&setup); + insert_logs(&setup); + setup.app_data.lock().containers.items[0] + .ports + .push(ContainerPorts { + ip: Some("127.0.0.1".to_owned()), + private: 8003, + public: Some(8003), + }); + + let expected = [ + " name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", + "╭ Containers 1/3 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮", + "│⚪ container_1 ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 image_1 0.00 kB 0.00 kB ││▶ pause │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB ││ restart │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB ││ stop │", + "│ ││ delete │", + "│ ││ │", + "│ ││ │", + "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯", + "╭ Logs 3/3 - container_1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│ line 1 │", + "│ line 2 │", + "│▶ line 3 │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + "╭───────────────────────── cpu 03.00% ──────────────────────────╮╭─────────────────────── memory 30.00 kB ───────────────────────╮╭────────── ports ───────────╮", + "│10.00%│ •••• ││100.00 kB│ ••• ││ ip private public│", + "│ │ ••• • ││ │ ••• • ││ 8001 │", + "│ │•• ••• ││ │•• ••• ││127.0.0.1 8003 8003│", + "│ │ ││ │ ││ │", + "╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯", + ]; + setup + .terminal + .draw(|f| { + draw_frame(f, &setup.app_data, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string(),); + } + } + } } - -// Draw nothing, as in a blank screen -// pub fn nothing(f: &mut Frame) { -// let whole_layout = Layout::default() -// .direction(Direction::Vertical) -// .constraints([Constraint::Min(100)].as_ref()) -// .split(f.size()); - -// let block = Block::default() -// .borders(Borders::NONE); -// f.render_widget(block, whole_layout[0]); -// } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 1b22599..79c3cb9 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -217,22 +217,6 @@ impl Ui { } } -#[cfg(not(debug_assertions))] -fn get_wholelayout(f: &Frame) -> std::rc::Rc<[ratatui::layout::Rect]> { - Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(1), Constraint::Min(100)].as_ref()) - .split(f.size()) -} - -#[cfg(debug_assertions)] -fn get_wholelayout(f: &Frame) -> std::rc::Rc<[ratatui::layout::Rect]> { - Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(1), Constraint::Min(1), Constraint::Min(100)].as_ref()) - .split(f.size()) -} - /// Frequent data required by multiple framde drawing functions, can reduce mutex reads by placing it all in here #[derive(Debug)] pub struct FrameData { @@ -279,21 +263,16 @@ impl From<(MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)> for FrameData { fn draw_frame(f: &mut Frame, app_data: &Arc>, gui_state: &Arc>) { let fd = FrameData::from((app_data.lock(), gui_state.lock())); - let whole_layout = get_wholelayout(f); - #[cfg(debug_assertions)] - draw_blocks::debug_bar(whole_layout[0], f, app_data.lock().get_debug_string()); - - #[cfg(debug_assertions)] - let whole_layout_split = (1, 2); - - #[cfg(not(debug_assertions))] - let whole_layout_split = (0, 1); + let whole_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(1), Constraint::Min(100)].as_ref()) + .split(f.size()); // Split into 3, containers+controls, logs, then graphs let upper_main = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Max(fd.height), Constraint::Percentage(50)].as_ref()) - .split(whole_layout[whole_layout_split.1]); + .split(whole_layout[1]); let top_split = if fd.has_containers { vec![Constraint::Percentage(90), Constraint::Percentage(10)] @@ -307,7 +286,7 @@ fn draw_frame(f: &mut Frame, app_data: &Arc>, gui_state: &Arc>, gui_state: &Arc>, gui_state: &Arc