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:
slhx agent
2026-06-05 06:52:37 +02:00
parent d4e865ef92
commit c33500440e
69 changed files with 1415 additions and 1415 deletions
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "hemx-xtask"
version.workspace = true
edition.workspace = true
publish = false
[[bin]]
name = "hemx-ci"
path = "src/main.rs"
+348
View File
@@ -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]);
}
}