diff --git a/.gitattributes b/.gitattributes index 4fcfd915dc42..c47df436e3d9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,3 +2,6 @@ *.zip -text diff Cargo.lock linguist-generated=false pixi.lock linguist-generated=true + +# git-lfs stuff +**/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/reusable_checks_rust.yml b/.github/workflows/reusable_checks_rust.yml index 0c0a916388f9..edaa097768d1 100644 --- a/.github/workflows/reusable_checks_rust.yml +++ b/.github/workflows/reusable_checks_rust.yml @@ -79,18 +79,22 @@ jobs: # Run some basics tests on Mac and Windows mac-windows-tests: - name: Test on macOS and Windows - if: ${{ inputs.CHANNEL == 'nightly' }} + name: Test on ${{ matrix.name }} strategy: matrix: include: - - os: macos-latest - name: macos - - os: windows-latest-8-cores - name: windows + # TODO(#8245): we run mac tests on `main` because that's the only platform where UI snapshot tests are covered. + # When the linux runners are able to run these tests (with a software renderer), we can move that back to all nightly. + - os: ${{ inputs.CHANNEL == 'main' && 'macos-latest' || 'windows-latest-8-cores' }} + name: ${{ inputs.CHANNEL == 'main' && 'macos' || 'windows' }} + + # Note: we can't use `matrix.os` here because its evaluated before the matrix stuff. + if: ${{ inputs.CHANNEL == 'main' || inputs.CHANNEL == 'nightly' }} runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 + with: + lfs: true - name: Set up Rust uses: ./.github/actions/setup-rust diff --git a/.gitignore b/.gitignore index 44cbc46e9872..43b4208ba656 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,8 @@ wheels # Screenshot comparison build /compare_screenshot +**/tests/snapshots/**/*.diff.png +**/tests/snapshots/**/*.new.png *.rrd diff --git a/BUILD.md b/BUILD.md index 814d9b91a29c..24aa759a8fe3 100644 --- a/BUILD.md +++ b/BUILD.md @@ -31,6 +31,21 @@ If you are using an Apple-silicon Mac (M1, M2), make sure `rustc -vV` outputs `h rustup set default-host aarch64-apple-darwin && rustup install 1.80.0 ``` +## Git-lfs + +We use [git-lfs](https://git-lfs.com/) to store big files in the repository, such as UI test snapshots. +We aim to keep this project buildable without the need of git-lfs (for example, icons and similar assets are checked in to the repo as regular files). +However, git-lfs is generally required for a proper development environment, e.g. to run tests. + +### Setting up git-lfs + +The TL;DR is to install git-lfs via your favorite package manager (`apt`, Homebrew, MacPorts, etc.) and run `git lfs install`. +See the many resources available online more details. + +You can ensure that everything is correctly installed by running `git lfs ls-files` from the repository root. +It should list some test snapshot files. + + ## Validating your environment You can validate your environment is set up correctly by running: ```sh diff --git a/Cargo.lock b/Cargo.lock index 070f3b33cee4..5173a619f4b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,9 +20,9 @@ checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" [[package]] name = "accesskit" -version = "0.16.3" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99b76d84ee70e30a4a7e39ab9018e2b17a6a09e31084176cc7c0b2dec036ba45" +checksum = "d3d3b8f9bae46a948369bc4a03e815d4ed6d616bd00de4051133a5019dc31c5a" dependencies = [ "enumn", "serde", @@ -30,12 +30,12 @@ dependencies = [ [[package]] name = "accesskit_atspi_common" -version = "0.9.3" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5393c75d4666f580f4cac0a968bc97c36076bb536a129f28210dac54ee127ed" +checksum = "7c5dd55e6e94949498698daf4d48fb5659e824d7abec0d394089656ceaf99d4f" dependencies = [ "accesskit", - "accesskit_consumer", + "accesskit_consumer 0.26.0", "atspi-common", "serde", "thiserror", @@ -44,33 +44,44 @@ dependencies = [ [[package]] name = "accesskit_consumer" -version = "0.24.3" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa3a17950ce0d911f132387777b9b3d05eddafb59b773ccaa53fceefaeb0228e" +dependencies = [ + "accesskit", + "immutable-chunkmap", +] + +[[package]] +name = "accesskit_consumer" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a12dc159d52233c43d9fe5415969433cbdd52c3d6e0df51bda7d447427b9986" +checksum = "f47983a1084940ba9a39c077a8c63e55c619388be5476ac04c804cfbd1e63459" dependencies = [ "accesskit", + "hashbrown 0.15.0", "immutable-chunkmap", ] [[package]] name = "accesskit_macos" -version = "0.17.4" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfc6c1ecd82053d127961ad80a8beaa6004fb851a3a5b96506d7a6bd462403f6" +checksum = "7329821f3bd1101e03a7d2e03bd339e3ac0dc64c70b4c9f9ae1949e3ba8dece1" dependencies = [ "accesskit", - "accesskit_consumer", + "accesskit_consumer 0.26.0", + "hashbrown 0.15.0", "objc2", "objc2-app-kit", "objc2-foundation", - "once_cell", ] [[package]] name = "accesskit_unix" -version = "0.12.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be7f5cf6165be10a54b2655fa2e0e12b2509f38ed6fc43e11c31fdb7ee6230bb" +checksum = "fcee751cc20d88678c33edaf9c07e8b693cd02819fe89053776f5313492273f5" dependencies = [ "accesskit", "accesskit_atspi_common", @@ -86,12 +97,13 @@ dependencies = [ [[package]] name = "accesskit_windows" -version = "0.23.2" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "974e96c347384d9133427167fb8a58c340cb0496988dacceebdc1ed27071023b" +checksum = "24fcd5d23d70670992b823e735e859374d694a3d12bfd8dd32bd3bd8bedb5d81" dependencies = [ "accesskit", - "accesskit_consumer", + "accesskit_consumer 0.26.0", + "hashbrown 0.15.0", "paste", "static_assertions", "windows 0.58.0", @@ -100,9 +112,9 @@ dependencies = [ [[package]] name = "accesskit_winit" -version = "0.22.4" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aea3522719f1c44564d03e9469a8e2f3a98b3a8a880bd66d0789c6b9c4a669dd" +checksum = "6a6a48dad5530b6deb9fc7a52cc6c3bf72cdd9eb8157ac9d32d69f2427a5e879" dependencies = [ "accesskit", "accesskit_macos", @@ -1248,6 +1260,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + [[package]] name = "chrono" version = "0.4.38" @@ -1383,6 +1404,16 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + [[package]] name = "combine" version = "4.6.7" @@ -1782,6 +1813,19 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edf215dbb8cb1409cca7645aaed35f9e39fb0a21855bba1ac48bc0334903bf66" +[[package]] +name = "dify" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11217d469eafa3b809ad84651eb9797ccbb440b4a916d5d85cb1b994e89787f6" +dependencies = [ + "anyhow", + "colored", + "getopts", + "image", + "rayon", +] + [[package]] name = "digest" version = "0.10.7" @@ -1873,7 +1917,7 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "ecolor" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=83a30064f4812d0029532675a5f2bf38c257ad0e#83a30064f4812d0029532675a5f2bf38c257ad0e" +source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66" dependencies = [ "bytemuck", "emath", @@ -1889,7 +1933,7 @@ checksum = "18aade80d5e09429040243ce1143ddc08a92d7a22820ac512610410a4dd5214f" [[package]] name = "eframe" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=83a30064f4812d0029532675a5f2bf38c257ad0e#83a30064f4812d0029532675a5f2bf38c257ad0e" +source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66" dependencies = [ "ahash", "bytemuck", @@ -1898,6 +1942,8 @@ dependencies = [ "egui-wgpu", "egui-winit", "egui_glow", + "glutin", + "glutin-winit", "home", "image", "js-sys", @@ -1926,7 +1972,7 @@ dependencies = [ [[package]] name = "egui" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=83a30064f4812d0029532675a5f2bf38c257ad0e#83a30064f4812d0029532675a5f2bf38c257ad0e" +source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66" dependencies = [ "accesskit", "ahash", @@ -1943,7 +1989,7 @@ dependencies = [ [[package]] name = "egui-wgpu" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=83a30064f4812d0029532675a5f2bf38c257ad0e#83a30064f4812d0029532675a5f2bf38c257ad0e" +source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66" dependencies = [ "ahash", "bytemuck", @@ -1962,7 +2008,7 @@ dependencies = [ [[package]] name = "egui-winit" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=83a30064f4812d0029532675a5f2bf38c257ad0e#83a30064f4812d0029532675a5f2bf38c257ad0e" +source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66" dependencies = [ "accesskit_winit", "ahash", @@ -2004,7 +2050,7 @@ dependencies = [ [[package]] name = "egui_extras" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=83a30064f4812d0029532675a5f2bf38c257ad0e#83a30064f4812d0029532675a5f2bf38c257ad0e" +source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66" dependencies = [ "ahash", "egui", @@ -2021,13 +2067,13 @@ dependencies = [ [[package]] name = "egui_glow" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=83a30064f4812d0029532675a5f2bf38c257ad0e#83a30064f4812d0029532675a5f2bf38c257ad0e" +source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66" dependencies = [ "ahash", "bytemuck", "egui", "egui-winit", - "glow", + "glow 0.16.0", "log", "memoffset", "puffin", @@ -2036,6 +2082,19 @@ dependencies = [ "winit", ] +[[package]] +name = "egui_kittest" +version = "0.29.1" +source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66" +dependencies = [ + "dify", + "egui", + "egui-wgpu", + "image", + "kittest", + "pollster 0.4.0", +] + [[package]] name = "egui_plot" version = "0.29.0" @@ -2095,7 +2154,7 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "emath" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=83a30064f4812d0029532675a5f2bf38c257ad0e#83a30064f4812d0029532675a5f2bf38c257ad0e" +source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66" dependencies = [ "bytemuck", "serde", @@ -2211,7 +2270,7 @@ dependencies = [ [[package]] name = "epaint" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=83a30064f4812d0029532675a5f2bf38c257ad0e#83a30064f4812d0029532675a5f2bf38c257ad0e" +source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66" dependencies = [ "ab_glyph", "ahash", @@ -2230,7 +2289,7 @@ dependencies = [ [[package]] name = "epaint_default_fonts" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=83a30064f4812d0029532675a5f2bf38c257ad0e#83a30064f4812d0029532675a5f2bf38c257ad0e" +source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66" [[package]] name = "equivalent" @@ -2604,6 +2663,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -2673,6 +2741,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "glow" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "gltf" version = "1.4.1" @@ -2712,6 +2792,63 @@ dependencies = [ "serde_json", ] +[[package]] +name = "glutin" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec69412a0bf07ea7607e638b415447857a808846c2b685a43c8aa18bc6d5e499" +dependencies = [ + "bitflags 2.6.0", + "cfg_aliases 0.2.1", + "cgl", + "core-foundation 0.9.4", + "dispatch", + "glutin_egl_sys", + "glutin_glx_sys", + "glutin_wgl_sys", + "libloading", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "raw-window-handle", + "wayland-sys", + "windows-sys 0.52.0", + "x11-dl", +] + +[[package]] +name = "glutin-winit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" +dependencies = [ + "cfg_aliases 0.2.1", + "glutin", + "raw-window-handle", + "winit", +] + +[[package]] +name = "glutin_egl_sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cae99fff4d2850dbe6fb8c1fa8e4fead5525bab715beaacfccf3fb994e01c827" +dependencies = [ + "gl_generator", + "windows-sys 0.52.0", +] + +[[package]] +name = "glutin_glx_sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c2b2d3918e76e18e08796b55eb64e8fe6ec67d5a6b2e2a7e2edce224ad24c63" +dependencies = [ + "gl_generator", + "x11-dl", +] + [[package]] name = "glutin_wgl_sys" version = "0.6.0" @@ -3454,6 +3591,16 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "kittest" +version = "0.1.0" +source = "git+https://github.com/rerun-io/kittest?branch=main#63c5b7d58178900e523428ca5edecbba007a2702" +dependencies = [ + "accesskit", + "accesskit_consumer 0.25.0", + "parking_lot", +] + [[package]] name = "kqueue" version = "1.0.8" @@ -3572,7 +3719,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -6269,6 +6416,7 @@ dependencies = [ "anyhow", "criterion", "egui", + "egui_kittest", "itertools 0.13.0", "nohash-hasher", "once_cell", @@ -9341,7 +9489,7 @@ dependencies = [ "bytemuck", "cfg_aliases 0.1.1", "core-graphics-types", - "glow", + "glow 0.14.2", "glutin_wgl_sys", "gpu-alloc", "gpu-descriptor", @@ -9401,7 +9549,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d99c70565f24..5e63e6e0c5d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -145,6 +145,7 @@ egui_extras = { version = "0.29.1", features = [ "puffin", "serde", ] } +egui_kittest = { version = "0.29.1", features = ["wgpu", "snapshot"] } egui_plot = "0.29.0" # https://github.com/emilk/egui_plot egui_table = "0.1.0" # https://github.com/rerun-io/egui_table egui_tiles = "0.10.1" # https://github.com/rerun-io/egui_tiles @@ -557,12 +558,14 @@ significant_drop_tightening = "allow" # An update of parking_lot made this trigg # As a last resport, patch with a commit to our own repository. # ALWAYS document what PR the commit hash is part of, or when it was merged into the upstream trunk. -ecolor = { git = "https://github.com/emilk/egui.git", rev = "83a30064f4812d0029532675a5f2bf38c257ad0e" } # egui master 2024-11-19 -eframe = { git = "https://github.com/emilk/egui.git", rev = "83a30064f4812d0029532675a5f2bf38c257ad0e" } # egui master 2024-11-19 -egui = { git = "https://github.com/emilk/egui.git", rev = "83a30064f4812d0029532675a5f2bf38c257ad0e" } # egui master 2024-11-19 -egui_extras = { git = "https://github.com/emilk/egui.git", rev = "83a30064f4812d0029532675a5f2bf38c257ad0e" } # egui master 2024-11-19 -egui-wgpu = { git = "https://github.com/emilk/egui.git", rev = "83a30064f4812d0029532675a5f2bf38c257ad0e" } # egui master 2024-11-19 -emath = { git = "https://github.com/emilk/egui.git", rev = "83a30064f4812d0029532675a5f2bf38c257ad0e" } # egui master 2024-11-19 +ecolor = { git = "https://github.com/emilk/egui.git", rev = "84cc1572b175d49a64f1b323a6d7e56b1f1fba66" } # egui master 2024-11-27 +eframe = { git = "https://github.com/emilk/egui.git", rev = "84cc1572b175d49a64f1b323a6d7e56b1f1fba66" } # egui master 2024-11-27 +egui = { git = "https://github.com/emilk/egui.git", rev = "84cc1572b175d49a64f1b323a6d7e56b1f1fba66" } # egui master 2024-11-27 +egui_extras = { git = "https://github.com/emilk/egui.git", rev = "84cc1572b175d49a64f1b323a6d7e56b1f1fba66" } # egui master 2024-11-27 +egui-wgpu = { git = "https://github.com/emilk/egui.git", rev = "84cc1572b175d49a64f1b323a6d7e56b1f1fba66" } # egui master 2024-11-27 +emath = { git = "https://github.com/emilk/egui.git", rev = "84cc1572b175d49a64f1b323a6d7e56b1f1fba66" } # egui master 2024-11-27 + +egui_kittest = { git = "https://github.com/emilk/egui.git", rev = "84cc1572b175d49a64f1b323a6d7e56b1f1fba66" } # egui master 2024-11-27 # Useful while developing: # ecolor = { path = "../../egui/crates/ecolor" } diff --git a/crates/viewer/re_selection_panel/src/lib.rs b/crates/viewer/re_selection_panel/src/lib.rs index b3ad8913a73c..9ebe73009d52 100644 --- a/crates/viewer/re_selection_panel/src/lib.rs +++ b/crates/viewer/re_selection_panel/src/lib.rs @@ -29,7 +29,7 @@ mod test { selection_state.set_selection(Item::SpaceView(SpaceViewId::random())); }); - test_ctx.run(|ctx, ui| { + test_ctx.run_in_egui_central_panel(|ctx, ui| { let (sender, _) = std::sync::mpsc::channel(); let blueprint = ViewportBlueprint::try_from_db( ctx.store_context.blueprint, diff --git a/crates/viewer/re_space_view_dataframe/src/view_query/blueprint.rs b/crates/viewer/re_space_view_dataframe/src/view_query/blueprint.rs index e0f399167592..aaae97e5ff5d 100644 --- a/crates/viewer/re_space_view_dataframe/src/view_query/blueprint.rs +++ b/crates/viewer/re_space_view_dataframe/src/view_query/blueprint.rs @@ -288,12 +288,13 @@ mod test { let view_id = SpaceViewId::random(); - test_context.run_and_handle_system_commands(|ctx, _| { + test_context.run_in_egui_central_panel(|ctx, _| { let query = Query::from_blueprint(ctx, view_id); query.save_latest_at_enabled(ctx, true); }); + test_context.handle_system_commands(); - test_context.run(|ctx, _| { + test_context.run_in_egui_central_panel(|ctx, _| { let query = Query::from_blueprint(ctx, view_id); assert!(query.latest_at_enabled().unwrap()); }); diff --git a/crates/viewer/re_time_panel/Cargo.toml b/crates/viewer/re_time_panel/Cargo.toml index 6e65bee0e6db..307898a5982e 100644 --- a/crates/viewer/re_time_panel/Cargo.toml +++ b/crates/viewer/re_time_panel/Cargo.toml @@ -43,6 +43,7 @@ vec1.workspace = true [dev-dependencies] anyhow.workspace = true criterion.workspace = true +egui_kittest.workspace = true rand.workspace = true [lib] diff --git a/crates/viewer/re_time_panel/src/lib.rs b/crates/viewer/re_time_panel/src/lib.rs index 5861bee290fb..31db40351a11 100644 --- a/crates/viewer/re_time_panel/src/lib.rs +++ b/crates/viewer/re_time_panel/src/lib.rs @@ -20,7 +20,7 @@ use egui::{pos2, Color32, CursorIcon, NumExt, Painter, PointerButton, Rect, Shap use re_context_menu::{context_menu_ui_for_item, SelectionUpdateBehavior}; use re_data_ui::DataUi as _; use re_data_ui::{item_ui::guess_instance_path_icon, sorted_component_list_for_ui}; -use re_entity_db::{EntityTree, InstancePath}; +use re_entity_db::{EntityDb, EntityTree, InstancePath}; use re_log_types::{ external::re_types_core::ComponentName, ComponentPath, EntityPath, EntityPathPart, ResolvedTimeRange, TimeInt, TimeReal, TimeType, @@ -128,7 +128,7 @@ pub struct TimePanel { impl Default for TimePanel { fn default() -> Self { - PathRecursiveChunksPerTimeline::ensure_registered(); + Self::ensure_registered_subscribers(); Self { data_density_graph_painter: Default::default(), @@ -142,6 +142,14 @@ impl Default for TimePanel { } impl TimePanel { + /// Ensures that all required store subscribers are correctly set up. + /// + /// This is implicitly called by [`Self::default`], but may need to be explicitly called in, + /// e.g., testing context. + pub fn ensure_registered_subscribers() { + PathRecursiveChunksPerTimeline::ensure_registered(); + } + pub fn new_blueprint_panel() -> Self { Self { source: TimePanelSource::Blueprint, @@ -173,8 +181,6 @@ impl TimePanel { // etc.) let screen_header_height = ui.cursor().top(); - let top_bar_height = re_ui::DesignTokens::top_bar_height(); - let margin = DesignTokens::bottom_panel_margin(); let mut panel_frame = DesignTokens::bottom_panel_frame(); if state.is_expanded() { @@ -214,50 +220,20 @@ impl TimePanel { if expansion < 1.0 { // Collapsed or animating ui.horizontal(|ui| { - ui.spacing_mut().interact_size = Vec2::splat(top_bar_height); + ui.spacing_mut().interact_size = + Vec2::splat(re_ui::DesignTokens::top_bar_height()); ui.visuals_mut().button_frame = true; self.collapsed_ui(ctx, entity_db, ui, &mut time_ctrl_after); }); } else { // Expanded: - ui.vertical(|ui| { - // Add back the margin we removed from the panel: - let mut top_row_frame = egui::Frame::default(); - top_row_frame.inner_margin.right = margin.right; - top_row_frame.inner_margin.bottom = margin.bottom; - let top_row_rect = top_row_frame - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.spacing_mut().interact_size = Vec2::splat(top_bar_height); - ui.visuals_mut().button_frame = true; - self.top_row_ui(ctx, entity_db, ui, &mut time_ctrl_after); - }); - }) - .response - .rect; - - // Draw separator between top bar and the rest: - ui.painter().hline( - 0.0..=top_row_rect.right(), - top_row_rect.bottom(), - ui.visuals().widgets.noninteractive.bg_stroke, - ); - - ui.spacing_mut().scroll.bar_outer_margin = 4.0; // needed, because we have no panel margin on the right side. - - // Add extra margin on the left which was intentionally missing on the controls. - let mut streams_frame = egui::Frame::default(); - streams_frame.inner_margin.left = margin.left; - streams_frame.show(ui, |ui| { - self.expanded_ui( - ctx, - viewport_blueprint, - entity_db, - ui, - &mut time_ctrl_after, - ); - }); - }); + self.show_expanded_with_header( + ctx, + viewport_blueprint, + entity_db, + &mut time_ctrl_after, + ui, + ); } }, ); @@ -271,6 +247,50 @@ impl TimePanel { } } + pub fn show_expanded_with_header( + &mut self, + ctx: &ViewerContext<'_>, + viewport_blueprint: &ViewportBlueprint, + entity_db: &EntityDb, + time_ctrl_after: &mut TimeControl, + ui: &mut Ui, + ) { + ui.vertical(|ui| { + // Add back the margin we removed from the panel: + let mut top_row_frame = egui::Frame::default(); + let margin = DesignTokens::bottom_panel_margin(); + top_row_frame.inner_margin.right = margin.right; + top_row_frame.inner_margin.bottom = margin.bottom; + let top_row_rect = top_row_frame + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.spacing_mut().interact_size = + Vec2::splat(re_ui::DesignTokens::top_bar_height()); + ui.visuals_mut().button_frame = true; + self.top_row_ui(ctx, entity_db, ui, time_ctrl_after); + }); + }) + .response + .rect; + + // Draw separator between top bar and the rest: + ui.painter().hline( + 0.0..=top_row_rect.right(), + top_row_rect.bottom(), + ui.visuals().widgets.noninteractive.bg_stroke, + ); + + ui.spacing_mut().scroll.bar_outer_margin = 4.0; // needed, because we have no panel margin on the right side. + + // Add extra margin on the left which was intentionally missing on the controls. + let mut streams_frame = egui::Frame::default(); + streams_frame.inner_margin.left = margin.left; + streams_frame.show(ui, |ui| { + self.expanded_ui(ctx, viewport_blueprint, entity_db, ui, time_ctrl_after); + }); + }); + } + #[allow(clippy::unused_self)] fn collapsed_ui( &mut self, diff --git a/crates/viewer/re_time_panel/tests/snapshots/time_panel_dense_data.png b/crates/viewer/re_time_panel/tests/snapshots/time_panel_dense_data.png new file mode 100644 index 000000000000..7eea4ae888f9 --- /dev/null +++ b/crates/viewer/re_time_panel/tests/snapshots/time_panel_dense_data.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2965a2ad75997674a64481d9a82d2eb7d64d1fdf09f2cf47ad87e150a8f1fd91 +size 27566 diff --git a/crates/viewer/re_time_panel/tests/snapshots/time_panel_two_sections.png b/crates/viewer/re_time_panel/tests/snapshots/time_panel_two_sections.png new file mode 100644 index 000000000000..2542f4c6339b --- /dev/null +++ b/crates/viewer/re_time_panel/tests/snapshots/time_panel_two_sections.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38d1bdf41d8ccc4b6ee9ea43c51fa2c16a3cf94071bd9e9a2cd57f649d268a17 +size 31954 diff --git a/crates/viewer/re_time_panel/tests/time_panel_tests.rs b/crates/viewer/re_time_panel/tests/time_panel_tests.rs new file mode 100644 index 000000000000..741ecb1a9b84 --- /dev/null +++ b/crates/viewer/re_time_panel/tests/time_panel_tests.rs @@ -0,0 +1,109 @@ +use std::sync::Arc; + +use egui::Vec2; + +use re_chunk_store::{Chunk, LatestAtQuery, RowId}; +use re_log_types::example_components::MyPoint; +use re_log_types::external::re_types_core::Component; +use re_log_types::{build_frame_nr, EntityPath}; +use re_time_panel::TimePanel; +use re_viewer_context::blueprint_timeline; +use re_viewer_context::test_context::TestContext; +use re_viewport_blueprint::ViewportBlueprint; + +#[test] +#[cfg_attr(not(target_os = "macos"), ignore)] +pub fn time_panel_two_sections_should_match_snapshot() { + TimePanel::ensure_registered_subscribers(); + let mut test_context = TestContext::default(); + + let points1 = MyPoint::from_iter(0..1); + for i in 0..2 { + let entity_path = EntityPath::from(format!("/entity/{i}")); + let mut builder = Chunk::builder(entity_path.clone()); + for frame in [10, 11, 12, 15, 18, 100, 102, 104].map(|frame| frame + i) { + builder = builder.with_sparse_component_batches( + RowId::new(), + [build_frame_nr(frame)], + [(MyPoint::name(), Some(&points1 as _))], + ); + } + test_context + .recording_store + .add_chunk(&Arc::new(builder.build().unwrap())) + .unwrap(); + } + + run_time_panel_and_save_snapshot(test_context, "time_panel_two_sections"); +} + +#[test] +#[cfg_attr(not(target_os = "macos"), ignore)] +pub fn time_panel_dense_data_should_match_snapshot() { + TimePanel::ensure_registered_subscribers(); + let mut test_context = TestContext::default(); + + let points1 = MyPoint::from_iter(0..1); + + let mut rng_seed = 0b1010_1010_1010_1010_1010_1010_1010_1010u64; + let mut rng = || { + rng_seed ^= rng_seed >> 12; + rng_seed ^= rng_seed << 25; + rng_seed ^= rng_seed >> 27; + rng_seed.wrapping_mul(0x2545_f491_4f6c_dd1d) + }; + + let entity_path = EntityPath::from("/entity"); + let mut builder = Chunk::builder(entity_path.clone()); + for frame in 0..1_000 { + if rng() & 0b1 == 0 { + continue; + } + + builder = builder.with_sparse_component_batches( + RowId::new(), + [build_frame_nr(frame)], + [(MyPoint::name(), Some(&points1 as _))], + ); + } + test_context + .recording_store + .add_chunk(&Arc::new(builder.build().unwrap())) + .unwrap(); + + run_time_panel_and_save_snapshot(test_context, "time_panel_dense_data"); +} + +fn run_time_panel_and_save_snapshot(mut test_context: TestContext, snapshot_name: &str) { + let mut panel = TimePanel::default(); + + //TODO(ab): this contains a lot of boilerplate which should be provided by helpers + let mut harness = egui_kittest::Harness::builder() + .with_size(Vec2::new(700.0, 300.0)) + .build_ui(|ui| { + test_context.run(&ui.ctx().clone(), |viewer_ctx| { + let (sender, _) = std::sync::mpsc::channel(); + let blueprint = ViewportBlueprint::try_from_db( + viewer_ctx.store_context.blueprint, + &LatestAtQuery::latest(blueprint_timeline()), + sender, + ); + + let mut time_ctrl = viewer_ctx.rec_cfg.time_ctrl.read().clone(); + + panel.show_expanded_with_header( + viewer_ctx, + &blueprint, + viewer_ctx.recording(), + &mut time_ctrl, + ui, + ); + }); + + test_context.handle_system_commands(); + }); + + harness.run(); + + harness.wgpu_snapshot(snapshot_name); +} diff --git a/crates/viewer/re_viewer_context/src/test_context.rs b/crates/viewer/re_viewer_context/src/test_context.rs index 33a003dafb1d..3e207bb77372 100644 --- a/crates/viewer/re_viewer_context/src/test_context.rs +++ b/crates/viewer/re_viewer_context/src/test_context.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use re_chunk_store::LatestAtQuery; use re_entity_db::EntityDb; -use re_log_types::{StoreId, StoreKind, Timeline}; +use re_log_types::{StoreId, StoreKind}; use crate::{ blueprint_timeline, command_channel, ApplicationSelectionState, CommandReceiver, CommandSender, @@ -18,7 +18,7 @@ use crate::{ /// use re_viewer_context::ViewerContext; /// /// let mut test_context = TestContext::default(); -/// test_context.run(|ctx: &ViewerContext, _| { +/// test_context.run_in_egui_central_panel(|ctx: &ViewerContext, _| { /// /* do something with ctx */ /// }); /// ``` @@ -27,7 +27,10 @@ pub struct TestContext { pub blueprint_store: EntityDb, pub space_view_class_registry: SpaceViewClassRegistry, pub selection_state: ApplicationSelectionState, - pub active_timeline: Timeline, + pub recording_config: RecordingConfig, + + blueprint_query: LatestAtQuery, + component_ui_registry: ComponentUiRegistry, command_sender: CommandSender, command_receiver: CommandReceiver, @@ -37,15 +40,25 @@ impl Default for TestContext { fn default() -> Self { let recording_store = EntityDb::new(StoreId::random(StoreKind::Recording)); let blueprint_store = EntityDb::new(StoreId::random(StoreKind::Blueprint)); - let active_timeline = Timeline::new("time", re_log_types::TimeType::Time); let (command_sender, command_receiver) = command_channel(); + + let recording_config = RecordingConfig::default(); + + let blueprint_query = LatestAtQuery::latest(blueprint_timeline()); + + let component_ui_registry = ComponentUiRegistry::new(Box::new( + |_ctx, _ui, _ui_layout, _query, _db, _entity_path, _row_id, _component| {}, + )); + Self { recording_store, blueprint_store, space_view_class_registry: Default::default(), selection_state: Default::default(), - active_timeline, + recording_config, + blueprint_query, + component_ui_registry, command_sender, command_receiver, } @@ -60,71 +73,74 @@ impl TestContext { self.selection_state.on_frame_start(|_| true, None); } - /// Run the given function with a [`ViewerContext`] produced by the [`Self`]. + /// Run the provided closure with a [`ViewerContext`] produced by the [`Self`]. + /// + /// IMPORTANT: call [`Self::handle_system_commands`] after calling this function if your test + /// relies on system commands. + pub fn run(&self, egui_ctx: &egui::Context, func: impl FnOnce(&ViewerContext<'_>)) { + re_ui::apply_style_and_install_loaders(egui_ctx); + + let store_context = StoreContext { + app_id: "rerun_test".into(), + blueprint: &self.blueprint_store, + default_blueprint: None, + recording: &self.recording_store, + bundle: &Default::default(), + caches: &Default::default(), + hub: &Default::default(), + }; + + let ctx = ViewerContext { + app_options: &Default::default(), + cache: &Default::default(), + reflection: &Default::default(), + component_ui_registry: &self.component_ui_registry, + space_view_class_registry: &self.space_view_class_registry, + store_context: &store_context, + applicable_entities_per_visualizer: &Default::default(), + indicated_entities_per_visualizer: &Default::default(), + query_results: &Default::default(), + rec_cfg: &self.recording_config, + blueprint_cfg: &Default::default(), + selection_state: &self.selection_state, + blueprint_query: &self.blueprint_query, + egui_ctx, + render_ctx: None, + command_sender: &self.command_sender, + focused_item: &None, + }; + + func(&ctx); + } + + /// Run the given function with a [`ViewerContext`] produced by the [`Self`], in the context of + /// an [`egui::CentralPanel`]. + /// + /// IMPORTANT: call [`Self::handle_system_commands`] after calling this function if your test + /// relies on system commands. /// - /// Note: there is a possibility that the closure will be called more than once, see - /// [`egui::Context::run`]. - pub fn run(&self, mut func: impl FnMut(&ViewerContext<'_>, &mut egui::Ui)) { + /// Notes: + /// - Uses [`egui::__run_test_ctx`]. + /// - There is a possibility that the closure will be called more than once, see + /// [`egui::Context::run`]. + //TODO(ab): replace this with a kittest-based helper. + pub fn run_in_egui_central_panel( + &self, + mut func: impl FnMut(&ViewerContext<'_>, &mut egui::Ui), + ) { egui::__run_test_ctx(|ctx| { egui::CentralPanel::default().show(ctx, |ui| { - re_ui::apply_style_and_install_loaders(ui.ctx()); - let blueprint_query = LatestAtQuery::latest(blueprint_timeline()); - - let component_ui_registry = ComponentUiRegistry::new(Box::new( - |_ctx, _ui, _ui_layout, _query, _db, _entity_path, _row_id, _component| {}, - )); - - let store_context = StoreContext { - app_id: "rerun_test".into(), - blueprint: &self.blueprint_store, - default_blueprint: None, - recording: &self.recording_store, - bundle: &Default::default(), - caches: &Default::default(), - hub: &Default::default(), - }; - - let rec_cfg = RecordingConfig::default(); - rec_cfg.time_ctrl.write().set_timeline(self.active_timeline); - - let egui_context = ui.ctx().clone(); - let ctx = ViewerContext { - app_options: &Default::default(), - cache: &Default::default(), - reflection: &Default::default(), - component_ui_registry: &component_ui_registry, - space_view_class_registry: &self.space_view_class_registry, - store_context: &store_context, - applicable_entities_per_visualizer: &Default::default(), - indicated_entities_per_visualizer: &Default::default(), - query_results: &Default::default(), - rec_cfg: &rec_cfg, - blueprint_cfg: &Default::default(), - selection_state: &self.selection_state, - blueprint_query: &blueprint_query, - egui_ctx: &egui_context, - render_ctx: None, - command_sender: &self.command_sender, - focused_item: &None, - }; - - func(&ctx, ui); + let egui_ctx = ui.ctx().clone(); + + self.run(&egui_ctx, |ctx| { + func(ctx, ui); + }); }); }); } - /// Run the given function with a [`ViewerContext`] produced by the [`Self`] and handle any - /// system commands issued during execution (see [`Self::handle_system_command`]). - pub fn run_and_handle_system_commands( - &mut self, - func: impl FnMut(&ViewerContext<'_>, &mut egui::Ui), - ) { - self.run(func); - self.handle_system_command(); - } - /// Best-effort attempt to meaningfully handle some of the system commands. - pub fn handle_system_command(&mut self) { + pub fn handle_system_commands(&mut self) { while let Some(command) = self.command_receiver.recv_system() { let mut handled = true; let command_name = format!("{command:?}"); @@ -151,7 +167,10 @@ impl TestContext { SystemCommand::SetActiveTimeline { rec_id, timeline } => { assert_eq!(rec_id, self.recording_store.store_id()); - self.active_timeline = timeline; + self.recording_config + .time_ctrl + .write() + .set_timeline(timeline); } // not implemented @@ -201,7 +220,7 @@ mod test { selection_state.set_selection(item.clone()); }); - test_context.run(|ctx, _| { + test_context.run_in_egui_central_panel(|ctx, _| { assert_eq!( ctx.selection_state.selected_items().single_item(), Some(&item) diff --git a/crates/viewer/re_viewport_blueprint/src/space_view.rs b/crates/viewer/re_viewport_blueprint/src/space_view.rs index bc04480d9fe5..11b51dc1bb93 100644 --- a/crates/viewer/re_viewport_blueprint/src/space_view.rs +++ b/crates/viewer/re_viewport_blueprint/src/space_view.rs @@ -778,11 +778,11 @@ mod tests { let mut query_result = contents.execute_query(&store_ctx, visualizable_entities); let mut view_states = ViewStates::default(); - test_ctx.run(|ctx, _ui| { + test_ctx.run_in_egui_central_panel(|ctx, _ui| { resolver.update_overrides( ctx.blueprint_db(), ctx.blueprint_query, - &test_ctx.active_timeline, + ctx.rec_cfg.time_ctrl.read().timeline(), ctx.space_view_class_registry, &mut query_result, &mut view_states, diff --git a/scripts/ci/check_large_files.py b/scripts/ci/check_large_files.py index 9c9a857e372e..a7faf9330173 100755 --- a/scripts/ci/check_large_files.py +++ b/scripts/ci/check_large_files.py @@ -3,33 +3,86 @@ import os import subprocess -#!/usr/bin/env python3 +# These files are allowed to be larger than our limit +FILES_ALLOWED_TO_BE_LARGE = { + "CHANGELOG.md", + "Cargo.lock", + "crates/build/re_types_builder/src/reflection.rs", + "crates/store/re_dataframe/src/query.rs", + "crates/store/re_types/src/datatypes/tensor_buffer.rs", + "crates/viewer/re_ui/data/Inter-Medium.otf", + "crates/viewer/re_viewer/src/reflection/mod.rs", + "pixi.lock", + "rerun_cpp/docs/Doxyfile", +} -# Check for files that are too large to be checked into the repository. -# Whenever we want to make an exception, we add it to `check_large_files_allow_list.txt` +# Paths with the following prefixes are allowed to contain PNG files that are not checked into LFS +PATH_PREFIXES_ALLOWED_TO_CONTAIN_NON_LFS_PNGS = ( + "crates/viewer/re_ui/data/icons/", + "crates/viewer/re_ui/data/logo_dark_mode.png", + "crates/viewer/re_ui/data/logo_light_mode.png", + "crates/viewer/re_viewer/data/app_icon_mac.png", + "crates/viewer/re_viewer/data/app_icon_windows.png", + "docs/snippets/all/archetypes/ferris.png", + "docs/snippets/src/snippets/ferris.png", + "examples/assets/example.png", +) -# Maximum file size, unless found in `check_large_files_allow_list.txt` -maximum_size = 100 * 1024 -result = 0 -script_path = os.path.dirname(os.path.realpath(__file__)) -os.chdir(os.path.join(script_path, "../..")) +def check_large_files(files_to_check: set[str]) -> int: + """Check for files that are too large to be checked into the repository.""" -# Get the list of tracked files using git ls-files command -tracked_files = subprocess.check_output(["git", "ls-files"]).decode().splitlines() + maximum_size = 100 * 1024 -for file_path in tracked_files: - actual_size = os.path.getsize(file_path) + result = 0 + for file_path in files_to_check: + actual_size = os.path.getsize(file_path) - if actual_size >= maximum_size: - allow_list_path = os.path.join(script_path, "check_large_files_allow_list.txt") + if actual_size >= maximum_size: + if file_path not in FILES_ALLOWED_TO_BE_LARGE: + print(f"{file_path} is {actual_size} bytes (max allowed is {maximum_size} bytes)") + result = 1 - with open(allow_list_path, encoding="utf8") as allow_list_file: - allow_list = allow_list_file.read().splitlines() + print(f"checked {len(files_to_check)} files") - if file_path not in allow_list: - print(f"{file_path} is {actual_size} bytes (max allowed is {maximum_size} bytes)") - result = 1 + return result -print(f"checked {len(tracked_files)} files") -exit(result) + +def check_for_non_lfs_pngs(files_to_check: set[str]) -> int: + """Check for PNG files that are not checked into LFS.""" + + result = 0 + for file_path in files_to_check: + if file_path.startswith(PATH_PREFIXES_ALLOWED_TO_CONTAIN_NON_LFS_PNGS): + continue + + print(f"{file_path} is a PNG file that is not checked into LFS") + result = 1 + + print(f"checked {len(files_to_check)} pngs") + + return result + + +def main() -> None: + script_path = os.path.dirname(os.path.realpath(__file__)) + os.chdir(os.path.join(script_path, "../..")) + + all_tracked_files = set(subprocess.check_output(["git", "ls-files"]).decode().splitlines()) + lfs_files = set(subprocess.check_output(["git", "lfs", "ls-files", "-n"]).decode().splitlines()) + not_lfs_files = all_tracked_files - lfs_files + + result = check_large_files(not_lfs_files) + if result != 0: + exit(result) + + all_tracked_pngs = {f for f in all_tracked_files if f.endswith(".png")} + not_lfs_pngs = all_tracked_pngs - lfs_files + + result = check_for_non_lfs_pngs(not_lfs_pngs) + if result != 0: + exit(result) + + +if __name__ == "__main__": + main() diff --git a/scripts/ci/check_large_files_allow_list.txt b/scripts/ci/check_large_files_allow_list.txt deleted file mode 100644 index d4073981b6e9..000000000000 --- a/scripts/ci/check_large_files_allow_list.txt +++ /dev/null @@ -1,9 +0,0 @@ -CHANGELOG.md -Cargo.lock -crates/build/re_types_builder/src/reflection.rs -crates/store/re_dataframe/src/query.rs -crates/store/re_types/src/datatypes/tensor_buffer.rs -crates/viewer/re_ui/data/Inter-Medium.otf -crates/viewer/re_viewer/src/reflection/mod.rs -pixi.lock -rerun_cpp/docs/Doxyfile