Merge branch 'develop' into dependabot/github_actions/nixbuild/nix-quick-install-action-33

This commit is contained in:
Marcin Kulik
2025-09-11 15:30:10 +02:00
committed by GitHub
17 changed files with 459 additions and 309 deletions

View File

@@ -18,7 +18,7 @@ jobs:
rust: [default, msrv]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Install Nix
uses: nixbuild/nix-quick-install-action@v33
@@ -32,9 +32,12 @@ jobs:
- name: Build
run: nix develop .#${{ matrix.rust }} --command cargo build --verbose
- name: Run tests
- name: Run cargo tests
run: nix develop .#${{ matrix.rust }} --command cargo test --verbose
- name: Run integration tests
run: nix develop .#${{ matrix.rust }} --command tests/integration.sh
- name: Check formatting
run: nix develop .#${{ matrix.rust }} --command cargo fmt --check

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Create the release
env:
@@ -53,7 +53,7 @@ jobs:
CARGO: cargo
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

47
Cargo.lock generated
View File

@@ -84,7 +84,7 @@ checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "asciinema"
version = "3.0.0-rc.5"
version = "3.0.0"
dependencies = [
"anyhow",
"async-trait",
@@ -813,11 +813,11 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "matchers"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata 0.1.10",
"regex-automata",
]
[[package]]
@@ -1060,27 +1060,6 @@ dependencies = [
"getrandom 0.3.3",
]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
@@ -1089,15 +1068,9 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.8.5",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.5"
@@ -1478,9 +1451,9 @@ dependencies = [
[[package]]
name = "slab"
version = "0.4.10"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
[[package]]
name = "smallvec"
@@ -1779,13 +1752,13 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.19"
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
dependencies = [
"matchers",
"once_cell",
"regex",
"regex-automata",
"sharded-slab",
"thread_local",
"tracing",

View File

@@ -1,18 +1,16 @@
[package]
name = "asciinema"
version = "3.0.0-rc.5"
version = "3.0.0"
edition = "2021"
authors = ["Marcin Kulik <m@ku1ik.com>"]
homepage = "https://asciinema.org"
repository = "https://github.com/asciinema/asciinema"
description = "Terminal session recorder"
description = "Terminal session recorder, streamer, and player"
license = "GPL-3.0"
# MSRV
rust-version = "1.75.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0"
nix = { version = "0.30", features = ["fs", "term", "process", "signal", "poll"] }
@@ -34,7 +32,7 @@ tokio-stream = { version = "0.1", default-features = false, features = ["sync",
rust-embed = "8.0"
tower-http = { version = "0.6", features = ["trace"] }
tracing = { version = "0.1", default-features = false }
tracing-subscriber = { version = "0.3.19", default-features = false, features = ["fmt", "env-filter"] }
tracing-subscriber = { version = "0.3.20", default-features = false, features = ["fmt", "env-filter"] }
rgb = { version = "0.8", default-features = false }
url = "2.5"
tokio-tungstenite = { version = "0.26", default-features = false, features = ["connect", "rustls-tls-native-roots"] }

View File

@@ -22,7 +22,7 @@ let
})
];
buildInputs = [ pkgs.bashInteractive ];
packages = [ pkgs.shellcheck ];
env.RUST_BACKTRACE = 1;
};

View File

@@ -244,7 +244,7 @@ mod tests {
version,
header,
events,
} = super::open_from_path("tests/casts/minimal.json").unwrap();
} = super::open_from_path("tests/casts/minimal-v1.json").unwrap();
let events = events.collect::<Result<Vec<Event>>>().unwrap();
@@ -262,7 +262,7 @@ mod tests {
version,
header,
events,
} = super::open_from_path("tests/casts/full.json").unwrap();
} = super::open_from_path("tests/casts/full-v1.json").unwrap();
let events = events.collect::<Result<Vec<Event>>>().unwrap();
assert_eq!(version, 1);

View File

