Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
16 changes: 11 additions & 5 deletions test/bash/builtin/set_options_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading