diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 171f58a..d903ba8 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -4,6 +4,7 @@ on: push: branches: ["main"] pull_request: + branches: ["main"] env: CARGO_TERM_COLOR: always @@ -13,13 +14,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Run tests - run: cargo test + run: cargo test --verbose miri: runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - name: Install Rust nightly toolchain uses: actions-rs/toolchain@v1 diff --git a/Cargo.lock b/Cargo.lock index 0ebe5ea..8a7435f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,643 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + [[package]] name = "cap_vec" -version = "0.2.0" +version = "0.3.0" +dependencies = [ + "criterion", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "criterion" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" +dependencies = [ + "alloca", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools", + "num-traits", + "oorandom", + "page_size", + "plotters", + "rayon", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index ca730fd..8a75901 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,21 @@ [package] -authors = ["Davide Di Carlo "] -description = "A heap-allocated, fixed-capacity, variable-size array, no_std compatible." name = "cap_vec" -version = "0.2.0" +version = "0.3.0" edition = "2024" +rust-version = "1.86" +authors = ["Davide Di Carlo "] +description = "A no_std heap-backed vector with fixed compile-time maximum capacity." license = "MIT" -keywords = ["array", "collections", "list", "vec", "heap"] -rust-version = "1.85" +readme = "README.md" repository = "https://github.com/daddinuz/cap_vec" +homepage = "https://github.com/daddinuz/cap_vec" +documentation = "https://docs.rs/cap_vec" +keywords = ["array", "collections", "list", "vec", "heap"] +categories = ["data-structures", "memory-management", "no-std"] + +[dev-dependencies] +criterion = "0.8" + +[[bench]] +name = "bench" +harness = false diff --git a/LICENSE b/LICENSE index 792f5f5..1979424 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2025 Davide Di Carlo +Copyright (c) 2026 Davide Di Carlo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/README.md b/README.md index cbc913f..494d920 100644 --- a/README.md +++ b/README.md @@ -4,76 +4,108 @@ [![Docs.rs](https://img.shields.io/docsrs/cap_vec)](https://docs.rs/cap_vec) [![License](https://img.shields.io/crates/l/cap_vec.svg)](https://github.com/daddinuz/cap_vec/blob/main/LICENSE) -A **heap-allocated**, **fixed-capacity**, **variable-size** array, `no_std` compatible. +A heap-backed vector with fixed maximum capacity for `no_std` environments. -`CapVec` provides a middle ground between stack-allocated arrays `[T; N]` and dynamically growing vectors `Vec`. -It allocates a **heap-backed buffer of fixed capacity `N`**, but allows the logical length to grow or shrink dynamically, up to that capacity. +`CapVec` stores up to `N` values of type `T` contiguously in its backing +buffer. Non-zero-sized buffers use heap storage. Unlike `Vec`, its maximum +capacity is fixed at compile time and never grows after construction. Its +logical length can still change dynamically from `0` to `N`. ---- +This is useful when a fixed upper bound is part of the program design, but a +stack-allocated `[T; N]` would be too large or too inflexible. -## ✨ Features +## Features -- ✅ **Fixed capacity** — set at compile time. -- ✅ **Heap allocation** — no stack overflow even for large `N`. -- ✅ **no_std compatible** — uses only `alloc`. -- ✅ Supports most common collection operations: - - `push`, `pop`, `insert`, `remove` - - `clear`, `extend`, `drain` - - `iter`, `iter_mut`, and `into_iter` -- ✅ Safe, ergonomic API — mirrors `Vec` where possible. -- ✅ Zero-cost iteration (implements `Iterator`, `DoubleEndedIterator`, `FusedIterator`). -- ✅ No hidden allocations after initialization. +- Fixed maximum capacity encoded in the const generic parameter `N`. +- Heap-backed storage for non-zero-sized buffers, avoiding large stack arrays + for large capacities. +- `#![no_std]` compatible on targets that provide `alloc`. +- Lazy buffer creation for `CapVec::new`; non-zero-sized buffers allocate on + the first successful `push`, `insert`, or `extend`. +- No reallocations or growth after the fixed backing buffer exists. +- Common vector operations: `push`, `pop`, `insert`, `remove`, `clear`, + `truncate`, `swap_remove`, `shrink_to_fit`, `extend`, and `drain`. +- Capacity inspection through `capacity`, `max_capacity`, and + `spare_capacity`. +- Slice access through `as_slice`, `as_mut_slice`, and `Deref`. +- Front, back, drain, and consuming iteration, including double-ended iterators + and remaining-item slice access where applicable. ---- +## Allocation Model -## 🚀 Example +`CapVec::new` creates an empty vector without allocating. The backing buffer is +created on the first successful `push`, `insert`, or `extend`, or when +constructing from a full array with `CapVec::from([..])`. + +For non-zero-sized buffers, the allocation is for exactly `N` elements. +Zero-sized buffers use a dangling non-null sentinel and do not allocate heap +storage. Operations such as `clear` remove the initialized elements but keep the +buffer available for later reuse. Calling `shrink_to_fit` on an empty vector +drops the backing buffer and makes `capacity()` return `0` again. + +Use `max_capacity()` to get the fixed upper bound `N`. Use `capacity()` to get +the currently-created backing buffer size, and `spare_capacity()` to get the +remaining room in that buffer. + +`CapVec` is valid and can never hold an element. + +## Example ```rust use cap_vec::CapVec; -fn main() { - // Create a CapVec with capacity for 4 elements - let mut cv = CapVec::::new(); +let mut values = CapVec::::new(); +assert!(values.is_empty()); +assert_eq!(values.capacity(), 0); +assert_eq!(values.max_capacity(), 4); - // Push values - cv.push(10).unwrap(); - cv.push(20).unwrap(); - cv.push(30).unwrap(); +values.push(10).unwrap(); +values.push(20).unwrap(); +values.push(30).unwrap(); - assert_eq!(cv.len(), 3); - assert_eq!(cv.capacity(), 4); - assert_eq!(cv.as_slice(), &[10, 20, 30]); +assert_eq!(values.as_slice(), &[10, 20, 30]); +assert_eq!(values.capacity(), 4); +assert_eq!(values.spare_capacity(), 1); - // Insert in the middle - cv.insert(1, 15).unwrap(); - assert_eq!(cv.as_slice(), &[10, 15, 20, 30]); +values.insert(1, 15).unwrap(); +assert_eq!(values.as_slice(), &[10, 15, 20, 30]); - // Remove one element - let removed = cv.remove(2); - assert_eq!(removed, Some(20)); - assert_eq!(cv.as_slice(), &[10, 15, 30]); +assert_eq!(values.push(40), Err(40)); +assert_eq!(values.remove(2), Some(20)); +assert_eq!(values.pop(), Some(30)); +assert_eq!(values.as_slice(), &[10, 15]); +``` - // Iterate immutably - for x in cv.iter() { - println!("{x}"); - } +## Draining - // Iterate mutably - for x in cv.iter_mut() { - *x *= 2; - } +`drain` accepts standard range bounds such as `..`, `start..`, `..end`, +`start..end`, and inclusive end bounds. It removes the selected elements and +returns them by value. The source vector is updated when the iterator is +dropped, even if the iterator is only partially consumed. - assert_eq!(cv.as_slice(), &[20, 30, 60]); +```rust +use cap_vec::CapVec; + +let mut values = CapVec::from([1, 2, 3, 4]); +let removed: Vec<_> = values.drain(1..).collect(); - // Consume into an iterator - let v: Vec<_> = cv.into_iter().collect(); - assert_eq!(v, vec![20, 30, 60]); -} +assert_eq!(removed, [2, 3, 4]); +assert_eq!(values.as_slice(), &[1]); ``` ---- +`Drain` and the consuming iterator expose their remaining items as slices. + +```rust +use cap_vec::CapVec; + +let values = CapVec::from([1, 2, 3, 4]); +let mut iter = values.into_iter(); + +assert_eq!(iter.next(), Some(1)); +assert_eq!(iter.as_slice(), &[2, 3, 4]); +``` -## 🔨 Installation +## Installation Add `cap_vec` to your `Cargo.toml`: @@ -81,23 +113,38 @@ Add `cap_vec` to your `Cargo.toml`: cargo add cap_vec ``` -or edit your Cargo.toml manually by adding: +or edit `Cargo.toml` manually: ```toml [dependencies] cap_vec = "0.2" ``` -## Safety and Coverage +## Safety and Verification -This crate contains a small portion of unsafe code. -All tests run under [miri](https://github.com/rust-lang/miri) and the tests cover about 80% of the code. -You can generate the coverage report using [tarpaulin](https://github.com/xd009642/tarpaulin). +This crate uses a small amount of unsafe code to manage partially initialized +backing storage. -## Contributions +The test suite covers regular operations, zero-sized types, zero capacity, +iterator behavior, and panic-safety paths for clone, clear, drain, and consuming +iteration. Useful verification commands are: + +```bash +cargo test +cargo +nightly miri test +``` -Contributions are always welcome! Feel free to open an issue or submit a pull request. +The CI also runs tests under [Miri](https://github.com/rust-lang/miri). + +## Benchmarks + +The benchmark suite compares common `CapVec` operations with `std::vec::Vec` +using the same fixed capacity: + +```bash +cargo bench --bench bench +``` ## License -This crate is licensed under the MIT License. See [LICENSE](LICENSE) for more details. +This crate is licensed under the MIT License. See [LICENSE](LICENSE) for details. diff --git a/benches/bench.rs b/benches/bench.rs new file mode 100644 index 0000000..1693616 --- /dev/null +++ b/benches/bench.rs @@ -0,0 +1,361 @@ +use std::hint::black_box; +use std::time::Duration; +use std::vec::Vec; + +use cap_vec::CapVec; +use criterion::{BatchSize, BenchmarkId, Criterion, criterion_group, criterion_main}; + +const CAP: usize = 512; + +struct Benchmark { + name: &'static str, + cap_vec: fn() -> usize, + vec: fn() -> usize, +} + +fn bench_vec_compare(c: &mut Criterion) { + let mut group = c.benchmark_group(format!("vec_compare/capacity_{CAP}")); + + for benchmark in construction_benchmarks() { + group.bench_function(BenchmarkId::new(benchmark.name, "CapVec"), |b| { + b.iter(|| black_box((benchmark.cap_vec)())) + }); + group.bench_function(BenchmarkId::new(benchmark.name, "Vec"), |b| { + b.iter(|| black_box((benchmark.vec)())) + }); + } + + group.bench_function( + BenchmarkId::new("swap_remove_middle_until_empty", "CapVec"), + |b| { + b.iter_batched( + cap_vec_filled, + |values| black_box(cap_vec_swap_remove_middle_until_empty(values)), + BatchSize::SmallInput, + ) + }, + ); + group.bench_function( + BenchmarkId::new("swap_remove_middle_until_empty", "Vec"), + |b| { + b.iter_batched( + vec_filled, + |values| black_box(vec_swap_remove_middle_until_empty(values)), + BatchSize::SmallInput, + ) + }, + ); + + group.bench_function(BenchmarkId::new("truncate_half", "CapVec"), |b| { + b.iter_batched( + cap_vec_filled, + |values| black_box(cap_vec_truncate_half(values)), + BatchSize::SmallInput, + ) + }); + group.bench_function(BenchmarkId::new("truncate_half", "Vec"), |b| { + b.iter_batched( + vec_filled, + |values| black_box(vec_truncate_half(values)), + BatchSize::SmallInput, + ) + }); + + group.bench_function(BenchmarkId::new("drain_middle", "CapVec"), |b| { + b.iter_batched( + cap_vec_filled, + |values| black_box(cap_vec_drain_middle(values)), + BatchSize::SmallInput, + ) + }); + group.bench_function(BenchmarkId::new("drain_middle", "Vec"), |b| { + b.iter_batched( + vec_filled, + |values| black_box(vec_drain_middle(values)), + BatchSize::SmallInput, + ) + }); + + group.bench_function(BenchmarkId::new("iter_sum", "CapVec"), |b| { + b.iter_batched( + cap_vec_filled, + |values| black_box(cap_vec_iter_sum(&values)), + BatchSize::SmallInput, + ) + }); + group.bench_function(BenchmarkId::new("iter_sum", "Vec"), |b| { + b.iter_batched( + vec_filled, + |values| black_box(vec_iter_sum(&values)), + BatchSize::SmallInput, + ) + }); + + group.bench_function(BenchmarkId::new("into_iter_sum", "CapVec"), |b| { + b.iter_batched( + cap_vec_filled, + |values| black_box(cap_vec_into_iter_sum(values)), + BatchSize::SmallInput, + ) + }); + group.bench_function(BenchmarkId::new("into_iter_sum", "Vec"), |b| { + b.iter_batched( + vec_filled, + |values| black_box(vec_into_iter_sum(values)), + BatchSize::SmallInput, + ) + }); + + group.finish(); +} + +fn construction_benchmarks() -> [Benchmark; 5] { + [ + Benchmark { + name: "push", + cap_vec: cap_vec_push, + vec: vec_push, + }, + Benchmark { + name: "extend", + cap_vec: cap_vec_extend, + vec: vec_extend, + }, + Benchmark { + name: "push_then_pop", + cap_vec: cap_vec_push_then_pop, + vec: vec_push_then_pop, + }, + Benchmark { + name: "insert_front_then_remove_front", + cap_vec: cap_vec_insert_front_then_remove_front, + vec: vec_insert_front_then_remove_front, + }, + Benchmark { + name: "insert_middle_then_remove_middle", + cap_vec: cap_vec_insert_middle_then_remove_middle, + vec: vec_insert_middle_then_remove_middle, + }, + ] +} + +fn cap_vec_filled() -> CapVec { + let mut values = CapVec::::new(); + values.extend(0..CAP); + values +} + +fn vec_filled() -> Vec { + let mut values = Vec::with_capacity(CAP); + values.extend(0..CAP); + values +} + +fn cap_vec_push() -> usize { + let mut values = CapVec::::new(); + + for value in 0..CAP { + values.push(black_box(value)).unwrap(); + } + + black_box(values.iter().sum::() + values.len()) +} + +fn vec_push() -> usize { + let mut values = Vec::with_capacity(CAP); + + for value in 0..CAP { + values.push(black_box(value)); + } + + black_box(values.iter().sum::() + values.len()) +} + +fn cap_vec_extend() -> usize { + let mut values = CapVec::::new(); + let mut leftover = values.extend(0..CAP); + + black_box(leftover.next()); + black_box(values.iter().sum::() + values.len()) +} + +fn vec_extend() -> usize { + let mut values = Vec::with_capacity(CAP); + values.extend(0..CAP); + + black_box(values.iter().sum::() + values.len()) +} + +fn cap_vec_push_then_pop() -> usize { + let mut values = CapVec::::new(); + + for value in 0..CAP { + values.push(value).unwrap(); + } + + let mut sum = 0; + while let Some(value) = values.pop() { + sum += black_box(value); + } + + black_box(sum) +} + +fn vec_push_then_pop() -> usize { + let mut values = Vec::with_capacity(CAP); + + for value in 0..CAP { + values.push(value); + } + + let mut sum = 0; + while let Some(value) = values.pop() { + sum += black_box(value); + } + + black_box(sum) +} + +fn cap_vec_insert_front_then_remove_front() -> usize { + let mut values = CapVec::::new(); + + for value in 0..CAP { + values.insert(0, black_box(value)).unwrap(); + } + + let mut sum = 0; + while let Some(value) = values.remove(0) { + sum += black_box(value); + } + + black_box(sum) +} + +fn vec_insert_front_then_remove_front() -> usize { + let mut values = Vec::with_capacity(CAP); + + for value in 0..CAP { + values.insert(0, black_box(value)); + } + + let mut sum = 0; + while !values.is_empty() { + sum += black_box(values.remove(0)); + } + + black_box(sum) +} + +fn cap_vec_insert_middle_then_remove_middle() -> usize { + let mut values = CapVec::::new(); + + for value in 0..CAP { + values.insert(values.len() / 2, black_box(value)).unwrap(); + } + + let mut sum = 0; + while !values.is_empty() { + sum += black_box(values.remove(values.len() / 2).unwrap()); + } + + black_box(sum) +} + +fn vec_insert_middle_then_remove_middle() -> usize { + let mut values = Vec::with_capacity(CAP); + + for value in 0..CAP { + values.insert(values.len() / 2, black_box(value)); + } + + let mut sum = 0; + while !values.is_empty() { + sum += black_box(values.remove(values.len() / 2)); + } + + black_box(sum) +} + +fn cap_vec_swap_remove_middle_until_empty(mut values: CapVec) -> usize { + let mut sum = 0; + while !values.is_empty() { + sum += black_box(values.swap_remove(values.len() / 2).unwrap()); + } + + black_box(sum) +} + +fn vec_swap_remove_middle_until_empty(mut values: Vec) -> usize { + let mut sum = 0; + while !values.is_empty() { + sum += black_box(values.swap_remove(values.len() / 2)); + } + + black_box(sum) +} + +fn cap_vec_truncate_half(mut values: CapVec) -> usize { + values.truncate(CAP / 2); + + black_box(values.iter().sum::() + values.len()) +} + +fn vec_truncate_half(mut values: Vec) -> usize { + values.truncate(CAP / 2); + + black_box(values.iter().sum::() + values.len()) +} + +fn cap_vec_drain_middle(mut values: CapVec) -> usize { + let sum = values + .drain(CAP / 4..CAP * 3 / 4) + .fold(0, |acc, value| acc + black_box(value)); + + black_box(sum + values.len()) +} + +fn vec_drain_middle(mut values: Vec) -> usize { + let sum = values + .drain(CAP / 4..CAP * 3 / 4) + .fold(0, |acc, value| acc + black_box(value)); + + black_box(sum + values.len()) +} + +fn cap_vec_iter_sum(values: &CapVec) -> usize { + let sum = values.iter().fold(0, |acc, value| acc + black_box(*value)); + + black_box(sum) +} + +fn vec_iter_sum(values: &[usize]) -> usize { + let sum = values.iter().fold(0, |acc, value| acc + black_box(*value)); + + black_box(sum) +} + +fn cap_vec_into_iter_sum(values: CapVec) -> usize { + let sum = values + .into_iter() + .fold(0, |acc, value| acc + black_box(value)); + + black_box(sum) +} + +fn vec_into_iter_sum(values: Vec) -> usize { + let sum = values + .into_iter() + .fold(0, |acc, value| acc + black_box(value)); + + black_box(sum) +} + +criterion_group! { + name = benches; + config = Criterion::default() + .sample_size(10) + .warm_up_time(Duration::from_millis(200)) + .measurement_time(Duration::from_millis(500)); + targets = bench_vec_compare +} +criterion_main!(benches); diff --git a/src/lib.rs b/src/lib.rs index 1a5d936..4b607a7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,16 +1,38 @@ #![no_std] -//! A **heap-allocated**, **fixed-capacity**, **variable-size** array, `no_std` compatible. +//! A heap-backed vector with fixed maximum capacity for `no_std` environments. //! -//! `CapVec` is a heap-allocated container that stores up to `N` elements -//! of type `T` contiguously in memory. -//! Unlike [`Vec`], the capacity is **fixed at compile-time**, and cannot grow. -//! However, its length (`len`) can vary dynamically up to `N`. +//! [`CapVec`](CapVec) stores up to `N` values of type `T` contiguously in +//! a backing buffer. Unlike [`alloc::vec::Vec`], its maximum capacity is fixed +//! at compile time and never grows after construction. Its logical length can +//! still change dynamically from `0` to `N`. //! -//! This type is ideal for cases where: -//! - You're working in `no_std` environments. -//! - You want a `Vec`-like API but need deterministic capacity. -//! - You prefer heap allocation over stack-based fixed arrays like `[T; N]`. +//! This is useful when a fixed upper bound is part of the program design, but a +//! stack-allocated `[T; N]` would be too large or too inflexible. The crate uses +//! `alloc` and is compatible with `#![no_std]` targets that provide a global +//! allocator. +//! +//! # Allocation model +//! +//! `CapVec::new` creates an empty vector without allocating. The backing buffer +//! is created on the first successful [`push`](CapVec::push), +//! [`insert`](CapVec::insert), [`extend`](CapVec::extend), or when constructing +//! from a full array. For non-zero-sized buffers this allocates heap storage for +//! exactly `N` elements; zero-sized buffers use a dangling non-null sentinel. +//! The buffer is reused by operations such as [`clear`](CapVec::clear). Use +//! [`shrink_to_fit`](CapVec::shrink_to_fit) on an empty vector to drop the +//! backing buffer. +//! +//! # Capacity +//! +//! Maximum capacity is encoded in the const generic parameter `N`. The +//! [`capacity`](CapVec::capacity) method returns the currently-created backing +//! buffer size: `0` before the buffer is created, otherwise `N`. Use +//! [`max_capacity`](CapVec::max_capacity) to get the fixed upper bound and +//! [`spare_capacity`](CapVec::spare_capacity) to get the remaining room before +//! the vector is full. Operations that would exceed the maximum capacity return +//! the provided element instead of reallocating. `CapVec` is valid and can +//! never hold an element. //! //! ```rust //! use cap_vec::CapVec; @@ -34,31 +56,111 @@ //! ``` extern crate alloc; +#[cfg(test)] +extern crate std; +use alloc::alloc::{alloc, handle_alloc_error}; use alloc::boxed::Box; +use core::alloc::Layout; use core::hash::Hash; use core::iter::FusedIterator; use core::mem::MaybeUninit; -use core::ops::{Deref, DerefMut, Range}; +use core::ops::{Bound, Deref, DerefMut, Range, RangeBounds}; +use core::ptr::NonNull; use core::slice::{Iter, IterMut}; -/// A heap-allocated, fixed-capacity, variable-size array. +/// A heap-backed vector with compile-time fixed maximum capacity. +/// +/// `CapVec` behaves like a small subset of [`alloc::vec::Vec`] with one +/// important difference: its maximum capacity is the const generic `N`, and it +/// never reallocates to grow beyond that bound. /// -/// `CapVec` is similar to [`Vec`], except: -/// - The **capacity** is fixed and known at compile time. -/// - No reallocation or growth occurs once the capacity is reached. -/// - The internal buffer is allocated on the **heap** and initialized lazily. +/// The backing buffer is lazy for empty vectors created with [`new`](Self::new). +/// Once created, the buffer remains available until the vector is dropped. +/// Calling [`clear`](Self::clear) removes the initialized elements but keeps the +/// buffer for later reuse. Calling [`shrink_to_fit`](Self::shrink_to_fit) on an +/// empty vector drops the buffer. +/// +/// The initialized prefix can be viewed as a slice through [`as_slice`](Self::as_slice), +/// [`as_mut_slice`](Self::as_mut_slice), or the [`Deref`] implementations. Slice +/// methods such as [`first`](slice::first), [`last`](slice::last), and +/// [`get`](slice::get) are therefore available directly on `CapVec`. #[derive(Default)] pub struct CapVec { len: usize, buf: Option; N]>>, } +fn new_uninit_buffer() -> Box<[MaybeUninit; N]> { + if core::mem::size_of::<[MaybeUninit; N]>() == 0 { + return unsafe { Box::from_raw(NonNull::<[MaybeUninit; N]>::dangling().as_ptr()) }; + } + + let layout = Layout::new::<[MaybeUninit; N]>(); + let ptr = unsafe { alloc(layout) }; + + if ptr.is_null() { + handle_alloc_error(layout); + } + + unsafe { Box::from_raw(ptr as *mut [MaybeUninit; N]) } +} + +unsafe fn drop_initialized_range(ptr: *mut MaybeUninit, range: Range) { + unsafe { + core::ptr::drop_in_place(core::ptr::slice_from_raw_parts_mut( + ptr.add(range.start) as *mut T, + range.len(), + )); + } +} + +fn clone_into_uninit_buffer(values: &[T]) -> Box<[MaybeUninit; N]> +where + T: Clone, +{ + let mut elements = new_uninit_buffer(); + + struct Guard<'a, T> { + elements: &'a mut [MaybeUninit], + initialized: usize, + } + + impl Drop for Guard<'_, T> { + fn drop(&mut self) { + unsafe { + drop_initialized_range(self.elements.as_mut_ptr(), 0..self.initialized); + } + } + } + + let mut guard = Guard { + elements: elements.as_mut_slice(), + initialized: 0, + }; + + for source in values { + guard.elements[guard.initialized].write(source.clone()); + guard.initialized += 1; + } + + guard.initialized = 0; + drop(guard); + + elements +} + impl From<[T; N]> for CapVec { fn from(value: [T; N]) -> Self { + let mut elements = new_uninit_buffer(); + + elements.iter_mut().zip(value).for_each(|(dest, source)| { + dest.write(source); + }); + Self { - len: value.len(), - buf: Some(Box::new(value.map(MaybeUninit::new))), + len: N, + buf: Some(elements), } } } @@ -70,30 +172,23 @@ where fn clone(&self) -> Self { Self { len: self.len, - buf: (!self.is_empty()).then(|| { - let mut elements = Box::new([const { MaybeUninit::uninit() }; N]); - elements - .iter_mut() - .zip(self.as_slice()) - .for_each(|(dest, source)| { - dest.write(source.clone()); - }); - - elements - }), + buf: (!self.is_empty()).then(|| clone_into_uninit_buffer(self.as_slice())), } } } impl CapVec { - /// Creates a new, empty `CapVec` with capacity `N`. - /// The internal buffer is not allocated until an element is inserted. + /// Creates a new, empty `CapVec` with maximum capacity `N`. + /// + /// The internal buffer is not created until an element is inserted. This + /// makes `new` cheap even when `N` is large. /// /// ``` /// use cap_vec::CapVec; /// /// let mut v = CapVec::::new(); - /// assert_eq!(v.capacity(), 8); + /// assert_eq!(v.capacity(), 0); + /// assert_eq!(v.max_capacity(), 8); /// assert_eq!(v.len(), 0); /// assert!(v.is_empty()); /// ``` @@ -101,8 +196,17 @@ impl CapVec { Self { len: 0, buf: None } } - /// Extends the `CapVec` with elements from an iterator, up to its capacity. - /// Returns the remaining iterator once the vector is full or the iterator ends. + /// Extends the vector with elements from an iterator until the vector is full + /// or the iterator is exhausted. + /// + /// The returned iterator is the same iterator after the consumed prefix has + /// been removed. If the vector becomes full before the source iterator is + /// exhausted, the next call to `next` on the returned iterator yields the + /// first uninserted element. + /// + /// This is an inherent method, not an implementation of [`Extend`], because + /// it reports the unconsumed tail instead of silently discarding or panicking + /// when the vector is full. /// /// ``` /// use cap_vec::CapVec; @@ -119,18 +223,34 @@ impl CapVec { { let mut iter = iter.into_iter(); + if self.len >= N { + return iter; + } + + let ptr = self.buf.get_or_insert_with(new_uninit_buffer).as_mut_ptr(); + for _ in 0..N - self.len { - match iter.next() { - Some(element) => unsafe { self.push(element).unwrap_unchecked() }, - None => break, + let Some(element) = iter.next() else { + break; + }; + + unsafe { + ptr.add(self.len).write(MaybeUninit::new(element)); } + + self.len += 1; } iter } - /// Appends an element to the end of the collection. - /// Returns `Ok(())` on success, or `Err(element)` if the vector is already full. + /// Appends an element to the end of the vector. + /// + /// Returns `Ok(())` when the element was inserted. Returns `Err(element)` + /// unchanged if the vector is already full. + /// + /// A successful first push creates the backing buffer when the vector was + /// created with [`new`](Self::new). /// /// ``` /// use cap_vec::CapVec; @@ -145,27 +265,21 @@ impl CapVec { return Err(element); } - let ptr = self - .buf - .get_or_insert_with(|| Box::new([const { MaybeUninit::uninit() }; N])) - .as_mut_ptr(); + let ptr = self.buf.get_or_insert_with(new_uninit_buffer).as_mut_ptr(); unsafe { - // Write the new element at the specified index ptr.add(self.len).write(MaybeUninit::new(element)); } - // Increment the size of the array self.len += 1; Ok(()) } - /// Inserts an element at the given index, shifting subsequent elements to the right. - /// Returns `Err(element)` if the index is out of bounds or the vector is full. + /// Inserts an element at `index`, shifting all following elements one slot to + /// the right. /// - /// # Safety - /// This function performs unchecked memory writes internally, - /// but all unsafe operations are encapsulated safely. + /// Inserting at `len` is equivalent to [`push`](Self::push). Returns + /// `Err(element)` if `index > len` or if the vector is already full. /// /// ``` /// use cap_vec::CapVec; @@ -180,25 +294,21 @@ impl CapVec { return Err(element); } - let ptr = self - .buf - .get_or_insert_with(|| Box::new([const { MaybeUninit::uninit() }; N])) - .as_mut_ptr(); + let ptr = self.buf.get_or_insert_with(new_uninit_buffer).as_mut_ptr(); unsafe { - // Shift elements starting from the index to the right core::ptr::copy(ptr.add(index), ptr.add(index + 1), self.len - index); - - // Write the new element at the specified index ptr.add(index).write(MaybeUninit::new(element)); } - // Increment the size of the array self.len += 1; Ok(()) } - /// Removes the last element and returns it, or `None` if the vector is empty. + /// Removes the last element and returns it. + /// + /// Returns `None` when the vector is empty. Popping the last element does not + /// release the backing buffer. /// /// ``` /// use cap_vec::CapVec; @@ -216,16 +326,48 @@ impl CapVec { debug_assert!(self.buf.is_some()); let buf = unsafe { self.buf.as_mut().unwrap_unchecked() }; - // Decrement the len of the array self.len -= 1; - - // Read the element to be removed let element = unsafe { buf[self.len].assume_init_read() }; Some(element) } - /// Removes and returns the element at the given index, shifting subsequent elements left. - /// Returns `None` if the index is out of bounds. + /// Shortens the vector, keeping the first `len` elements and dropping the + /// rest. + /// + /// If `len` is greater than or equal to the current length, this has no + /// effect. The backing buffer, if present, is retained for later insertions. + /// + /// ``` + /// use cap_vec::CapVec; + /// + /// let mut v = CapVec::::new(); + /// v.extend([1, 2, 3, 4]); + /// v.truncate(2); + /// assert_eq!(v.as_slice(), &[1, 2]); + /// + /// v.truncate(10); + /// assert_eq!(v.as_slice(), &[1, 2]); + /// ``` + pub fn truncate(&mut self, len: usize) { + if len >= self.len { + return; + } + + let old_len = self.len; + self.len = len; + + if let Some(buf) = self.buf.as_mut() { + unsafe { + drop_initialized_range(buf.as_mut_ptr(), len..old_len); + } + } + } + + /// Removes and returns the element at `index`, shifting all following + /// elements one slot to the left. + /// + /// Returns `None` if `index` is out of bounds. The backing buffer, if + /// present, is retained for later insertions. /// /// ``` /// use cap_vec::CapVec; @@ -244,13 +386,10 @@ impl CapVec { let buf = unsafe { self.buf.as_mut().unwrap_unchecked() }; let ptr = buf.as_mut_ptr(); - // Read the element to be removed let element = unsafe { buf[index].assume_init_read() }; - // Decrement the len of the array self.len -= 1; - // Shift elements to fill the gap unsafe { core::ptr::copy(ptr.add(index + 1), ptr.add(index), self.len - index); } @@ -258,22 +397,85 @@ impl CapVec { Some(element) } - /// Returns a shared slice over the initialized portion of the buffer. + /// Removes and returns the element at `index`, replacing it with the last + /// element. + /// + /// This is equivalent to [`alloc::vec::Vec::swap_remove`] except it returns + /// `None` instead of panicking when `index` is out of bounds, matching + /// [`remove`](Self::remove). + /// + /// The backing buffer, if present, is retained for later insertions. + /// + /// ``` + /// use cap_vec::CapVec; + /// + /// let mut v = CapVec::::new(); + /// v.extend([10, 20, 30, 40]); + /// assert_eq!(v.swap_remove(1), Some(20)); + /// assert_eq!(v.as_slice(), &[10, 40, 30]); + /// assert_eq!(v.swap_remove(3), None); + /// ``` + pub fn swap_remove(&mut self, index: usize) -> Option { + if index >= self.len { + return None; + } + + debug_assert!(self.buf.is_some()); + let buf = unsafe { self.buf.as_mut().unwrap_unchecked() }; + + self.len -= 1; + let element = unsafe { buf[index].assume_init_read() }; + + if index != self.len { + let last = unsafe { buf[self.len].assume_init_read() }; + buf[index].write(last); + } + + Some(element) + } + + /// Returns a shared slice of all initialized elements. + /// + /// The slice length is always equal to [`len`](Self::len). pub fn as_slice(&self) -> &[T] { self.deref() } - /// Returns a mutable slice over the initialized portion of the buffer. + /// Returns a mutable slice of all initialized elements. + /// + /// Mutating the returned slice can change element values and order, but it + /// cannot change the vector length, current capacity, or maximum capacity. pub fn as_mut_slice(&mut self) -> &mut [T] { self.deref_mut() } - /// Returns the compile-time capacity of the vector. - pub const fn capacity(&self) -> usize { + /// Returns the currently-created backing buffer capacity. + /// + /// This follows [`alloc::vec::Vec`] semantics: a newly-created empty vector + /// has capacity `0`, while a vector with a backing buffer has capacity `N`. + /// Calling [`shrink_to_fit`](Self::shrink_to_fit) on an empty vector drops + /// the backing buffer and returns this value to `0`. + pub fn capacity(&self) -> usize { + if self.buf.is_some() { N } else { 0 } + } + + /// Returns the fixed maximum capacity `N`. + /// + /// This value is known at compile time and never changes. + pub const fn max_capacity(&self) -> usize { N } - /// Returns the current number of initialized elements. + /// Returns how many more elements can be inserted before the vector is full. + /// + /// This is equivalent to `capacity() - len()`: it returns `0` before the + /// backing buffer is created and then tracks the unused slots in the fixed + /// buffer. + pub fn spare_capacity(&self) -> usize { + self.capacity() - self.len + } + + /// Returns the number of initialized elements currently in the vector. pub const fn len(&self) -> usize { self.len } @@ -283,24 +485,26 @@ impl CapVec { self.len == 0 } - /// Removes the subslice indicated by the given range from the vector, - /// returning a double-ended iterator over the removed subslice. + /// Removes the elements in `range`, returning an iterator over the removed + /// elements. /// - /// If the iterator is dropped before being fully consumed, - /// it drops the remaining removed elements. + /// The returned [`Drain`] is double-ended, so removed elements can be + /// consumed from either side. If the iterator is dropped before being fully + /// consumed, it drops the remaining removed elements and moves the original + /// tail back into place. /// - /// The returned iterator keeps a mutable borrow on the vector to optimize - /// its implementation. + /// While the iterator exists, it keeps a mutable borrow of the vector. /// /// # Panics /// - /// Panics if the starting point is greater than the end point or if - /// the end point is greater than the length of the vector. + /// Panics if the range start is greater than the range end, if the range + /// end exceeds `self.len()`, or if an excluded start or included end bound + /// overflows `usize`. /// /// # Leaking /// /// If the returned iterator goes out of scope without being dropped (due to - /// [`mem::forget`], for example), the vector may have lost and leaked + /// [`core::mem::forget`], for example), the vector may have lost and leaked /// elements arbitrarily, including elements outside the range. /// /// # Examples @@ -309,28 +513,32 @@ impl CapVec { /// use cap_vec::CapVec; /// /// let mut v = CapVec::from([1, 2, 3]); - /// let u: Vec<_> = v.drain(1..v.len()).collect(); + /// let u: Vec<_> = v.drain(1..).collect(); /// assert_eq!(v.as_slice(), &[1]); /// assert_eq!(u, &[2, 3]); /// /// // A full range clears the vector, like `clear()` does - /// v.drain(0..v.len()); + /// v.drain(..); /// assert_eq!(v.as_slice(), &[]); /// ``` - pub fn drain(&mut self, range: Range) -> Drain<'_, T, N> { + pub fn drain(&mut self, range: R) -> Drain<'_, T, N> + where + R: RangeBounds, + { + let range = drain_range(range, self.len); + assert!(range.start <= range.end); assert!(range.end <= self.len); // Memory safety // - // When the Drain created, it shortens the length of the source vector - // to make sure no uninitialized or moved-from elements are accessible - // at all if the Drain's destructor never gets to run. + // Creating Drain shortens the source vector so uninitialized or moved- + // from elements are not accessible if the Drain is leaked. // - // Drain will ptr::read out the values to remove. + // Drain reads removed values by pointer. // - // When finished, remaining tail of the vec is copied back to cover - // the hole, and the vector length is restored to the new length. + // When Drain is dropped, the remaining tail is copied back to cover the + // hole, and the vector length is restored to the new length. let original_len = self.len(); self.len = range.start; @@ -353,7 +561,9 @@ impl CapVec { } /// Drops all elements and resets the vector to an empty state. - /// The capacity remains allocated. + /// + /// The backing buffer remains available for later reuse. If an element's + /// destructor panics and the panic is caught, the vector is still left empty. /// /// ``` /// use cap_vec::CapVec; @@ -364,16 +574,58 @@ impl CapVec { /// assert!(v.is_empty()); /// ``` pub fn clear(&mut self) { - if let Some(slice) = self.buf.as_mut() { - slice[..self.len] - .iter_mut() - .for_each(|e| unsafe { e.assume_init_drop() }); + let len = self.len; + self.len = 0; + + if let Some(buf) = self.buf.as_mut() { + unsafe { + drop_initialized_range(buf.as_mut_ptr(), 0..len); + } } + } - self.len = 0; + /// Drops the backing buffer when the vector is empty. + /// + /// This method releases the lazily-created backing buffer when + /// [`len`](Self::len) is zero, making [`capacity`](Self::capacity) return + /// `0`. It never changes [`max_capacity`](Self::max_capacity), and non-empty + /// vectors are left unchanged. + /// + /// ``` + /// use cap_vec::CapVec; + /// + /// let mut v = CapVec::::new(); + /// v.extend([1, 2, 3]); + /// v.clear(); + /// v.shrink_to_fit(); + /// + /// assert_eq!(v.capacity(), 0); + /// assert_eq!(v.max_capacity(), 4); + /// assert!(v.is_empty()); + /// ``` + pub fn shrink_to_fit(&mut self) { + if self.is_empty() { + self.buf = None; + } } } +fn drain_range(range: impl RangeBounds, len: usize) -> Range { + let start = match range.start_bound() { + Bound::Included(&index) => index, + Bound::Excluded(&index) => index.checked_add(1).expect("range start index overflow"), + Bound::Unbounded => 0, + }; + + let end = match range.end_bound() { + Bound::Included(&index) => index.checked_add(1).expect("range end index overflow"), + Bound::Excluded(&index) => index, + Bound::Unbounded => len, + }; + + start..end +} + impl Deref for CapVec { type Target = [T]; @@ -403,10 +655,12 @@ impl IntoIterator for CapVec { type IntoIter = IntoIter; fn into_iter(mut self) -> Self::IntoIter { + let len = self.len; match self.buf.take() { None => IntoIter::default(), Some(buf) => IntoIter { - inner: buf.into_iter().take(self.len), + buf: Some(buf), + alive: 0..len, }, } } @@ -537,8 +791,24 @@ impl Drop for CapVec { } } -// --- - +/// An iterator over elements removed by [`CapVec::drain`]. +/// +/// `Drain` owns the removed elements as they are yielded. Dropping the iterator +/// drops any removed elements that have not yet been yielded, restores the +/// original tail of the vector, and updates the vector length. +/// +/// The iterator borrows the source vector mutably for its entire lifetime, so +/// the vector cannot be used again until the `Drain` is dropped. +/// +/// ``` +/// use cap_vec::CapVec; +/// +/// let mut values = CapVec::from([1, 2, 3, 4]); +/// let removed: Vec<_> = values.drain(1..3).rev().collect(); +/// +/// assert_eq!(removed, [3, 2]); +/// assert_eq!(values.as_slice(), &[1, 4]); +/// ``` pub struct Drain<'a, T, const N: usize> { leftover_range: Range, drop_range: Range, @@ -546,6 +816,39 @@ pub struct Drain<'a, T, const N: usize> { original_len: usize, } +impl Drain<'_, T, N> { + /// Returns a shared slice of the removed elements that have not been yielded + /// yet. + /// + /// ``` + /// use cap_vec::CapVec; + /// + /// let mut values = CapVec::from([1, 2, 3, 4]); + /// let mut drain = values.drain(1..); + /// + /// assert_eq!(drain.as_slice(), &[2, 3, 4]); + /// assert_eq!(drain.next(), Some(2)); + /// assert_eq!(drain.as_slice(), &[3, 4]); + /// ``` + pub fn as_slice(&self) -> &[T] { + self.vec.buf.as_ref().map_or(&[], |buf| unsafe { + core::slice::from_raw_parts( + buf.as_ptr().add(self.leftover_range.start) as *const T, + self.leftover_range.len(), + ) + }) + } +} + +impl core::fmt::Debug for Drain<'_, T, N> +where + T: core::fmt::Debug, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("Drain").field(&self.as_slice()).finish() + } +} + impl<'a, T, const N: usize> Iterator for Drain<'a, T, N> { type Item = T; @@ -559,6 +862,35 @@ impl<'a, T, const N: usize> Iterator for Drain<'a, T, N> { fn size_hint(&self) -> (usize, Option) { self.leftover_range.size_hint() } + + fn nth(&mut self, n: usize) -> Option { + let len = self.leftover_range.len(); + let count = n.min(len); + let start = self.leftover_range.start; + self.leftover_range.start += count; + + if count > 0 { + debug_assert!(self.vec.buf.is_some()); + let buf = unsafe { self.vec.buf.as_mut().unwrap_unchecked() }; + + unsafe { + drop_initialized_range(buf.as_mut_ptr(), start..start + count); + } + } + + (n < len).then(|| unsafe { + let index = self.leftover_range.next().unwrap_unchecked(); + self.vec.buf.as_mut().unwrap_unchecked()[index].assume_init_read() + }) + } + + fn last(mut self) -> Option { + self.next_back() + } + + fn count(self) -> usize { + self.leftover_range.len() + } } impl DoubleEndedIterator for Drain<'_, T, N> { @@ -568,6 +900,29 @@ impl DoubleEndedIterator for Drain<'_, T, N> { let element = unsafe { self.vec.buf.as_mut().unwrap_unchecked()[index].assume_init_read() }; Some(element) } + + fn nth_back(&mut self, n: usize) -> Option { + let len = self.leftover_range.len(); + let count = n.min(len); + self.leftover_range.end -= count; + + if count > 0 { + debug_assert!(self.vec.buf.is_some()); + let buf = unsafe { self.vec.buf.as_mut().unwrap_unchecked() }; + + unsafe { + drop_initialized_range( + buf.as_mut_ptr(), + self.leftover_range.end..self.leftover_range.end + count, + ); + } + } + + (n < len).then(|| unsafe { + let index = self.leftover_range.next_back().unwrap_unchecked(); + self.vec.buf.as_mut().unwrap_unchecked()[index].assume_init_read() + }) + } } impl FusedIterator for Drain<'_, T, N> {} @@ -580,36 +935,134 @@ impl ExactSizeIterator for Drain<'_, T, N> { impl Drop for Drain<'_, T, N> { fn drop(&mut self) { - if let Some(buf) = self.vec.buf.as_mut() { - for index in self.leftover_range.clone() { - unsafe { buf[index].assume_init_drop() }; + struct Guard<'a, T, const N: usize> { + vec: &'a mut CapVec, + drop_range: Range, + original_len: usize, + } + + impl Drop for Guard<'_, T, N> { + fn drop(&mut self) { + if let Some(buf) = self.vec.buf.as_mut() { + let ptr = buf.as_mut_ptr(); + + unsafe { + core::ptr::copy( + ptr.add(self.drop_range.end), + ptr.add(self.drop_range.start), + self.original_len - self.drop_range.end, + ); + } + } + + self.vec.len = self.original_len - self.drop_range.len(); } + } - let ptr = buf.as_mut_ptr(); + let guard = Guard { + vec: self.vec, + drop_range: self.drop_range.clone(), + original_len: self.original_len, + }; + if let Some(buf) = guard.vec.buf.as_mut() { unsafe { - core::ptr::copy( - ptr.add(self.drop_range.end), - ptr.add(self.drop_range.start), - self.original_len - self.drop_range.end, - ); + drop_initialized_range(buf.as_mut_ptr(), self.leftover_range.clone()); } } - - self.vec.len = self.original_len - self.drop_range.len(); } } -// --- - +/// A consuming iterator over a [`CapVec`]. +/// +/// This iterator owns the vector's backing buffer and yields initialized +/// elements by value. Elements can be consumed from the front with +/// [`Iterator::next`] or from the back with [`DoubleEndedIterator::next_back`]. +/// Any elements left in the iterator are dropped when the iterator is dropped. +/// +/// ``` +/// use cap_vec::CapVec; +/// +/// let values = CapVec::from(["a", "b", "c"]); +/// let mut iter = values.into_iter(); +/// +/// assert_eq!(iter.next(), Some("a")); +/// assert_eq!(iter.next_back(), Some("c")); +/// assert_eq!(iter.next(), Some("b")); +/// assert_eq!(iter.next(), None); +/// ``` pub struct IntoIter { - inner: core::iter::Take, N>>, + buf: Option; N]>>, + alive: Range, } impl Default for IntoIter { fn default() -> Self { Self { - inner: [const { MaybeUninit::uninit() }; N].into_iter().take(0), + buf: None, + alive: 0..0, + } + } +} + +impl IntoIter { + /// Returns a shared slice of the elements that have not been yielded yet. + /// + /// ``` + /// use cap_vec::CapVec; + /// + /// let values = CapVec::from([1, 2, 3]); + /// let mut iter = values.into_iter(); + /// + /// assert_eq!(iter.as_slice(), &[1, 2, 3]); + /// assert_eq!(iter.next(), Some(1)); + /// assert_eq!(iter.as_slice(), &[2, 3]); + /// ``` + pub fn as_slice(&self) -> &[T] { + self.buf.as_ref().map_or(&[], |buf| unsafe { + core::slice::from_raw_parts( + buf.as_ptr().add(self.alive.start) as *const T, + self.alive.len(), + ) + }) + } + + /// Returns a mutable slice of the elements that have not been yielded yet. + /// + /// ``` + /// use cap_vec::CapVec; + /// + /// let values = CapVec::from([1, 2, 3]); + /// let mut iter = values.into_iter(); + /// + /// iter.as_mut_slice()[1] = 20; + /// assert_eq!(iter.collect::>(), [1, 20, 3]); + /// ``` + pub fn as_mut_slice(&mut self) -> &mut [T] { + self.buf.as_mut().map_or(&mut [], |buf| unsafe { + core::slice::from_raw_parts_mut( + buf.as_mut_ptr().add(self.alive.start) as *mut T, + self.alive.len(), + ) + }) + } +} + +impl Clone for IntoIter +where + T: Clone, +{ + fn clone(&self) -> Self { + let values = self.as_slice(); + let len = values.len(); + + if values.is_empty() { + return Self::default(); + } + + Self { + buf: Some(clone_into_uninit_buffer(values)), + alive: 0..len, } } } @@ -619,9 +1072,7 @@ where T: core::fmt::Debug, { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("IntoIter") - .field("inner", &self.inner) - .finish() + f.debug_tuple("IntoIter").field(&self.as_slice()).finish() } } @@ -629,17 +1080,70 @@ impl Iterator for IntoIter { type Item = T; fn next(&mut self) -> Option { - self.inner.next().map(|e| unsafe { e.assume_init() }) + let index = self.alive.next()?; + debug_assert!(self.buf.is_some()); + Some(unsafe { self.buf.as_mut().unwrap_unchecked()[index].assume_init_read() }) } fn size_hint(&self) -> (usize, Option) { - self.inner.size_hint() + self.alive.size_hint() + } + + fn nth(&mut self, n: usize) -> Option { + let len = self.alive.len(); + let count = n.min(len); + let start = self.alive.start; + self.alive.start += count; + + if count > 0 { + debug_assert!(self.buf.is_some()); + let buf = unsafe { self.buf.as_mut().unwrap_unchecked() }; + + unsafe { + drop_initialized_range(buf.as_mut_ptr(), start..start + count); + } + } + + (n < len).then(|| unsafe { + let index = self.alive.next().unwrap_unchecked(); + self.buf.as_mut().unwrap_unchecked()[index].assume_init_read() + }) + } + + fn last(mut self) -> Option { + self.next_back() + } + + fn count(self) -> usize { + self.alive.len() } } impl DoubleEndedIterator for IntoIter { fn next_back(&mut self) -> Option { - self.inner.next_back().map(|e| unsafe { e.assume_init() }) + let index = self.alive.next_back()?; + debug_assert!(self.buf.is_some()); + Some(unsafe { self.buf.as_mut().unwrap_unchecked()[index].assume_init_read() }) + } + + fn nth_back(&mut self, n: usize) -> Option { + let len = self.alive.len(); + let count = n.min(len); + self.alive.end -= count; + + if count > 0 { + debug_assert!(self.buf.is_some()); + let buf = unsafe { self.buf.as_mut().unwrap_unchecked() }; + + unsafe { + drop_initialized_range(buf.as_mut_ptr(), self.alive.end..self.alive.end + count); + } + } + + (n < len).then(|| unsafe { + let index = self.alive.next_back().unwrap_unchecked(); + self.buf.as_mut().unwrap_unchecked()[index].assume_init_read() + }) } } @@ -647,14 +1151,16 @@ impl FusedIterator for IntoIter {} impl ExactSizeIterator for IntoIter { fn len(&self) -> usize { - self.inner.len() + self.alive.len() } } impl Drop for IntoIter { fn drop(&mut self) { - for mut element in self.inner.by_ref() { - unsafe { element.assume_init_drop() }; + if let Some(buf) = self.buf.as_mut() { + unsafe { + drop_initialized_range(buf.as_mut_ptr(), self.alive.clone()); + } } } } @@ -662,192 +1168,374 @@ impl Drop for IntoIter { #[cfg(test)] mod tests { use alloc::boxed::Box; + use alloc::rc::Rc; use alloc::vec::Vec; + use core::cell::Cell; + use core::hash::{Hash, Hasher}; + use core::panic::AssertUnwindSafe; + use std::collections::hash_map::DefaultHasher; + use std::format; + use std::panic::catch_unwind; use super::CapVec; + struct DropCounter { + id: usize, + dropped: Rc>, + } + + impl Drop for DropCounter { + fn drop(&mut self) { + self.dropped.set(self.dropped.get() + 1); + } + } + + fn drop_counter(id: usize, dropped: &Rc>) -> DropCounter { + DropCounter { + id, + dropped: Rc::clone(dropped), + } + } + #[test] - fn test_default() { + fn test_default_creates_empty_vector() { let sut = CapVec::::default(); - assert_eq!(sut.len(), 0, "CapVec should start empty"); - assert_eq!( - sut.capacity(), - 8, - "Capacity must match compile-time constant" - ); - assert!(sut.is_empty(), "CapVec should be empty by default"); - assert!(sut.as_slice().is_empty(), "Slice view should also be empty"); + assert_eq!(sut.len(), 0); + assert_eq!(sut.capacity(), 0); + assert_eq!(sut.max_capacity(), 8); + assert_eq!(sut.spare_capacity(), 0); + assert!(sut.is_empty()); + assert!(sut.as_slice().is_empty()); } #[test] - fn test_from_array() { + fn test_from_array_creates_full_vector() { const SEED: [i32; 4] = [1, 2, 3, 4]; let sut = CapVec::::from(SEED); - assert_eq!(sut.len(), 4, "Length should equal array length"); - assert_eq!(sut.capacity(), 4, "Capacity should equal compile-time N"); - assert!(!sut.is_empty(), "CapVec from array should not be empty"); - assert_eq!(sut.as_slice(), &SEED, "Contents must match source array"); - } + assert_eq!(sut.len(), 4); + assert_eq!(sut.capacity(), 4); + assert_eq!(sut.max_capacity(), 4); + assert_eq!(sut.spare_capacity(), 0); + assert!(!sut.is_empty()); + assert_eq!(sut.as_slice(), &SEED); + } #[test] - fn test_clone_non_empty_vec() { + fn test_clone_preserves_non_empty_vector() { let mut base = CapVec::::new(); base.extend([1, 2, 3]); let sut = base.clone(); - assert_eq!(sut.len(), base.len(), "Clone should preserve length"); - assert_eq!( - sut.capacity(), - base.capacity(), - "Clone should preserve capacity" - ); - assert_eq!(sut.as_slice(), base.as_slice(), "Clone contents must match"); + assert_eq!(sut.len(), base.len()); + assert_eq!(sut.capacity(), base.capacity()); + assert_eq!(sut.max_capacity(), base.max_capacity()); + assert_eq!(sut.spare_capacity(), base.spare_capacity()); + assert_eq!(sut.as_slice(), base.as_slice()); base.push(4).unwrap(); - assert_ne!( - sut.len(), - base.len(), - "Clone must not reflect later mutations" - ); - assert_eq!( - sut.as_slice(), - &[1, 2, 3], - "Clone contents must remain unaffected after modifying original" - ); + assert_ne!(sut.len(), base.len()); + assert_eq!(sut.as_slice(), &[1, 2, 3]); } #[test] - fn test_clone_empty_vec() { + fn test_clone_preserves_empty_vector() { let mut base = CapVec::::new(); let sut = base.clone(); - assert!(sut.is_empty(), "Clone of empty CapVec should be empty"); - assert_eq!(sut.capacity(), 5); + assert!(sut.is_empty()); + assert_eq!(sut.capacity(), 0); + assert_eq!(sut.max_capacity(), 5); + assert_eq!(sut.spare_capacity(), 0); assert!(sut.as_slice().is_empty()); base.push(0).unwrap(); - assert_ne!( - sut.len(), - base.len(), - "Clone must not reflect later mutations" - ); - assert_eq!( - sut.as_slice(), - &[], - "Clone contents must remain unaffected after modifying original" - ); + assert_ne!(sut.len(), base.len()); + assert_eq!(sut.as_slice(), &[]); } #[test] - fn test_new() { + fn test_clone_drops_initialized_elements_when_later_clone_panics() { + struct PanicOnSecondClone { + alive: Rc>, + clone_calls: Rc>, + } + + impl PanicOnSecondClone { + fn new(alive: Rc>, clone_calls: Rc>) -> Self { + alive.set(alive.get() + 1); + Self { alive, clone_calls } + } + } + + impl Clone for PanicOnSecondClone { + fn clone(&self) -> Self { + let clone_calls = self.clone_calls.get() + 1; + self.clone_calls.set(clone_calls); + + if clone_calls == 2 { + panic!("intentional clone panic"); + } + + self.alive.set(self.alive.get() + 1); + Self { + alive: Rc::clone(&self.alive), + clone_calls: Rc::clone(&self.clone_calls), + } + } + } + + impl Drop for PanicOnSecondClone { + fn drop(&mut self) { + self.alive.set(self.alive.get() - 1); + } + } + + let alive = Rc::new(Cell::new(0)); + let clone_calls = Rc::new(Cell::new(0)); + let base = CapVec::<_, 2>::from([ + PanicOnSecondClone::new(Rc::clone(&alive), Rc::clone(&clone_calls)), + PanicOnSecondClone::new(Rc::clone(&alive), Rc::clone(&clone_calls)), + ]); + + let result = catch_unwind(AssertUnwindSafe(|| { + let _ = base.clone(); + })); + + assert!(result.is_err()); + assert_eq!(alive.get(), 2); + } + + #[test] + fn test_new_creates_empty_vector() { let sut = CapVec::::new(); - assert_eq!(sut.len(), 0, "CapVec should start empty"); - assert_eq!( - sut.capacity(), - 8, - "Capacity must match compile-time constant" - ); - assert!(sut.is_empty(), "CapVec should be empty by default"); - assert!(sut.as_slice().is_empty(), "Slice view should also be empty"); + assert_eq!(sut.len(), 0); + assert_eq!(sut.capacity(), 0); + assert_eq!(sut.max_capacity(), 8); + assert_eq!(sut.spare_capacity(), 0); + assert!(sut.is_empty()); + assert!(sut.as_slice().is_empty()); } #[test] - fn test_len() { + fn test_len_tracks_initialized_elements() { let mut sut = CapVec::::new(); - assert_eq!(sut.len(), 0, "CapVec should start empty"); - assert_eq!( - sut.capacity(), - 3, - "Capacity must match compile-time constant" - ); - assert!(sut.is_empty(), "CapVec should be empty by default"); + assert_eq!(sut.len(), 0); + assert_eq!(sut.capacity(), 0); + assert_eq!(sut.max_capacity(), 3); + assert_eq!(sut.spare_capacity(), 0); + assert!(sut.is_empty()); sut.push(10).unwrap(); - assert_eq!(sut.len(), 1, "Length should increase after one push"); + assert_eq!(sut.len(), 1); + assert_eq!(sut.capacity(), 3); + assert_eq!(sut.spare_capacity(), 2); sut.push(20).unwrap(); - assert_eq!( - sut.len(), - 2, - "Length should update correctly after two pushes" - ); + assert_eq!(sut.len(), 2); + assert_eq!(sut.spare_capacity(), 1); sut.pop(); - assert_eq!(sut.len(), 1, "Length should decrease after a pop"); + assert_eq!(sut.len(), 1); + assert_eq!(sut.spare_capacity(), 2); sut.pop(); - assert_eq!(sut.len(), 0, "Length should be zero after the last pop"); + assert_eq!(sut.len(), 0); + assert_eq!(sut.capacity(), 3); + assert_eq!(sut.spare_capacity(), 3); } #[test] - fn test_capacity() { + fn test_capacity_tracks_created_backing_buffer() { + let mut sut = CapVec::::new(); + assert_eq!(sut.capacity(), 0); + + sut.push(1).unwrap(); + assert_eq!(sut.capacity(), 4); + + sut.clear(); + assert_eq!(sut.capacity(), 4); + + sut.shrink_to_fit(); + assert_eq!(sut.capacity(), 0); + } + + #[test] + fn test_max_capacity_returns_const_generic_bound() { let sut = CapVec::<(), 4>::new(); - assert_eq!( - sut.capacity(), - 4, - "Capacity must match compile-time constant" - ); + assert_eq!(sut.max_capacity(), 4); let sut = CapVec::::new(); - assert_eq!( - sut.capacity(), - 16, - "Capacity must match compile-time constant" - ); + assert_eq!(sut.max_capacity(), 16); let sut = CapVec::::new(); - assert_eq!( - sut.capacity(), - 1, - "Capacity must match compile-time constant" - ); + assert_eq!(sut.max_capacity(), 1); } #[test] - fn test_is_empty() { + fn test_spare_capacity_tracks_remaining_buffer_slots() { + let mut sut = CapVec::::new(); + assert_eq!(sut.capacity(), 0); + assert_eq!(sut.spare_capacity(), 0); + + sut.extend([1, 2, 3]); + assert_eq!(sut.capacity(), 4); + assert_eq!(sut.spare_capacity(), 1); + + sut.push(4).unwrap(); + assert_eq!(sut.spare_capacity(), 0); + + sut.clear(); + assert_eq!(sut.capacity(), 4); + assert_eq!(sut.spare_capacity(), 4); + + sut.shrink_to_fit(); + assert_eq!(sut.capacity(), 0); + assert_eq!(sut.spare_capacity(), 0); + } + + #[test] + fn test_is_empty_tracks_length() { let mut sut = CapVec::::new(); - assert!(sut.is_empty(), "CapVec should be empty by default"); + assert!(sut.is_empty()); sut.push(1).unwrap(); - assert!(!sut.is_empty(), "CapVec should not be empty after push"); + assert!(!sut.is_empty()); sut.clear(); - assert!(sut.is_empty(), "CapVec should be empty again after clear()"); + assert!(sut.is_empty()); } #[test] - fn test_clear() { + fn test_clear_removes_all_elements_and_reuses_buffer() { let mut sut = CapVec::, 3>::new(); sut.push("a".into()).unwrap(); sut.push("b".into()).unwrap(); - assert_eq!(sut.len(), 2, "Should have two elements before clear"); + assert_eq!(sut.len(), 2); sut.clear(); - assert_eq!(sut.len(), 0, "Clear should reset length to zero"); - assert!(sut.is_empty(), "CapVec should be empty after clear"); + assert_eq!(sut.len(), 0); + assert!(sut.is_empty()); - // Ensure subsequent reuse works fine sut.push("c".into()).unwrap(); - assert_eq!( - sut.as_slice(), - &["c".into()], - "CapVec should accept new elements after clear()" + assert_eq!(sut.as_slice(), &["c".into()]); + } + + #[test] + fn test_shrink_to_fit_drops_buffer_when_empty() { + let mut sut = CapVec::::new(); + sut.extend([1, 2, 3]); + assert!(sut.buf.is_some()); + + sut.clear(); + assert!(sut.buf.is_some()); + + sut.shrink_to_fit(); + assert!(sut.buf.is_none()); + assert_eq!(sut.len(), 0); + assert_eq!(sut.max_capacity(), 4); + + sut.push(4).unwrap(); + assert_eq!(sut.as_slice(), &[4]); + assert!(sut.buf.is_some()); + } + + #[test] + fn test_shrink_to_fit_preserves_non_empty_buffer() { + let mut sut = CapVec::::new(); + sut.extend([1, 2]); + let ptr = sut.buf.as_ref().unwrap().as_ptr(); + let capacity = sut.capacity(); + let spare_capacity = sut.spare_capacity(); + + sut.shrink_to_fit(); + + assert_eq!(sut.as_slice(), &[1, 2]); + assert_eq!(sut.capacity(), capacity); + assert_eq!(sut.spare_capacity(), spare_capacity); + assert_eq!(sut.buf.as_ref().unwrap().as_ptr(), ptr); + } + + #[test] + fn test_shrink_to_fit_drops_empty_array_buffer() { + let mut sut = CapVec::from([(); 0]); + assert!(sut.buf.is_some()); + + sut.shrink_to_fit(); + + assert!(sut.buf.is_none()); + assert_eq!(sut.capacity(), 0); + assert_eq!(sut.max_capacity(), 0); + assert_eq!(sut.spare_capacity(), 0); + } + + #[test] + fn test_clear_resets_len_before_dropping_elements() { + struct PanicOnDrop { + alive: Rc>, + has_panicked: Rc>, + } + + impl PanicOnDrop { + fn new(alive: Rc>, has_panicked: Rc>) -> Self { + alive.set(alive.get() + 1); + Self { + alive, + has_panicked, + } + } + } + + impl Drop for PanicOnDrop { + fn drop(&mut self) { + self.alive.set(self.alive.get() - 1); + + if !self.has_panicked.replace(true) { + panic!("intentional drop panic"); + } + } + } + + let alive = Rc::new(Cell::new(0)); + let has_panicked = Rc::new(Cell::new(false)); + let mut sut = CapVec::<_, 3>::from([ + PanicOnDrop::new(Rc::clone(&alive), Rc::clone(&has_panicked)), + PanicOnDrop::new(Rc::clone(&alive), Rc::clone(&has_panicked)), + PanicOnDrop::new(Rc::clone(&alive), Rc::clone(&has_panicked)), + ]); + + let result = catch_unwind(AssertUnwindSafe(|| { + sut.clear(); + })); + + assert!(result.is_err()); + assert!(sut.is_empty()); + assert_eq!(alive.get(), 0); + + assert!( + sut.push(PanicOnDrop::new( + Rc::clone(&alive), + Rc::clone(&has_panicked), + )) + .is_ok() ); + + assert_eq!(sut.len(), 1); + assert_eq!(alive.get(), 1); } #[test] - fn test_as_slice() { + fn test_as_slice_returns_initialized_elements() { let mut sut = CapVec::::new(); sut.push(10).unwrap(); sut.push(20).unwrap(); sut.push(30).unwrap(); let slice = sut.as_slice(); - assert_eq!(slice, &[10, 20, 30], "Slice should match inserted elements"); + assert_eq!(slice, &[10, 20, 30]); } #[test] - fn test_as_mut_slice() { + fn test_as_mut_slice_allows_mutating_initialized_elements() { let mut sut = CapVec::::new(); sut.extend([1, 2, 3]); @@ -858,31 +1546,20 @@ mod tests { slice[2] = 30; } - assert_eq!( - sut.as_slice(), - &[10, 20, 30], - "Mutable slice should allow modifying elements" - ); + assert_eq!(sut.as_slice(), &[10, 20, 30]); - // Mutate using standard slice APIs sut.as_mut_slice().reverse(); - assert_eq!( - sut.as_slice(), - &[30, 20, 10], - "Reversal via as_mut_slice() should apply" - ); + assert_eq!(sut.as_slice(), &[30, 20, 10]); } - // ------------------------------------------------------------------------ - #[test] - fn test_partially_fill_empty_vec_zst() { + fn test_extend_partially_fills_empty_zst_vector() { let mut sut = CapVec::<(), 4>::new(); assert!(sut.is_empty()); assert_eq!(sut.len(), 0); assert_eq!(sut.as_slice(), []); - let mut leftover = sut.extend(core::iter::repeat(()).take(2)); + let mut leftover = sut.extend(core::iter::repeat_n((), 2)); assert_eq!(leftover.next(), None); assert!(!sut.is_empty()); @@ -891,7 +1568,7 @@ mod tests { } #[test] - fn test_fill_empty_vec_zst() { + fn test_extend_fills_empty_zst_vector() { let mut sut = CapVec::<(), 4>::new(); assert!(sut.is_empty()); assert_eq!(sut.len(), 0); @@ -906,20 +1583,36 @@ mod tests { } #[test] - fn test_extend_partially_filled_vec_zst() { + fn test_large_capacity_zst_does_not_need_stack_buffer() { + let mut sut = CapVec::<(), { usize::MAX }>::new(); + assert_eq!(sut.capacity(), 0); + assert_eq!(sut.max_capacity(), usize::MAX); + assert_eq!(sut.spare_capacity(), 0); + + sut.push(()).unwrap(); + + assert_eq!(sut.len(), 1); + assert_eq!(sut.capacity(), usize::MAX); + assert_eq!(sut.max_capacity(), usize::MAX); + assert_eq!(sut.spare_capacity(), usize::MAX - 1); + assert_eq!(sut.as_slice(), [()]); + } + + #[test] + fn test_extend_appends_to_partially_filled_zst_vector() { let mut sut = CapVec::<(), 8>::new(); assert!(sut.is_empty()); assert_eq!(sut.len(), 0); assert_eq!(sut.as_slice(), []); - let mut leftover = sut.extend(core::iter::repeat(()).take(3)); + let mut leftover = sut.extend(core::iter::repeat_n((), 3)); assert_eq!(leftover.next(), None); assert!(!sut.is_empty()); assert_eq!(sut.len(), 3); assert_eq!(sut.as_slice(), [(); 3]); - let mut leftover = sut.extend(core::iter::repeat(()).take(3)); + let mut leftover = sut.extend(core::iter::repeat_n((), 3)); assert_eq!(leftover.next(), None); assert!(!sut.is_empty()); @@ -935,7 +1628,7 @@ mod tests { } #[test] - fn test_extend_zero_capacity_vec() { + fn test_extend_zero_capacity_vector_returns_all_items() { let mut sut = CapVec::<(), 0>::new(); assert!(sut.is_empty()); assert_eq!(sut.len(), 0); @@ -948,46 +1641,149 @@ mod tests { assert_eq!(sut.as_slice(), []); } - // ------------------------------------------------------------------------ + #[test] + fn test_extend_preserves_initialized_elements_when_iterator_panics() { + struct PanicAfterTwo { + next_id: usize, + dropped: Rc>, + } + + impl Iterator for PanicAfterTwo { + type Item = DropCounter; + + fn next(&mut self) -> Option { + if self.next_id == 2 { + panic!("intentional extend iterator panic"); + } + + let item = drop_counter(self.next_id, &self.dropped); + self.next_id += 1; + Some(item) + } + } + + let dropped = Rc::new(Cell::new(0)); + let mut sut = CapVec::<_, 4>::new(); + + let result = catch_unwind(AssertUnwindSafe(|| { + sut.extend(PanicAfterTwo { + next_id: 0, + dropped: Rc::clone(&dropped), + }); + })); + + assert!(result.is_err()); + assert_eq!(dropped.get(), 0); + assert_eq!(sut.len(), 2); + assert_eq!( + sut.iter().map(|element| element.id).collect::>(), + [0, 1] + ); + + drop(sut); + assert_eq!(dropped.get(), 2); + } + + #[test] + fn test_push_and_insert_zero_capacity_vector_return_uninserted_items() { + let mut sut = CapVec::::new(); + + assert_eq!(sut.push(1), Err(1)); + assert_eq!(sut.insert(0, 2), Err(2)); + assert_eq!(sut.insert(usize::MAX, 3), Err(3)); + assert!(sut.is_empty()); + assert_eq!(sut.as_slice(), []); + } + + #[test] + fn test_zero_capacity_non_zst_array_supports_empty_iterators() { + let mut sut = CapVec::::from([]); + + assert_eq!(sut.capacity(), 0); + assert_eq!(sut.max_capacity(), 0); + assert!(sut.as_slice().is_empty()); + assert_eq!(sut.drain(..).count(), 0); + assert_eq!(CapVec::::from([]).into_iter().count(), 0); + } #[test] - fn test_insert() { + fn test_drop_tracked_values_survive_mixed_shifts_drains_and_consumption() { + let dropped = Rc::new(Cell::new(0)); + let mut sut = CapVec::<_, 6>::new(); + + assert!(sut.push(drop_counter(0, &dropped)).is_ok()); + assert!(sut.push(drop_counter(1, &dropped)).is_ok()); + assert!(sut.push(drop_counter(2, &dropped)).is_ok()); + assert!(sut.push(drop_counter(3, &dropped)).is_ok()); + assert!(sut.insert(2, drop_counter(4, &dropped)).is_ok()); + + let removed = sut.remove(1).unwrap(); + assert_eq!(removed.id, 1); + drop(removed); + assert_eq!(dropped.get(), 1); + + let mut drain = sut.drain(1..3); + let item = drain.next_back().unwrap(); + assert_eq!(item.id, 2); + drop(item); + assert_eq!(dropped.get(), 2); + + drop(drain); + assert_eq!(dropped.get(), 3); + assert_eq!( + sut.iter().map(|element| element.id).collect::>(), + [0, 3] + ); + + assert!(sut.push(drop_counter(5, &dropped)).is_ok()); + assert!(sut.insert(1, drop_counter(6, &dropped)).is_ok()); + + let ids = sut + .into_iter() + .map(|element| element.id) + .collect::>(); + assert_eq!(ids, [0, 6, 3, 5]); + assert_eq!(dropped.get(), 7); + } + + #[test] + fn test_insert_places_elements_at_requested_index() { let mut sut: CapVec = CapVec::new(); assert_eq!(sut.len(), 0); assert!(sut.is_empty()); - assert_eq!(Ok(()), sut.insert(0, 10)); - assert_eq!(sut.get(0), Some(&10)); + assert_eq!(sut.insert(0, 10), Ok(())); + assert_eq!(sut.first(), Some(&10)); assert_eq!(sut.len(), 1); - assert_eq!(Ok(()), sut.insert(1, 15)); - assert_eq!(sut.get(0), Some(&10)); + assert_eq!(sut.insert(1, 15), Ok(())); + assert_eq!(sut.first(), Some(&10)); assert_eq!(sut.get(1), Some(&15)); assert_eq!(sut.len(), 2); - assert_eq!(Ok(()), sut.insert(0, 5)); - assert_eq!(sut.get(0), Some(&5)); + assert_eq!(sut.insert(0, 5), Ok(())); + assert_eq!(sut.first(), Some(&5)); assert_eq!(sut.get(1), Some(&10)); assert_eq!(sut.get(2), Some(&15)); assert_eq!(sut.len(), 3); - assert_eq!(Ok(()), sut.insert(3, 20)); - assert_eq!(sut.get(0), Some(&5)); + assert_eq!(sut.insert(3, 20), Ok(())); + assert_eq!(sut.first(), Some(&5)); assert_eq!(sut.get(1), Some(&10)); assert_eq!(sut.get(2), Some(&15)); assert_eq!(sut.get(3), Some(&20)); assert_eq!(sut.len(), 4); - assert_eq!(Ok(()), sut.insert(2, 13)); - assert_eq!(sut.get(0), Some(&5)); + assert_eq!(sut.insert(2, 13), Ok(())); + assert_eq!(sut.first(), Some(&5)); assert_eq!(sut.get(1), Some(&10)); assert_eq!(sut.get(2), Some(&13)); assert_eq!(sut.get(3), Some(&15)); assert_eq!(sut.get(4), Some(&20)); assert_eq!(sut.len(), 5); - assert_eq!(Ok(()), sut.insert(4, 17)); - assert_eq!(sut.get(0), Some(&5)); + assert_eq!(sut.insert(4, 17), Ok(())); + assert_eq!(sut.first(), Some(&5)); assert_eq!(sut.get(1), Some(&10)); assert_eq!(sut.get(2), Some(&13)); assert_eq!(sut.get(3), Some(&15)); @@ -995,12 +1791,12 @@ mod tests { assert_eq!(sut.get(5), Some(&20)); assert_eq!(sut.len(), 6); - assert_eq!(Err(100), sut.insert(6, 100)); - assert_eq!(Err(100), sut.insert(usize::MAX, 100)); + assert_eq!(sut.insert(6, 100), Err(100)); + assert_eq!(sut.insert(usize::MAX, 100), Err(100)); } #[test] - fn test_push() { + fn test_push_appends_until_full() { let mut sut: CapVec = CapVec::new(); assert_eq!(sut.len(), 0); assert!(sut.is_empty()); @@ -1033,7 +1829,7 @@ mod tests { } #[test] - fn test_remove() { + fn test_remove_shifts_tail_left() { let mut sut: CapVec = CapVec::new(); assert_eq!(sut.len(), 0); assert!(sut.is_empty()); @@ -1048,7 +1844,7 @@ mod tests { assert_eq!(sut.remove(2), Some(2)); assert_eq!(sut.len(), 5); - assert_eq!(sut.get(0), Some(&0)); + assert_eq!(sut.first(), Some(&0)); assert_eq!(sut.get(1), Some(&1)); assert_eq!(sut.get(2), Some(&3)); assert_eq!(sut.get(3), Some(&4)); @@ -1056,25 +1852,25 @@ mod tests { assert_eq!(sut.remove(3), Some(4)); assert_eq!(sut.len(), 4); - assert_eq!(sut.get(0), Some(&0)); + assert_eq!(sut.first(), Some(&0)); assert_eq!(sut.get(1), Some(&1)); assert_eq!(sut.get(2), Some(&3)); assert_eq!(sut.get(3), Some(&5)); assert_eq!(sut.remove(1), Some(1)); assert_eq!(sut.len(), 3); - assert_eq!(sut.get(0), Some(&0)); + assert_eq!(sut.first(), Some(&0)); assert_eq!(sut.get(1), Some(&3)); assert_eq!(sut.get(2), Some(&5)); assert_eq!(sut.remove(0), Some(0)); assert_eq!(sut.len(), 2); - assert_eq!(sut.get(0), Some(&3)); + assert_eq!(sut.first(), Some(&3)); assert_eq!(sut.get(1), Some(&5)); assert_eq!(sut.remove(1), Some(5)); assert_eq!(sut.len(), 1); - assert_eq!(sut.get(0), Some(&3)); + assert_eq!(sut.first(), Some(&3)); assert_eq!(sut.remove(0), Some(3)); assert_eq!(sut.len(), 0); @@ -1083,7 +1879,93 @@ mod tests { } #[test] - fn test_pop() { + fn test_truncate_drops_tail_and_retains_buffer() { + let dropped = Rc::new(Cell::new(0)); + let mut sut = CapVec::<_, 5>::from([ + drop_counter(0, &dropped), + drop_counter(1, &dropped), + drop_counter(2, &dropped), + drop_counter(3, &dropped), + drop_counter(4, &dropped), + ]); + let capacity = sut.capacity(); + + sut.truncate(3); + + assert_eq!(dropped.get(), 2); + assert_eq!(sut.len(), 3); + assert_eq!(sut.capacity(), capacity); + assert_eq!( + sut.iter().map(|element| element.id).collect::>(), + [0, 1, 2] + ); + + sut.truncate(5); + assert_eq!(dropped.get(), 2); + assert_eq!(sut.len(), 3); + + sut.truncate(0); + assert_eq!(dropped.get(), 5); + assert!(sut.is_empty()); + assert_eq!(sut.capacity(), capacity); + } + + #[test] + fn test_truncate_empty_vector_is_noop() { + let mut sut = CapVec::::new(); + + sut.truncate(0); + sut.truncate(10); + + assert!(sut.is_empty()); + assert_eq!(sut.capacity(), 0); + } + + #[test] + fn test_swap_remove_replaces_item_with_last_element() { + let mut sut = CapVec::::from([10, 20, 30, 40, 50]); + + assert_eq!(sut.swap_remove(1), Some(20)); + assert_eq!(sut.as_slice(), &[10, 50, 30, 40]); + assert_eq!(sut.len(), 4); + + assert_eq!(sut.swap_remove(3), Some(40)); + assert_eq!(sut.as_slice(), &[10, 50, 30]); + assert_eq!(sut.len(), 3); + + assert_eq!(sut.swap_remove(3), None); + assert_eq!(sut.swap_remove(usize::MAX), None); + assert_eq!(sut.as_slice(), &[10, 50, 30]); + } + + #[test] + fn test_swap_remove_drops_only_removed_value() { + let dropped = Rc::new(Cell::new(0)); + let mut sut = CapVec::<_, 4>::from([ + drop_counter(0, &dropped), + drop_counter(1, &dropped), + drop_counter(2, &dropped), + drop_counter(3, &dropped), + ]); + + let removed = sut.swap_remove(1).unwrap(); + + assert_eq!(dropped.get(), 0); + assert_eq!(removed.id, 1); + assert_eq!( + sut.iter().map(|element| element.id).collect::>(), + [0, 3, 2] + ); + + drop(removed); + assert_eq!(dropped.get(), 1); + + drop(sut); + assert_eq!(dropped.get(), 4); + } + + #[test] + fn test_pop_removes_elements_from_back() { let mut sut: CapVec = CapVec::new(); let mut leftover = sut.extend(0..5); @@ -1102,7 +1984,7 @@ mod tests { } #[test] - fn test_mutability() { + fn test_index_mut_allows_mutating_elements() { let mut sut: CapVec = CapVec::new(); let mut leftover = sut.extend(0..5); @@ -1123,15 +2005,15 @@ mod tests { } #[test] - fn test_get_on_empty_vec() { + fn test_get_on_empty_vector_returns_none() { let sut: CapVec = CapVec::new(); assert_eq!(sut.len(), 0); assert!(sut.is_empty()); - assert_eq!(sut.get(0), None); + assert_eq!(<[i64]>::get(&sut, 0), None); } #[test] - fn test_get_mut_on_empty_vec() { + fn test_get_mut_on_empty_vector_returns_none() { let mut sut: CapVec = CapVec::new(); assert_eq!(sut.len(), 0); assert!(sut.is_empty()); @@ -1139,7 +2021,7 @@ mod tests { } #[test] - fn test_comparison() { + fn test_comparison_matches_slice_behavior() { let sut: CapVec = CapVec::from([0, 1, 2, 3]); assert_eq!(sut, sut); @@ -1170,7 +2052,31 @@ mod tests { } #[test] - fn test_drain_partially() { + fn test_debug_ordering_and_hash_match_slice_behavior() { + let sut = CapVec::from([1, 2, 3]); + let lesser = CapVec::from([1, 2, 2]); + let greater = CapVec::from([1, 2, 4]); + + assert_eq!(format!("{sut:?}"), "[1, 2, 3]"); + assert!(lesser < sut); + assert!(greater > sut); + assert_eq!( + sut.partial_cmp(&[1, 2, 3]), + Some(core::cmp::Ordering::Equal) + ); + assert_eq!(sut.partial_cmp(&[1, 2, 4]), Some(core::cmp::Ordering::Less)); + + let mut cap_vec_hasher = DefaultHasher::new(); + sut.hash(&mut cap_vec_hasher); + + let mut slice_hasher = DefaultHasher::new(); + [1, 2, 3].as_slice().hash(&mut slice_hasher); + + assert_eq!(cap_vec_hasher.finish(), slice_hasher.finish()); + } + + #[test] + fn test_drain_partial_range_removes_items() { let mut sut = CapVec::<_, 8>::new(); let mut leftover = sut.extend((0..8).map(Box::new)); assert_eq!(leftover.next(), None); @@ -1188,7 +2094,7 @@ mod tests { } #[test] - fn test_drain_from_both_ends() { + fn test_drain_yields_items_from_both_ends() { let mut sut = CapVec::<_, 8>::new(); let mut leftover = sut.extend((0..8).map(Box::new)); assert_eq!(leftover.next(), None); @@ -1208,7 +2114,260 @@ mod tests { } #[test] - fn test_drain_all() { + fn test_drain_empty_range_preserves_vector() { + let mut sut = CapVec::from([1, 2, 3, 4]); + + let drained = sut.drain(2..2).collect::>(); + + assert_eq!(drained, []); + assert_eq!(sut.as_slice(), &[1, 2, 3, 4]); + assert_eq!(sut.len(), 4); + } + + #[test] + fn test_drain_tail_range_removes_items() { + let mut sut = CapVec::from([1, 2, 3, 4]); + + let drained = sut.drain(2..4).collect::>(); + + assert_eq!(drained, [3, 4]); + assert_eq!(sut.as_slice(), &[1, 2]); + assert_eq!(sut.len(), 2); + } + + #[test] + fn test_drain_accepts_all_range_bounds() { + use core::ops::Bound::{Excluded, Included}; + + let mut sut = CapVec::from([1, 2, 3, 4]); + assert_eq!(sut.drain(..).collect::>(), [1, 2, 3, 4]); + assert_eq!(sut.as_slice(), &[]); + + let mut sut = CapVec::from([1, 2, 3, 4]); + assert_eq!(sut.drain(2..).collect::>(), [3, 4]); + assert_eq!(sut.as_slice(), &[1, 2]); + + let mut sut = CapVec::from([1, 2, 3, 4]); + assert_eq!(sut.drain(..2).collect::>(), [1, 2]); + assert_eq!(sut.as_slice(), &[3, 4]); + + let mut sut = CapVec::from([1, 2, 3, 4]); + assert_eq!(sut.drain(1..=2).collect::>(), [2, 3]); + assert_eq!(sut.as_slice(), &[1, 4]); + + let mut sut = CapVec::from([1, 2, 3, 4]); + assert_eq!( + sut.drain((Excluded(0), Included(2))).collect::>(), + [2, 3] + ); + assert_eq!(sut.as_slice(), &[1, 4]); + } + + #[test] + fn test_drain_as_slice_tracks_remaining_items() { + let mut sut = CapVec::from([1, 2, 3, 4, 5]); + let mut drain = sut.drain(1..4); + + assert_eq!(drain.as_slice(), &[2, 3, 4]); + assert_eq!(drain.next(), Some(2)); + assert_eq!(drain.as_slice(), &[3, 4]); + assert_eq!(drain.next_back(), Some(4)); + assert_eq!(drain.as_slice(), &[3]); + } + + #[test] + fn test_drain_debug_formats_remaining_items() { + let mut sut = CapVec::from([1, 2, 3, 4]); + let mut drain = sut.drain(1..); + + assert_eq!(format!("{drain:?}"), "Drain([2, 3, 4])"); + assert_eq!(drain.next(), Some(2)); + assert_eq!(format!("{drain:?}"), "Drain([3, 4])"); + drain.for_each(drop); + } + + #[test] + fn test_drain_debug_formats_empty_iterator() { + let mut sut = CapVec::from([1, 2, 3, 4]); + let drain = sut.drain(2..2); + + assert_eq!(format!("{drain:?}"), "Drain([])"); + } + + #[test] + fn test_drain_nth_drops_skipped_items() { + let dropped = Rc::new(Cell::new(0)); + let mut base = CapVec::<_, 5>::from([ + drop_counter(0, &dropped), + drop_counter(1, &dropped), + drop_counter(2, &dropped), + drop_counter(3, &dropped), + drop_counter(4, &dropped), + ]); + + let mut drain = base.drain(1..4); + let item = drain.nth(1).unwrap(); + + assert_eq!(dropped.get(), 1); + assert_eq!(item.id, 2); + + drop(item); + assert_eq!(dropped.get(), 2); + + drop(drain); + assert_eq!(dropped.get(), 3); + assert_eq!( + base.iter().map(|element| element.id).collect::>(), + [0, 4] + ); + } + + #[test] + fn test_drain_nth_back_drops_skipped_items() { + let dropped = Rc::new(Cell::new(0)); + let mut base = CapVec::<_, 5>::from([ + drop_counter(0, &dropped), + drop_counter(1, &dropped), + drop_counter(2, &dropped), + drop_counter(3, &dropped), + drop_counter(4, &dropped), + ]); + + let mut drain = base.drain(1..4); + let item = drain.nth_back(1).unwrap(); + + assert_eq!(dropped.get(), 1); + assert_eq!(item.id, 2); + + drop(item); + assert_eq!(dropped.get(), 2); + + drop(drain); + assert_eq!(dropped.get(), 3); + assert_eq!( + base.iter().map(|element| element.id).collect::>(), + [0, 4] + ); + } + + #[test] + fn test_drain_last_drops_skipped_items_and_restores_tail() { + let dropped = Rc::new(Cell::new(0)); + let mut base = CapVec::<_, 5>::from([ + drop_counter(0, &dropped), + drop_counter(1, &dropped), + drop_counter(2, &dropped), + drop_counter(3, &dropped), + drop_counter(4, &dropped), + ]); + + let item = base.drain(1..4).last().unwrap(); + + assert_eq!(dropped.get(), 2); + assert_eq!(item.id, 3); + assert_eq!( + base.iter().map(|element| element.id).collect::>(), + [0, 4] + ); + + drop(item); + assert_eq!(dropped.get(), 3); + + drop(base); + assert_eq!(dropped.get(), 5); + } + + #[test] + fn test_drain_count_drops_remaining_items_and_restores_tail() { + let dropped = Rc::new(Cell::new(0)); + let mut base = CapVec::<_, 5>::from([ + drop_counter(0, &dropped), + drop_counter(1, &dropped), + drop_counter(2, &dropped), + drop_counter(3, &dropped), + drop_counter(4, &dropped), + ]); + + assert_eq!(base.drain(1..4).count(), 3); + assert_eq!(dropped.get(), 3); + assert_eq!( + base.iter().map(|element| element.id).collect::>(), + [0, 4] + ); + + drop(base); + assert_eq!(dropped.get(), 5); + } + + #[test] + #[should_panic] + fn test_drain_panics_when_range_start_is_after_end() { + let mut sut = CapVec::from([1, 2, 3, 4]); + let start = sut.len() - 1; + let end = start - 1; + sut.drain(start..end); + } + + #[test] + #[should_panic] + fn test_drain_panics_when_range_end_exceeds_len() { + let mut sut = CapVec::from([1, 2, 3, 4]); + sut.drain(0..5); + } + + #[test] + fn test_drain_restores_tail_when_dropping_drained_element_panics() { + struct PanicOnDrop { + id: usize, + alive: Rc>, + has_panicked: Rc>, + } + + impl PanicOnDrop { + fn new(id: usize, alive: Rc>, has_panicked: Rc>) -> Self { + alive.set(alive.get() + 1); + Self { + id, + alive, + has_panicked, + } + } + } + + impl Drop for PanicOnDrop { + fn drop(&mut self) { + self.alive.set(self.alive.get() - 1); + + if self.id == 1 && !self.has_panicked.replace(true) { + panic!("intentional drain drop panic"); + } + } + } + + let alive = Rc::new(Cell::new(0)); + let has_panicked = Rc::new(Cell::new(false)); + let mut sut = CapVec::<_, 4>::from([ + PanicOnDrop::new(0, Rc::clone(&alive), Rc::clone(&has_panicked)), + PanicOnDrop::new(1, Rc::clone(&alive), Rc::clone(&has_panicked)), + PanicOnDrop::new(2, Rc::clone(&alive), Rc::clone(&has_panicked)), + PanicOnDrop::new(3, Rc::clone(&alive), Rc::clone(&has_panicked)), + ]); + + let result = catch_unwind(AssertUnwindSafe(|| { + drop(sut.drain(1..3)); + })); + + assert!(result.is_err()); + assert_eq!(sut.len(), 2); + assert_eq!( + sut.iter().map(|element| element.id).collect::>(), + [0, 3] + ); + assert_eq!(alive.get(), 2); + } + + #[test] + fn test_drain_full_range_removes_all_items() { let mut sut = CapVec::<_, 8>::new(); let mut leftover = sut.extend((0..8).map(Box::new)); assert_eq!(leftover.next(), None); @@ -1229,7 +2388,7 @@ mod tests { } #[test] - fn test_drain_partially_zst() { + fn test_drain_partial_range_zst_removes_items() { let mut sut = CapVec::<_, 8>::new(); let mut leftover = sut.extend([(); 8]); assert_eq!(leftover.next(), None); @@ -1247,7 +2406,7 @@ mod tests { } #[test] - fn test_drain_all_zst() { + fn test_drain_full_range_zst_removes_all_items() { let mut sut = CapVec::<_, 8>::new(); let mut leftover = sut.extend([(); 8]); assert_eq!(leftover.next(), None); @@ -1265,24 +2424,16 @@ mod tests { } #[test] - fn test_iter_empty() { + fn test_iter_empty_vector_yields_none() { let base = CapVec::::new(); let mut sut = base.iter(); - assert_eq!(sut.len(), 0, "Iterator over empty CapVec should have len 0"); - assert_eq!( - sut.next(), - None, - "Iterator over empty CapVec should return None" - ); - assert_eq!( - sut.clone().count(), - 0, - "Cloned iterator should also be empty" - ); + assert_eq!(sut.len(), 0); + assert_eq!(sut.next(), None); + assert_eq!(sut.clone().count(), 0); } #[test] - fn test_iter_basic_forward_iteration() { + fn test_iter_forward_iteration() { let mut base = CapVec::::new(); base.extend([10, 20, 30]); @@ -1294,7 +2445,7 @@ mod tests { } #[test] - fn test_iter_clone() { + fn test_iter_clone_preserves_position() { let base = CapVec::::from([1, 2, 3]); let mut sut1 = base.iter(); let mut sut2 = sut1.clone(); @@ -1309,19 +2460,12 @@ mod tests { } #[test] - fn test_iter_collect() { + fn test_iter_collects_items_in_order() { let base = CapVec::::from([5, 6, 7, 8]); let collected: Vec<_> = base.iter().copied().collect(); - assert_eq!( - collected, - [5, 6, 7, 8], - "Iter should produce same items as Vec" - ); + assert_eq!(collected, [5, 6, 7, 8]); - assert!( - base.iter().eq([5, 6, 7, 8].iter()), - "CapVec::iter must produce same sequence as Vec::iter" - ); + assert!(base.iter().eq([5, 6, 7, 8].iter())); } #[test] @@ -1338,26 +2482,18 @@ mod tests { } #[test] - fn test_iter_fused_behaviour() { + fn test_iter_fused_behavior() { let base = CapVec::::from([9, 8, 7]); let mut sut = base.iter(); for _ in 0..3 { sut.next(); } - assert_eq!( - sut.next(), - None, - "Iterator should yield None after exhaustion" - ); - assert_eq!( - sut.next(), - None, - "Iterator should stay fused (repeated None)" - ); + assert_eq!(sut.next(), None); + assert_eq!(sut.next(), None); } #[test] - fn test_iter_len_consistency() { + fn test_iter_len_tracks_remaining_items() { let base = CapVec::::from([10, 20, 30, 40]); let mut sut = base.iter(); assert_eq!(sut.len(), 4); @@ -1372,35 +2508,23 @@ mod tests { } #[test] - fn test_iter_mut_empty() { + fn test_iter_mut_empty_vector_yields_none() { let mut base = CapVec::::new(); let mut sut = base.iter_mut(); - assert_eq!(sut.len(), 0, "Iterator over empty CapVec should have len 0"); - assert_eq!( - sut.next(), - None, - "Iterator over empty CapVec should yield None" - ); - assert_eq!( - sut.next_back(), - None, - "Double-ended iterator should yield None on empty CapVec" - ); + assert_eq!(sut.len(), 0); + assert_eq!(sut.next(), None); + assert_eq!(sut.next_back(), None); } #[test] - fn test_iter_mut_forward_iteration_and_mutation() { + fn test_iter_mut_forward_iteration_allows_mutation() { let mut base = CapVec::::new(); base.extend([1, 2, 3]); for x in base.iter_mut() { *x *= 10; } - assert_eq!( - base.as_slice(), - &[10, 20, 30], - "Mutable iterator should allow modifying items in order" - ); + assert_eq!(base.as_slice(), &[10, 20, 30]); } #[test] @@ -1416,22 +2540,18 @@ mod tests { } #[test] - fn test_iter_mut_backward_iteration_and_mutation() { + fn test_iter_mut_backward_iteration_allows_mutation() { let mut base = CapVec::::new(); base.extend([1, 2, 3]); for x in base.iter_mut().rev() { *x *= 10; } - assert_eq!( - base.as_slice(), - &[10, 20, 30], - "Mutable iterator should allow modifying items in order" - ); + assert_eq!(base.as_slice(), &[10, 20, 30]); } #[test] - fn test_iter_mut_double_ended_iteration_and_mutation() { + fn test_iter_mut_double_ended_iteration_allows_mutation() { let mut base = CapVec::::from([1, 2, 3, 4]); let mut sut = base.iter_mut(); @@ -1443,15 +2563,11 @@ mod tests { *back *= 3; } - assert_eq!( - base.as_slice(), - &[2, 2, 3, 12], - "Mutations through iter_mut should persist" - ); + assert_eq!(base.as_slice(), &[2, 2, 3, 12]); } #[test] - fn test_iter_mut_vs_vec_iter_mut() { + fn test_iter_mut_matches_slice_iter_mut() { let mut base = CapVec::::from([5, 6, 7, 8]); let mut v = [5, 6, 7, 8].to_vec(); @@ -1460,15 +2576,11 @@ mod tests { *w += 1; } - assert_eq!( - base.as_slice(), - v.as_slice(), - "CapVec::iter_mut should behave like Vec::iter_mut" - ); + assert_eq!(base.as_slice(), v.as_slice()); } #[test] - fn test_iter_mut_len_consistency() { + fn test_iter_mut_len_tracks_remaining_items() { let mut base = CapVec::::from([10, 20, 30, 40]); let mut sut = base.iter_mut(); assert_eq!(sut.len(), 4); @@ -1491,39 +2603,22 @@ mod tests { sut.next(); } - assert_eq!( - sut.next(), - None, - "Iterator should yield None after exhaustion" - ); - - assert_eq!( - sut.next(), - None, - "Iterator should stay fused (repeated None)" - ); + assert_eq!(sut.next(), None); + assert_eq!(sut.next(), None); } #[test] - fn test_into_iter_empty() { + fn test_into_iter_empty_vector_yields_none() { let base = CapVec::::new(); let mut sut = base.into_iter(); - assert_eq!(sut.len(), 0, "Iterator from empty CapVec should have len 0"); - assert_eq!( - sut.next(), - None, - "Iterator from empty CapVec should yield None" - ); - assert_eq!( - sut.next_back(), - None, - "Double-ended iterator should yield None on empty CapVec" - ); + assert_eq!(sut.len(), 0); + assert_eq!(sut.next(), None); + assert_eq!(sut.next_back(), None); } #[test] - fn test_into_iter_basic_forward_iteration() { + fn test_into_iter_forward_iteration() { let mut base = CapVec::::new(); base.extend([1, 2, 3]); @@ -1548,30 +2643,219 @@ mod tests { } #[test] - fn test_into_iter_collect() { + fn test_into_iter_next_back_on_partially_filled_vector() { + let mut base = CapVec::::new(); + base.extend([1, 2, 3]); + + let mut sut = base.into_iter(); + assert_eq!(sut.len(), 3); + assert_eq!(sut.next_back(), Some(3)); + assert_eq!(sut.next_back(), Some(2)); + assert_eq!(sut.next_back(), Some(1)); + assert_eq!(sut.next_back(), None); + } + + #[test] + fn test_into_iter_as_slice_tracks_remaining_items() { + let base = CapVec::from([1, 2, 3, 4]); + let mut sut = base.into_iter(); + + assert_eq!(sut.as_slice(), &[1, 2, 3, 4]); + assert_eq!(sut.next(), Some(1)); + assert_eq!(sut.as_slice(), &[2, 3, 4]); + assert_eq!(sut.next_back(), Some(4)); + assert_eq!(sut.as_slice(), &[2, 3]); + } + + #[test] + fn test_into_iter_as_mut_slice_allows_mutating_remaining_items() { + let base = CapVec::from([1, 2, 3, 4]); + let mut sut = base.into_iter(); + + assert_eq!(sut.next(), Some(1)); + sut.as_mut_slice() + .iter_mut() + .for_each(|element| *element *= 10); + + assert_eq!(sut.collect::>(), [20, 30, 40]); + } + + #[test] + fn test_into_iter_clone_preserves_remaining_items() { + let base = CapVec::from([1, 2, 3, 4]); + let mut sut = base.into_iter(); + assert_eq!(sut.next(), Some(1)); + assert_eq!(sut.next_back(), Some(4)); + + let clone = sut.clone(); + + assert_eq!(sut.collect::>(), [2, 3]); + assert_eq!(clone.collect::>(), [2, 3]); + } + + #[test] + fn test_into_iter_clone_drops_initialized_elements_when_later_clone_panics() { + struct PanicOnSecondClone { + alive: Rc>, + clone_calls: Rc>, + } + + impl PanicOnSecondClone { + fn new(alive: Rc>, clone_calls: Rc>) -> Self { + alive.set(alive.get() + 1); + Self { alive, clone_calls } + } + } + + impl Clone for PanicOnSecondClone { + fn clone(&self) -> Self { + let clone_calls = self.clone_calls.get() + 1; + self.clone_calls.set(clone_calls); + + if clone_calls == 2 { + panic!("intentional into_iter clone panic"); + } + + self.alive.set(self.alive.get() + 1); + Self { + alive: Rc::clone(&self.alive), + clone_calls: Rc::clone(&self.clone_calls), + } + } + } + + impl Drop for PanicOnSecondClone { + fn drop(&mut self) { + self.alive.set(self.alive.get() - 1); + } + } + + let alive = Rc::new(Cell::new(0)); + let clone_calls = Rc::new(Cell::new(0)); + let base = CapVec::<_, 2>::from([ + PanicOnSecondClone::new(Rc::clone(&alive), Rc::clone(&clone_calls)), + PanicOnSecondClone::new(Rc::clone(&alive), Rc::clone(&clone_calls)), + ]) + .into_iter(); + + let result = catch_unwind(AssertUnwindSafe(|| { + let _ = base.clone(); + })); + + assert!(result.is_err()); + assert_eq!(alive.get(), 2); + } + + #[test] + fn test_into_iter_debug_formats_empty_and_remaining_items() { + let base = CapVec::from([1, 2, 3, 4]); + let mut sut = base.into_iter(); + + assert_eq!(format!("{sut:?}"), "IntoIter([1, 2, 3, 4])"); + assert_eq!(sut.next(), Some(1)); + assert_eq!(sut.next_back(), Some(4)); + assert_eq!(format!("{sut:?}"), "IntoIter([2, 3])"); + + while sut.next().is_some() {} + assert_eq!(format!("{sut:?}"), "IntoIter([])"); + } + + #[test] + fn test_into_iter_nth_drops_skipped_items() { + let dropped = Rc::new(Cell::new(0)); + let base = CapVec::<_, 4>::from([ + drop_counter(0, &dropped), + drop_counter(1, &dropped), + drop_counter(2, &dropped), + drop_counter(3, &dropped), + ]); + + let mut sut = base.into_iter(); + let item = sut.nth(2).unwrap(); + + assert_eq!(dropped.get(), 2); + assert_eq!(item.id, 2); + + drop(item); + assert_eq!(dropped.get(), 3); + + drop(sut); + assert_eq!(dropped.get(), 4); + } + + #[test] + fn test_into_iter_nth_back_drops_skipped_items() { + let dropped = Rc::new(Cell::new(0)); + let base = CapVec::<_, 4>::from([ + drop_counter(0, &dropped), + drop_counter(1, &dropped), + drop_counter(2, &dropped), + drop_counter(3, &dropped), + ]); + + let mut sut = base.into_iter(); + let item = sut.nth_back(2).unwrap(); + + assert_eq!(dropped.get(), 2); + assert_eq!(item.id, 1); + + drop(item); + assert_eq!(dropped.get(), 3); + + drop(sut); + assert_eq!(dropped.get(), 4); + } + + #[test] + fn test_into_iter_last_drops_skipped_items() { + let dropped = Rc::new(Cell::new(0)); + let base = CapVec::<_, 4>::from([ + drop_counter(0, &dropped), + drop_counter(1, &dropped), + drop_counter(2, &dropped), + drop_counter(3, &dropped), + ]); + + let item = base.into_iter().last().unwrap(); + + assert_eq!(dropped.get(), 3); + assert_eq!(item.id, 3); + + drop(item); + assert_eq!(dropped.get(), 4); + } + + #[test] + fn test_into_iter_count_drops_remaining_items() { + let dropped = Rc::new(Cell::new(0)); + let base = CapVec::<_, 4>::from([ + drop_counter(0, &dropped), + drop_counter(1, &dropped), + drop_counter(2, &dropped), + drop_counter(3, &dropped), + ]); + + assert_eq!(base.into_iter().count(), 4); + assert_eq!(dropped.get(), 4); + } + + #[test] + fn test_into_iter_collects_items_in_order() { let base = CapVec::::from([5, 6, 7, 8]); let collected: Vec<_> = base.into_iter().collect(); - assert_eq!( - collected, - [5, 6, 7, 8], - "into_iter() should produce same items as Vec" - ); + assert_eq!(collected, [5, 6, 7, 8]); } #[test] - fn test_into_iter_vs_vec_into_iter() { + fn test_into_iter_matches_vec_into_iter() { let base = CapVec::::from([1, 2, 3, 4]); let v = [1, 2, 3, 4].to_vec(); - assert_eq!( - base.into_iter().collect::>(), - v, - "CapVec::into_iter() should yield identical results to Vec::into_iter()" - ); + assert_eq!(base.into_iter().collect::>(), v); } #[test] - fn test_into_iter_len_consistency() { + fn test_into_iter_len_tracks_remaining_items() { let base = CapVec::::from([10, 20, 30, 40]); let mut sut = base.into_iter(); assert_eq!(sut.len(), 4); @@ -1594,21 +2878,12 @@ mod tests { sut.next(); } - assert_eq!( - sut.next(), - None, - "Iterator should yield None after exhaustion" - ); - - assert_eq!( - sut.next(), - None, - "Iterator should remain fused (repeated None)" - ); + assert_eq!(sut.next(), None); + assert_eq!(sut.next(), None); } #[test] - fn test_into_iter_double_ended_len_updates() { + fn test_into_iter_double_ended_iteration_updates_len() { let base = CapVec::::from([1, 2, 3, 4, 5]); let mut sut = base.into_iter(); @@ -1627,19 +2902,63 @@ mod tests { } #[test] - fn test_into_iter_drop() { + fn test_into_iter_drop_drops_remaining_elements() { let base = CapVec::, 4>::from(["a", "b", "c", "d"].map(Box::from)); let mut sut = base.into_iter(); assert_eq!(sut.next().as_deref(), Some("a")); assert_eq!(sut.next_back().as_deref(), Some("d")); - // ensure memory gets release correctly core::mem::drop(sut); } #[test] - fn test_ref_into_iter_equals_iter() { + fn test_into_iter_drops_remaining_elements_when_drop_panics() { + struct PanicOnDrop { + id: usize, + alive: Rc>, + has_panicked: Rc>, + } + + impl PanicOnDrop { + fn new(id: usize, alive: Rc>, has_panicked: Rc>) -> Self { + alive.set(alive.get() + 1); + Self { + id, + alive, + has_panicked, + } + } + } + + impl Drop for PanicOnDrop { + fn drop(&mut self) { + self.alive.set(self.alive.get() - 1); + + if self.id == 0 && !self.has_panicked.replace(true) { + panic!("intentional into_iter drop panic"); + } + } + } + + let alive = Rc::new(Cell::new(0)); + let has_panicked = Rc::new(Cell::new(false)); + let base = CapVec::<_, 3>::from([ + PanicOnDrop::new(0, Rc::clone(&alive), Rc::clone(&has_panicked)), + PanicOnDrop::new(1, Rc::clone(&alive), Rc::clone(&has_panicked)), + PanicOnDrop::new(2, Rc::clone(&alive), Rc::clone(&has_panicked)), + ]); + + let result = catch_unwind(AssertUnwindSafe(|| { + drop(base.into_iter()); + })); + + assert!(result.is_err()); + assert_eq!(alive.get(), 0); + } + + #[test] + fn test_ref_into_iter_matches_iter() { let base = CapVec::from([0, 1, 2, 3]); assert!(base.iter().eq(&base)); @@ -1654,7 +2973,7 @@ mod tests { } #[test] - fn test_ref_mut_into_iter_equals_iter_mut() { + fn test_ref_mut_into_iter_matches_iter_mut() { let mut base = CapVec::from([0, 1, 2, 3]); assert!([0, 1, 2, 3].iter_mut().eq(&mut base));