diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2929c53 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,149 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Test + +on: + push: + branches: ["main"] + pull_request: + +permissions: + contents: read + +jobs: + build: + name: Build and test (${{ matrix.elixir }} / OTP ${{ matrix.otp }}) + runs-on: ubuntu-latest + env: + MIX_ENV: test + strategy: + fail-fast: false + matrix: + include: + - elixir: "1.19.0" + otp: "28.0" + bootstrap_beam: false + lint: true + # OTP 29 RC's :httpc can't reach builds.hex.pm, so we tell setup-beam + # to skip Hex/Rebar autoinstall and curl them ourselves below. + - elixir: "1.20.0" + otp: "29.0" + bootstrap_beam: true + lint: false + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ matrix.elixir }} + otp-version: ${{ matrix.otp }} + version-type: strict + install-hex: ${{ !matrix.bootstrap_beam }} + install-rebar: ${{ !matrix.bootstrap_beam }} + + - name: Bootstrap Hex/Rebar via curl (OTP 29 RC :httpc workaround) + if: matrix.bootstrap_beam + run: | + curl -fsSL --retry 3 -o /tmp/hex.ez \ + https://builds.hex.pm/installs/1.19.0/hex-2.4.2-otp-28.ez + mix archive.install /tmp/hex.ez --force + curl -fsSL --retry 3 -o /tmp/rebar3 \ + https://github.com/erlang/rebar3/releases/download/3.25.1/rebar3 + chmod +x /tmp/rebar3 + mix local.rebar rebar3 /tmp/rebar3 --force + + - name: Restore dependencies cache + uses: actions/cache@v4 + with: + path: deps + key: ${{ runner.os }}-elixir${{ matrix.elixir }}-otp${{ matrix.otp }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-elixir${{ matrix.elixir }}-otp${{ matrix.otp }}-mix- + + - name: Restore build cache + uses: actions/cache@v4 + with: + path: _build + key: ${{ runner.os }}-elixir${{ matrix.elixir }}-otp${{ matrix.otp }}-build-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-elixir${{ matrix.elixir }}-otp${{ matrix.otp }}-build- + + - name: Install dependencies + run: mix deps.get + + - name: Check formatting + if: matrix.lint + run: mix format --check-formatted + + - name: Check for unused dependencies + if: matrix.lint + run: mix deps.unlock --check-unused + + - name: Compile (warnings as errors) + run: mix compile --warnings-as-errors --force + + - name: Run tests + # The `spec` conformance suite is run separately (see spec-suite job) + # because it tracks known gaps against reference Bash behavior and + # should not block PRs. + run: mix test --exclude spec + + spec-suite: + name: Conformance spec suite + runs-on: ubuntu-latest + # Reports the pass/fail count for the Oils-format conformance specs but + # does not block PRs — these track known gaps against reference Bash. + continue-on-error: true + env: + MIX_ENV: test + strategy: + matrix: + include: + - elixir: "1.19.0" + otp: "28.0" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ matrix.elixir }} + otp-version: ${{ matrix.otp }} + version-type: strict + + - name: Restore dependencies cache + uses: actions/cache@v4 + with: + path: deps + key: ${{ runner.os }}-elixir${{ matrix.elixir }}-otp${{ matrix.otp }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-elixir${{ matrix.elixir }}-otp${{ matrix.otp }}-mix- + + - name: Restore build cache + uses: actions/cache@v4 + with: + path: _build + key: ${{ runner.os }}-elixir${{ matrix.elixir }}-otp${{ matrix.otp }}-build-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-elixir${{ matrix.elixir }}-otp${{ matrix.otp }}-build- + + - name: Install dependencies + run: mix deps.get + + - name: Run conformance spec suite + run: | + mix test --only spec 2>&1 | tee spec-output.txt + summary=$(grep -E '[0-9]+ tests?, [0-9]+ failures' spec-output.txt | tail -1) + { + echo "## Conformance spec suite" + echo "" + echo "\`\`\`" + echo "$summary" + echo "\`\`\`" + } >> "$GITHUB_STEP_SUMMARY" + # Always succeed so the count is reported; the job is non-blocking. + exit 0 diff --git a/test/bash/builtin/set_options_test.exs b/test/bash/builtin/set_options_test.exs index ce1177c..24687b4 100644 --- a/test/bash/builtin/set_options_test.exs +++ b/test/bash/builtin/set_options_test.exs @@ -182,17 +182,23 @@ defmodule Bash.Builtin.SetOptionsTest do temp_file = Path.join(tmp_dir, "noclobber_test") File.write!(temp_file, "original content") + # Quote the redirect target: the tmp_dir path can contain shell-special + # characters (e.g. "(" from the test name), which would otherwise break + # parsing of an unquoted redirect target. result = run_script(session, """ set -C - echo "new content" > #{temp_file} + echo "new content" > "#{temp_file}" """) - # Command should have non-zero exit code - assert result.exit_code != 0 + # Command should have a non-zero exit code + assert result.exit_code not in [0, nil] - # Error message should be in stderr - assert String.contains?(get_stderr(result), "cannot overwrite existing file") + # Error message should be in stderr. Read from the session rather than the + # result: the result-embedded collector is transferred to the session's + # persistent collector and cleaned up asynchronously after execution. + {_out, err} = flush_session_output(session) + assert String.contains?(err, "cannot overwrite existing file") # Original content should be preserved assert File.read!(temp_file) == "original content"