repositories / termux-janitor
termux-janitor
Interactive cleanup assistant for Termux: transparent, safe, confirmed disk reclamation.
owned by admin
tests/fixtures/cli_terminal.zig
Raw// Fixture for terminal admission (TJ-TERM-01). Verifies that an interactive
// invocation started without terminal stdin, with piped stdout, and with a dumb
// or absent TERM environment variable exits nonzero with one concise stderr
// diagnostic, empty stdout, and no state beneath the isolated working directory.
const std = @import("std");
const spec_data = @import("spec_data");
pub const StandardInput = enum { ignored };
pub const WorkingDirectory = enum { empty };
/// Admitted invocations observed by one execution of this fixture.
const cases_max = 8;
/// Arguments one listed invocation may carry, excluding the program name.
const case_arguments_max: usize = 4;
/// Bytes one startup diagnostic may occupy.
const diagnostic_bytes_max = 512;
/// The registered failure exit status.
const failure_status = spec_data.cliExitCode(.failure);
const stdin_diagnostic = spec_data.cli_executable ++ ": interactive mode requires a terminal stdin\n";
const stdout_diagnostic = spec_data.cli_executable ++ ": interactive mode requires a terminal stdout\n";
const term_diagnostic = spec_data.cli_executable ++ ": TERM must name a non-dumb terminal\n";
const Case = struct {
arguments: []const []const u8,
expected_diagnostic: []const u8,
result: ?std.process.RunResult = null,
};
pub const Fixture = struct {
temporary: std.testing.TmpDir,
executable: ?[:0]u8 = null,
cases: [cases_max]Case = undefined,
case_count: usize = 0,
stdin_ignored: bool = false,
executed: bool = false,
pub fn init() !Fixture {
return .{ .temporary = std.testing.tmpDir(.{ .iterate = true }) };
}
pub fn deinit(fixture: *Fixture) void {
for (fixture.cases[0..fixture.case_count]) |case| {
if (case.result) |result| {
std.testing.allocator.free(result.stdout);
std.testing.allocator.free(result.stderr);
}
}
if (fixture.executable) |executable| std.testing.allocator.free(executable);
fixture.temporary.cleanup();
fixture.* = undefined;
}
pub fn given_program(fixture: *Fixture, executable_name: []const u8) !void {
std.debug.assert(fixture.executable == null);
try std.testing.expectEqualStrings(spec_data.cli_executable, executable_name);
fixture.executable = try std.Io.Dir.cwd().realPathFileAlloc(
std.testing.io,
"zig-out/bin/termux-janitor",
std.testing.allocator,
);
}
pub fn given_standard_input(fixture: *Fixture, mode: StandardInput) !void {
std.debug.assert(!fixture.stdin_ignored);
try std.testing.expectEqual(StandardInput.ignored, mode);
fixture.stdin_ignored = true;
}
pub fn given_working_directory(fixture: *Fixture, state: WorkingDirectory) !void {
try std.testing.expectEqual(WorkingDirectory.empty, state);
var entries = fixture.temporary.dir.iterate();
try std.testing.expect(try entries.next(std.testing.io) == null);
}
/// One admission case with no arguments: interactive mode attempted against a
/// piped nonterminal stdin and an inherited TERM, which fails at stdin check.
pub fn given_no_arguments(fixture: *Fixture) !void {
std.debug.assert(!fixture.executed);
try std.testing.expect(fixture.case_count < cases_max);
try std.testing.expect(fixture.stdin_ignored);
fixture.cases[fixture.case_count] = .{
.arguments = &[_][]const u8{},
.expected_diagnostic = stdin_diagnostic,
};
fixture.case_count += 1;
}
/// One admission case with TERM=dumb: passes the stdin check on a TTY but fails
/// the TERM check. Used only when stdin is a real terminal.
pub fn given_dumb_term(fixture: *Fixture) !void {
std.debug.assert(!fixture.executed);
try std.testing.expect(fixture.case_count < cases_max);
fixture.cases[fixture.case_count] = .{
.arguments = &[_][]const u8{},
.expected_diagnostic = term_diagnostic,
};
fixture.case_count += 1;
}
/// One admission case with a nonterminal stdout: bypasses stdin (a TTY) but
/// fails the stdout check. Used only when stdin is a real terminal.
pub fn given_stdout_not_tty(fixture: *Fixture) !void {
std.debug.assert(!fixture.executed);
try std.testing.expect(fixture.case_count < cases_max);
fixture.cases[fixture.case_count] = .{
.arguments = &[_][]const u8{},
.expected_diagnostic = stdout_diagnostic,
};
fixture.case_count += 1;
}
pub fn when_run_each(fixture: *Fixture) !void {
const executable = fixture.executable orelse return error.ExecutableNotConfigured;
if (fixture.case_count == 0) return error.CasesNotConfigured;
if (!fixture.stdin_ignored) return error.StandardInputNotConfigured;
for (fixture.cases[0..fixture.case_count]) |*case| {
var argv: [1 + case_arguments_max][]const u8 = undefined;
argv[0] = executable;
case.result = try fixture.run(argv[0 .. case.arguments.len + 1]);
}
fixture.executed = true;
}
pub fn then_inv_term_status(fixture: *Fixture) !void {
for (fixture.cases[0..fixture.case_count]) |case| {
const result = case.result orelse return error.ProcessNotRun;
switch (result.term) {
.exited => |status| try std.testing.expectEqual(
failure_status,
status,
),
else => return error.UnexpectedTermination,
}
}
}
pub fn then_inv_term_diagnostic(fixture: *Fixture) !void {
for (fixture.cases[0..fixture.case_count]) |case| {
const stderr = case.result.?.stderr;
try std.testing.expect(stderr.len <= diagnostic_bytes_max);
try std.testing.expectEqualStrings(case.expected_diagnostic, stderr);
try std.testing.expect(std.unicode.utf8ValidateSlice(stderr));
for (stderr) |byte| {
if (byte == '\n') continue;
try std.testing.expect(byte >= 0x20 and byte != 0x7f);
}
}
}
pub fn then_inv_term_not_implemented(fixture: *Fixture) !void {
// Terminal passes admission is not covered by this fixture.
std.debug.assert(fixture.case_count == 0);
}
pub fn forbid_no_term_stdout(fixture: *Fixture) !void {
for (fixture.cases[0..fixture.case_count]) |case| {
try std.testing.expectEqualStrings("", case.result.?.stdout);
}
}
pub fn forbid_no_term_fixture_mutation(fixture: *Fixture) !void {
var entries = fixture.temporary.dir.iterate();
try std.testing.expect(try entries.next(std.testing.io) == null);
}
fn run(fixture: *Fixture, argv: []const []const u8) !std.process.RunResult {
std.debug.assert(argv.len > 0);
std.debug.assert(fixture.stdin_ignored);
var env_map = std.process.Environ.Map.init(std.testing.allocator);
defer env_map.deinit();
return std.process.run(std.testing.allocator, std.testing.io, .{
.argv = argv,
.stderr_limit = .limited(@intCast(spec_data.limit_value.captured_stderr_bytes)),
.stdout_limit = .limited(@intCast(spec_data.limit_value.captured_stdout_bytes)),
.reserve_amount = diagnostic_bytes_max,
.cwd = .{ .dir = fixture.temporary.dir },
.environ_map = &env_map,
.expand_arg0 = .no_expand,
.progress_node = .none,
.create_no_window = true,
.disable_aslr = false,
.timeout = .{ .duration = .{
.raw = .fromSeconds(@intCast(spec_data.limit_value.test_process_timeout)),
.clock = .awake,
} },
});
}
};