From ce10bad5cbb2f32b042b92aeec1e598089dbfc39 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 10:30:20 +0000 Subject: [PATCH 01/13] Bump nixbuild/nix-quick-install-action from 31 to 32 Bumps [nixbuild/nix-quick-install-action](https://github.com/nixbuild/nix-quick-install-action) from 31 to 32. - [Release notes](https://github.com/nixbuild/nix-quick-install-action/releases) - [Changelog](https://github.com/nixbuild/nix-quick-install-action/blob/master/RELEASE) - [Commits](https://github.com/nixbuild/nix-quick-install-action/compare/v31...v32) --- updated-dependencies: - dependency-name: nixbuild/nix-quick-install-action dependency-version: '32' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb0a0a9..54b6fa1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v4 - name: Install Nix - uses: nixbuild/nix-quick-install-action@v31 + uses: nixbuild/nix-quick-install-action@v32 - name: Setup Nix cache uses: nix-community/cache-nix-action@v6 From 8aab1b8d99211bb7836a484298136d5eb411c201 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:56:52 +0000 Subject: [PATCH 02/13] Bump actions/checkout from 4 to 5 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb0a0a9..2ea6ee6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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@v31 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d12bcb7..ad88e3c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 From 56f4c06c8ccf2e6d1c2f3e181cdf09f22e0a0f08 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Aug 2025 20:57:14 +0000 Subject: [PATCH 03/13] Bump tracing-subscriber from 0.3.19 to 0.3.20 Bumps [tracing-subscriber](https://github.com/tokio-rs/tracing) from 0.3.19 to 0.3.20. - [Release notes](https://github.com/tokio-rs/tracing/releases) - [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.19...tracing-subscriber-0.3.20) --- updated-dependencies: - dependency-name: tracing-subscriber dependency-version: 0.3.20 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Cargo.lock | 41 +++++++---------------------------------- Cargo.toml | 2 +- 2 files changed, 8 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6e933d1..8ef10ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 1d33d2d..4185f40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,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"] } From 1eda16b912a3f5d095ab331b4457092c43905168 Mon Sep 17 00:00:00 2001 From: Marcin Kulik Date: Sun, 31 Aug 2025 21:32:49 +0200 Subject: [PATCH 04/13] Remove old tests for Python impl --- tests/distros.sh | 38 --------------------------------- tests/distros/Dockerfile.alpine | 19 ----------------- tests/distros/Dockerfile.arch | 22 ------------------- tests/distros/Dockerfile.centos | 18 ---------------- tests/distros/Dockerfile.debian | 33 ---------------------------- tests/distros/Dockerfile.fedora | 20 ----------------- tests/distros/Dockerfile.ubuntu | 32 --------------------------- 7 files changed, 182 deletions(-) delete mode 100755 tests/distros.sh delete mode 100644 tests/distros/Dockerfile.alpine delete mode 100644 tests/distros/Dockerfile.arch delete mode 100644 tests/distros/Dockerfile.centos delete mode 100644 tests/distros/Dockerfile.debian delete mode 100644 tests/distros/Dockerfile.fedora delete mode 100644 tests/distros/Dockerfile.ubuntu diff --git a/tests/distros.sh b/tests/distros.sh deleted file mode 100755 index c34d272..0000000 --- a/tests/distros.sh +++ /dev/null @@ -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" diff --git a/tests/distros/Dockerfile.alpine b/tests/distros/Dockerfile.alpine deleted file mode 100644 index 9716325..0000000 --- a/tests/distros/Dockerfile.alpine +++ /dev/null @@ -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 diff --git a/tests/distros/Dockerfile.arch b/tests/distros/Dockerfile.arch deleted file mode 100644 index 3224495..0000000 --- a/tests/distros/Dockerfile.arch +++ /dev/null @@ -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 diff --git a/tests/distros/Dockerfile.centos b/tests/distros/Dockerfile.centos deleted file mode 100644 index bc4fd7e..0000000 --- a/tests/distros/Dockerfile.centos +++ /dev/null @@ -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 diff --git a/tests/distros/Dockerfile.debian b/tests/distros/Dockerfile.debian deleted file mode 100644 index 6c14287..0000000 --- a/tests/distros/Dockerfile.debian +++ /dev/null @@ -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 diff --git a/tests/distros/Dockerfile.fedora b/tests/distros/Dockerfile.fedora deleted file mode 100644 index e5abb51..0000000 --- a/tests/distros/Dockerfile.fedora +++ /dev/null @@ -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 diff --git a/tests/distros/Dockerfile.ubuntu b/tests/distros/Dockerfile.ubuntu deleted file mode 100644 index 38223c2..0000000 --- a/tests/distros/Dockerfile.ubuntu +++ /dev/null @@ -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 From b0eeb2f84ed01cbd3c50e9f3f17798cfa446dbd2 Mon Sep 17 00:00:00 2001 From: Marcin Kulik Date: Sun, 31 Aug 2025 21:36:34 +0200 Subject: [PATCH 05/13] Rename test fixtures --- src/asciicast.rs | 4 ++-- tests/casts/{full.json => full-v1.json} | 0 tests/casts/{minimal.json => minimal-v1.json} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename tests/casts/{full.json => full-v1.json} (100%) rename tests/casts/{minimal.json => minimal-v1.json} (100%) diff --git a/src/asciicast.rs b/src/asciicast.rs index 72963ff..b535387 100644 --- a/src/asciicast.rs +++ b/src/asciicast.rs @@ -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::>>().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::>>().unwrap(); assert_eq!(version, 1); diff --git a/tests/casts/full.json b/tests/casts/full-v1.json similarity index 100% rename from tests/casts/full.json rename to tests/casts/full-v1.json diff --git a/tests/casts/minimal.json b/tests/casts/minimal-v1.json similarity index 100% rename from tests/casts/minimal.json rename to tests/casts/minimal-v1.json From 2aa84253dd3e243f2b43c91c012beb29d971091c Mon Sep 17 00:00:00 2001 From: Marcin Kulik Date: Mon, 1 Sep 2025 16:28:22 +0200 Subject: [PATCH 06/13] Add shellcheck to nix dev shell --- shell.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell.nix b/shell.nix index 279e3c1..802bbaa 100644 --- a/shell.nix +++ b/shell.nix @@ -22,7 +22,7 @@ let }) ]; - buildInputs = [ pkgs.bashInteractive ]; + packages = [ pkgs.shellcheck ]; env.RUST_BACKTRACE = 1; }; From 04c82f05dea372ca74adcd974bba94d8fba6b792 Mon Sep 17 00:00:00 2001 From: Marcin Kulik Date: Sun, 31 Aug 2025 22:43:42 +0200 Subject: [PATCH 07/13] Fix reading output from PTY after child's death --- src/session.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/session.rs b/src/session.rs index e3fb4e1..0cc2f27 100644 --- a/src/session.rs +++ b/src/session.rs @@ -202,8 +202,16 @@ impl Session { } } + 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; } From 7bf8a66fd7b8615866bcb5ad4a1bdad233724355 Mon Sep 17 00:00:00 2001 From: Marcin Kulik Date: Sun, 31 Aug 2025 22:43:42 +0200 Subject: [PATCH 08/13] Fix forwarding session events to outputs --- src/session.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/session.rs b/src/session.rs index 0cc2f27..58990a8 100644 --- a/src/session.rs +++ b/src/session.rs @@ -81,9 +81,9 @@ pub async fn run, T: Tty + ?Sized, N: Notifier>( let (events_tx, events_rx) = mpsc::channel::(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, 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, outputs: Vec>) { @@ -131,7 +134,7 @@ async fn forward_event(mut output: Box, event: Event) -> Option Session { - async fn run(&mut self, pty: Pty, tty: &mut T) -> anyhow::Result { + async fn run(mut self, pty: Pty, tty: &mut T) -> anyhow::Result { let mut signals = Signals::new([SIGWINCH, SIGINT, SIGTERM, SIGQUIT, SIGHUP, SIGALRM, SIGCHLD])?; let mut output_buf = [0u8; BUF_SIZE]; From a76b54c61b3d55d3109f75a41ad0af47ee5d484e Mon Sep 17 00:00:00 2001 From: Marcin Kulik Date: Sun, 31 Aug 2025 21:31:47 +0200 Subject: [PATCH 09/13] New integration tests --- .github/workflows/ci.yml | 5 +- tests/integration.sh | 493 +++++++++++++++++++++++++++++++++------ 2 files changed, 424 insertions(+), 74 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb0a0a9..55d4b6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/tests/integration.sh b/tests/integration.sh index 57dcde2..62d5f3a 100755 --- a/tests/integration.sh +++ b/tests/integration.sh @@ -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 From 7a43456dbe6211592dc6cb1c7d07137dd47f78a4 Mon Sep 17 00:00:00 2001 From: Marcin Kulik Date: Mon, 1 Sep 2025 18:14:46 +0200 Subject: [PATCH 10/13] Bump version --- Cargo.lock | 2 +- Cargo.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8ef10ab..b91db2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,7 +84,7 @@ checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "asciinema" -version = "3.0.0-rc.5" +version = "3.0.0" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 4185f40..88f0033 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "asciinema" -version = "3.0.0-rc.5" +version = "3.0.0" edition = "2021" authors = ["Marcin Kulik "] homepage = "https://asciinema.org" repository = "https://github.com/asciinema/asciinema" -description = "Terminal session recorder" +description = "Terminal session recorder, player, and streamer" license = "GPL-3.0" # MSRV From 160b89e0ada5c3ce5ee3ea191b732c000ab4d2a8 Mon Sep 17 00:00:00 2001 From: Marcin Kulik Date: Mon, 1 Sep 2025 18:15:10 +0200 Subject: [PATCH 11/13] Remove unnecessary comment from Cargo.toml --- Cargo.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 88f0033..407e05d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,8 +11,6 @@ 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"] } From a91a9adf58b36568f427f1782f547b03ef0953f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 22:27:53 +0000 Subject: [PATCH 12/13] Bump slab from 0.4.10 to 0.4.11 Bumps [slab](https://github.com/tokio-rs/slab) from 0.4.10 to 0.4.11. - [Release notes](https://github.com/tokio-rs/slab/releases) - [Changelog](https://github.com/tokio-rs/slab/blob/master/CHANGELOG.md) - [Commits](https://github.com/tokio-rs/slab/compare/v0.4.10...v0.4.11) --- updated-dependencies: - dependency-name: slab dependency-version: 0.4.11 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8ef10ab..476a361 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1451,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" From 5c624572a40740756cac43e44b7567d8740fd498 Mon Sep 17 00:00:00 2001 From: Marcin Kulik Date: Mon, 1 Sep 2025 20:12:10 +0200 Subject: [PATCH 13/13] Update crate description --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 407e05d..ac5fc3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" authors = ["Marcin Kulik "] homepage = "https://asciinema.org" repository = "https://github.com/asciinema/asciinema" -description = "Terminal session recorder, player, and streamer" +description = "Terminal session recorder, streamer, and player" license = "GPL-3.0" # MSRV