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

View File

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

47
Cargo.lock generated
View File

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

View File

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

View File

@@ -244,7 +244,7 @@ mod tests {
version, version,
header, header,
events, 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(); let events = events.collect::<Result<Vec<Event>>>().unwrap();
@@ -262,7 +262,7 @@ mod tests {
version, version,
header, header,
events, 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(); let events = events.collect::<Result<Vec<Event>>>().unwrap();
assert_eq!(version, 1); 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 (events_tx, events_rx) = mpsc::channel::<Event>(1024);
let winsize = tty.get_size(); let winsize = tty.get_size();
let pty = pty::spawn(command, winsize, extra_env)?; 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, epoch,
events_tx, events_tx,
input_decoder: Utf8Decoder::new(), input_decoder: Utf8Decoder::new(),
@@ -97,7 +97,10 @@ pub async fn run<S: AsRef<str>, T: Tty + ?Sized, N: Notifier>(
tty_size: winsize.into(), 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>>) { 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> { 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 = let mut signals =
Signals::new([SIGWINCH, SIGINT, SIGTERM, SIGQUIT, SIGHUP, SIGALRM, SIGCHLD])?; Signals::new([SIGWINCH, SIGINT, SIGTERM, SIGQUIT, SIGHUP, SIGALRM, SIGCHLD])?;
let mut output_buf = [0u8; BUF_SIZE]; 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() { if !output.is_empty() {
self.handle_output(&output).await;
let _ = tty.write_all(&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 #!/usr/bin/env bash
set -eExuo pipefail set -eEuo pipefail -o errtrace
if ! command -v "pkill" >/dev/null 2>&1; then # Colors for output (disabled if no TTY or NO_COLOR set)
printf "error: pkill not installed\n" if [[ ! -t 1 ]] || [[ -n "${NO_COLOR:-}" ]]; then
exit 1 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 fi
python3 -V # Test tracking
TESTS_RUN=0
TESTS_PASSED=0
TESTS_FAILED=0
# Helper functions
log_info() {
printf "%b\n" "${BLUE}INFO:${NC} $*"
}
log_success() {
printf "%b\n" "${GREEN}PASS:${NC} $*"
((TESTS_PASSED++))
}
log_error() {
printf "%b\n" "${RED}FAIL:${NC} $*"
((TESTS_FAILED++))
}
log_warning() {
printf "%b\n" "${YELLOW}WARN:${NC} $*"
}
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
}
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
}
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
}
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
}
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
}
# SETUP
setup() {
log_info "Setting up test environment..."
ASCIINEMA_CONFIG_HOME="$( ASCIINEMA_CONFIG_HOME="$(
mktemp -d 2>/dev/null || mktemp -d -t asciinema-config-home mktemp -d 2>/dev/null || mktemp -d -t asciinema-config-home
)" )"
export ASCIINEMA_CONFIG_HOME ASCIINEMA_STATE_HOME="$(
mktemp -d 2>/dev/null || mktemp -d -t asciinema-state-home
)"
ASCIINEMA_GEN_DIR="$(
mktemp -d 2>/dev/null || mktemp -d -t asciinema-gen-dir
)"
export ASCIINEMA_CONFIG_HOME ASCIINEMA_STATE_HOME ASCIINEMA_GEN_DIR
export ASCIINEMA_SERVER_URL=https://asciinema.example.com
TMP_DATA_DIR="$(mktemp -d 2>/dev/null || mktemp -d -t asciinema-data-dir)" 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"
trap 'rm -rf ${ASCIINEMA_CONFIG_HOME} ${TMP_DATA_DIR}' EXIT trap 'cleanup' EXIT
asciinema() { log_info "Building release binary..."
python3 -m asciinema "${@}" cargo build --release --locked
# disable notifications
printf "[notifications]\nenabled = false\n" >> "${ASCIINEMA_CONFIG_HOME}/config.toml"
log_info "Setup complete"
} }
## disable notifications cleanup() {
log_info "Cleaning up..."
rm -rf "${ASCIINEMA_CONFIG_HOME:-}" "${ASCIINEMA_STATE_HOME:-}" "${ASCIINEMA_GEN_DIR:-}" "${TMP_DATA_DIR:-}"
}
printf "[notifications]\nenabled = no\n" >> "${ASCIINEMA_CONFIG_HOME}/config" # Test runner function
run_test() {
local test_name="$1"
shift
## test help message if [[ -z "${TEST:-}" || "${TEST:-}" == "$test_name" ]]; then
echo
echo "#################### TEST $test_name ####################"
"$@" || true # Don't exit on test failure
fi
}
asciinema -h # TEST FUNCTIONS
## test version command test_help() {
log_info "Testing help command..."
asciinema --version # 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 auth command # 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"
asciinema auth # 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"
}
## test play command test_version() {
log_info "Testing version command..."
# asciicast v1 # Test short version
asciinema play -s 5 tests/demo.json local output rc
asciinema play -s 5 -i 0.2 tests/demo.json if output=$("$ASCIINEMA_BIN" -V 2>&1); then rc=0; else rc=$?; fi
# shellcheck disable=SC2002 assert_exit_code 0 "$rc" "version short flag"
cat tests/demo.json | asciinema play -s 5 - assert_output_contains "asciinema" "$output" "version output format"
# asciicast v2 # Test long version
asciinema play -s 5 tests/demo.cast if output=$("$ASCIINEMA_BIN" --version 2>&1); then rc=0; else rc=$?; fi
asciinema play -s 5 -i 0.2 tests/demo.cast assert_exit_code 0 "$rc" "version long flag"
# shellcheck disable=SC2002 assert_output_contains "asciinema" "$output" "version output format"
cat tests/demo.cast | asciinema play -s 5 - }
## test cat command test_auth() {
log_info "Testing auth command..."
# asciicast v1 # Test auth command (should handle offline gracefully)
asciinema cat tests/demo.json local output rc
# shellcheck disable=SC2002 if output=$("$ASCIINEMA_BIN" auth 2>&1); then rc=0; else rc=$?; fi
cat tests/demo.json | asciinema cat -
# asciicast v2 # Auth should complete without hanging and show expected message
asciinema cat tests/demo.cast assert_exit_code 0 "$rc" "auth"
# shellcheck disable=SC2002 assert_output_contains "Open the following URL in a web browser" "$output" "auth command output"
cat tests/demo.cast | asciinema cat - }
## test rec command test_record() {
log_info "Testing record command..."
# normal program # Test basic recording
asciinema rec -c 'bash -c "echo t3st; sleep 2; echo ok"' "${TMP_DATA_DIR}/1a.cast" local file1="$TMP_DATA_DIR/record_basic.cast"
grep '"o",' "${TMP_DATA_DIR}/1a.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"
# very quickly exiting program # Test different formats
asciinema rec -c whoami "${TMP_DATA_DIR}/1b.cast" local file2="$TMP_DATA_DIR/record_v2.cast"
grep '"o",' "${TMP_DATA_DIR}/1b.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"
# signal handling local file3="$TMP_DATA_DIR/record_v3.cast"
bash -c "sleep 1; pkill -28 -n -f 'm asciinema'" & if "$ASCIINEMA_BIN" record --headless --command 'echo "test v3"' --output-format asciicast-v3 --return "$file3"; then rc=0; else rc=$?; fi
asciinema rec -c 'bash -c "echo t3st; sleep 2; echo ok"' "${TMP_DATA_DIR}/2.cast" assert_exit_code 0 "$rc" "record v3 format"
assert_file_not_empty "$file3" "record v3 format"
bash -c "sleep 1; pkill -n -f 'bash -c echo t3st'" & # Test raw format
asciinema rec -c 'bash -c "echo t3st; sleep 2; echo ok"' "${TMP_DATA_DIR}/3.cast" 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"
bash -c "sleep 1; pkill -9 -n -f 'bash -c echo t3st'" & # Test txt format
asciinema rec -c 'bash -c "echo t3st; sleep 2; echo ok"' "${TMP_DATA_DIR}/4.cast" 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"
# with stdin recording # Test return flag with failure
echo "ls" | asciinema rec --stdin -c 'bash -c "sleep 1"' "${TMP_DATA_DIR}/5.cast" local file6="$TMP_DATA_DIR/record_fail.cast"
cat "${TMP_DATA_DIR}/5.cast" if "$ASCIINEMA_BIN" record --headless --command 'exit 42' --return "$file6"; then rc=0; else rc=$?; fi
grep '"i", "ls\\n"' "${TMP_DATA_DIR}/5.cast" assert_exit_code 42 "$rc" "record return flag with failure"
grep '"o",' "${TMP_DATA_DIR}/5.cast" assert_file_not_empty "$file6" "record failure"
# raw output recording # Test append mode
asciinema rec --raw -c 'bash -c "echo t3st; sleep 1; echo ok"' "${TMP_DATA_DIR}/6.raw" 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"
# appending to existing recording # Test idle time limits
asciinema rec -c 'echo allright!; sleep 0.1' "${TMP_DATA_DIR}/7.cast" local file8="$TMP_DATA_DIR/record_idle.cast"
asciinema rec --append -c uptime "${TMP_DATA_DIR}/7.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 test_stream() {
printf "[record]\nadd_marker_key = C-b\n" >> "${ASCIINEMA_CONFIG_HOME}/config" log_info "Testing stream command..."
(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 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