toolkit for mdBook [mirror of my GitHub repo] docs.tonywu.dev/mdbookkit/
permalinks rust-analyzer mdbook
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore(ci): setup cmd tests

+762 -163
+1 -1
.cargo/config.toml
··· 1 1 [alias] 2 - bin = ["run", "--quiet", "--release", "--package", "cargo-bin", "--"] 2 + bin = ["run", "--quiet", "--package", "cargo-bin", "--"]
+46 -24
.github/workflows/ci.yml
··· 1 - # TODO: still need cross platform E2E testing 2 - 3 1 name: CI 4 2 5 3 on: ··· 15 13 - cron: "27 0 * * *" # nightly test for RA 16 14 17 15 env: 18 - CARGO_TERM_COLOR: always 16 + CARGO_TERM_COLOR: "always" 17 + RUST_LOG: "info" 18 + RUST_BACKTRACE: "1" 19 19 20 20 concurrency: 21 21 group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} ··· 25 25 test: 26 26 name: Test 27 27 28 - runs-on: ubuntu-latest 29 28 strategy: 30 29 matrix: 31 30 toolchain: 32 31 - stable 32 + platform: 33 + - ubuntu-latest 34 + - windows-latest 35 + fail-fast: false 36 + 37 + runs-on: ${{ matrix.platform }} 33 38 34 39 permissions: 35 40 contents: read ··· 44 49 with: 45 50 toolchain: ${{ matrix.toolchain }} 46 51 52 + - uses: cargo-bins/cargo-binstall@b9bf4400702f721d469eec4d280125f650c85638 53 + 47 54 - name: Get cache key 48 55 run: | 49 56 CARGO_VERSION=$(cargo --version) 50 - echo "CACHE_KEY=${{runner.os}}-${{runner.arch}}-$CARGO_VERSION-${{hashFiles('**/Cargo.toml')}}-${{hashFiles('**/Cargo.lock')}}-${{github.workflow}}-${{github.job}}" >> "$GITHUB_OUTPUT" 57 + echo "CACHE_KEY=${{ runner.os }}-${{ runner.arch }}-$CARGO_VERSION-${{hashFiles('**/Cargo.toml')}}-${{hashFiles('**/Cargo.lock')}}-${{github.workflow}}-${{github.job}}" >> "$GITHUB_OUTPUT" 51 58 { 52 59 echo "CACHE_KEY_RESTORE<<EOF" 53 - echo "${{runner.os}}-${{runner.arch}}-$CARGO_VERSION-${{hashFiles('**/Cargo.toml')}}-${{hashFiles('**/Cargo.lock')}}-" 54 - echo "${{runner.os}}-${{runner.arch}}-$CARGO_VERSION-${{hashFiles('**/Cargo.toml')}}-" 55 - echo "${{runner.os}}-${{runner.arch}}-$CARGO_VERSION-" 60 + echo "${{ runner.os }}-${{ runner.arch }}-$CARGO_VERSION-${{hashFiles('**/Cargo.toml')}}-${{hashFiles('**/Cargo.lock')}}-" 61 + echo "${{ runner.os }}-${{ runner.arch }}-$CARGO_VERSION-${{hashFiles('**/Cargo.toml')}}-" 62 + echo "${{ runner.os }}-${{ runner.arch }}-$CARGO_VERSION-" 56 63 echo "EOF" 57 64 } >> "$GITHUB_OUTPUT" 58 65 id: cache-key 66 + shell: bash 59 67 60 68 - uses: actions/cache/restore@v4 61 69 with: ··· 65 73 restore-keys: ${{ steps.cache-key.outputs.CACHE_KEY_RESTORE }} 66 74 id: cache-restore 67 75 68 - - name: Prepare rust-analyzer 69 - run: cargo run --package util-rust-analyzer -- download 76 + - name: Prepare binaries 77 + run: | 78 + cargo bin --install 79 + cargo run --package util-rust-analyzer -- download 70 80 71 - - run: cargo test --release --all-features 72 - env: 73 - RUST_LOG: info 81 + - run: cargo test --all-features --no-fail-fast -- --include-ignored 74 82 75 83 - name: Evict cache 76 84 run: gh cache delete '${{ steps.cache-key.outputs.CACHE_KEY }}' ··· 86 94 target/ 87 95 key: ${{ steps.cache-key.outputs.CACHE_KEY }} 88 96 97 + - name: Save cache key 98 + if: matrix.platform == 'ubuntu-latest' 99 + id: save-cache-key 100 + run: | 101 + echo "CACHE_KEY=${{ steps.cache-key.outputs.CACHE_KEY }}" >> "$GITHUB_OUTPUT" 102 + { 103 + echo "CACHE_KEY_RESTORE<<EOF" 104 + echo "${{ steps.cache-key.outputs.CACHE_KEY }}" 105 + echo "EOF" 106 + } >> "$GITHUB_OUTPUT" 107 + shell: bash 108 + 89 109 outputs: 90 - CACHE_KEY: ${{ steps.cache-key.outputs.CACHE_KEY }} 91 - CACHE_KEY_RESTORE: ${{ steps.cache-key.outputs.CACHE_KEY_RESTORE }} 110 + CACHE_KEY: ${{ steps.save-cache-key.outputs.CACHE_KEY }} 111 + CACHE_KEY_RESTORE: ${{ steps.save-cache-key.outputs.CACHE_KEY_RESTORE }} 92 112 93 113 build_features: 94 - name: Build 95 - runs-on: ubuntu-latest 114 + name: Build features 96 115 97 116 needs: 98 117 - test ··· 106 125 - link-forever 107 126 - lib-link-forever,common-logger 108 127 - lib-link-forever 128 + fail-fast: false 129 + 130 + runs-on: ubuntu-latest 109 131 110 132 steps: 111 133 - uses: actions/checkout@v4 ··· 123 145 key: ${{ needs.test.outputs.CACHE_KEY }} 124 146 restore-keys: ${{ needs.test.outputs.CACHE_KEY_RESTORE }} 125 147 126 - - run: cargo build --release --features ${{ matrix.features }} 148 + - run: cargo build --features ${{ matrix.features }} 127 149 128 150 test_rustdoc_link_ra: 129 - name: rustdoc-link RA 130 - runs-on: ubuntu-latest 151 + name: (rustdoc-link) Test RA 131 152 132 153 needs: 133 154 - test ··· 138 159 - "2025-03-17" 139 160 - "2025-03-04" 140 161 - "nightly" 162 + fail-fast: false 163 + 164 + runs-on: ubuntu-latest 141 165 142 166 steps: 143 167 - uses: actions/checkout@v4 ··· 155 179 key: ${{ needs.test.outputs.CACHE_KEY }} 156 180 restore-keys: ${{ needs.test.outputs.CACHE_KEY_RESTORE }} 157 181 158 - - run: cargo run --package util-rust-analyzer -- download 182 + - run: cargo run --package util-rust-analyzer -- download 159 183 env: 160 184 RA_VERSION: ${{ matrix.ra-version }} 161 185 162 - - run: cargo test --release --features rustdoc-link 163 - env: 164 - RUST_LOG: info 186 + - run: cargo test --features rustdoc-link
+158 -2
Cargo.lock
··· 104 104 checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" 105 105 106 106 [[package]] 107 + name = "arbitrary" 108 + version = "1.4.1" 109 + source = "registry+https://github.com/rust-lang/crates.io-index" 110 + checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" 111 + dependencies = [ 112 + "derive_arbitrary", 113 + ] 114 + 115 + [[package]] 116 + name = "assert_cmd" 117 + version = "2.0.16" 118 + source = "registry+https://github.com/rust-lang/crates.io-index" 119 + checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" 120 + dependencies = [ 121 + "anstyle", 122 + "bstr", 123 + "doc-comment", 124 + "libc", 125 + "predicates", 126 + "predicates-core", 127 + "predicates-tree", 128 + "wait-timeout", 129 + ] 130 + 131 + [[package]] 107 132 name = "async-lsp" 108 133 version = "0.2.2" 109 134 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 392 417 ] 393 418 394 419 [[package]] 420 + name = "crossbeam-utils" 421 + version = "0.8.21" 422 + source = "registry+https://github.com/rust-lang/crates.io-index" 423 + checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 424 + 425 + [[package]] 395 426 name = "crypto-common" 396 427 version = "0.1.6" 397 428 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 475 506 ] 476 507 477 508 [[package]] 509 + name = "derive_arbitrary" 510 + version = "1.4.1" 511 + source = "registry+https://github.com/rust-lang/crates.io-index" 512 + checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" 513 + dependencies = [ 514 + "proc-macro2", 515 + "quote", 516 + "syn 2.0.100", 517 + ] 518 + 519 + [[package]] 478 520 name = "derive_builder" 479 521 version = "0.20.2" 480 522 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 519 561 ] 520 562 521 563 [[package]] 564 + name = "difflib" 565 + version = "0.4.0" 566 + source = "registry+https://github.com/rust-lang/crates.io-index" 567 + checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 568 + 569 + [[package]] 522 570 name = "digest" 523 571 version = "0.10.7" 524 572 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 561 609 ] 562 610 563 611 [[package]] 612 + name = "doc-comment" 613 + version = "0.3.3" 614 + source = "registry+https://github.com/rust-lang/crates.io-index" 615 + checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 616 + 617 + [[package]] 564 618 name = "dtoa" 565 619 version = "1.0.10" 566 620 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 658 712 dependencies = [ 659 713 "crc32fast", 660 714 "miniz_oxide", 715 + ] 716 + 717 + [[package]] 718 + name = "float-cmp" 719 + version = "0.10.0" 720 + source = "registry+https://github.com/rust-lang/crates.io-index" 721 + checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" 722 + dependencies = [ 723 + "num-traits", 661 724 ] 662 725 663 726 [[package]] ··· 1452 1515 checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" 1453 1516 1454 1517 [[package]] 1518 + name = "lockfree-object-pool" 1519 + version = "0.1.6" 1520 + source = "registry+https://github.com/rust-lang/crates.io-index" 1521 + checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" 1522 + 1523 + [[package]] 1455 1524 name = "log" 1456 1525 version = "0.4.26" 1457 1526 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1526 1595 version = "1.0.0" 1527 1596 dependencies = [ 1528 1597 "anyhow", 1598 + "assert_cmd", 1529 1599 "async-lsp", 1600 + "cargo-run-bin", 1530 1601 "cargo_toml", 1531 1602 "chrono", 1532 1603 "clap", ··· 1543 1614 "miette", 1544 1615 "owo-colors", 1545 1616 "percent-encoding", 1617 + "predicates", 1546 1618 "proc-macro2", 1547 1619 "pulldown-cmark 0.13.0", 1548 1620 "pulldown-cmark-to-cmark", ··· 1658 1730 version = "0.1.14" 1659 1731 source = "registry+https://github.com/rust-lang/crates.io-index" 1660 1732 checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" 1733 + 1734 + [[package]] 1735 + name = "normalize-line-endings" 1736 + version = "0.3.0" 1737 + source = "registry+https://github.com/rust-lang/crates.io-index" 1738 + checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 1661 1739 1662 1740 [[package]] 1663 1741 name = "normpath" ··· 1996 2074 checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" 1997 2075 1998 2076 [[package]] 2077 + name = "predicates" 2078 + version = "3.1.3" 2079 + source = "registry+https://github.com/rust-lang/crates.io-index" 2080 + checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" 2081 + dependencies = [ 2082 + "anstyle", 2083 + "difflib", 2084 + "float-cmp", 2085 + "normalize-line-endings", 2086 + "predicates-core", 2087 + "regex", 2088 + ] 2089 + 2090 + [[package]] 2091 + name = "predicates-core" 2092 + version = "1.0.9" 2093 + source = "registry+https://github.com/rust-lang/crates.io-index" 2094 + checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" 2095 + 2096 + [[package]] 2097 + name = "predicates-tree" 2098 + version = "1.0.12" 2099 + source = "registry+https://github.com/rust-lang/crates.io-index" 2100 + checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" 2101 + dependencies = [ 2102 + "predicates-core", 2103 + "termtree", 2104 + ] 2105 + 2106 + [[package]] 1999 2107 name = "proc-macro-hack" 2000 2108 version = "0.5.20+deprecated" 2001 2109 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2194 2302 2195 2303 [[package]] 2196 2304 name = "reqwest" 2197 - version = "0.12.14" 2305 + version = "0.12.15" 2198 2306 source = "registry+https://github.com/rust-lang/crates.io-index" 2199 - checksum = "989e327e510263980e231de548a33e63d34962d29ae61b467389a1a09627a254" 2307 + checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" 2200 2308 dependencies = [ 2201 2309 "base64", 2202 2310 "bytes", ··· 2674 2782 ] 2675 2783 2676 2784 [[package]] 2785 + name = "termtree" 2786 + version = "0.5.1" 2787 + source = "registry+https://github.com/rust-lang/crates.io-index" 2788 + checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" 2789 + 2790 + [[package]] 2677 2791 name = "textwrap" 2678 2792 version = "0.16.2" 2679 2793 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3041 3155 "reqwest", 3042 3156 "serde_json", 3043 3157 "tap", 3158 + "tempfile", 3159 + "zip", 3044 3160 ] 3045 3161 3046 3162 [[package]] ··· 3048 3164 version = "0.1.0" 3049 3165 dependencies = [ 3050 3166 "anyhow", 3167 + "cargo-run-bin", 3051 3168 "insta", 3169 + "log", 3052 3170 "once_cell", 3053 3171 "serde", 3054 3172 "serde_json", ··· 3067 3185 version = "0.9.5" 3068 3186 source = "registry+https://github.com/rust-lang/crates.io-index" 3069 3187 checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 3188 + 3189 + [[package]] 3190 + name = "wait-timeout" 3191 + version = "0.2.1" 3192 + source = "registry+https://github.com/rust-lang/crates.io-index" 3193 + checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" 3194 + dependencies = [ 3195 + "libc", 3196 + ] 3070 3197 3071 3198 [[package]] 3072 3199 name = "waitpid-any" ··· 3554 3681 "quote", 3555 3682 "syn 2.0.100", 3556 3683 ] 3684 + 3685 + [[package]] 3686 + name = "zip" 3687 + version = "2.6.1" 3688 + source = "registry+https://github.com/rust-lang/crates.io-index" 3689 + checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744" 3690 + dependencies = [ 3691 + "arbitrary", 3692 + "crc32fast", 3693 + "crossbeam-utils", 3694 + "flate2", 3695 + "indexmap", 3696 + "memchr", 3697 + "zopfli", 3698 + ] 3699 + 3700 + [[package]] 3701 + name = "zopfli" 3702 + version = "0.8.1" 3703 + source = "registry+https://github.com/rust-lang/crates.io-index" 3704 + checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" 3705 + dependencies = [ 3706 + "bumpalo", 3707 + "crc32fast", 3708 + "lockfree-object-pool", 3709 + "log", 3710 + "once_cell", 3711 + "simd-adler32", 3712 + ]
+3
Cargo.toml
··· 19 19 20 20 [workspace.dependencies] 21 21 anyhow = "1.0.95" 22 + assert_cmd = "2.0.16" 22 23 cargo-run-bin = { version = "1.7.4", default-features = false } 23 24 clap = { version = "4.5.31", features = ["derive"] } 24 25 env_logger = "0.11.6" ··· 29 30 "fancy-no-backtrace", 30 31 ], default-features = false } 31 32 minijinja = "2.9.0" 33 + predicates = "3.1.3" 32 34 pulldown-cmark = "0.13.0" 33 35 pulldown-cmark-to-cmark = "21.0.0" 34 36 serde = { version = "1", features = ["derive"] } ··· 36 38 shlex = "1.3.0" 37 39 similar = { version = "2.7.0" } 38 40 tap = "1.0.1" 41 + tempfile = "3.18.0" 39 42 tokio = { version = "1", features = ["macros"] } 40 43 toml = "0.5" 41 44 url = "2.5.4"
-12
crates/mdbookkit/CHANGELOG.md
··· 1 - # Changelog 2 - 3 - All notable changes to this project will be documented in this file. 4 - 5 - The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and 6 - this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 - 8 - ## [Unreleased] 9 - 10 - ## [1.0.0](https://tonywu6.github.com/tonywu6/mdbookkit/releases/tag/mdbookkit-v1.0.0) - 2025-04-03 11 - 12 - Initial release.
+4 -1
crates/mdbookkit/Cargo.toml
··· 50 50 shlex = { workspace = true, optional = true } 51 51 syn = { version = "2.0.99", optional = true } 52 52 tap = { workspace = true } 53 - tempfile = { version = "3.18.0", optional = true } 53 + tempfile = { workspace = true, optional = true } 54 54 tokio = { workspace = true, optional = true } 55 55 tokio-util = { version = "0.7.13", features = ["compat"], optional = true } 56 56 toml = { workspace = true, optional = true } ··· 58 58 url = { workspace = true, features = ["serde"], optional = true } 59 59 60 60 [dev-dependencies] 61 + assert_cmd = { workspace = true } 62 + cargo-run-bin = { workspace = true } 61 63 insta = { workspace = true } 64 + predicates = { workspace = true } 62 65 similar = { workspace = true } 63 66 util-testing = { workspace = true } 64 67
+1 -1
crates/mdbookkit/src/bin/rustdoc_link/env.rs
··· 327 327 } 328 328 } 329 329 330 - fn find_code_extension() -> Option<PathBuf> { 330 + pub fn find_code_extension() -> Option<PathBuf> { 331 331 let home = dirs::home_dir()?; 332 332 [ 333 333 home.join(".vscode/extensions"),
+39 -26
crates/mdbookkit/src/logging/terminal.rs
··· 6 6 time::{Duration, Instant}, 7 7 }; 8 8 9 - use anyhow::Context; 9 + use anyhow::{Context, Result}; 10 10 use console::{colors_enabled_stderr, set_colors_enabled, StyledObject, Term}; 11 11 use env_logger::Logger; 12 12 use indicatif::{HumanDuration, ProgressBar, ProgressDrawTarget, ProgressStyle}; 13 13 use log::{Level, LevelFilter, Log}; 14 - use tap::Pipe; 14 + use tap::{Pipe, Tap}; 15 15 16 16 use super::{styled, Message}; 17 17 ··· 36 36 impl ConsoleLogger { 37 37 /// Install a [`ConsoleLogger`] as the global [`log`] logger. 38 38 pub fn install(name: &str) { 39 - maybe_spinner(name); 40 - let logger = Box::new(Self::new()); 41 - log::set_boxed_logger(logger).expect("logger should not have been set"); 39 + Self::try_install(name).expect("logger should not have been set"); 40 + } 41 + 42 + pub fn try_install(name: &str) -> Result<()> { 43 + log::set_boxed_logger(Box::new(Self::new(name)))?; 42 44 log::set_max_level(LevelFilter::max()); 45 + Ok(()) 43 46 } 44 47 45 - fn new() -> Self { 46 - if let Some(spinner) = SPINNER.get() { 47 - Self::Console(spinner.term.clone()) 48 - } else { 49 - env_logger::Builder::new() 48 + fn new(name: &str) -> Self { 49 + match maybe_logging() { 50 + Some(LevelFilter::Off) => SPINNER 51 + .get_or_init(|| spawn_spinner(name)) 52 + .term 53 + .clone() 54 + .pipe(Self::Console), 55 + level => env_logger::Builder::new() 50 56 .format(log_format) 51 57 .parse_default_env() 58 + .tap_mut(|builder| { 59 + if let Some(level) = level { 60 + builder.filter_level(level); 61 + } 62 + }) 52 63 .build() 53 - .pipe(Self::Logger) 64 + .pipe(Self::Logger), 54 65 } 55 66 } 56 67 } 57 68 58 - fn maybe_spinner(name: &str) { 59 - fn rust_log() -> bool { 60 - std::env::var("RUST_LOG") 61 - .map(|v| !v.is_empty()) 62 - .unwrap_or(false) 69 + fn maybe_logging() -> Option<LevelFilter> { 70 + if std::env::var("RUST_LOG") 71 + .map(|v| !v.is_empty()) 72 + .unwrap_or(false) 73 + { 74 + // RUST_LOG to be parsed by env_logger 75 + None 76 + } else if !console::user_attended_stderr() { 77 + // RUST_LOG not set but stderr isn't a terminal 78 + // log warnings and above 79 + Some(LevelFilter::Warn) 80 + } else { 81 + // use spinner instead 82 + Some(LevelFilter::Off) 63 83 } 64 - 65 - fn attended() -> bool { 66 - console::user_attended_stderr() 67 - } 68 - 69 - if rust_log() || !attended() { 70 - return; 71 - } 72 - 73 - SPINNER.set(spawn_spinner(name)).ok(); 74 84 } 75 85 76 86 impl Log for ConsoleLogger { ··· 136 146 137 147 let target = term.clone(); 138 148 let template = format!("{{spinner:.cyan}} [{name}] {{prefix}} ... {{msg}}",); 149 + 150 + // this thread is detached. this is okay in usage because SPINNER.get_or_init 151 + // guarantees this function is called at most once 139 152 140 153 thread::spawn(move || { 141 154 struct Bar {
+172 -14
crates/mdbookkit/tests/rustdoc_link.rs
··· 1 - use std::sync::Arc; 1 + use std::{io::Write, sync::Arc}; 2 2 3 - use anyhow::{bail, Result}; 3 + use anyhow::{bail, Context, Result}; 4 4 5 - use log::LevelFilter; 5 + use assert_cmd::{prelude::*, Command}; 6 + use predicates::prelude::*; 6 7 use similar::{ChangeTag, TextDiff}; 7 8 use tap::Pipe; 9 + use tempfile::TempDir; 8 10 use tokio::task::JoinSet; 9 11 10 - use mdbookkit::{ 11 - bin::rustdoc_link::{ 12 - env::{Config, Environment}, 13 - Client, Pages, Resolver, 14 - }, 15 - logging::ConsoleLogger, 12 + use mdbookkit::bin::rustdoc_link::{ 13 + env::{find_code_extension, Config, Environment}, 14 + Client, Pages, Resolver, 16 15 }; 17 - use util_testing::{portable_snapshots, test_document, TestDocument}; 16 + use util_testing::{may_skip, portable_snapshots, setup_paths, test_document, TestDocument}; 17 + 18 + mod util; 18 19 19 20 async fn snapshot( 20 21 client: Arc<Client>, ··· 34 35 35 36 let report = page 36 37 .reporter() 37 - .level(LevelFilter::Info) 38 + .level(log::LevelFilter::Info) 38 39 .names(|_| name.clone()) 39 40 .colored(false) 40 41 .logging(false) ··· 70 71 71 72 #[tokio::test] 72 73 async fn test_snapshots() -> Result<()> { 73 - let client = setup()?; 74 + util::setup_logging(); 75 + 76 + let client = client()?; 74 77 75 78 let tests = [ 76 79 test_document!("../../../docs/src/rustdoc-link/supported-syntax.md"), ··· 104 107 Ok(()) 105 108 } 106 109 107 - fn setup() -> Result<Arc<Client>> { 108 - ConsoleLogger::install("rustdoc-link"); 110 + fn client() -> Result<Arc<Client>> { 109 111 Config { 110 112 rust_analyzer: Some("cargo run --package util-rust-analyzer -- analyzer".into()), 111 113 cargo_features: vec!["rustdoc-link".into()], ··· 116 118 .pipe(Arc::new) 117 119 .pipe(Ok) 118 120 } 121 + 122 + #[test] 123 + #[ignore = "should run in CI"] 124 + fn test_minimum_env() -> Result<()> { 125 + util::setup_logging(); 126 + 127 + log::info!("setup: compile self"); 128 + Command::new("cargo") 129 + .args([ 130 + "build", 131 + "--package", 132 + env!("CARGO_PKG_NAME"), 133 + "--all-features", 134 + "--bin", 135 + "mdbook-rustdoc-link", 136 + ]) 137 + .arg(if cfg!(debug_assertions) { 138 + "--profile=dev" 139 + } else { 140 + "--profile=release" 141 + }) 142 + .assert() 143 + .success(); 144 + 145 + let path = setup_paths()?; 146 + 147 + let root = TempDir::new()?; 148 + 149 + log::debug!("{root:?}"); 150 + 151 + log::info!("given: a book"); 152 + Command::new("mdbook") 153 + .args(["init", "--force"]) 154 + .env("PATH", &path) 155 + .current_dir(&root) 156 + .unwrap() 157 + .assert() 158 + .success(); 159 + 160 + log::info!("given: preprocessor is enabled"); 161 + std::fs::File::options() 162 + .append(true) 163 + .open(root.path().join("book.toml"))? 164 + .pipe(|mut file| file.write_all("[preprocessor.rustdoc-link]\n".as_bytes()))?; 165 + 166 + log::info!("when: book is not a Cargo project"); 167 + log::info!("then: preprocessor fails"); 168 + Command::new("mdbook") 169 + .arg("build") 170 + .env("PATH", &path) 171 + .current_dir(&root) 172 + .assert() 173 + .failure() 174 + .stderr(predicate::str::contains( 175 + "failed to determine the current Cargo project", 176 + )); 177 + 178 + log::info!("given: book is a Cargo project"); 179 + Command::new("cargo") 180 + .arg("init") 181 + .args(["--name", "temp"]) 182 + .env("PATH", &path) 183 + .current_dir(&root) 184 + .assert() 185 + .success(); 186 + 187 + if find_code_extension().is_some() 188 + && may_skip("rust-analyzer code extension is already installed") 189 + { 190 + log::info!("when: book has item links"); 191 + std::fs::File::options() 192 + .append(true) 193 + .open(root.path().join("src/chapter_1.md"))? 194 + .pipe(|mut file| file.write_all("\n[std::thread]\n".as_bytes()))?; 195 + 196 + log::info!("then: book builds without errors"); 197 + Command::new("mdbook") 198 + .arg("build") 199 + .env("PATH", &path) 200 + .current_dir(&root) 201 + .assert() 202 + .success(); 203 + } else if Command::new("rust-analyzer") 204 + .arg("--version") 205 + .assert() 206 + .try_success() 207 + .is_ok() 208 + && may_skip("rust-analyzer is already available") 209 + { 210 + log::info!("skip testing mdbook build without rust-analyzer") 211 + } else { 212 + log::info!("when: rust-analyzer is not configured"); 213 + 214 + log::info!("when: book has no item links"); 215 + 216 + log::info!("then: book builds without errors"); 217 + Command::new("mdbook") 218 + .arg("build") 219 + .env("PATH", &path) 220 + .current_dir(&root) 221 + .assert() 222 + .success(); 223 + 224 + log::info!("when: book has item links"); 225 + std::fs::File::options() 226 + .append(true) 227 + .open(root.path().join("src/chapter_1.md"))? 228 + .pipe(|mut file| file.write_all("\n[std]\n".as_bytes()))?; 229 + 230 + log::info!("then: preprocessor fails"); 231 + Command::new("mdbook") 232 + .arg("build") 233 + .env("PATH", &path) 234 + .current_dir(&root) 235 + .assert() 236 + .failure() 237 + .stderr( 238 + predicate::str::contains("failed to spawn rust-analyzer") 239 + // https://github.com/rust-lang/rustup/issues/3846 240 + // rustup shims rust-analyzer even when it's not installed 241 + .or(predicate::str::contains("Unknown binary 'rust-analyzer")), 242 + ); 243 + 244 + log::info!("when: code extension is installed"); 245 + 246 + let extension_dir = tempfile::Builder::new() 247 + .prefix(".vscode") 248 + .suffix("") 249 + .rand_bytes(0) 250 + .tempdir_in(dirs::home_dir().context("failed to get home dir")?)?; 251 + 252 + let ra_executable = extension_dir 253 + .path() 254 + .join("extensions/rust-lang.rust-analyzer-lorem-ipsum") 255 + .join("server/rust-analyzer"); 256 + 257 + Command::new("cargo") 258 + .args(["run", "--package", "util-rust-analyzer", "--"]) 259 + .arg("--ra-path") 260 + .arg(ra_executable) 261 + .arg("download") 262 + .unwrap() 263 + .assert() 264 + .success(); 265 + 266 + log::info!("then: book builds without errors"); 267 + Command::new("mdbook") 268 + .arg("build") 269 + .env("PATH", &path) 270 + .current_dir(&root) 271 + .assert() 272 + .success(); 273 + } 274 + 275 + Ok(()) 276 + }
+7
crates/mdbookkit/tests/util.rs
··· 1 + use log::LevelFilter; 2 + use mdbookkit::logging::ConsoleLogger; 3 + 4 + pub fn setup_logging() { 5 + ConsoleLogger::try_install(env!("CARGO_PKG_NAME")).ok(); 6 + log::set_max_level(LevelFilter::Debug); 7 + }
+5 -1
utils/rust-analyzer/Cargo.toml
··· 15 15 flate2 = "1.1.0" 16 16 indicatif = "0.17.11" 17 17 mdbook = { workspace = true, optional = true } 18 - reqwest = { version = "0.12.14", features = ["blocking"] } 18 + reqwest = { version = "0.12.15", features = ["blocking"] } 19 19 serde_json = { workspace = true } 20 20 tap = { workspace = true } 21 + tempfile = { workspace = true } 22 + zip = { version = "2.6.1", features = [ 23 + "deflate", # https://github.com/rust-lang/rust-analyzer/blob/2025-03-17/xtask/src/dist.rs#L134 24 + ], default-features = false } 21 25 22 26 [features] 23 27 ra-version = ["dep:mdbook"]
+136 -73
utils/rust-analyzer/src/main.rs
··· 1 1 //! Download a copy of rust-analyzer to /.bin to use in testing. 2 - //! 3 - //! Version can be controlled with the `RA_VERSION` environment variable. 4 2 5 3 use std::{ 6 - fs, io, 7 - os::unix::fs::PermissionsExt, 4 + fs, 5 + io::{self, Seek, SeekFrom}, 8 6 path::PathBuf, 9 - process::{Command, Stdio}, 7 + process::{self, Stdio}, 10 8 time::Duration, 11 9 }; 12 10 ··· 16 14 use flate2::write::GzDecoder; 17 15 use indicatif::{ProgressBar, ProgressStyle}; 18 16 use tap::{Pipe, Tap}; 17 + use tempfile::tempfile; 18 + 19 + struct Download { 20 + release: String, 21 + path: PathBuf, 22 + } 23 + 24 + impl Download { 25 + fn download(&self) -> Result<()> { 26 + #[cfg(not(target_os = "windows"))] 27 + { 28 + self.download_gzip() 29 + } 30 + #[cfg(target_os = "windows")] // ugh 31 + { 32 + self.download_zip() 33 + } 34 + } 35 + 36 + #[cfg_attr(target_os = "windows", allow(unused))] 37 + fn download_gzip(&self) -> Result<()> { 38 + let Self { release, path } = self; 39 + 40 + let platform = env!("TARGET"); 41 + let url = format!("https://github.com/rust-lang/rust-analyzer/releases/download/{release}/rust-analyzer-{platform}.gz"); 42 + 43 + let mut res = reqwest::blocking::get(url)?; 44 + 45 + fs::create_dir_all(path.parent().unwrap())?; 46 + 47 + fs::File::create(path)? 48 + .pipe(io::BufWriter::new) 49 + .pipe(GzDecoder::new) 50 + .pipe(Progress::new(Self::progress_bar(&res))) 51 + .pipe(|mut w| res.copy_to(&mut w).and(Ok(w)))? 52 + .0 53 + .finish()?; 54 + 55 + #[cfg(any(target_os = "macos", target_os = "linux"))] 56 + { 57 + use std::os::unix::fs::PermissionsExt; 58 + fs::metadata(path)? 59 + .permissions() 60 + .tap_mut(|p| p.set_mode(0o755)) 61 + .pipe(|p| fs::set_permissions(path, p))?; 62 + } 63 + 64 + Ok(()) 65 + } 66 + 67 + #[cfg_attr(not(target_os = "windows"), allow(unused))] 68 + fn download_zip(&self) -> Result<()> { 69 + let Self { release, path } = self; 70 + 71 + let temp = tempfile()?; 72 + 73 + let platform = env!("TARGET"); 74 + let url = format!("https://github.com/rust-lang/rust-analyzer/releases/download/{release}/rust-analyzer-{platform}.zip"); 75 + 76 + let mut res = reqwest::blocking::get(url)?; 77 + 78 + let temp = temp 79 + .pipe(io::BufWriter::new) 80 + .pipe(Progress::new(Self::progress_bar(&res))) 81 + .pipe(|mut w| res.copy_to(&mut w).and(Ok(w)))? 82 + .0 83 + .into_inner() 84 + .unwrap() 85 + .tap_mut(|file| file.seek(SeekFrom::Start(0)).map(|_| ()).unwrap()); 86 + 87 + let mut archive = zip::ZipArchive::new(temp)?; 88 + 89 + fs::create_dir_all(path.parent().unwrap())?; 90 + 91 + let mut bin = archive.by_name("rust-analyzer.exe")?; 92 + let mut out = fs::File::create(path)?; 93 + 94 + std::io::copy(&mut bin, &mut out)?; 95 + 96 + Ok(()) 97 + } 98 + 99 + fn progress_bar(res: &reqwest::blocking::Response) -> ProgressBar { 100 + static BAR_TEMPLATE: &str = "{spinner:.cyan} {prefix} {bar:20.green} {binary_bytes:.yellow} {binary_total_bytes:.yellow} {binary_bytes_per_sec:.yellow}"; 101 + 102 + if let Some(len) = res.content_length() { 103 + ProgressBar::new(len) 104 + } else { 105 + ProgressBar::new_spinner() 106 + } 107 + .with_prefix("downloading rust-analyzer") 108 + .with_style( 109 + ProgressStyle::with_template(BAR_TEMPLATE) 110 + .unwrap() 111 + .tick_chars("⠇⠋⠙⠸⠴⠦⠿") 112 + .progress_chars("⠿⠦⠴⠸⠙⠋⠇ "), 113 + ) 114 + .tap(|b| b.enable_steady_tick(Duration::from_millis(100))) 115 + } 116 + } 19 117 20 118 #[derive(clap::Parser, Debug)] 21 - enum Program { 119 + struct Program { 120 + #[arg(long)] 121 + ra_version: Option<String>, 122 + #[arg(long)] 123 + ra_path: Option<PathBuf>, 124 + #[clap(subcommand)] 125 + command: Command, 126 + } 127 + 128 + #[derive(clap::Subcommand, Debug)] 129 + enum Command { 22 130 Download, 23 131 Analyzer { 24 132 #[arg(trailing_var_arg = true, allow_hyphen_values = true, hide = true)] ··· 35 143 Supports { renderer: String }, 36 144 } 37 145 38 - struct Config { 39 - release: String, 40 - path: PathBuf, 41 - } 42 - 43 146 fn main() -> Result<()> { 44 - let release = std::env::var("RA_VERSION") 45 - .ok() 46 - .unwrap_or("2025-03-17".into()); 147 + let program = Program::parse(); 47 148 48 - let path = get_project_root()? 49 - .join(".bin/rust-analyzer") 50 - .join(&release) 51 - .join("rust-analyzer"); 149 + let release = program.ra_version.unwrap_or("2025-03-17".into()); 52 150 53 - let config = Config { release, path }; 151 + let path = match program.ra_path { 152 + Some(path) => path, 153 + None => get_project_root()? 154 + .join(".bin/rust-analyzer") 155 + .join(&release) 156 + .join("rust-analyzer"), 157 + }; 54 158 55 - match Program::parse() { 56 - Program::Download => download(&config), 57 - Program::Analyzer { args } => analyzer(&config, args), 58 - Program::Version { version } => match version { 159 + let download = Download { release, path }; 160 + 161 + match program.command { 162 + Command::Download => download.download(), 163 + Command::Analyzer { args } => analyzer(&download, args), 164 + Command::Version { version } => match version { 59 165 Some(Version::Supports { .. }) => Ok(()), 60 166 None => { 61 167 #[cfg(feature = "ra-version")] 62 168 { 63 - ra_version::preprocessor(&config) 169 + ra_version::preprocessor(&download) 64 170 } 65 171 #[cfg(not(feature = "ra-version"))] 66 172 { ··· 71 177 } 72 178 } 73 179 74 - fn analyzer(config: &Config, args: Vec<String>) -> Result<()> { 75 - if !config.path.exists() { 76 - download(config)?; 180 + fn analyzer(download: &Download, args: Vec<String>) -> Result<()> { 181 + if !download.path.exists() { 182 + download.download()?; 77 183 } 78 - Command::new(&config.path) 184 + process::Command::new(&download.path) 79 185 .args(args) 80 186 .stdin(Stdio::inherit()) 81 187 .stdout(Stdio::inherit()) ··· 86 192 .pipe(std::process::exit); 87 193 } 88 194 89 - fn download(Config { release, path }: &Config) -> Result<()> { 90 - let platform = env!("TARGET"); 91 - 92 - let url = format!("https://github.com/rust-lang/rust-analyzer/releases/download/{release}/rust-analyzer-{platform}.gz"); 93 - // rust-analyzer uses zip files for windows so this won't work on windows 94 - 95 - let mut res = reqwest::blocking::get(url)?; 96 - 97 - let bar = if let Some(len) = res.content_length() { 98 - ProgressBar::new(len) 99 - } else { 100 - ProgressBar::new_spinner() 101 - } 102 - .with_prefix("downloading rust-analyzer") 103 - .with_style( 104 - ProgressStyle::with_template(BAR_TEMPLATE) 105 - .unwrap() 106 - .tick_chars("⠇⠋⠙⠸⠴⠦⠿") 107 - .progress_chars("⠿⠦⠴⠸⠙⠋⠇ "), 108 - ) 109 - .tap(|b| b.enable_steady_tick(Duration::from_millis(100))); 110 - 111 - static BAR_TEMPLATE: &str = "{spinner:.cyan} {prefix} {bar:20.green} {binary_bytes:.yellow} {binary_total_bytes:.yellow} {binary_bytes_per_sec:.yellow}"; 112 - 113 - fs::create_dir_all(path.parent().unwrap())?; 114 - 115 - fs::File::create(path)? 116 - .pipe(io::BufWriter::new) 117 - .pipe(GzDecoder::new) 118 - .pipe(Progress::new(bar)) 119 - .pipe(|mut w| res.copy_to(&mut w).and(Ok(w)))? 120 - .0 121 - .finish()?; 122 - 123 - #[cfg(any(target_os = "macos", target_os = "linux"))] 124 - fs::metadata(path)? 125 - .permissions() 126 - .tap_mut(|p| p.set_mode(0o755)) 127 - .pipe(|p| fs::set_permissions(path, p))?; 128 - 129 - Ok(()) 130 - } 131 - 132 195 struct Progress<W>(W, ProgressBar); 133 196 134 197 impl<W: io::Write> io::Write for Progress<W> { ··· 170 233 use mdbook::{book::Book, preprocess::PreprocessorContext, BookItem}; 171 234 use tap::Pipe; 172 235 173 - use crate::Config; 236 + use crate::Download; 174 237 175 - pub fn preprocessor(Config { release, .. }: &Config) -> Result<()> { 238 + pub fn preprocessor(Download { release, .. }: &Download) -> Result<()> { 176 239 let (_, mut book): (PreprocessorContext, Book) = Vec::new() 177 240 .pipe(|mut buf| std::io::stdin().read_to_end(&mut buf).and(Ok(buf)))? 178 241 .pipe(String::from_utf8)?
+2
utils/testing/Cargo.toml
··· 10 10 11 11 [dependencies] 12 12 anyhow = { workspace = true } 13 + cargo-run-bin = { workspace = true, default-features = false } 13 14 insta = { workspace = true } 15 + log = { workspace = true } 14 16 once_cell = "1.21.3" 15 17 serde = { workspace = true } 16 18 serde_json = { workspace = true }
+47 -1
utils/testing/src/lib.rs
··· 1 1 //! Test helpers. 2 2 3 - use std::path::Path; 3 + use std::{ffi::OsString, path::Path}; 4 4 5 5 use anyhow::Result; 6 6 use once_cell::sync::Lazy; ··· 90 90 } 91 91 }; 92 92 } 93 + 94 + pub fn may_skip<D: std::fmt::Display>(because: D) -> bool { 95 + let ci = std::env::var("CI").unwrap_or("".into()); 96 + if matches!(ci.as_str(), "" | "0" | "false") { 97 + log::info!("{because}"); 98 + true 99 + } else { 100 + panic!("{because} but CI=true") 101 + } 102 + } 103 + 104 + pub fn setup_paths() -> Result<OsString> { 105 + let mut path = if let Some(path) = std::env::var_os("PATH") { 106 + std::env::split_paths(&path) 107 + .collect::<Vec<_>>() 108 + .into_iter() 109 + .rev() 110 + .collect() 111 + } else { 112 + vec![] 113 + }; 114 + 115 + path.push(Path::new(env!("CARGO_HOME")).join("bin")); 116 + 117 + path.extend( 118 + cargo_run_bin::metadata::get_binary_packages()? 119 + .into_iter() 120 + .map(cargo_run_bin::binary::install) 121 + .map(|path| Ok(Path::new(&path?).parent().unwrap().to_owned())) 122 + .collect::<Result<Vec<_>>>()?, 123 + ); 124 + 125 + path.push( 126 + CARGO_WORKSPACE_DIR 127 + .join("target")? 128 + .to_file_path() 129 + .unwrap() 130 + .join(if cfg!(debug_assertions) { 131 + "debug" 132 + } else { 133 + "release" 134 + }), 135 + ); 136 + 137 + Ok(std::env::join_paths(path.into_iter().rev())?) 138 + }