diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f2466c..9bc2b1b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,42 +6,27 @@ on: pull_request: branches: [main] +permissions: + contents: read + jobs: test: name: Test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - - uses: docker/setup-buildx-action@v3 - - - name: Build soffice image - uses: docker/build-push-action@v6 - with: - context: benchmarks - file: benchmarks/Dockerfile.soffice-debian - tags: soffice:latest - load: true - cache-from: type=gha - cache-to: type=gha,mode=max + - uses: actions/checkout@v6 - - name: Start soffice - run: | - docker run --detach --name soffice \ - --publish 2002:2002 \ - --volume /tmp:/tmp \ - soffice - timeout 30 bash -c 'until nc -z localhost 2002; do sleep 1; done' + - uses: cachix/install-nix-action@v31 - uses: erlef/setup-beam@v1 id: beam with: - elixir-version: 1.19.5 - otp-version: 28.3 + elixir-version: 1.20.2 + otp-version: 29.0.3 - name: Restore dependencies cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | deps @@ -50,7 +35,7 @@ jobs: restore-keys: ${{ runner.os }}-mix- - name: Restore PLT cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: priv/plts key: ${{ runner.os }}-plt-otp${{ steps.beam.outputs.otp-version }}-${{ hashFiles('**/mix.lock') }}-${{ github.sha }} @@ -67,8 +52,46 @@ jobs: - name: Check formatting run: mix format --check-formatted - - name: Run tests - run: mix test --include lo26 + - name: Check unused dependencies + run: mix deps.unlock --check-unused + + - name: Check dependency cycles + run: mix xref graph --format cycles --fail-above 0 + + - name: Run unit tests without soffice + run: mix test + + - name: Build documentation + run: mix docs --warnings-as-errors + + - name: Audit Hex dependencies + run: mix hex.audit + + - name: Check Nix flake + run: nix flake check - name: Run dialyzer run: mix dialyzer + + - uses: docker/setup-buildx-action@v4 + + - name: Build soffice image + uses: docker/build-push-action@v7 + with: + context: benchmarks + file: benchmarks/Dockerfile.soffice-debian + tags: soffice:latest + load: true + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Start soffice + run: | + docker run --detach --name soffice \ + --publish 2002:2002 \ + --volume /tmp:/tmp \ + soffice + timeout 30 bash -c 'until nc -z localhost 2002; do sleep 1; done' + + - name: Run integration tests + run: mix test --include lo26 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f6cdcf0..05b92f5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,15 +14,15 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: erlef/setup-beam@v1 with: - elixir-version: "1.19.5" - otp-version: "28.3" + elixir-version: "1.20.2" + otp-version: "29.0.3" - name: Restore dependencies cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | deps diff --git a/PERFORMANCE.md b/PERFORMANCE.md index 25d3dc9..d238948 100644 --- a/PERFORMANCE.md +++ b/PERFORMANCE.md @@ -11,7 +11,9 @@ docker compose --file benchmarks/docker-compose.yml up --detach --wait mix run benchmarks/bench.exs ``` -Both URP and Gotenberg run LibreOffice 26.2.0 on Debian (glibc). +The results below were recorded on July 14, 2026, on Apple M3 Max with +Elixir 1.20.2 and Erlang/OTP 29.0.3. URP's Debian container ran +LibreOffice 26.2.4.2; Gotenberg 8.32.0 bundled LibreOffice 26.2.2.2. The fixture uses Liberation fonts only — regenerate with `uv run --with python-docx --with Pillow --with numpy benchmarks/generate_fixture.py` (pass `--size 15` for the large variant). @@ -22,22 +24,39 @@ The fixture uses Liberation fonts only — regenerate with ``` Name ips average deviation median 99th % -URP 1.05 0.95 s ±7.60% 0.94 s 1.20 s -Gotenberg 0.81 1.23 s ±7.47% 1.19 s 1.42 s +URP 1.37 0.73 s ±4.74% 0.72 s 0.82 s +Gotenberg 0.84 1.19 s ±5.46% 1.16 s 1.37 s ``` **15.5 MB input → 62 MB PDF:** ``` Name ips average deviation median 99th % -URP 0.145 6.87 s ±7.44% 6.73 s 7.44 s -Gotenberg 0.087 11.45 s ±1.21% 11.45 s 11.54 s +URP 0.196 5.10 s ±2.12% 5.05 s 5.22 s +Gotenberg 0.135 7.39 s ±0.88% 7.36 s 7.46 s ``` -**27% faster** for small documents, **67% faster** for large ones. -The gap grows because Gotenberg's Go/HTTP overhead (multipart parsing, -queue management, response framing) scales with document size, while -URP talks to soffice directly over a TCP socket. +URP had **39% lower average latency** for the small document and **31% +lower average latency** for the large document. The absolute advantage +grew from 0.46 s to 2.29 s. These measurements compare the complete +stacks, including their slightly different LibreOffice patch versions; +they do not isolate dependency or runtime upgrades individually. + +### Process overhead sanity check + +`benchmarks/convert.exs` compares the persistent URP connection with a +cold `soffice --convert-to` process and Gotenberg using the 33 KB +`sample3.docx` fixture. Across five timed iterations, the averages were: + +| Method | Average | Range | +|--------|---------|-------| +| URP | 46 ms | 43–49 ms | +| Gotenberg | 153 ms | 146–160 ms | +| LibreOffice CLI | 280 ms | 267–302 ms | + +This is a process-overhead check, not an apples-to-apples transport +benchmark: URP reuses a live office process, while the CLI measurement +starts a new process for every conversion. ## I/O strategies @@ -53,10 +72,25 @@ and transfers them over URP in ~6 round-trips. **Stream I/O** (`:stream`) pipes bytes over the URP socket via XInputStream/XOutputStream — no temp disk, but more round-trips. -Stream input is the bottleneck (~40-50% slower) because ZIP-based formats -(docx, xlsx, pptx) require thousands of XInputStream/XSeekable random-access -round-trips. Stream output adds negligible overhead — soffice writes in -fixed [32 767-byte chunks](https://github.com/LibreOffice/core/blob/libreoffice-26-2-0/sfx2/source/doc/docfile.cxx#L2573), +Current results on Elixir 1.20.2 / OTP 29.0.3: + +| Strategy | 2.7 MB | 16.2 MB | 35.0 MB | +|----------|--------|---------|---------| +| File → file | 1.13 s | 7.66 s | 39.17 s | +| File → stream | 1.19 s | 8.06 s | 37.19 s | +| Stream → file | 1.72 s | 11.59 s | 41.53 s | +| Stream → stream | 1.64 s | 9.06 s | 46.02 s | + +Stream input remains the bottleneck because ZIP-based formats (docx, +xlsx, pptx) require thousands of XInputStream/XSeekable random-access +round-trips, but its measured penalty now ranges from roughly 12% to 52% +depending on document size and output mode. Stream output remains within +about 5% of file output. The 35 MB fixture completes only one iteration +per scenario with the default 10-second Benchee window, so treat those +figures as directional rather than statistically stable. + +Soffice writes stream output in fixed +[32 767-byte chunks](https://github.com/LibreOffice/core/blob/libreoffice-26-2-0/sfx2/source/doc/docfile.cxx#L2573), so the round-trip count is predictable. | Strategy | Input | Output | Best for | @@ -109,22 +143,22 @@ and can be mitigated with It bundles OpenJDK 11, 130+ Noto font packages, and 450 packages total. -As of March 2026, Alpine ships LO 25.8.x (Still) while Debian +As of July 2026, Alpine ships LO 25.8.x (Still) while Debian trixie-backports has 26.2.x (Fresh). Carlito is missing from the stock Alpine image (`apk add font-carlito` to fix). | Setup | 2.6 MB | 15.5 MB | LO version | Image size | |-------|--------|---------|------------|------------| -| URP → Debian glibc | 0.94 s | 6.73 s | 26.2.0 | ~564 MB | -| URP → Alpine musl | 1.20 s | 11.11 s | 25.8.1 | ~1.78 GB | -| Gotenberg (Debian glibc) | 1.19 s | 11.45 s | 26.2.0 | ~1.86 GB | +| URP → Debian glibc | 0.72 s | 5.05 s | 26.2.4.2 | ~564 MB | +| URP → Alpine musl | 1.18 s | 7.94 s | 25.8.1.1 | ~1.78 GB | +| Gotenberg (Debian glibc) | 1.16 s | 7.36 s | 26.2.2.2 | ~1.68 GB |
Reproducing the strace analysis ```sh docker compose --file benchmarks/docker-compose.yml up --detach --wait -SOFFICE=benchmarks-soffice-1 +SOFFICE=benchmarks-soffice-alpine-1 docker exec $SOFFICE apk add --no-cache strace docker exec $SOFFICE pgrep -f soffice.bin # note the PID diff --git a/README.md b/README.md index 7a4b0d6..c60f2bf 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ no Python, no wrappers, no sidecars. ## Installation ```elixir -{:urp, "~> 0.8"} +{:urp, "~> 0.10"} ``` ## Prerequisites @@ -69,6 +69,14 @@ URP.Test.stub(fn _input, _opts -> {:ok, "/tmp/fake.pdf"} end) assert {:ok, _} = MyApp.generate_invoice(order) ``` +When soffice is unavailable, tests tagged `:integration` are excluded automatically. +Run the complete suite, including the LibreOffice 26.2+ coverage, with: + +```sh +docker compose --file benchmarks/docker-compose.yml up --detach --wait soffice +mix test --include lo26 +``` + ### Telemetry Every operation emits `[:urp, :call, :stop]` with queue, service, and diff --git a/benchmarks/Dockerfile.soffice-debian b/benchmarks/Dockerfile.soffice-debian index 6f114c6..dbb6d18 100644 --- a/benchmarks/Dockerfile.soffice-debian +++ b/benchmarks/Dockerfile.soffice-debian @@ -1,4 +1,4 @@ -# Minimal Debian soffice with LO from trixie-backports (same version as Gotenberg). +# Minimal Debian soffice with LibreOffice 26.2+ from trixie-backports. # Only free fonts — Liberation (Arial/Times metric-compatible) and Carlito (Calibri). FROM debian:trixie-slim @@ -16,5 +16,4 @@ ENV PORT=2002 EXPOSE ${PORT} -CMD soffice --headless --norestore \ - --accept="socket,host=${HOST},port=${PORT},tcpNoDelay=1;urp;" +CMD ["sh", "-c", "exec soffice --headless --norestore --accept=\"socket,host=${HOST},port=${PORT},tcpNoDelay=1;urp;\""] diff --git a/benchmarks/bench.exs b/benchmarks/bench.exs index 15f9885..f438b1a 100644 --- a/benchmarks/bench.exs +++ b/benchmarks/bench.exs @@ -18,7 +18,7 @@ gotenberg_url = "http://localhost:3002/forms/libreoffice/convert" IO.puts("Fixture: #{fixture} (#{div(byte_size(docx_bytes), 1024)} KB)\n") # Gotenberg-equivalent FilterData — matches DefaultOptions() from -# https://github.com/gotenberg/gotenberg/blob/v8.27.0/pkg/modules/libreoffice/api/api.go +# https://github.com/gotenberg/gotenberg/blob/v8.32.0/pkg/modules/libreoffice/api/api.go gotenberg_filter_data = [ ExportFormFields: true, AllowDuplicateFieldNames: false, @@ -52,26 +52,26 @@ Application.ensure_all_started(:telemetry) {:ok, _} = Finch.start_link(name: Req.Finch) form_data = [files: {docx_bytes, filename: fixture, content_type: "application/octet-stream"}] -# ── Debian pool (port 2003) ── +# ── Alpine pool (port 2003) ── -{:ok, _} = URP.Pool.start_link(name: :debian, host: "localhost", port: 2003, pool_size: 1) +{:ok, _} = URP.Pool.start_link(name: :alpine, host: "localhost", port: 2003, pool_size: 1) # ── Warmup all services ── {:ok, _} = URP.convert({:binary, docx_bytes}, urp_opts) -{:ok, _} = URP.Pool.convert(:debian, {:binary, docx_bytes}, urp_opts) +{:ok, _} = URP.Pool.convert(:alpine, {:binary, docx_bytes}, urp_opts) Req.post!(gotenberg_url, form_multipart: form_data) # ── Benchmark ── Benchee.run( %{ - "URP → Alpine musl" => fn -> + "URP → Debian glibc" => fn -> {:ok, pdf} = URP.convert({:binary, docx_bytes}, urp_opts) pdf end, - "URP → Debian glibc" => fn -> - {:ok, pdf} = URP.Pool.convert(:debian, {:binary, docx_bytes}, urp_opts) + "URP → Alpine musl" => fn -> + {:ok, pdf} = URP.Pool.convert(:alpine, {:binary, docx_bytes}, urp_opts) pdf end, "Gotenberg (HTTP)" => fn -> diff --git a/benchmarks/convert.exs b/benchmarks/convert.exs index 66248b4..b9f3bbb 100644 --- a/benchmarks/convert.exs +++ b/benchmarks/convert.exs @@ -1,13 +1,13 @@ # Simple benchmark: URP vs CLI shell-out vs Gotenberg # -# All three use LibreOffice 25.8.1 for apples-to-apples comparison. +# Record each service's LibreOffice version when comparing benchmark results. # # Prerequisites: # 1. soffice container on port 2002 (URP) # 2. soffice-cli container with /fixtures mount (CLI — separate instance to avoid lock) -# 3. gotenberg:8.23.1 on GOTENBERG_PORT (HTTP API) +# 3. Gotenberg service from docker-compose.yml on GOTENBERG_PORT (HTTP API) # -# cd benchmarks && docker compose up -d +# docker compose --file benchmarks/docker-compose.yml up --detach --wait # # Run: # nix develop --command mix run benchmarks/convert.exs diff --git a/benchmarks/docker-compose.yml b/benchmarks/docker-compose.yml index f1f12e4..03722df 100644 --- a/benchmarks/docker-compose.yml +++ b/benchmarks/docker-compose.yml @@ -5,29 +5,39 @@ # nix develop --command mix run benchmarks/bench.exs # # Services: -# soffice — Alpine (musl), URP on port 2002 -# soffice-debian — Debian (glibc), URP on port 2003, LO from trixie-backports +# soffice — Debian (glibc), URP on port 2002, LO 26.2+ from trixie-backports +# soffice-cli — idle Debian container for cold CLI shell-out comparisons +# soffice-alpine — Alpine (musl), URP on port 2003, comparison service # gotenberg — Gotenberg full stack (Debian glibc), HTTP on port 3002 services: soffice: - image: libreofficedocker/alpine:3.23 - command: - - soffice - - --headless - - --norestore - - --accept=socket,host=0.0.0.0,port=2002,tcpNoDelay=1;urp; + build: + context: . + dockerfile: Dockerfile.soffice-debian ports: - "2002:2002" - soffice-debian: + soffice-cli: build: context: . dockerfile: Dockerfile.soffice-debian + container_name: soffice-cli + command: ["sleep", "infinity"] + volumes: + - ./fixtures:/fixtures:ro + + soffice-alpine: + image: libreofficedocker/alpine:3.23 + command: + - soffice + - --headless + - --norestore + - --accept=socket,host=0.0.0.0,port=2002,tcpNoDelay=1;urp; ports: - "2003:2002" gotenberg: - image: gotenberg/gotenberg:8.27.0 + image: gotenberg/gotenberg:8.32.0 ports: - "3002:3000" diff --git a/benchmarks/io_bench.exs b/benchmarks/io_bench.exs index 64c0195..3444851 100644 --- a/benchmarks/io_bench.exs +++ b/benchmarks/io_bench.exs @@ -14,7 +14,7 @@ # Environment: # FIXTURES — comma-separated filenames in benchmarks/fixtures/ (default: all) # FILTER — export filter name (default: writer_pdf_Export) -# PORT — soffice port (default: 2003, the Debian glibc instance) +# PORT — soffice port (default: 2002, the Debian glibc instance) # # Stream output overhead is negligible (<5%) regardless of file size. # soffice writes in fixed 32767-byte chunks — a hardcoded literal in @@ -27,7 +27,7 @@ default_fixtures = "benchmark.docx,benchmark-15mb.docx,benchmark-50mb.docx" fixtures = System.get_env("FIXTURES", default_fixtures) |> String.split(",", trim: true) filter = System.get_env("FILTER", "writer_pdf_Export") -port = System.get_env("PORT", "2003") |> String.to_integer() +port = System.get_env("PORT", "2002") |> String.to_integer() Application.ensure_all_started(:telemetry) diff --git a/flake.lock b/flake.lock index 450d227..6bea735 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1771923393, - "narHash": "sha256-Fy0+UXELv9hOE8WjYhJt8fMDLYTU2Dqn3cX4BwoGBos=", + "lastModified": 1783915482, + "narHash": "sha256-FmieJB8/OUvNxbkboi7+IGfIuSXY3nF/hZQm8kD0r50=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "ea7f1f06811ce7fcc81d6c6fd4213150c23edcf2", + "rev": "6cdc7fc76e8bf7fde9fa43a849fcaaa70e230dee", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index c486c22..4c4d4d8 100644 --- a/flake.nix +++ b/flake.nix @@ -11,13 +11,13 @@ devShells = forAllSystems (system: let pkgs = nixpkgs.legacyPackages.${system}; - beamPackages = pkgs.beam.packages.erlang_28; + beamPackages = pkgs.beam.packages.erlang_29; in { default = pkgs.mkShell { packages = [ beamPackages.erlang - beamPackages.elixir_1_19 + beamPackages.elixir_1_20 pkgs.git pkgs.uv ]; diff --git a/lib/urp/bridge.ex b/lib/urp/bridge.ex index df0d56a..314b077 100644 --- a/lib/urp/bridge.ex +++ b/lib/urp/bridge.ex @@ -608,9 +608,6 @@ defmodule URP.Bridge do ## Send + receive + parse - # Error-guard: short-circuit if an earlier step failed. - defp call(%__MODULE__{error: e} = conn, _frame) when not is_nil(e), do: conn - defp call(%__MODULE__{} = conn, frame) do conn |> send_frame(frame) diff --git a/lib/urp/stream.ex b/lib/urp/stream.ex index 8a3949a..1994c7a 100644 --- a/lib/urp/stream.ex +++ b/lib/urp/stream.ex @@ -27,7 +27,6 @@ defmodule URP.Stream do XOutputStream: writeBytes=3, flush=4, closeOutput=5 """ - alias URP.Bridge alias URP.Protocol, as: P import Bitwise @@ -38,6 +37,7 @@ defmodule URP.Stream do | {:file, pid(), non_neg_integer()} | {:enum, binary(), pid() | :eof} @type sink :: nil | {:path, Path.t()} | (binary() -> any()) + @type connection :: map() @tc_void 0 @tc_new 0x80 @@ -67,8 +67,8 @@ defmodule URP.Stream do Dispatches calls until we receive the reply to our pending request. Returns `{reply_payload, updated_conn}`. """ - @spec recv_handling_input(Bridge.t(), input_source() | source(), String.t() | nil) :: - {binary(), Bridge.t()} + @spec recv_handling_input(connection(), input_source() | source(), String.t() | nil) :: + {binary(), connection()} def recv_handling_input(conn, data, stream_oid \\ nil) def recv_handling_input(conn, data, stream_oid) when is_binary(data) do @@ -126,10 +126,10 @@ defmodule URP.Stream do outside the main stream recv loop. Returns `:not_input` if the request doesn't match the active input stream context. """ - @spec try_handle_input(Bridge.t(), binary()) :: {:handled, Bridge.t()} | :not_input - def try_handle_input(%Bridge{input_ctx: nil}, _payload), do: :not_input + @spec try_handle_input(connection(), binary()) :: {:handled, connection()} | :not_input + def try_handle_input(%{input_ctx: nil}, _payload), do: :not_input - def try_handle_input(%Bridge{input_ctx: ctx} = conn, payload) do + def try_handle_input(%{input_ctx: ctx} = conn, payload) do %{func_id: func_id, body: body, type_cache: type_cache, tid: new_tid} = P.parse_request(payload) @@ -286,7 +286,7 @@ defmodule URP.Stream do * `{:path, path}` — write chunks to file as they arrive, returns `{reply, :ok, conn}` * `fun/1` — call with each chunk, returns `{reply, :ok, conn}` """ - @spec recv_handling_output(Bridge.t(), sink()) :: {binary(), binary() | :ok, Bridge.t()} + @spec recv_handling_output(connection(), sink()) :: {binary(), binary() | :ok, connection()} def recv_handling_output(conn, sink \\ nil) def recv_handling_output(conn, nil) do @@ -399,7 +399,7 @@ defmodule URP.Stream do defp read_chunk({:enum, buffer, reader}, n) do {buffer, reader} = fill_buffer(buffer, reader, n) to_read = min(n, byte_size(buffer)) - <> = buffer + <> = buffer {chunk, {:enum, rest, reader}} end @@ -418,7 +418,7 @@ defmodule URP.Stream do defp skip_chunk({:enum, buffer, reader}, n) do {buffer, reader} = fill_buffer(buffer, reader, n) skip = min(n, byte_size(buffer)) - <<_::binary-size(skip), rest::binary>> = buffer + <<_::binary-size(^skip), rest::binary>> = buffer {:enum, rest, reader} end diff --git a/mix.exs b/mix.exs index 690d073..29029db 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,7 @@ defmodule URP.MixProject do [ app: :urp, version: @version, - elixir: "~> 1.19", + elixir: "~> 1.20", description: "Pure Elixir client for the UNO Remote Protocol — convert documents via LibreOffice over TCP", package: package(), @@ -48,7 +48,7 @@ defmodule URP.MixProject do {:nimble_ownership, "~> 1.0"}, {:nimble_pool, "~> 1.1"}, {:telemetry, "~> 1.0"}, - {:req, "~> 0.5", only: :dev, runtime: false}, + {:req, "~> 0.6", only: :dev, runtime: false}, {:benchee, "~> 1.3", only: :dev, runtime: false} ] end diff --git a/mix.lock b/mix.lock index 9399ac8..7c2456f 100644 --- a/mix.lock +++ b/mix.lock @@ -1,23 +1,23 @@ %{ - "benchee": {:hex, :benchee, "1.5.0", "4d812c31d54b0ec0167e91278e7de3f596324a78a096fd3d0bea68bb0c513b10", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.1", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "5b075393aea81b8ae74eadd1c28b1d87e8a63696c649d8293db7c4df3eb67535"}, - "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, + "benchee": {:hex, :benchee, "1.5.1", "b95cbc36c4b98969a5c592a246e171041eb683c56bad1cb4f49a3b081ba66087", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.1", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a539301f8dfd4efc5c5123bfb9d47ebde20092a863a5b5b16c2a60d2243dfce7"}, + "deep_merge": {:hex, :deep_merge, "1.0.2", "476aa7ea61c54de96220051b998d893869069094da65b96101aebf79416f8a1e", [:mix], [], "hexpm", "737a53cdc9758fedbb608bdc213969e65729466c4ef3cd8e8726d0335dff116c"}, "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, - "erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"}, - "ex_doc": {:hex, :ex_doc, "0.40.1", "67542e4b6dde74811cfd580e2c0149b78010fd13001fda7cfeb2b2c2ffb1344d", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "bcef0e2d360d93ac19f01a85d58f91752d930c0a30e2681145feea6bd3516e00"}, - "finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"}, - "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, - "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, - "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.45", "cba8369ab2a1342e419bc2760eec731b17be828941dcf494045d44766227e1d5", [:mix], [], "hexpm", "d3ec045bf122965db20c0bdb420e19ee1415843135327124918473feb4b328e8"}, + "erlex": {:hex, :erlex, "0.2.9", "7debbbaa9f4f368b8cd648983e0f1d7963028508e9c59e9d4ed504e94ef52a55", [:mix], [], "hexpm", "8cfffc0ec7159e6d73de2ab28a588064de80f88b2798d5cbe4482cbbc200178b"}, + "ex_doc": {:hex, :ex_doc, "0.40.3", "4a972ffe64bc07dc605af487e98fc19b72a4185f55ca031b94c0552d6071c1d9", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2756e357742fecd9749b489b85d67c9ce99c465f2e75728d9e6dc8d704b973de"}, + "finch": {:hex, :finch, "0.23.0", "e3f9287ac25a8832f848b144c2b57346aac65b205e2e0629a52adfe6507fd837", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "80e58d3f936f57e3fdf404f83a3642897ae6d9fb642934e46da4d8fe761b99d5"}, + "hpax": {:hex, :hpax, "1.0.4", "777de5d433b0fbdc7c418159c8055910faa8047ffdb3d6b31098d2a46cd7685c", [:mix], [], "hexpm", "afc7cb142ebcc2d01ce7816190b98ce5dd49e799111b24249f3443d730f377ca"}, + "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, + "makeup": {:hex, :makeup, "1.2.2", "882d46dc0905e9ff7abf2aab61a7e6b3dcc555533977d8a23b06019e6c89ac94", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "9a1a24e5b343b8ae16abea0822c10a6f75da27af7fa802ada5251f7579bfccfa"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, - "makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.1.0", "835f7e60792e08824cda445639555d7bf1bbbddb1b60b306e33cb6f6db24dc74", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "1cd6780fb1dd1a03979abaed0fe82712b0625118fd5257d3ebbf73f960c73c3c"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, - "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, + "mint": {:hex, :mint, "1.9.2", "6e89e698d69cc29be001afd02c2cf4bae2d0994efe69a2ced7aab440d71584f9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d8e952b432fdac2321f570d29a68b9eb2664dc80a7a2ef9464531a5425abf222"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_ownership": {:hex, :nimble_ownership, "1.0.2", "fa8a6f2d8c592ad4d79b2ca617473c6aefd5869abfa02563a77682038bf916cf", [:mix], [], "hexpm", "098af64e1f6f8609c6672127cfe9e9590a5d3fcdd82bc17a377b8692fd81a879"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, - "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"}, - "statistex": {:hex, :statistex, "1.1.0", "7fec1eb2f580a0d2c1a05ed27396a084ab064a40cfc84246dbfb0c72a5c761e5", [:mix], [], "hexpm", "f5950ea26ad43246ba2cce54324ac394a4e7408fdcf98b8e230f503a0cba9cf5"}, - "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "req": {:hex, :req, "0.6.2", "b9b2024f35bcf60a92cc8cad2eaaf9d4e7aace463ff74be1afe5986830184413", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cc9cd30a2ddd04989929b887178e1610c940456d962c6c3a52df6146d2eef9bf"}, + "statistex": {:hex, :statistex, "1.1.1", "73612aa7f79e53c30569be065fd121e380f1cf57bc4c2da5b41be9246da18df9", [:mix], [], "hexpm", "310c4b49b34adf683de3103639006bed233ab54c08a4add65a531448e653857c"}, + "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, } diff --git a/test/test_helper.exs b/test/test_helper.exs index 698e073..781e72c 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,11 +1,19 @@ -case :gen_tcp.connect(~c"localhost", 2002, [:binary], 1000) do - {:ok, sock} -> - :gen_tcp.close(sock) +soffice_available? = + case :gen_tcp.connect(~c"localhost", 2002, [:binary], 1000) do + {:ok, sock} -> + :gen_tcp.close(sock) + true - {:error, _} -> - raise "soffice not reachable on localhost:2002 — start it before running tests" -end + {:error, _} -> + false + end # Tests tagged :lo26 require LibreOffice 26.2+. Excluded by default for # local development with older soffice. CI includes them (--include lo26). -ExUnit.start(exclude: [:lo26]) +excluded_tags = if soffice_available?, do: [:lo26], else: [:integration, :lo26] + +unless soffice_available? do + IO.puts("soffice not reachable on localhost:2002 — excluding integration tests") +end + +ExUnit.start(exclude: excluded_tags) diff --git a/test/urp/telemetry_test.exs b/test/urp/telemetry_test.exs index 2d0d7a3..ce2fd92 100644 --- a/test/urp/telemetry_test.exs +++ b/test/urp/telemetry_test.exs @@ -56,5 +56,4 @@ defmodule URP.TelemetryTest do assert_receive {:telemetry_event, [:urp, :call, :stop], _measurements, metadata} assert metadata.result == :error end - end diff --git a/test/urp_test.exs b/test/urp_test.exs index 1bdcf79..e18c11c 100644 --- a/test/urp_test.exs +++ b/test/urp_test.exs @@ -166,7 +166,6 @@ defmodule URPTest do assert "%PDF-" <> _ = pdf end - end describe "settings" do @@ -333,7 +332,6 @@ defmodule URPTest do Bridge.delete_file(conn, conn.cleanup_url) Bridge.close!(conn) end - end describe "pool queuing" do @@ -344,13 +342,13 @@ defmodule URPTest do task1 = Task.async(fn -> result = URP.convert({:binary, docx1}, filter: "Text", output: :binary) - {result, System.monotonic_time(:millisecond)} + {result, System.monotonic_time(:microsecond)} end) task2 = Task.async(fn -> result = URP.convert({:binary, docx2}, filter: "Text", output: :binary) - {result, System.monotonic_time(:millisecond)} + {result, System.monotonic_time(:microsecond)} end) {{:ok, text1}, t1} = Task.await(task1, 15_000) @@ -361,10 +359,9 @@ defmodule URPTest do assert text2 =~ "Document Two" # With pool_size 1, conversions are serial. The second task must wait - # for the first to finish and return the worker. If they ran in parallel - # they'd finish at roughly the same time; serial execution means the - # completion timestamps are at least one conversion apart. - assert abs(t2 - t1) >= 10 + # for the first to finish and return the worker, so their completion + # timestamps should be measurably separated even on fast machines. + assert abs(t2 - t1) >= 1_000 end end @@ -429,7 +426,6 @@ defmodule URPTest do nil -> 0 end end - end defmodule URP.DocTest do