// 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, } }, }); } };