refactor!: rename slhx to hemx
Rename the tracked product identity, crate/package names, Rust paths/macros, generated artifacts, runtime files, public attributes, examples, docs, requirements, and tests from slhx to hemx without compatibility shims. Verified with cargo run -p hemx-xtask -- test, cargo test -p hemx-derive --test compile_fail, cargo test -p hemx-js, cargo test -p hemx-axum, cargo test -p hemx-v0-examples, cargo check --workspace, redgate list, redgate refs, redgate health --strict, git diff --check, and git grep/ls-files legacy-name audits. req: misc/001 req: codegen/001 req: component/004 req: runtime/001
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "hemx-xtask"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "hemx-ci"
|
||||
path = "src/main.rs"
|
||||
@@ -0,0 +1,348 @@
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
use std::process::{Command, ExitCode};
|
||||
use std::time::Instant;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let mut args = env::args().skip(1);
|
||||
match args.next().as_deref() {
|
||||
Some("test") | None => run_test_plan(),
|
||||
Some("bench") => run_bench_plan(),
|
||||
Some("help") | Some("--help") | Some("-h") => {
|
||||
print_help();
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Some(command) => {
|
||||
eprintln!("unknown hemx-ci command `{command}`\n");
|
||||
print_help();
|
||||
ExitCode::from(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
println!(
|
||||
"hemx-ci — resource-aware project checks\n\n cargo run -p hemx-xtask -- test\n cargo run -p hemx-xtask -- bench\n\nEnvironment overrides:\n HEMX_CI_JOBS=N compile jobs, capped by detected resources\n HEMX_CI_TEST_THREADS=N Rust test threads, capped by detected resources\n HEMX_CI_SKIP_BROWSER=1 skip browser E2E"
|
||||
);
|
||||
}
|
||||
|
||||
fn run_test_plan() -> ExitCode {
|
||||
// req: test/004
|
||||
let budget = Budget::detect();
|
||||
budget.report();
|
||||
|
||||
let mut steps = vec![
|
||||
Step::new(
|
||||
"workspace-no-techdemo",
|
||||
["test", "--workspace", "--exclude", "hemx-techdemo"],
|
||||
),
|
||||
Step::new(
|
||||
"techdemo-unit-http",
|
||||
["test", "-p", "hemx-techdemo", "--test", "e2e"],
|
||||
),
|
||||
Step::new("redgate", ["health", "--strict"]).tool("redgate"),
|
||||
];
|
||||
|
||||
if !budget.skip_browser {
|
||||
steps.insert(
|
||||
2,
|
||||
Step::new(
|
||||
"techdemo-browser",
|
||||
["test", "-p", "hemx-techdemo", "--test", "browser_e2e"],
|
||||
)
|
||||
.test_threads(1),
|
||||
);
|
||||
}
|
||||
|
||||
for step in steps {
|
||||
if let Err(code) = step.run(&budget) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
fn run_bench_plan() -> ExitCode {
|
||||
// req: test/004
|
||||
let budget = Budget::detect();
|
||||
budget.report();
|
||||
eprintln!(
|
||||
"hemx-ci: benchmarking small safe test slices from 1 to {} job(s)",
|
||||
budget.jobs
|
||||
);
|
||||
|
||||
let steps = [
|
||||
Step::new("bench-xtask", ["test", "-p", "hemx-xtask"]),
|
||||
Step::new("bench-runtime", ["test", "-p", "hemx-js"]),
|
||||
];
|
||||
|
||||
for jobs in bench_values(budget.jobs) {
|
||||
let bench_budget = budget.with_jobs(jobs);
|
||||
for step in &steps {
|
||||
let started = Instant::now();
|
||||
if let Err(code) = step.run(&bench_budget) {
|
||||
return code;
|
||||
}
|
||||
println!(
|
||||
"bench\t{}\tjobs={}\ttest_threads={}\telapsed_ms={}",
|
||||
step.name,
|
||||
bench_budget.jobs,
|
||||
bench_budget.test_threads,
|
||||
started.elapsed().as_millis()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct Budget {
|
||||
cpus: usize,
|
||||
mem_gib: Option<usize>,
|
||||
jobs: usize,
|
||||
test_threads: usize,
|
||||
skip_browser: bool,
|
||||
}
|
||||
|
||||
impl Budget {
|
||||
fn detect() -> Self {
|
||||
let cpus = std::thread::available_parallelism()
|
||||
.map_or(1, usize::from)
|
||||
.max(1);
|
||||
let mem_gib = available_mem_gib();
|
||||
let skip_browser = env::var_os("HEMX_CI_SKIP_BROWSER").is_some()
|
||||
|| env::var_os("CI_NO_BROWSER").is_some()
|
||||
|| !has_command("geckodriver");
|
||||
Self::from_resources(
|
||||
cpus,
|
||||
mem_gib,
|
||||
env_usize("HEMX_CI_JOBS"),
|
||||
env_usize("HEMX_CI_TEST_THREADS"),
|
||||
skip_browser,
|
||||
)
|
||||
}
|
||||
|
||||
fn from_resources(
|
||||
cpus: usize,
|
||||
mem_gib: Option<usize>,
|
||||
jobs_override: Option<usize>,
|
||||
test_threads_override: Option<usize>,
|
||||
skip_browser: bool,
|
||||
) -> Self {
|
||||
let cpus = cpus.max(1);
|
||||
let mem_jobs = mem_gib.map_or(cpus, |gib| (gib / 2).max(1));
|
||||
let auto_jobs = cpus.min(mem_jobs).clamp(1, 6);
|
||||
let jobs = jobs_override.unwrap_or(auto_jobs).clamp(1, auto_jobs);
|
||||
let max_test_threads = jobs.min(4);
|
||||
let test_threads = test_threads_override
|
||||
.unwrap_or(max_test_threads)
|
||||
.clamp(1, max_test_threads);
|
||||
Self {
|
||||
cpus,
|
||||
mem_gib,
|
||||
jobs,
|
||||
test_threads,
|
||||
skip_browser,
|
||||
}
|
||||
}
|
||||
|
||||
fn report(&self) {
|
||||
eprintln!(
|
||||
"hemx-ci: cpus={} mem={}GiB jobs={} test_threads={} browser={}",
|
||||
self.cpus,
|
||||
self.mem_gib
|
||||
.map(|mem| mem.to_string())
|
||||
.unwrap_or_else(|| "unknown".into()),
|
||||
self.jobs,
|
||||
self.test_threads,
|
||||
if self.skip_browser { "skip" } else { "run" }
|
||||
);
|
||||
}
|
||||
|
||||
fn with_jobs(self, jobs: usize) -> Self {
|
||||
let jobs = jobs.clamp(1, self.jobs);
|
||||
Self {
|
||||
jobs,
|
||||
test_threads: self.test_threads.min(jobs.min(4)).max(1),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn bench_values(limit: usize) -> Vec<usize> {
|
||||
let limit = limit.max(1);
|
||||
let mut values = Vec::new();
|
||||
let mut value = 1;
|
||||
while value < limit {
|
||||
values.push(value);
|
||||
value *= 2;
|
||||
}
|
||||
values.push(limit);
|
||||
values.dedup();
|
||||
values
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Step {
|
||||
name: &'static str,
|
||||
tool: &'static str,
|
||||
args: Vec<&'static str>,
|
||||
test_threads: Option<usize>,
|
||||
}
|
||||
|
||||
impl Step {
|
||||
fn new<const N: usize>(name: &'static str, args: [&'static str; N]) -> Self {
|
||||
Self {
|
||||
name,
|
||||
tool: "cargo",
|
||||
args: args.into(),
|
||||
test_threads: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn tool(mut self, tool: &'static str) -> Self {
|
||||
self.tool = tool;
|
||||
self
|
||||
}
|
||||
|
||||
fn test_threads(mut self, threads: usize) -> Self {
|
||||
self.test_threads = Some(threads);
|
||||
self
|
||||
}
|
||||
|
||||
fn run(&self, budget: &Budget) -> Result<(), ExitCode> {
|
||||
eprintln!("\n==> {}", self.name);
|
||||
let mut command = Command::new(self.tool);
|
||||
command.args(&self.args);
|
||||
if self.tool == "cargo" {
|
||||
command.env("CARGO_BUILD_JOBS", budget.jobs.to_string());
|
||||
command.env(
|
||||
"RUST_TEST_THREADS",
|
||||
self.test_threads.unwrap_or(budget.test_threads).to_string(),
|
||||
);
|
||||
}
|
||||
let status = command.status().map_err(|err| {
|
||||
eprintln!("failed to run {}: {err}", self.name);
|
||||
ExitCode::FAILURE
|
||||
})?;
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
eprintln!("{} failed with {status}", self.name);
|
||||
Err(ExitCode::from(status.code().unwrap_or(1) as u8))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn env_usize(key: &str) -> Option<usize> {
|
||||
env::var(key).ok()?.parse().ok()
|
||||
}
|
||||
|
||||
fn available_mem_gib() -> Option<usize> {
|
||||
const GIB: u64 = 1024 * 1024 * 1024;
|
||||
let bytes = available_mem_bytes()?;
|
||||
Some(((bytes / GIB) as usize).max(1))
|
||||
}
|
||||
|
||||
fn available_mem_bytes() -> Option<u64> {
|
||||
let mut candidates = Vec::new();
|
||||
if let Some(bytes) = proc_mem_available_bytes() {
|
||||
candidates.push(bytes);
|
||||
}
|
||||
if let Some(bytes) = cgroup_mem_available_bytes() {
|
||||
candidates.push(bytes);
|
||||
}
|
||||
candidates.into_iter().min()
|
||||
}
|
||||
|
||||
fn proc_mem_available_bytes() -> Option<u64> {
|
||||
let meminfo = std::fs::read_to_string("/proc/meminfo").ok()?;
|
||||
let kb = meminfo.lines().find_map(|line| {
|
||||
let rest = line.strip_prefix("MemAvailable:")?;
|
||||
rest.split_whitespace().next()?.parse::<u64>().ok()
|
||||
})?;
|
||||
kb.checked_mul(1024)
|
||||
}
|
||||
|
||||
fn cgroup_mem_available_bytes() -> Option<u64> {
|
||||
cgroup_mem_available_v2().or_else(cgroup_mem_available_v1)
|
||||
}
|
||||
|
||||
fn cgroup_mem_available_v2() -> Option<u64> {
|
||||
let limit = read_cgroup_limit("/sys/fs/cgroup/memory.max")?;
|
||||
let current = read_u64_file("/sys/fs/cgroup/memory.current").unwrap_or(0);
|
||||
Some(limit.saturating_sub(current).max(1))
|
||||
}
|
||||
|
||||
fn cgroup_mem_available_v1() -> Option<u64> {
|
||||
let limit = read_cgroup_limit("/sys/fs/cgroup/memory/memory.limit_in_bytes")?;
|
||||
let current = read_u64_file("/sys/fs/cgroup/memory/memory.usage_in_bytes").unwrap_or(0);
|
||||
Some(limit.saturating_sub(current).max(1))
|
||||
}
|
||||
|
||||
fn read_cgroup_limit(path: &str) -> Option<u64> {
|
||||
let raw = std::fs::read_to_string(path).ok()?;
|
||||
let trimmed = raw.trim();
|
||||
if trimmed == "max" {
|
||||
return None;
|
||||
}
|
||||
let value = trimmed.parse::<u64>().ok()?;
|
||||
if value >= (1 << 60) {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
}
|
||||
|
||||
fn read_u64_file(path: &str) -> Option<u64> {
|
||||
std::fs::read_to_string(path).ok()?.trim().parse().ok()
|
||||
}
|
||||
|
||||
fn has_command(name: &str) -> bool {
|
||||
let Some(paths) = env::var_os("PATH") else {
|
||||
return false;
|
||||
};
|
||||
env::split_paths(&paths).any(|dir| is_executable(dir.join(name)))
|
||||
}
|
||||
|
||||
fn is_executable(path: impl AsRef<Path>) -> bool {
|
||||
path.as_ref().is_file()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Budget;
|
||||
|
||||
#[test]
|
||||
fn budget_is_capped_by_available_memory() {
|
||||
// req: test/004
|
||||
let budget = Budget::from_resources(22, Some(1), None, None, true);
|
||||
|
||||
assert_eq!(budget.jobs, 1);
|
||||
assert_eq!(budget.test_threads, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn budget_caps_large_machines_by_default() {
|
||||
let budget = Budget::from_resources(64, Some(128), None, None, true);
|
||||
|
||||
assert_eq!(budget.jobs, 6);
|
||||
assert_eq!(budget.test_threads, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn budget_clamps_explicit_overrides_to_resource_budget() {
|
||||
let budget = Budget::from_resources(2, Some(2), Some(8), Some(7), true);
|
||||
|
||||
assert_eq!(budget.jobs, 1);
|
||||
assert_eq!(budget.test_threads, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bench_values_grow_gradually_and_include_limit() {
|
||||
assert_eq!(super::bench_values(1), vec![1]);
|
||||
assert_eq!(super::bench_values(6), vec![1, 2, 4, 6]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user