use lowfat_core::level::Level;
use lowfat_plugin::plugin::{FilterInput, FilterPlugin, PluginInfo};
use lowfat_runner::process::ProcessFilter;
use std::io::Write;
use std::path::PathBuf;
fn temp_plugin(name: &str, script: &str) -> (ProcessFilter, PathBuf) {
let dir = std::env::temp_dir().join(format!(
"lowfat-e2e-{name}-{}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("filter.sh");
let mut f = std::fs::File::create(&path).unwrap();
f.write_all(script.as_bytes()).unwrap();
let filter = ProcessFilter {
info: PluginInfo {
name: format!("{name}-compact"),
version: "0.1.0".into(),
commands: vec![name.into()],
subcommands: vec![],
},
entry: path,
base_dir: dir.clone(),
};
(filter, dir)
}
fn make_input(
raw: &str,
command: &str,
subcommand: &str,
args: Vec<&str>,
level: Level,
exit_code: i32,
) -> FilterInput {
FilterInput {
raw: raw.to_string(),
command: command.to_string(),
subcommand: subcommand.to_string(),
args: args.into_iter().map(String::from).collect(),
level,
head_limit: level.head_limit(40),
exit_code,
}
}
#[test]
fn args_env_contains_full_argument_string() {
let script = r#"#!/bin/sh
echo "ARGS=$LOWFAT_ARGS"
"#;
let (filter, _dir) = temp_plugin("argtest", script);
let input = make_input(
"ignored",
"kubectl",
"get",
vec!["get", "pods", "-n", "kube-system", "-o", "wide"],
Level::Full,
0,
);
let result = filter.filter(&input).unwrap();
assert!(
result.text.contains("ARGS=get pods -n kube-system -o wide"),
"expected full args, got: {}",
result.text
);
}
#[test]
fn args_env_empty_when_no_args() {
let script = r#"#!/bin/sh
echo "ARGS=[$LOWFAT_ARGS]"
"#;
let (filter, _dir) = temp_plugin("noargs", script);
let input = make_input("ignored", "git", "", vec![], Level::Full, 0);
let result = filter.filter(&input).unwrap();
assert!(
result.text.contains("ARGS=[]"),
"expected empty args, got: {}",
result.text
);
}
const KUBECTL_SCRIPT: &str = r#"#!/bin/sh
RAW=$(cat)
LEVEL="${LOWFAT_LEVEL:-full}"
SUB="${LOWFAT_SUBCOMMAND}"
ARGS="${LOWFAT_ARGS}"
# Extract resource type from args
RESOURCE=""
for arg in $ARGS; do
case "$arg" in
"$SUB") continue ;;
-*) continue ;;
*) RESOURCE="$arg"; break ;;
esac
done
# Passthrough structured output
case "$ARGS" in
*"-o json"*|*"-o yaml"*)
echo "$RAW"
exit 0
;;
esac
case "$SUB" in
get)
case "$RESOURCE" in
pods|po)
if [ "$LEVEL" = "ultra" ]; then
echo "$RAW" | awk 'NR==1 || !/Running/' | head -n 15
else
echo "$RAW" | head -n 30
fi
;;
events|ev)
if [ "$LEVEL" = "ultra" ]; then
echo "$RAW" | awk 'NR==1 || /Warning/' | head -n 15
else
echo "$RAW" | tail -n 30
fi
;;
*)
echo "$RAW" | head -n 30
;;
esac
;;
*)
echo "$RAW" | head -n 30
;;
esac
"#;
const KUBECTL_PODS_OUTPUT: &str = "\
NAME READY STATUS RESTARTS AGE
nginx-abc123 1/1 Running 0 5d
redis-def456 1/1 Running 0 3d
crash-ghi789 0/1 CrashLoopBackOff 5 1h
pending-jkl012 0/1 Pending 0 30m
api-mno345 1/1 Running 0 7d";
const KUBECTL_EVENTS_OUTPUT: &str = "\
LAST SEEN TYPE REASON OBJECT MESSAGE
5m Normal Scheduled pod/nginx-abc123 Successfully assigned
4m Normal Pulled pod/nginx-abc123 Container image pulled
3m Warning BackOff pod/crash-ghi789 Back-off restarting
2m Warning Failed pod/crash-ghi789 Error: CrashLoopBackOff
1m Normal Created pod/api-mno345 Created container";
#[test]
fn kubectl_get_pods_ultra_filters_running() {
let (filter, _dir) = temp_plugin("kubectl", KUBECTL_SCRIPT);
let input = make_input(
KUBECTL_PODS_OUTPUT,
"kubectl",
"get",
vec!["get", "pods"],
Level::Ultra,
0,
);
let result = filter.filter(&input).unwrap();
assert!(result.text.contains("NAME"), "header missing");
assert!(result.text.contains("CrashLoopBackOff"), "CrashLoop pod missing");
assert!(result.text.contains("Pending"), "Pending pod missing");
assert!(!result.text.contains("nginx-abc123"), "Running pod should be filtered");
assert!(!result.text.contains("redis-def456"), "Running pod should be filtered");
}
#[test]
fn kubectl_get_pods_full_keeps_all() {
let (filter, _dir) = temp_plugin("kubectl-full", KUBECTL_SCRIPT);
let input = make_input(
KUBECTL_PODS_OUTPUT,
"kubectl",
"get",
vec!["get", "pods"],
Level::Full,
0,
);
let result = filter.filter(&input).unwrap();
assert!(result.text.contains("nginx-abc123"), "all pods should be present at full");
assert!(result.text.contains("CrashLoopBackOff"), "all pods should be present at full");
}
#[test]
fn kubectl_get_events_ultra_warnings_only() {
let (filter, _dir) = temp_plugin("kubectl-events", KUBECTL_SCRIPT);
let input = make_input(
KUBECTL_EVENTS_OUTPUT,
"kubectl",
"get",
vec!["get", "events"],
Level::Ultra,
0,
);
let result = filter.filter(&input).unwrap();
assert!(result.text.contains("LAST SEEN"), "header missing");
assert!(result.text.contains("BackOff"), "Warning event missing");
assert!(result.text.contains("Failed"), "Warning event missing");
assert!(!result.text.contains("Scheduled"), "Normal event should be filtered");
assert!(!result.text.contains("Pulled"), "Normal event should be filtered");
}
#[test]
fn kubectl_get_pods_json_passthrough() {
let json_output = r#"{"items": [{"metadata": {"name": "nginx"}}]}"#;
let (filter, _dir) = temp_plugin("kubectl-json", KUBECTL_SCRIPT);
let input = make_input(
json_output,
"kubectl",
"get",
vec!["get", "pods", "-o", "json"],
Level::Ultra,
0,
);
let result = filter.filter(&input).unwrap();
assert!(
result.text.contains(r#""items""#),
"JSON should pass through, got: {}",
result.text
);
}
#[test]
fn kubectl_get_pods_yaml_passthrough() {
let yaml_output = "apiVersion: v1\nkind: Pod\nmetadata:\n name: nginx\n";
let (filter, _dir) = temp_plugin("kubectl-yaml", KUBECTL_SCRIPT);
let input = make_input(
yaml_output,
"kubectl",
"get",
vec!["get", "pods", "-o", "yaml"],
Level::Ultra,
0,
);
let result = filter.filter(&input).unwrap();
assert!(
result.text.contains("apiVersion"),
"YAML should pass through, got: {}",
result.text
);
}
#[test]
fn kubectl_get_pods_with_namespace_flag() {
let (filter, _dir) = temp_plugin("kubectl-ns", KUBECTL_SCRIPT);
let input = make_input(
KUBECTL_PODS_OUTPUT,
"kubectl",
"get",
vec!["get", "pods", "-n", "kube-system"],
Level::Ultra,
0,
);
let result = filter.filter(&input).unwrap();
assert!(result.text.contains("NAME"), "header missing — resource type not parsed correctly");
assert!(result.text.contains("CrashLoopBackOff"), "should use pods-specific filter");
assert!(!result.text.contains("nginx-abc123"), "Running pods should be filtered in ultra");
}
const CARGO_SCRIPT: &str = r#"#!/bin/sh
RAW=$(cat)
LEVEL="${LOWFAT_LEVEL:-full}"
SUB="${LOWFAT_SUBCOMMAND}"
case "$SUB" in
build|check)
if [ "$LEVEL" = "ultra" ]; then
ISSUES=$(echo "$RAW" | grep -E '^(error|warning)\b' | head -n 15)
if [ -z "$ISSUES" ]; then
echo "cargo $SUB: ok"
else
echo "$ISSUES"
fi
else
LIMIT=$( [ "$LEVEL" = "lite" ] && echo 60 || echo 30 )
echo "$RAW" | grep -vE '^\s*(Compiling|Downloading|Checking|Blocking|Updating|Locking) ' | head -n "$LIMIT"
fi
;;
test)
if [ "$LEVEL" = "ultra" ]; then
echo "$RAW" | grep -E '^(test result:|failures:|test .+ FAILED| Running|FAILED)' | head -n 15
else
LIMIT=$( [ "$LEVEL" = "lite" ] && echo 60 || echo 30 )
echo "$RAW" | grep -vE '^\s*(Compiling|Downloading|Checking|Blocking|Updating|Locking) |\.\.\.+ ok$' | head -n "$LIMIT"
fi
;;
*)
echo "$RAW" | head -n 30
;;
esac
"#;
const CARGO_BUILD_OUTPUT: &str = "\
Compiling serde v1.0.200
Compiling tokio v1.37.0
Compiling myapp v0.1.0 (/home/user/myapp)
warning: unused variable: `x`
--> src/main.rs:10:9
|
10 | let x = 42;
| ^ help: if this is intentional, prefix it with an underscore: `_x`
|
= note: `#[warn(unused_variables)]` on by default
Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.32s";
const CARGO_TEST_OUTPUT: &str = "\
Compiling myapp v0.1.0
Running unittests src/lib.rs (target/debug/deps/myapp-abc123)
running 5 tests
test tests::basic_add ... ok
test tests::basic_sub ... ok
test tests::edge_case ... ok
test tests::overflow_check ... FAILED
test tests::negative_check ... ok
failures:
---- tests::overflow_check stdout ----
thread 'tests::overflow_check' panicked at 'assertion failed'
failures:
tests::overflow_check
test result: FAILED. 4 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out";
#[test]
fn cargo_build_ultra_shows_warnings_only() {
let (filter, _dir) = temp_plugin("cargo-build", CARGO_SCRIPT);
let input = make_input(
CARGO_BUILD_OUTPUT,
"cargo",
"build",
vec!["build"],
Level::Ultra,
0,
);
let result = filter.filter(&input).unwrap();
assert!(result.text.contains("warning: unused variable"), "warning line missing");
assert!(!result.text.contains("Compiling serde"), "Compiling noise should be stripped");
assert!(!result.text.contains("Finished"), "Finished line should be stripped");
}
#[test]
fn cargo_build_ultra_clean_shows_ok() {
let clean_output = "\
Compiling myapp v0.1.0
Finished `dev` profile in 2.0s";
let (filter, _dir) = temp_plugin("cargo-clean", CARGO_SCRIPT);
let input = make_input(clean_output, "cargo", "build", vec!["build"], Level::Ultra, 0);
let result = filter.filter(&input).unwrap();
assert!(
result.text.contains("cargo build: ok"),
"clean build should show ok, got: {}",
result.text
);
}
#[test]
fn cargo_build_full_strips_compiling_noise() {
let (filter, _dir) = temp_plugin("cargo-full", CARGO_SCRIPT);
let input = make_input(
CARGO_BUILD_OUTPUT,
"cargo",
"build",
vec!["build"],
Level::Full,
0,
);
let result = filter.filter(&input).unwrap();
assert!(!result.text.contains("Compiling serde"), "Compiling noise should be stripped");
assert!(result.text.contains("warning: unused variable"), "warnings should remain");
assert!(result.text.contains("Finished"), "Finished line should remain");
}
#[test]
fn cargo_test_ultra_shows_failures_only() {
let (filter, _dir) = temp_plugin("cargo-test", CARGO_SCRIPT);
let input = make_input(
CARGO_TEST_OUTPUT,
"cargo",
"test",
vec!["test"],
Level::Ultra,
101,
);
let result = filter.filter(&input).unwrap();
assert!(result.text.contains("test result: FAILED"), "result summary missing");
assert!(result.text.contains("overflow_check"), "failed test name missing");
assert!(!result.text.contains("basic_add"), "passing tests should be stripped");
}
#[test]
fn cargo_test_full_strips_ok_tests() {
let (filter, _dir) = temp_plugin("cargo-test-full", CARGO_SCRIPT);
let input = make_input(
CARGO_TEST_OUTPUT,
"cargo",
"test",
vec!["test"],
Level::Full,
101,
);
let result = filter.filter(&input).unwrap();
assert!(!result.text.contains("Compiling myapp"), "Compiling should be stripped");
assert!(result.text.contains("FAILED"), "FAILED should remain");
assert!(result.text.contains("test result:"), "result summary should remain");
}
#[test]
fn pipeline_with_shell_plugin_and_builtins() {
use lowfat_core::pipeline::Pipeline;
use lowfat_runner::runner::execute_pipeline;
use std::collections::HashMap;
let upper_script = "#!/bin/sh\ncat | tr '[:lower:]' '[:upper:]'";
let (filter, _dir) = temp_plugin("upper", upper_script);
let pipeline = Pipeline::parse("strip-ansi | upper-compact | head");
let raw = "\x1b[31mhello world\x1b[0m\nsecond line\nthird line";
let input = make_input(raw, "test", "", vec![], Level::Full, 0);
let mut plugin_map: HashMap<String, Box<dyn FilterPlugin>> = HashMap::new();
plugin_map.insert("upper-compact".to_string(), Box::new(filter));
let result = execute_pipeline(&pipeline, raw, &input, &plugin_map).unwrap();
assert!(result.contains("HELLO WORLD"), "should be uppercased: {}", result);
assert!(!result.contains("\x1b["), "ANSI should be stripped");
}
#[test]
fn all_levels_propagated() {
let script = r#"#!/bin/sh
echo "level=$LOWFAT_LEVEL"
"#;
let (filter, _dir) = temp_plugin("leveltest", script);
for (level, expected) in [
(Level::Lite, "level=lite"),
(Level::Full, "level=full"),
(Level::Ultra, "level=ultra"),
] {
let input = make_input("ignored", "test", "", vec![], level, 0);
let result = filter.filter(&input).unwrap();
assert!(
result.text.contains(expected),
"level {:?}: expected '{}', got '{}'",
level,
expected,
result.text.trim()
);
}
}
#[test]
fn exit_code_reaches_script() {
let script = r#"#!/bin/sh
RAW=$(cat)
EXIT="$LOWFAT_EXIT_CODE"
if [ "$EXIT" != "0" ]; then
echo "ERROR (exit $EXIT):"
echo "$RAW"
else
echo "$RAW" | head -n 5
fi
"#;
let (filter, _dir) = temp_plugin("exitcode", script);
let input = make_input("all good", "test", "", vec![], Level::Full, 0);
let result = filter.filter(&input).unwrap();
assert!(!result.text.contains("ERROR"), "should not show error on exit 0");
let input = make_input("something broke", "test", "", vec![], Level::Full, 1);
let result = filter.filter(&input).unwrap();
assert!(result.text.contains("ERROR (exit 1)"), "should show error on exit 1");
assert!(result.text.contains("something broke"), "should preserve raw on error");
}