@@ -81,9 +81,9 @@ pub async fn run<S: AsRef<str>, T: Tty + ?Sized, N: Notifier>(
let (events_tx, events_rx) = mpsc::channel::<Event>(1024);
let winsize = tty.get_size();
let pty = pty::spawn(command, winsize, extra_env)?;
tokio::spawn(forward_events(events_rx, outputs));
let forwarder = tokio::spawn(forward_events(events_rx, outputs));
let mut session = Session {
let session = Session {
epoch,
events_tx,
input_decoder: Utf8Decoder::new(),
@@ -97,7 +97,10 @@ pub async fn run<S: AsRef<str>, T: Tty + ?Sized, N: Notifier>(
tty_size: winsize.into(),
};
session.run(pty, tty).await
let result = session.run(pty, tty).await;
let _ = forwarder.await;
result
}
async fn forward_events(mut events_rx: mpsc::Receiver<Event>, outputs: Vec<Box<dyn Output>>) {
@@ -131,7 +134,7 @@ async fn forward_event(mut output: Box<dyn Output>, event: Event) -> Option<Box<
}
impl<N: Notifier> Session<N> {
async fn run<T: Tty + ?Sized>(&mut self, pty: Pty, tty: &mut T) -> anyhow::Result<i32> {
async fn run<T: Tty + ?Sized>(mut self, pty: Pty, tty: &mut T) -> anyhow::Result<i32> {
let mut signals =
Signals::new([SIGWINCH, SIGINT, SIGTERM, SIGQUIT, SIGHUP, SIGALRM, SIGCHLD])?;
let mut output_buf = [0u8; BUF_SIZE];
@@ -202,8 +205,16 @@ impl<N: Notifier> Session<N> {
}
}
while let Ok(n) = pty.read(&mut output_buf).await {
if n > 0 {
self.handle_output(&output_buf[..n]).await;
output.extend_from_slice(&output_buf[0..n]);
} else {
break;
}
}
if !output.is_empty() {
self.handle_output(&output).await;
let _ = tty.write_all(&output).await;
}

View File

@@ -1,38 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
readonly DISTROS=(
'arch'
'alpine'
'centos'
'debian'
'fedora'
'ubuntu'
)
readonly DOCKER='docker'
# do not redefine builtin `test`
test_() {
local -r tag="${1}"
local -ra docker_opts=(
"--tag=asciinema/asciinema:${tag}"
"--file=tests/distros/Dockerfile.${tag}"
)
printf "\e[1;32mTesting on %s...\e[0m\n\n" "${tag}"
# shellcheck disable=SC2068
"${DOCKER}" build ${docker_opts[@]} .
"${DOCKER}" run --rm -it "asciinema/asciinema:${tag}" tests/integration.sh
}
for distro in "${DISTROS[@]}"; do
test_ "${distro}"
done
printf "\n\e[1;32mAll tests passed.\e[0m\n"

View File

@@ -1,19 +0,0 @@
# syntax=docker/dockerfile:1.3
FROM docker.io/library/alpine:3.15
# https://github.com/actions/runner/issues/241
RUN apk --no-cache add bash ca-certificates make python3 util-linux
WORKDIR /usr/src/app
COPY asciinema/ asciinema/
COPY tests/ tests/
ENV LANG="en_US.utf8"
USER nobody
ENTRYPOINT ["/bin/bash"]
# vim:ft=dockerfile

View File

@@ -1,22 +0,0 @@
# syntax=docker/dockerfile:1.3
FROM docker.io/library/archlinux:latest
RUN pacman-key --init \
&& pacman --sync --refresh --sysupgrade --noconfirm make python3 \
&& printf "LANG=en_US.UTF-8\n" > /etc/locale.conf \
&& locale-gen \
&& pacman --sync --clean --clean --noconfirm
WORKDIR /usr/src/app
COPY asciinema/ asciinema/
COPY tests/ tests/
ENV LANG="en_US.utf8"
USER nobody
ENTRYPOINT ["/bin/bash"]
# vim:ft=dockerfile

View File

@@ -1,18 +0,0 @@
# syntax=docker/dockerfile:1.3
FROM docker.io/library/centos:7
RUN yum install -y epel-release && yum install -y make python36 && yum clean all
WORKDIR /usr/src/app
COPY asciinema/ asciinema/
COPY tests/ tests/
ENV LANG="en_US.utf8"
USER nobody
ENTRYPOINT ["/bin/bash"]
# vim:ft=dockerfile

View File

@@ -1,33 +0,0 @@
# syntax=docker/dockerfile:1.3
FROM docker.io/library/debian:bullseye
ENV DEBIAN_FRONTENT="noninteractive"
RUN apt-get update \
&& apt-get install -y \
ca-certificates \
locales \
make \
procps \
python3 \
&& localedef \
-i en_US \
-c \
-f UTF-8 \
-A /usr/share/locale/locale.alias \
en_US.UTF-8 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src/app
COPY asciinema/ asciinema/
COPY tests/ tests/
ENV LANG="en_US.utf8"
USER nobody
ENV SHELL="/bin/bash"
# vim:ft=dockerfile

View File

@@ -1,20 +0,0 @@
# syntax=docker/dockerfile:1.3
# https://medium.com/nttlabs/ubuntu-21-10-and-fedora-35-do-not-work-on-docker-20-10-9-1cd439d9921
# https://www.mail-archive.com/ubuntu-bugs@lists.ubuntu.com/msg5971024.html
FROM registry.fedoraproject.org/fedora:34
RUN dnf install -y make python3 procps && dnf clean all
WORKDIR /usr/src/app
COPY asciinema/ asciinema/
COPY tests/ tests/
ENV LANG="en_US.utf8"
ENV SHELL="/bin/bash"
USER nobody
ENTRYPOINT ["/bin/bash"]
# vim:ft=dockerfile

View File

@@ -1,32 +0,0 @@
# syntax=docker/dockerfile:1.3
FROM docker.io/library/ubuntu:20.04
ENV DEBIAN_FRONTENT="noninteractive"
RUN apt-get update \
&& apt-get install -y \
ca-certificates \
locales \
make \
python3 \
&& localedef \
-i en_US \
-c \
-f UTF-8 \
-A /usr/share/locale/locale.alias \
en_US.UTF-8 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src/app
COPY asciinema/ asciinema/
COPY tests/ tests/
ENV LANG="en_US.utf8"
USER nobody
ENTRYPOINT ["/bin/bash"]
# vim:ft=dockerfile

View File

@@ -1,104 +1,451 @@
#!/usr/bin/env bash
set -eExuo pipefail
set -eEuo pipefail -o errtrace
if ! command -v "pkill" >/dev/null 2>&1; then
printf "error: pkill not installed\n"
exit 1
# Colors for output (disabled if no TTY or NO_COLOR set)
if [[ ! -t 1 ]] || [[ -n "${NO_COLOR:-}" ]]; then
RED=""
GREEN=""
YELLOW=""
BLUE=""
NC=""
else
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
fi
python3 -V
# Test tracking
TESTS_RUN=0
TESTS_PASSED=0
TESTS_FAILED=0
ASCIINEMA_CONFIG_HOME="$(
mktemp -d 2>/dev/null || mktemp -d -t asciinema-config-home
)"
export ASCIINEMA_CONFIG_HOME
TMP_DATA_DIR="$(mktemp -d 2>/dev/null || mktemp -d -t asciinema-data-dir)"
trap 'rm -rf ${ASCIINEMA_CONFIG_HOME} ${TMP_DATA_DIR}' EXIT
asciinema() {
python3 -m asciinema "${@}"
# Helper functions
log_info() {
printf "%b\n" "${BLUE}INFO:${NC} $*"
}
## disable notifications
log_success() {
printf "%b\n" "${GREEN}PASS:${NC} $*"
((TESTS_PASSED++))
}
printf "[notifications]\nenabled = no\n" >> "${ASCIINEMA_CONFIG_HOME}/config"
log_error() {
printf "%b\n" "${RED}FAIL:${NC} $*"
((TESTS_FAILED++))
}
## test help message
log_warning() {
printf "%b\n" "${YELLOW}WARN:${NC} $*"
}
asciinema -h
assert_exit_code() {
local expected=$1
local actual=$2
local test_name=$3
((TESTS_RUN++))
if [[ $actual -eq $expected ]]; then
log_success "$test_name - exit code $actual"
else
log_error "$test_name - expected exit code $expected, got $actual"
return 1
fi
}
## test version command
assert_file_exists() {
local file=$1
local test_name=$2
((TESTS_RUN++))
if [[ -f "$file" ]]; then
log_success "$test_name - file exists: $file"
else
log_error "$test_name - file does not exist: $file"
return 1
fi
}
asciinema --version
assert_file_not_empty() {
local file=$1
local test_name=$2
((TESTS_RUN++))
if [[ -s "$file" ]]; then
log_success "$test_name - file not empty: $file"
else
log_error "$test_name - file is empty: $file"
return 1
fi
}
## test auth command
assert_output_contains() {
local expected=$1
local output=$2
local test_name=$3
((TESTS_RUN++))
if echo "$output" | grep -q "$expected"; then
log_success "$test_name - output contains: $expected"
else
log_error "$test_name - output missing: $expected"
log_error "Actual output: $output"
return 1
fi
}
asciinema auth
assert_file_contains() {
local expected=$1
local file=$2
local test_name=$3
((TESTS_RUN++))
if grep -q "$expected" "$file"; then
log_success "$test_name - file contains: $expected"
else
log_error "$test_name - file missing: $expected"
return 1
fi
}
## test play command
# SETUP
setup() {
log_info "Setting up test environment..."
ASCIINEMA_CONFIG_HOME="$(
mktemp -d 2>/dev/null || mktemp -d -t asciinema-config-home
)"
# asciicast v1
asciinema play -s 5 tests/demo.json
asciinema play -s 5 -i 0.2 tests/demo.json
# shellcheck disable=SC2002
cat tests/demo.json | asciinema play -s 5 -
ASCIINEMA_STATE_HOME="$(
mktemp -d 2>/dev/null || mktemp -d -t asciinema-state-home
)"
# asciicast v2
asciinema play -s 5 tests/demo.cast
asciinema play -s 5 -i 0.2 tests/demo.cast
# shellcheck disable=SC2002
cat tests/demo.cast | asciinema play -s 5 -
ASCIINEMA_GEN_DIR="$(
mktemp -d 2>/dev/null || mktemp -d -t asciinema-gen-dir
)"
## test cat command
export ASCIINEMA_CONFIG_HOME ASCIINEMA_STATE_HOME ASCIINEMA_GEN_DIR
export ASCIINEMA_SERVER_URL=https://asciinema.example.com
# asciicast v1
asciinema cat tests/demo.json
# shellcheck disable=SC2002
cat tests/demo.json | asciinema cat -
TMP_DATA_DIR="$(mktemp -d 2>/dev/null || mktemp -d -t asciinema-data-dir)"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
FIXTURES="$SCRIPT_DIR/casts"
ASCIINEMA_BIN="$SCRIPT_DIR/../target/release/asciinema"
# asciicast v2
asciinema cat tests/demo.cast
# shellcheck disable=SC2002
cat tests/demo.cast | asciinema cat -
trap 'cleanup' EXIT
## test rec command
log_info "Building release binary..."
cargo build --release --locked
# normal program
asciinema rec -c 'bash -c "echo t3st; sleep 2; echo ok"' "${TMP_DATA_DIR}/1a.cast"
grep '"o",' "${TMP_DATA_DIR}/1a.cast"
# disable notifications
printf "[notifications]\nenabled = false\n" >> "${ASCIINEMA_CONFIG_HOME}/config.toml"
log_info "Setup complete"
}
# very quickly exiting program
asciinema rec -c whoami "${TMP_DATA_DIR}/1b.cast"
grep '"o",' "${TMP_DATA_DIR}/1b.cast"
cleanup() {
log_info "Cleaning up..."
rm -rf "${ASCIINEMA_CONFIG_HOME:-}" "${ASCIINEMA_STATE_HOME:-}" "${ASCIINEMA_GEN_DIR:-}" "${TMP_DATA_DIR:-}"
}
# signal handling
bash -c "sleep 1; pkill -28 -n -f 'm asciinema'" &
asciinema rec -c 'bash -c "echo t3st; sleep 2; echo ok"' "${TMP_DATA_DIR}/2.cast"
# Test runner function
run_test() {
local test_name="$1"
shift
if [[ -z "${TEST:-}" || "${TEST:-}" == "$test_name" ]]; then
echo
echo "#################### TEST $test_name ####################"
"$@" || true # Don't exit on test failure
fi
}
bash -c "sleep 1; pkill -n -f 'bash -c echo t3st'" &
asciinema rec -c 'bash -c "echo t3st; sleep 2; echo ok"' "${TMP_DATA_DIR}/3.cast"
# TEST FUNCTIONS
bash -c "sleep 1; pkill -9 -n -f 'bash -c echo t3st'" &
asciinema rec -c 'bash -c "echo t3st; sleep 2; echo ok"' "${TMP_DATA_DIR}/4.cast"
test_help() {
log_info "Testing help command..."
# Test short help
local output rc
if output=$("$ASCIINEMA_BIN" -h 2>&1); then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "help short flag"
assert_output_contains "Terminal session recorder" "$output" "help content"
assert_output_contains "Commands:" "$output" "help shows commands"
# Test long help
if output=$("$ASCIINEMA_BIN" --help 2>&1); then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "help long flag"
assert_output_contains "Terminal session recorder" "$output" "help content"
# Test help subcommand
if output=$("$ASCIINEMA_BIN" help 2>&1); then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "help subcommand"
assert_output_contains "Terminal session recorder" "$output" "help subcommand content"
}
# with stdin recording
echo "ls" | asciinema rec --stdin -c 'bash -c "sleep 1"' "${TMP_DATA_DIR}/5.cast"
cat "${TMP_DATA_DIR}/5.cast"
grep '"i", "ls\\n"' "${TMP_DATA_DIR}/5.cast"
grep '"o",' "${TMP_DATA_DIR}/5.cast"
test_version() {
log_info "Testing version command..."
# Test short version
local output rc
if output=$("$ASCIINEMA_BIN" -V 2>&1); then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "version short flag"
assert_output_contains "asciinema" "$output" "version output format"
# Test long version
if output=$("$ASCIINEMA_BIN" --version 2>&1); then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "version long flag"
assert_output_contains "asciinema" "$output" "version output format"
}
# raw output recording
asciinema rec --raw -c 'bash -c "echo t3st; sleep 1; echo ok"' "${TMP_DATA_DIR}/6.raw"
test_auth() {
log_info "Testing auth command..."
# Test auth command (should handle offline gracefully)
local output rc
if output=$("$ASCIINEMA_BIN" auth 2>&1); then rc=0; else rc=$?; fi
# Auth should complete without hanging and show expected message
assert_exit_code 0 "$rc" "auth"
assert_output_contains "Open the following URL in a web browser" "$output" "auth command output"
}
# appending to existing recording
asciinema rec -c 'echo allright!; sleep 0.1' "${TMP_DATA_DIR}/7.cast"
asciinema rec --append -c uptime "${TMP_DATA_DIR}/7.cast"
test_record() {
log_info "Testing record command..."
# Test basic recording
local file1="$TMP_DATA_DIR/record_basic.cast"
local rc
if "$ASCIINEMA_BIN" record --headless --command 'echo "hello world"' --return "$file1"; then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "record basic"
assert_file_contains '"o",' "$file1" "record output event"
assert_file_contains 'hello world' "$file1" "record output content"
# Test different formats
local file2="$TMP_DATA_DIR/record_v2.cast"
if "$ASCIINEMA_BIN" record --headless --command 'echo "test v2"' --output-format asciicast-v2 --return "$file2"; then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "record v2 format"
assert_file_not_empty "$file2" "record v2 format"
local file3="$TMP_DATA_DIR/record_v3.cast"
if "$ASCIINEMA_BIN" record --headless --command 'echo "test v3"' --output-format asciicast-v3 --return "$file3"; then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "record v3 format"
assert_file_not_empty "$file3" "record v3 format"
# Test raw format
local file4="$TMP_DATA_DIR/record_raw.raw"
if "$ASCIINEMA_BIN" record --headless --command 'echo "test raw"' --output-format raw --return "$file4"; then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "record raw format"
assert_file_not_empty "$file4" "record raw format"
# Test txt format
local file5="$TMP_DATA_DIR/record_txt.txt"
if "$ASCIINEMA_BIN" record --headless --command 'echo "test txt"' --output-format txt --return "$file5"; then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "record txt format"
assert_file_not_empty "$file5" "record txt format"
# Test return flag with failure
local file6="$TMP_DATA_DIR/record_fail.cast"
if "$ASCIINEMA_BIN" record --headless --command 'exit 42' --return "$file6"; then rc=0; else rc=$?; fi
assert_exit_code 42 "$rc" "record return flag with failure"
assert_file_not_empty "$file6" "record failure"
# Test append mode
local file7="$TMP_DATA_DIR/record_append.cast"
if "$ASCIINEMA_BIN" record --headless --command 'echo "first"' --return "$file7"; then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "record append setup"
if "$ASCIINEMA_BIN" record --headless --command 'echo "second"' --append --return "$file7"; then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "record append"
assert_file_contains 'first' "$file7" "record append first content"
assert_file_contains 'second' "$file7" "record append second content"
# Test idle time limits
local file8="$TMP_DATA_DIR/record_idle.cast"
if "$ASCIINEMA_BIN" record --headless --command 'bash -c "echo start; sleep 2; echo end"' --idle-time-limit 1 --return "$file8"; then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "record idle time limit"
assert_file_not_empty "$file8" "record idle time limit"
}
# adding a marker
printf "[record]\nadd_marker_key = C-b\n" >> "${ASCIINEMA_CONFIG_HOME}/config"
(bash -c "sleep 1; printf '.'; sleep 0.5; printf '\x08'; sleep 0.5; printf '\x02'; sleep 0.5; printf '\x04'") | asciinema rec -c /bin/bash "${TMP_DATA_DIR}/8.cast"
grep '"m",' "${TMP_DATA_DIR}/8.cast"
test_stream() {
log_info "Testing stream command..."
# Test local streaming
timeout 10s "$ASCIINEMA_BIN" stream --headless --local 127.0.0.1:8081 --command 'bash -c "echo streaming test; sleep 3; echo done"' --return &
local stream_pid=$!
# Wait a moment for server to start
sleep 1
# Test if HTTP server is responding and serving the player
local curl_output rc
if curl_output=$(curl -fsS "http://127.0.0.1:8081" 2>&1); then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "stream server responding"
assert_output_contains "AsciinemaPlayer" "$curl_output" "stream server AsciinemaPlayer content"
# Clean up
kill $stream_pid 2>/dev/null || true
wait $stream_pid 2>/dev/null || true
}
test_session() {
log_info "Testing session command..."
# Test session with file output
local file1="$TMP_DATA_DIR/session_basic.cast"
local rc
if "$ASCIINEMA_BIN" session --headless --output-file "$file1" --command 'echo "session test"' --return; then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "session basic"
assert_file_contains 'session test' "$file1" "session output content"
# Test session with return flag failure
local file2="$TMP_DATA_DIR/session_fail.cast"
if "$ASCIINEMA_BIN" session --headless --output-file "$file2" --command 'exit 13' --return; then rc=0; else rc=$?; fi
assert_exit_code 13 "$rc" "session return flag with failure"
assert_file_contains '"x", "13"' "$file2" "session exit event"
# Test session with local streaming + file output
local file3="$TMP_DATA_DIR/session_stream.cast"
timeout 8s "$ASCIINEMA_BIN" session --headless --output-file "$file3" --stream-local 127.0.0.1:8081 --command 'bash -c "echo stream session; sleep 3; echo done"' --return &
local session_pid=$!
# Wait a moment for server to start
sleep 1
# Test if both file and HTTP server work
local curl_output
if curl_output=$(curl -fsS "http://127.0.0.1:8081" 2>&1); then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "stream server responding"
assert_output_contains "AsciinemaPlayer" "$curl_output" "session streaming server AsciinemaPlayer content"
# Clean up and check file
kill $session_pid 2>/dev/null || true
wait $session_pid 2>/dev/null || true
if [[ -f "$file3" ]]; then
assert_file_contains 'stream session' "$file3" "session output content"
fi
# Test different output formats
local file4="$TMP_DATA_DIR/session_v2.cast"
if "$ASCIINEMA_BIN" session --headless --output-file "$file4" --output-format asciicast-v2 --command 'echo "session v2"' --return; then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "session v2 format"
assert_file_contains 'session v2' "$file4" "session output content"
# Test append mode
local file5="$TMP_DATA_DIR/session_append.cast"
if "$ASCIINEMA_BIN" session --headless --output-file "$file5" --command 'echo "first session"' --return; then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "session append setup"
if "$ASCIINEMA_BIN" session --headless --output-file "$file5" --append --command 'echo "second session"' --return; then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "session append"
assert_file_contains 'first session' "$file5" "session append first content"
assert_file_contains 'second session' "$file5" "session append second content"
}
test_cat() {
log_info "Testing cat command..."
# Create test recordings first
local file1="$TMP_DATA_DIR/cat_input1.cast"
local file2="$TMP_DATA_DIR/cat_input2.cast"
local rc
if "$ASCIINEMA_BIN" record --headless --command 'echo "first recording"' --return "$file1"; then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "cat setup first recording"
if "$ASCIINEMA_BIN" record --headless --command 'echo "second recording"' --return "$file2"; then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "cat setup second recording"
# Test concatenation to stdout
local output
if output=$("$ASCIINEMA_BIN" cat "$file1" "$file2" 2>&1); then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "cat concatenate"
assert_output_contains 'first recording' "$output" "cat first content"
assert_output_contains 'second recording' "$output" "cat second content"
# Test with different format inputs (using fixtures, v2+v3 only since v1 can't be concatenated)
if output=$("$ASCIINEMA_BIN" cat "$FIXTURES/minimal-v2.cast" "$FIXTURES/minimal-v3.cast" 2>&1); then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "cat mixed formats"
assert_output_contains '"version":' "$output" "cat mixed formats output"
}
test_convert() {
log_info "Testing convert command..."
# Test v1 to v3 conversion
local file1="$TMP_DATA_DIR/convert_v1_to_v3.cast"
local rc
if "$ASCIINEMA_BIN" convert "$FIXTURES/minimal-v1.json" "$file1"; then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "convert v1 to v3"
assert_file_contains '"version":3' "$file1" "convert v1 to v3 version"
# Test v2 to v3 conversion
local file2="$TMP_DATA_DIR/convert_v2_to_v3.cast"
if "$ASCIINEMA_BIN" convert "$FIXTURES/minimal-v2.cast" "$file2"; then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "convert v2 to v3"
assert_file_contains '"version":3' "$file2" "convert v2 to v3 version"
# Test to raw format
local file3="$TMP_DATA_DIR/convert_to_raw.raw"
if "$ASCIINEMA_BIN" convert --output-format raw "$FIXTURES/minimal-v2.cast" "$file3"; then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "convert to raw"
assert_file_exists "$file3" "convert to raw output"
# Test to txt format
local file4="$TMP_DATA_DIR/convert_to_txt.txt"
if "$ASCIINEMA_BIN" convert --output-format txt "$FIXTURES/minimal-v2.cast" "$file4"; then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "convert to txt"
assert_file_exists "$file4" "convert to txt output"
# Test output to stdout
local output
if output=$("$ASCIINEMA_BIN" convert "$FIXTURES/minimal-v2.cast" - 2>&1); then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "convert to stdout"
assert_output_contains '"version":3' "$output" "convert stdout version"
# Test overwrite behavior
local file5="$TMP_DATA_DIR/convert_overwrite.cast"
echo "existing content" > "$file5"
if "$ASCIINEMA_BIN" convert --overwrite "$FIXTURES/minimal-v2.cast" "$file5"; then rc=0; else rc=$?; fi
assert_exit_code 0 "$rc" "convert overwrite"
assert_file_contains '"version":3' "$file5" "convert overwrite content"
}
# MAIN EXECUTION
# Setup always runs
setup
echo
echo "######################################################"
echo "# ASCIINEMA CLI INTEGRATION TESTS"
echo "######################################################"
echo "# Test filter: ${TEST:-ALL}"
echo "######################################################"
# Individual test blocks
run_test "help" test_help
run_test "version" test_version
run_test "auth" test_auth
run_test "record" test_record
run_test "stream" test_stream
run_test "session" test_session
run_test "cat" test_cat
run_test "convert" test_convert
# Final summary
echo
echo "######################################################"
echo "# TEST SUMMARY"
echo "######################################################"
echo "Tests run: $TESTS_RUN"
printf "%bTests passed: %b%s%b\n" "" "${GREEN}" "$TESTS_PASSED" "${NC}"
if [[ $TESTS_FAILED -gt 0 ]]; then
printf "%bTests failed: %b%s%b\n" "" "${RED}" "$TESTS_FAILED" "${NC}"
echo "OVERALL RESULT: FAILED"
exit 1
else
printf "%bTests failed: %b%s%b\n" "" "${GREEN}" "0" "${NC}"
echo "OVERALL RESULT: SUCCESS"
fi