diff --git a/.cargo/config.toml b/.cargo/config.toml index d9c635df5dc7..bcd1e95edda0 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -2,6 +2,7 @@ bless = "test --config env.RUSTC_BLESS='1'" uitest = "test --test compile-test" uibless = "bless --test compile-test" +guitest = "test --test gui" dev = "run --package clippy_dev --bin clippy_dev --manifest-path clippy_dev/Cargo.toml --" lintcheck = "run --package lintcheck --bin lintcheck --manifest-path lintcheck/Cargo.toml -- " collect-metadata = "test --test compile-test --config env.COLLECT_METADATA='1'" diff --git a/.github/workflows/clippy_mq.yml b/.github/workflows/clippy_mq.yml index 496220480508..1d7e95fe97be 100644 --- a/.github/workflows/clippy_mq.yml +++ b/.github/workflows/clippy_mq.yml @@ -9,6 +9,7 @@ env: NO_FMT_TEST: 1 CARGO_INCREMENTAL: 0 RUSTFLAGS: -D warnings + BROWSER_UI_TEST_VERSION: '0.18.2' defaults: run: @@ -76,6 +77,14 @@ jobs: rustup set default-host ${{ matrix.host }} rustup show active-toolchain + - name: Install npm + uses: actions/setup-node@v3 + with: + node-version: 20 + + - name: Install browser-ui-test + run: npm install browser-ui-test@"${BROWSER_UI_TEST_VERSION}" + # Run - name: Build run: cargo build --tests --features internal @@ -113,6 +122,9 @@ jobs: env: OS: ${{ runner.os }} + - name: Test clippy lints page + run: cargo guitest + metadata_collection: needs: changelog runs-on: ubuntu-latest diff --git a/.github/workflows/clippy_pr.yml b/.github/workflows/clippy_pr.yml index 2e5b5bd41dfb..74e0e4623757 100644 --- a/.github/workflows/clippy_pr.yml +++ b/.github/workflows/clippy_pr.yml @@ -9,6 +9,7 @@ env: NO_FMT_TEST: 1 CARGO_INCREMENTAL: 0 RUSTFLAGS: -D warnings + BROWSER_UI_TEST_VERSION: '0.18.2' concurrency: # For a given workflow, if we push to the same PR, cancel all previous builds on that PR. @@ -29,6 +30,14 @@ jobs: - name: Install toolchain run: rustup show active-toolchain + - name: Install npm + uses: actions/setup-node@v3 + with: + node-version: 20 + + - name: Install browser-ui-test + run: npm install browser-ui-test@"${BROWSER_UI_TEST_VERSION}" + # Run - name: Build run: cargo build --tests --features internal @@ -57,6 +66,9 @@ jobs: env: OS: ${{ runner.os }} +- name: Test clippy lints page + run: cargo guitest + # We need to have the "conclusion" job also on PR CI, to make it possible # to add PRs to a merge queue. conclusion: diff --git a/.gitignore b/.gitignore index a7c25b29021f..15040f7a68d1 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,8 @@ helper.txt # mdbook generated output /book/book + +# GUI tests +node_modules +package-lock.json +package.json diff --git a/book/src/development/basics.md b/book/src/development/basics.md index 166b6aab9fb3..bc35eb15df62 100644 --- a/book/src/development/basics.md +++ b/book/src/development/basics.md @@ -60,6 +60,8 @@ cargo uitest TESTNAME="test_" cargo uitest # only run dogfood tests cargo dev dogfood +# only run GUI tests (clippy lints page) +cargo guitest ``` If the output of a [UI test] differs from the expected output, you can update diff --git a/tests/gui.rs b/tests/gui.rs new file mode 100644 index 000000000000..fcb1ce241fbe --- /dev/null +++ b/tests/gui.rs @@ -0,0 +1,110 @@ +// This test ensures that the clippy lints page is working as expected. + +use std::ffi::OsStr; +use std::fs::read_to_string; +use std::path::Path; +use std::process::Command; +use std::time::SystemTime; + +fn get_available_browser_ui_test_version_inner(global: bool) -> Option { + let mut command = Command::new("npm"); + command.arg("list").arg("--parseable").arg("--long").arg("--depth=0"); + if global { + command.arg("--global"); + } + let stdout = command.output().expect("`npm` command not found").stdout; + let lines = String::from_utf8_lossy(&stdout); + lines + .lines() + .find_map(|l| l.split(':').nth(1)?.strip_prefix("browser-ui-test@")) + .map(std::borrow::ToOwned::to_owned) +} + +fn get_available_browser_ui_test_version() -> Option { + get_available_browser_ui_test_version_inner(false).or_else(|| get_available_browser_ui_test_version_inner(true)) +} + +fn expected_browser_ui_test_version() -> String { + let content = + read_to_string(".github/workflows/clippy.yml").expect("failed to read `.github/workflows/clippy.yml`"); + for line in content.lines() { + let line = line.trim(); + if let Some(version) = line.strip_prefix("BROWSER_UI_TEST_VERSION:") { + return version.trim().replace('\'', ""); + } + } + panic!("failed to retrieved `browser-ui-test` version"); +} + +fn mtime(path: impl AsRef) -> SystemTime { + let path = path.as_ref(); + if path.is_dir() { + path.read_dir() + .into_iter() + .flatten() + .flatten() + .map(|entry| mtime(entry.path())) + .max() + .unwrap_or(SystemTime::UNIX_EPOCH) + } else { + path.metadata() + .and_then(|metadata| metadata.modified()) + .unwrap_or(SystemTime::UNIX_EPOCH) + } +} + +#[test] +fn check_clippy_lints_page() { + // do not run this test inside the upstream rustc repo. + if option_env!("RUSTC_TEST_SUITE").is_some() { + return; + } + let browser_ui_test_version = expected_browser_ui_test_version(); + match get_available_browser_ui_test_version() { + Some(version) => { + if version != browser_ui_test_version { + eprintln!( + "⚠️ Installed version of browser-ui-test (`{version}`) is different than the \ + one used in the CI (`{browser_ui_test_version}`) You can install this version \ + using `npm update browser-ui-test` or by using `npm install browser-ui-test\ + @{browser_ui_test_version}`", + ); + } + }, + None => { + panic!( + "`browser-ui-test` is not installed. You can install this package using `npm \ + update browser-ui-test` or by using `npm install browser-ui-test\ + @{browser_ui_test_version}`", + ); + }, + } + + // We build the lints page only if needed. + let index_time = mtime("util/gh-pages/index.html"); + + if (index_time < mtime("clippy_lints/src") || index_time < mtime("util/gh-pages/index_template.html")) + && !Command::new("cargo") + .arg("collect-metadata") + .status() + .is_ok_and(|status| status.success()) + { + panic!("failed to run `cargo collect-metadata`"); + } + + let current_dir = std::env::current_dir() + .expect("failed to retrieve current directory") + .join("util/gh-pages/index.html"); + let current_dir = format!("file://{}", current_dir.display()); + let mut command = Command::new("npx"); + command + .arg("browser-ui-test") + .args(["--variable", "DOC_PATH", current_dir.as_str()]) + .args(["--test-folder", "tests/gui"]); + if std::env::var_os("DISABLE_HEADLESS_TEST").is_some_and(|value| value == OsStr::new("1")) { + command.arg("--no-headless"); + } + + // Then we run the GUI tests on it. + assert!(command.status().is_ok_and(|status| status.success())); +} diff --git a/tests/gui/README.md b/tests/gui/README.md new file mode 100644 index 000000000000..229dd9fb1907 --- /dev/null +++ b/tests/gui/README.md @@ -0,0 +1,19 @@ +The tests present here are used to test the clippy lints page. The +goal is to prevent unsound/unexpected GUI (breaking) changes. + +This is using the [browser-ui-test] framework to do so. It works as follows: + +It wraps [puppeteer] to send commands to a web browser in order to navigate and +test what's being currently displayed in the web page. + +You can find more information and its documentation in its [repository][browser-ui-test]. + +If you don't want to run in headless mode (helpful to debug sometimes), you can use +`DISABLE_HEADLESS_TEST=1`: + +```bash +$ DISABLE_HEADLESS_TEST=1 cargo guitest +``` + +[browser-ui-test]: https://github.com/GuillaumeGomez/browser-UI-test/ +[puppeteer]: https://pptr.dev/ diff --git a/tests/gui/hash.goml b/tests/gui/hash.goml new file mode 100644 index 000000000000..91e5afa578b8 --- /dev/null +++ b/tests/gui/hash.goml @@ -0,0 +1,22 @@ +// This GUI test ensures that when the URL has a hash, it will open the target lint. + +go-to: |DOC_PATH| +// First we ensure that by default, the lint is not displayed. +assert-css: ("#alloc_instead_of_core .lint-docs", {"display": "none"}) +// First we move the mouse cursor to the lint to make the anchor appear. +move-cursor-to: "#alloc_instead_of_core" +// We wait for the anchor to be visible. +wait-for-css-false: ("#alloc_instead_of_core .anchor", {"display": "none"}) +click: "#alloc_instead_of_core .anchor" +// Clicking on the anchor should have two effects: +// 1. Change the URL hash. +// 2. Open the lint. +wait-for-css: ("#alloc_instead_of_core .lint-docs", {"display": "block"}) +wait-for-document-property: {"location"."hash": "#alloc_instead_of_core"} + +// Now we reload the page. The lint should still be open since the hash is +// targetting it. +go-to: |DOC_PATH| + "#alloc_instead_of_core" +wait-for-css: ("#alloc_instead_of_core .lint-docs", {"display": "block"}) +// Other lints should not be expanded. +wait-for-css: ("#absolute_paths .lint-docs", {"display": "none"}) diff --git a/tests/gui/no-js.goml b/tests/gui/no-js.goml new file mode 100644 index 000000000000..e505e8e420f1 --- /dev/null +++ b/tests/gui/no-js.goml @@ -0,0 +1,48 @@ +// This GUI test checks the lints page works as expected when JS is disabled. +javascript: false // disabling javascript +go-to: |DOC_PATH| + +define-function: ( + "check-expanded-collapsed", + [display, content], + block { + wait-for-css: ("#absolute_paths > .lint-docs", {"display": |display|}) + assert-css: ("#absolute_paths .label-doc-folding::before", {"content": |content|}) + }, +) + +define-function: ( + "check-expand-collapse-action", + [selector], + block { + // We confirm it's collapsed. + call-function: ("check-expanded-collapsed", { + "display": "none", + "content": '"+"', + }) + // We click on the item to expand it. + click: |selector| + // We confirm it's expanded. + call-function: ("check-expanded-collapsed", { + "display": "block", + "content": '"−"', + }) + // We collapse it again. + click: |selector| + // We confirm it's collapsed again. + call-function: ("check-expanded-collapsed", { + "display": "none", + "content": '"+"', + }) + }, +) + +// First we check that we can expand/collapse a lint by clicking on the lint. +call-function: ("check-expand-collapse-action", {"selector": "#lint-absolute_paths"}) +// Then we check the expand/collapse works when clicking on the +/- button. +call-function: ("check-expand-collapse-action", {"selector": "#absolute_paths .label-doc-folding"}) + +// Checking click on the anchor changes the location hash. +assert-document-property: {"location"."hash": ""} +click: "#absolute_paths .panel-title .anchor" +assert-document-property: {"location"."hash": "#absolute_paths"} diff --git a/tests/gui/search.goml b/tests/gui/search.goml new file mode 100644 index 000000000000..9a71933e7517 --- /dev/null +++ b/tests/gui/search.goml @@ -0,0 +1,15 @@ +// This test ensures that the search is filtering lints correctly. +go-to: |DOC_PATH| + +assert-css: ("#absurd_extreme_comparisons", {"display": "block"}) +assert-css: ("#absolute_paths", {"display": "block"}) +assert-css: ("#join_absolute_paths", {"display": "block"}) + +// We update the search. +write-into: ("#search-input", "absolute_paths") + +// `absolute_paths` and `join_absolute_path` should still be visible, but +// not `absurde_extreme_comparisons`. +wait-for-css: ("#absurd_extreme_comparisons", {"display": "none"}) +assert-css: ("#absolute_paths", {"display": "block"}) +assert-css: ("#join_absolute_paths", {"display": "block"})