Skip to content

Commit 3071849

Browse files
authored
ci: skip running tests on PRs if able (#32214)
1 parent 899974e commit 3071849

File tree

10 files changed

+193
-0
lines changed

10 files changed

+193
-0
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,3 +561,5 @@ opt-level = 3
561561
opt-level = 3
562562
[profile.release.package.deno_npm_cache]
563563
opt-level = 3
564+
[profile.release.package.twox-hash]
565+
opt-level = 3

tests/integration/mod.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,20 @@ mod upgrade;
7474
mod watcher;
7575

7676
pub fn main() {
77+
if test_util::hash::should_skip_on_ci("integration", |hasher| {
78+
let tests = test_util::tests_path();
79+
hasher
80+
.hash_dir(tests.join("integration"))
81+
.hash_dir(tests.join("util"))
82+
.hash_dir(tests.join("testdata"))
83+
.hash_dir(tests.join("registry"))
84+
.hash_file(test_util::deno_exe_path())
85+
.hash_file(test_util::test_server_path())
86+
.hash_file(test_util::denort_exe_path());
87+
}) {
88+
return;
89+
}
90+
7791
let _ = rustls::crypto::ring::default_provider().install_default();
7892
let mut main_category: CollectedTestCategory<&'static TestMacroCase> =
7993
CollectedTestCategory {

tests/node_compat/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,17 @@ struct CollectedResult {
118118
}
119119

120120
fn main() {
121+
if test_util::hash::should_skip_on_ci("node_compat", |hasher| {
122+
let tests = test_util::tests_path();
123+
hasher
124+
.hash_dir(tests.join("node_compat"))
125+
.hash_dir(tests.join("util"))
126+
.hash_file(test_util::deno_exe_path())
127+
.hash_file(test_util::test_server_path());
128+
}) {
129+
return;
130+
}
131+
121132
let cli_args = parse_cli_args();
122133
let config = load_config();
123134
let mut category = if cli_args.report {

tests/specs/mod.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,20 @@ struct StepMetaData {
246246
}
247247

248248
pub fn main() {
249+
if test_util::hash::should_skip_on_ci("specs", |hasher| {
250+
let tests = test_util::tests_path();
251+
hasher
252+
.hash_dir(tests.join("specs"))
253+
.hash_dir(tests.join("util"))
254+
.hash_dir(tests.join("testdata"))
255+
.hash_dir(tests.join("registry"))
256+
.hash_file(test_util::deno_exe_path())
257+
.hash_file(test_util::test_server_path())
258+
.hash_file(test_util::denort_exe_path());
259+
}) {
260+
return;
261+
}
262+
249263
let root_category =
250264
collect_tests_or_exit::<serde_json::Value>(CollectOptions {
251265
base: tests_path().join("specs").to_path_buf(),

tests/unit/mod.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ use test_util::test_runner::flaky_test_ci;
1616
use test_util::tests_path;
1717

1818
fn main() {
19+
if test_util::hash::should_skip_on_ci("unit", |hasher| {
20+
let tests = test_util::tests_path();
21+
hasher
22+
.hash_dir(tests.join("unit"))
23+
.hash_dir(tests.join("util"))
24+
.hash_dir(tests.join("testdata"))
25+
.hash_file(test_util::deno_exe_path())
26+
.hash_file(test_util::test_server_path());
27+
}) {
28+
return;
29+
}
30+
1931
let category = collect_tests_or_exit(CollectOptions {
2032
base: tests_path().join("unit").to_path_buf(),
2133
strategy: Box::new(TestPerFileCollectionStrategy {

tests/unit_node/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,19 @@ use util::deno_config_path;
1717
use util::env_vars_for_npm_tests;
1818

1919
fn main() {
20+
if test_util::hash::should_skip_on_ci("unit_node", |hasher| {
21+
let tests = test_util::tests_path();
22+
hasher
23+
.hash_dir(tests.join("unit_node"))
24+
.hash_dir(tests.join("util"))
25+
.hash_dir(tests.join("testdata"))
26+
.hash_dir(tests.join("registry"))
27+
.hash_file(test_util::deno_exe_path())
28+
.hash_file(test_util::test_server_path());
29+
}) {
30+
return;
31+
}
32+
2033
let category = collect_tests_or_exit(CollectOptions {
2134
base: tests_path().join("unit_node").to_path_buf(),
2235
strategy: Box::new(TestPerFileCollectionStrategy {

tests/util/lib/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ sha2.workspace = true
3737
tempfile.workspace = true
3838
termcolor.workspace = true
3939
test_macro.workspace = true
40+
twox-hash.workspace = true
4041
url.workspace = true
4142

4243
# optional deps behind "lsp" feature

tests/util/lib/hash.rs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright 2018-2026 the Deno authors. MIT license.
2+
3+
use std::hash::Hasher;
4+
use std::path::Path;
5+
use std::path::PathBuf;
6+
use std::time::SystemTime;
7+
8+
use crate::eprintln;
9+
10+
/// A fast hasher for computing a combined hash of files and directory mtimes.
11+
/// Uses xxhash64 for speed.
12+
pub struct InputHasher(twox_hash::XxHash64);
13+
14+
impl InputHasher {
15+
pub fn new_with_cli_args() -> Self {
16+
let mut hasher = Self(Default::default());
17+
for arg in std::env::args() {
18+
hasher.0.write(arg.as_bytes());
19+
}
20+
hasher
21+
}
22+
23+
/// Hash a single file's mtime. Skips if file doesn't exist.
24+
pub fn hash_file(&mut self, path: impl AsRef<Path>) -> &mut Self {
25+
if let Ok(meta) = std::fs::metadata(path.as_ref()) {
26+
self.hash_mtime(meta.modified().ok());
27+
}
28+
self
29+
}
30+
31+
/// Recursively hash all file mtimes in a directory (sorted for determinism).
32+
/// Skips if directory doesn't exist.
33+
pub fn hash_dir(&mut self, path: impl AsRef<Path>) -> &mut Self {
34+
let path = path.as_ref();
35+
let mut entries = Vec::new();
36+
collect_entries_recursive(path, &mut entries);
37+
entries.sort_by(|(a, _), (b, _)| a.cmp(b));
38+
for (_, mtime) in &entries {
39+
self.hash_mtime(*mtime);
40+
}
41+
self
42+
}
43+
44+
fn hash_mtime(&mut self, mtime: Option<SystemTime>) {
45+
if let Some(mtime) = mtime
46+
&& let Ok(d) = mtime.duration_since(SystemTime::UNIX_EPOCH)
47+
{
48+
self.0.write_u64(d.as_secs());
49+
}
50+
}
51+
52+
pub fn finish(&self) -> u64 {
53+
self.0.finish()
54+
}
55+
}
56+
57+
fn collect_entries_recursive(
58+
dir: &Path,
59+
out: &mut Vec<(PathBuf, Option<SystemTime>)>,
60+
) {
61+
let entries = match std::fs::read_dir(dir) {
62+
Ok(entries) => entries,
63+
Err(_) => return,
64+
};
65+
for entry in entries.flatten() {
66+
let file_type = match entry.file_type() {
67+
Ok(ft) => ft,
68+
Err(_) => continue,
69+
};
70+
if file_type.is_dir() {
71+
collect_entries_recursive(&entry.path(), out);
72+
} else {
73+
let mtime = entry.metadata().ok().and_then(|m| m.modified().ok());
74+
out.push((entry.path(), mtime));
75+
}
76+
}
77+
}
78+
79+
/// Check if tests can be skipped on CI by comparing input hashes.
80+
///
81+
/// `name` is used for the hash file name and log messages (e.g. "specs",
82+
/// "unit"). `configure` receives an `InputHasher` to add whatever files/dirs
83+
/// are relevant.
84+
///
85+
/// Returns `true` if the hash is unchanged and tests should be skipped.
86+
///
87+
/// ```ignore
88+
/// if test_util::hash::should_skip_on_ci("specs", |hasher| {
89+
/// hasher
90+
/// .hash_dir(tests.join("specs"))
91+
/// .hash_file(deno_exe_path());
92+
/// }) {
93+
/// return;
94+
/// }
95+
/// ```
96+
pub fn should_skip_on_ci(
97+
name: &str,
98+
configure: impl FnOnce(&mut InputHasher),
99+
) -> bool {
100+
if !*crate::IS_CI {
101+
return false;
102+
}
103+
104+
let start = std::time::Instant::now();
105+
let hash_path = crate::target_dir()
106+
.join(format!("{name}_input_hash"))
107+
.to_path_buf();
108+
let mut hasher = InputHasher::new_with_cli_args();
109+
configure(&mut hasher);
110+
let new_hash = hasher.finish().to_string();
111+
112+
eprintln!("ci hash took {}ms", start.elapsed().as_millis());
113+
114+
if let Ok(old_hash) = std::fs::read_to_string(&hash_path)
115+
&& old_hash.trim() == new_hash
116+
{
117+
eprintln!("{name} input hash unchanged ({new_hash}), skipping");
118+
return true;
119+
}
120+
121+
eprintln!("{name} input hash changed, writing new hash ({new_hash})");
122+
std::fs::write(&hash_path, &new_hash).ok();
123+
false
124+
}

tests/util/lib/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub mod assertions;
1818
mod builders;
1919
pub mod consts;
2020
mod fs;
21+
pub mod hash;
2122
#[cfg(feature = "lsp")]
2223
pub mod lsp;
2324
mod macros;

0 commit comments

Comments
 (0)