diff --git a/.codeclimate.yml b/.codeclimate.yml
deleted file mode 100644
index fdf53d058..000000000
--- a/.codeclimate.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-#
-# ---Choose Your Languages---
-# To disable analysis for a certain language, set the language to `false`.
-# For help setting your languages:
-# http://docs.codeclimate.com/article/169-configuring-analysis-languages
-#
-languages:
- Ruby: true
- JavaScript: false
- Python: false
- PHP: false
-#
-# ---Exclude Files or Directories---
-# List the files or directories you would like excluded from our analysis.
-# For help setting your exclude paths:
-# http://docs.codeclimate.com/article/166-excluding-files-folders
-#
-exclude_paths:
- - "sample/**/*"
- - "test/**/*"
diff --git a/.github/docker/Dockerfile.legacy b/.github/docker/Dockerfile.legacy
new file mode 100644
index 000000000..5cb65c226
--- /dev/null
+++ b/.github/docker/Dockerfile.legacy
@@ -0,0 +1,32 @@
+# Builds a specific legacy Ruby version from source via rbenv/ruby-build.
+# Debian bullseye is used (not a newer release) because it still ships
+# OpenSSL 1.1.1 and an older GCC, both of which ruby-build's patches for
+# ancient Rubies (1.8.7 - 2.1.x) target. Newer OpenSSL 3.x breaks these
+# builds even with ruby-build's compatibility patches.
+FROM debian:bullseye-slim
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ build-essential autoconf bison git ca-certificates curl pkg-config \
+ libssl-dev libreadline-dev zlib1g-dev libyaml-dev libncurses5-dev \
+ libffi-dev libgdbm-dev libxml2-dev libxslt1-dev libcurl4-openssl-dev \
+ psmisc \
+ && rm -rf /var/lib/apt/lists/*
+
+ENV RBENV_ROOT=/root/.rbenv
+RUN git clone --depth 1 https://github.com/rbenv/rbenv.git "$RBENV_ROOT" \
+ && git clone --depth 1 https://github.com/rbenv/ruby-build.git "$RBENV_ROOT/plugins/ruby-build"
+ENV PATH="$RBENV_ROOT/bin:$RBENV_ROOT/shims:$PATH"
+
+ARG RUBY_VERSION
+# MAKE_OPTS=-j1 is required, not just a slowdown-for-safety measure: ruby-build
+# defaults to `make -j$(nproc)`, and on a many-core build host this ancient
+# Makefile (bigdecimal's ext build, specifically) loses a directory-creation
+# race under real parallelism -- confirmed reproducing "cp: cannot create
+# regular file '../../.ext/common/bigdecimal': No such file or directory"
+# on a 12-core machine, and confirmed fixed by forcing a serial build. This
+# bit us as a non-deterministic CI failure (succeeded once, then failed on
+# an identical rebuild minutes later) before being tracked down.
+RUN MAKE_OPTS="-j1" RUBY_CONFIGURE_OPTS="--disable-install-doc" rbenv install "$RUBY_VERSION" \
+ && rbenv global "$RUBY_VERSION"
+
+RUN echo 'eval "$(rbenv init -)"' >> /root/.bashrc
diff --git a/.github/docker/Dockerfile.legacy187 b/.github/docker/Dockerfile.legacy187
new file mode 100644
index 000000000..918494d75
--- /dev/null
+++ b/.github/docker/Dockerfile.legacy187
@@ -0,0 +1,56 @@
+# Builds Ruby 1.8.7 from source against a vendored OpenSSL 1.0.2u.
+#
+# Unlike 1.9.3-2.1.x, ruby-build's own 1.8.7-p374 definition has no OpenSSL
+# handling at all (it's a bare install_package), so rbenv/ruby-build can't
+# get this one for free. Ruby 1.8.7's ext/openssl accesses OpenSSL struct
+# internals that became fully opaque in OpenSSL 1.1.0 (2016), so it can't
+# compile against this (or any modern) base image's system OpenSSL. Instead
+# we build OpenSSL 1.0.2u (the same version ruby-build vendors automatically
+# for 1.9.3+) from source ourselves and point Ruby's own ./configure at it
+# directly -- confirmed working: openssl.so builds, and
+# OpenSSL::SSL::SSLContext.new instantiates correctly at runtime.
+FROM debian:bullseye-slim
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ build-essential curl ca-certificates git \
+ zlib1g-dev libreadline-dev libyaml-dev libncurses5-dev \
+ libffi-dev libgdbm-dev libxml2-dev libxslt1-dev libcurl4-openssl-dev \
+ psmisc \
+ && rm -rf /var/lib/apt/lists/*
+
+# Vendored OpenSSL 1.0.2u -- last release of the pre-opaque-struct 1.0 line.
+RUN cd /tmp \
+ && curl -sL -o openssl-1.0.2u.tar.gz https://www.openssl.org/source/openssl-1.0.2u.tar.gz \
+ && tar xzf openssl-1.0.2u.tar.gz \
+ && cd openssl-1.0.2u \
+ && ./config --prefix=/opt/openssl102 --openssldir=/opt/openssl102/ssl shared zlib -fPIC \
+ && make -j"$(nproc)" \
+ && make install_sw \
+ && cd / && rm -rf /tmp/openssl-1.0.2u /tmp/openssl-1.0.2u.tar.gz
+
+# Ruby 1.8.7-p374, configured against the vendored OpenSSL above.
+# -j1: confirmed (via Dockerfile.legacy's rbenv build of 1.9.3, same era of
+# Makefile) that these ancient ext/ build systems lose a directory-creation
+# race under real parallelism on a many-core host -- serial build trades a
+# few extra minutes for a build that doesn't fail nondeterministically.
+RUN cd /tmp \
+ && curl -sL -o ruby-1.8.7-p374.tar.bz2 https://cache.ruby-lang.org/pub/ruby/1.8/ruby-1.8.7-p374.tar.bz2 \
+ && tar xjf ruby-1.8.7-p374.tar.bz2 \
+ && cd ruby-1.8.7-p374 \
+ && ./configure --prefix=/opt/ruby187 --with-openssl-dir=/opt/openssl102 \
+ LDFLAGS="-Wl,-rpath,/opt/openssl102/lib" \
+ && make -j1 \
+ && make install \
+ && cd / && rm -rf /tmp/ruby-1.8.7-p374 /tmp/ruby-1.8.7-p374.tar.bz2
+
+ENV PATH="/opt/ruby187/bin:${PATH}"
+
+# RubyGems wasn't bundled with the interpreter until Ruby 1.9 -- 1.8.7 needs
+# it installed as a wholly separate package (this is the same rubygems
+# release ruby-build itself pairs with 1.8.7).
+RUN cd /tmp \
+ && curl -sL -o rubygems-1.6.2.tgz https://rubygems.org/rubygems/rubygems-1.6.2.tgz \
+ && tar xzf rubygems-1.6.2.tgz \
+ && cd rubygems-1.6.2 \
+ && ruby setup.rb \
+ && cd / && rm -rf /tmp/rubygems-1.6.2 /tmp/rubygems-1.6.2.tgz
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 000000000..1922dbbd6
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,320 @@
+name: CI
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master]
+ workflow_dispatch:
+
+jobs:
+ # Runs test:deep inside the exact same official Docker Hub `ruby` image
+ # used for local validation, via plain `docker run` (not the job-level
+ # `container:` key -- that injects a Node runtime these old images can't
+ # run). See CHANGELOG.md for the full architecture rationale.
+ test:
+ name: Ruby ${{ matrix.ruby }}
+ runs-on: ubuntu-latest
+ # 3.0.7/3.1.7/3.2.11 have one known, non-soap4r-bug failure each -- see
+ # "Known Test Suite Exceptions" in the README.
+ continue-on-error: ${{ contains(fromJSON('["3.0.7", "3.1.7", "3.2.11"]'), matrix.ruby) }}
+ strategy:
+ fail-fast: false
+ matrix:
+ ruby:
+ - '2.1.10'
+ - '2.2.10'
+ - '2.3.8'
+ - '2.4.10'
+ - '2.5.9'
+ - '2.6.10'
+ - '2.7.8'
+ - '3.0.7'
+ - '3.1.7'
+ - '3.2.11'
+ - '3.3.11'
+ - '3.4.10'
+ - '4.0.5'
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Run all 5 parsers
+ run: |
+ docker run --rm -v "${{ github.workspace }}":/repo-ro:ro ruby:${{ matrix.ruby }} bash -c '
+ set -e
+ apt-get update -qq && apt-get install -y -qq psmisc
+ mkdir -p /app && cp -r /repo-ro/. /app/ && cd /app
+ gem install bundler --no-document 2>&1 \
+ || gem install bundler -v 1.17.3 --no-document 2>&1 \
+ || gem install bundler -v 1.17.3 --no-ri --no-rdoc
+ bundle install
+ # set -e must not wrap the loop itself: rake test:deep exits
+ # non-zero per failure, and some versions have exactly one
+ # guaranteed failure on every parser (see README) -- track
+ # failures instead so every parser still runs. See CHANGELOG.md.
+ set +e
+ FAILED=0
+ for parser in oxparser nokogiriparser libxmlparser ogaparser rexmlparser; do
+ # Force-clear the shared test port between runs -- see CHANGELOG.md.
+ fuser -k -n tcp 17171 >/dev/null 2>&1 || true
+ echo "--- SOAP4R_PARSERS=$parser ---"
+ SOAP4R_PARSERS=$parser bundle exec rake test:deep
+ [ $? -ne 0 ] && FAILED=1
+ sleep 2
+ done
+ exit $FAILED
+ '
+
+ # HTTP client backend is independently selectable (lib/soap/httpbackend.rb).
+ # httpclient is already exercised above (the default); this adds the
+ # net_http fallback. One parser (oxparser) is enough -- this tests the
+ # HTTP backend, not the XML parser.
+ - name: Run net_http HTTP client backend (oxparser)
+ run: |
+ docker run --rm -v "${{ github.workspace }}":/repo-ro:ro ruby:${{ matrix.ruby }} bash -c '
+ set -e
+ apt-get update -qq && apt-get install -y -qq psmisc
+ mkdir -p /app && cp -r /repo-ro/. /app/ && cd /app
+ gem install bundler --no-document 2>&1 \
+ || gem install bundler -v 1.17.3 --no-document 2>&1 \
+ || gem install bundler -v 1.17.3 --no-ri --no-rdoc
+ bundle install
+ fuser -k -n tcp 17171 >/dev/null 2>&1 || true
+ SOAP4R_HTTP_CLIENTS=net_http SOAP4R_PARSERS=oxparser bundle exec rake test:deep
+ '
+
+ # curb is opt-in (Gemfile group :http_curb, optional: true) and gated
+ # to Ruby >= 2.4; below that floor this step skips explicitly rather
+ # than hard-failing the job. Uses BUNDLE_WITH (env var), not `bundle
+ # config set with "..."` -- the latter is Bundler 2.x-only syntax and
+ # silently no-ops on the Bundler 1.17.3 every Ruby below 3.2 falls
+ # back to. See CHANGELOG.md for both bugs in full.
+ #
+ # continue-on-error scoped to just this step (not the whole job) for
+ # 2.4.10/2.5.9 specifically: their Docker images ship a libcurl/OpenSSL
+ # pair that fails CA chain validation on test_ca_verification/
+ # test_ciphers -- a known, environment-specific CANTFIX (see "Known
+ # Test Suite Exceptions" in README.md), not a real regression. Every
+ # other step for these two versions (main parser sweep, net_http,
+ # faraday) is left fully enforced.
+ - name: Run curb HTTP client backend (oxparser)
+ continue-on-error: ${{ contains(fromJSON('["2.4.10", "2.5.9"]'), matrix.ruby) }}
+ run: |
+ docker run --rm -v "${{ github.workspace }}":/repo-ro:ro ruby:${{ matrix.ruby }} bash -c '
+ set -e
+ if ! ruby -e "exit(RUBY_VERSION.to_f >= 2.4 ? 0 : 1)"; then
+ echo "Skipping: curb is gated to Ruby >= 2.4 in the Gemfile."
+ exit 0
+ fi
+ export BUNDLE_WITH="http_curb"
+ apt-get update -qq && apt-get install -y -qq psmisc libcurl4-openssl-dev pkg-config build-essential
+ mkdir -p /app && cp -r /repo-ro/. /app/ && cd /app
+ gem install bundler --no-document 2>&1 \
+ || gem install bundler -v 1.17.3 --no-document 2>&1 \
+ || gem install bundler -v 1.17.3 --no-ri --no-rdoc
+ bundle install
+ fuser -k -n tcp 17171 >/dev/null 2>&1 || true
+ SOAP4R_HTTP_CLIENTS=curb SOAP4R_PARSERS=oxparser bundle exec rake test:deep
+ '
+
+ # Faraday's default adapter (:net_http) doesn't get its own step --
+ # net_http above already covers that path end-to-end. :typhoeus
+ # (libcurl-based, via ethon/FFI) is the adapter this project tests
+ # against instead, and gated to Ruby >= 2.6 same as Faraday itself
+ # (see Gemfile/CHANGELOG.md); below that floor this step skips
+ # explicitly rather than hard-failing the job.
+ - name: Run faraday HTTP client backend, typhoeus adapter (oxparser)
+ run: |
+ docker run --rm -v "${{ github.workspace }}":/repo-ro:ro ruby:${{ matrix.ruby }} bash -c '
+ set -e
+ if ! ruby -e "exit((RUBY_VERSION.to_f >= 2.6 && RUBY_PLATFORM !~ /java/) ? 0 : 1)"; then
+ echo "Skipping: Faraday is gated to Ruby >= 2.6 in the Gemfile."
+ exit 0
+ fi
+ export BUNDLE_WITH="http_faraday"
+ apt-get update -qq && apt-get install -y -qq psmisc libcurl4-openssl-dev
+ mkdir -p /app && cp -r /repo-ro/. /app/ && cd /app
+ gem install bundler --no-document 2>&1 \
+ || gem install bundler -v 1.17.3 --no-document 2>&1 \
+ || gem install bundler -v 1.17.3 --no-ri --no-rdoc
+ bundle install
+ fuser -k -n tcp 17171 >/dev/null 2>&1 || true
+ SOAP4R_HTTP_CLIENTS=faraday SOAP4R_FARADAY_ADAPTER=typhoeus SOAP4R_PARSERS=oxparser bundle exec rake test:deep
+ '
+
+ # :patron was tried as a third spot-check adapter and dropped from CI:
+ # a real upstream patron bug, not ours to fix -- see "Known Test Suite
+ # Exceptions" in README.md.
+
+ # 1.9.3/2.0.0's official Docker Hub images use the legacy manifest v1
+ # schema and are no longer pullable at all. Built from source instead via
+ # rbenv/ruby-build on Debian bullseye (Dockerfile.legacy). See CHANGELOG.md.
+ test-legacy-source-build:
+ name: Ruby ${{ matrix.ruby }} (built from source via rbenv)
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ ruby:
+ - '1.9.3-p551'
+ - '2.0.0-p648'
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Build image
+ run: |
+ docker build -f .github/docker/Dockerfile.legacy \
+ --build-arg RUBY_VERSION=${{ matrix.ruby }} \
+ -t soap4r-legacy:${{ matrix.ruby }} .github/docker
+
+ - name: Run all 5 parsers
+ run: |
+ docker run --rm -v "${{ github.workspace }}":/repo-ro:ro soap4r-legacy:${{ matrix.ruby }} bash -c '
+ set -e
+ mkdir -p /app && cp -r /repo-ro/. /app/ && cd /app
+ gem install bundler --no-document 2>&1 \
+ || gem install bundler -v 1.17.3 --no-document 2>&1 \
+ || gem install bundler -v 1.17.3 --no-ri --no-rdoc
+ rbenv rehash 2>&1 || true
+ bundle install
+ # See the `test` job above re: set -e and port-clearing.
+ set +e
+ FAILED=0
+ for parser in oxparser nokogiriparser libxmlparser ogaparser rexmlparser; do
+ fuser -k -n tcp 17171 >/dev/null 2>&1 || true
+ echo "--- SOAP4R_PARSERS=$parser ---"
+ SOAP4R_PARSERS=$parser bundle exec rake test:deep
+ [ $? -ne 0 ] && FAILED=1
+ sleep 2
+ done
+ exit $FAILED
+ '
+
+ # Ruby 1.8.7 has no official Docker Hub image; Dockerfile.legacy187 builds
+ # it from source against a vendored OpenSSL 1.0.2u. oga is excluded from
+ # the parser loop (its ruby-ll dependency genuinely needs Ruby >= 2.1).
+ # continue-on-error: known, understood failures -- see "Known Test Suite
+ # Exceptions" in README.md / CHANGELOG.md.
+ test-ruby-1_8_7:
+ name: Ruby 1.8.7 (built from source, vendored OpenSSL)
+ runs-on: ubuntu-latest
+ continue-on-error: true
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Build Ruby 1.8.7 image
+ run: docker build -f .github/docker/Dockerfile.legacy187 -t soap4r-ruby187 .github/docker
+
+ - name: Run 4 parsers (oga is never installed below Ruby 1.9.3)
+ run: |
+ docker run --rm -v "${{ github.workspace }}":/repo-ro:ro soap4r-ruby187 bash -c '
+ set -e
+ mkdir -p /app && cp -r /repo-ro/. /app/ && cd /app
+ gem install bundler --no-document 2>&1 \
+ || gem install bundler -v 1.17.3 --no-document 2>&1 \
+ || gem install bundler -v 1.17.3 --no-ri --no-rdoc
+ bundle install
+ # See the `test` job above re: set -e and port-clearing.
+ set +e
+ FAILED=0
+ for parser in oxparser nokogiriparser libxmlparser rexmlparser; do
+ fuser -k -n tcp 17171 >/dev/null 2>&1 || true
+ echo "--- SOAP4R_PARSERS=$parser ---"
+ SOAP4R_PARSERS=$parser bundle exec rake test:deep
+ [ $? -ne 0 ] && FAILED=1
+ sleep 2
+ done
+ exit $FAILED
+ '
+
+ # JRuby: ox and libxml-ruby have no viable JRuby gem, so both are excluded
+ # from the parser loop. continue-on-error: remaining differences are known
+ # environment/dependency quirks, not soap4r-ng bugs -- see "Known Test
+ # Suite Exceptions" in README.md.
+ test-jruby:
+ name: JRuby ${{ matrix.ruby }}
+ runs-on: ubuntu-latest
+ continue-on-error: true
+ strategy:
+ fail-fast: false
+ matrix:
+ ruby:
+ - '9.4.15.0'
+ - '10.1.0.0'
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Run 3 parsers (ox and libxml-ruby have no JRuby port)
+ run: |
+ docker run --rm -v "${{ github.workspace }}":/repo-ro:ro jruby:${{ matrix.ruby }} bash -c '
+ set -e
+ # git: Gemfile pulls rubyjedi-testunitxml from GitHub. psmisc: port-clearing below.
+ apt-get update -qq && apt-get install -y -qq git psmisc
+ mkdir -p /app && cp -r /repo-ro/. /app/ && cd /app
+ gem install bundler --no-document
+ bundle install
+ # See the `test` job above re: set -e and port-clearing.
+ set +e
+ FAILED=0
+ for parser in nokogiriparser ogaparser rexmlparser; do
+ fuser -k -n tcp 17171 >/dev/null 2>&1 || true
+ echo "--- SOAP4R_PARSERS=$parser ---"
+ SOAP4R_PARSERS=$parser bundle exec rake test:deep
+ [ $? -ne 0 ] && FAILED=1
+ sleep 2
+ done
+ exit $FAILED
+ '
+
+ # Aggregates every job above (including each matrix expansion) into one
+ # markdown table on the run's summary page, so the full version x parser
+ # picture is visible at a glance instead of requiring a click into each
+ # job individually. `if: always()` so this still renders when something
+ # upstream failed -- that's the case it's most useful for.
+ summary:
+ name: Test Matrix Summary
+ needs: [test, test-legacy-source-build, test-ruby-1_8_7, test-jruby]
+ if: always()
+ runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ steps:
+ - uses: actions/github-script@v7
+ with:
+ script: |
+ const { data } = await github.rest.actions.listJobsForWorkflowRun({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ run_id: context.runId,
+ per_page: 100,
+ });
+
+ const icon = (conclusion) => {
+ switch (conclusion) {
+ case 'success': return ':white_check_mark:';
+ case 'skipped': return ':heavy_minus_sign:';
+ case 'cancelled': return ':no_entry_sign:';
+ case null: return ':hourglass:'; // still in progress
+ default: return ':x:'; // failure, timed_out, action_required, etc.
+ }
+ };
+
+ // addTable() emits a real
, not markdown pipe syntax --
+ // a markdown table right after addHeading()'s doesn't
+ // render as a table at all (GFM block-adjacency rule). See CHANGELOG.md.
+ const rows = [
+ [
+ { data: 'Job', header: true },
+ { data: 'Result', header: true },
+ ],
+ ];
+ for (const job of data.jobs) {
+ if (job.name === 'Test Matrix Summary') continue;
+ rows.push([job.name, `${icon(job.conclusion)} ${job.conclusion ?? 'in progress'}`]);
+ }
+
+ await core.summary
+ .addHeading('Test Matrix Results')
+ .addTable(rows)
+ .addRaw('\n_See "Known Test Suite Exceptions" in the README for why a handful of JRuby and legacy-Ruby jobs are expected to show failures._')
+ .write();
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 45daf2218..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,42 +0,0 @@
-language: ruby
-rvm:
- - 1.8.7
- - 1.9.3
- - 2.0.0
- - 2.1
- - 2.2
- - 2.3
-env:
- - SOAP4R_PARSERS=oxparser
- - SOAP4R_PARSERS=nokogiriparser
- - SOAP4R_PARSERS=libxmlparser
- - SOAP4R_PARSERS=ogaparser
- - SOAP4R_PARSERS=rexmlparser
-
-gemfile:
- - gemfiles/httpclient.gemfile
-
-matrix:
- exclude:
- - rvm: 1.8.7
- env: SOAP4R_PARSERS=ogaparser
-
- allow_failures:
- - rvm: 1.8.7
- env: SOAP4R_PARSERS=oxparser
- - rvm: jruby
- - rvm: rbx
- - env: SOAP4R_PARSERS=libxmlparser
- - rvm: 2.3
-
-notifications:
- recipients:
- - coder.notify@not404.com
-
-branches:
- only:
- - master
-
-addons:
- code_climate:
- repo_token: ec0b3e0a786f4b93a2522db298c4155c77e25c2137592df0d4f9c2f1a491b89a
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000..930f97029
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,406 @@
+# Changelog
+
+Detailed rationale and investigation notes for non-obvious dependency and
+compatibility decisions. Kept separate from README.md/code comments so
+those stay skimmable; this is the place to look for the "why" in full.
+
+## Unreleased
+
+### Faraday gated to Ruby >= 2.6
+
+`gem 'faraday'` was previously unconstrained, which resolved *some* old
+release for every Ruby down to 1.9.3 -- but that old-Faraday territory
+turned out to be broken, not just untested:
+
+- Below Faraday 1.10 (resolved as far up as Ruby 2.3.8), net_http support
+ is bundled directly into the main `faraday` gem with no separate
+ `faraday-net_http` package. `lib/soap/faradayClient.rb`'s
+ `require "faraday/#{adapter}"` convention assumes the modern
+ split-into-per-adapter-gems architecture, so it can't load any adapter
+ at all there -- an architecture mismatch, not a version-selection bug.
+- Ruby 2.4.10/2.5.9 resolve Faraday 1.10.6, which does carry a real
+ `faraday-net_http` dependency and gets further, but hits a distinct bug:
+ `TypeError: wrong argument (String)! (Expected kind of
+ OpenSSL::X509::Certificate)` deep inside `Net::HTTP#connect`, from
+ `faradayClient.rb` passing a raw file-path string where curb's own
+ client already correctly converts to a parsed certificate object.
+
+Decision: Faraday is a modern-transport enhancement, not a legacy
+capability worth carrying forward two Ruby versions for. Gated to
+`>= 2.6` outright, matching `faraday-typhoeus`'s own real floor
+(confirmed: its oldest RubyGems release needs Ruby >= 2.5's `logger`, and
+the next release requires Ruby >= 2.6 directly). `faraday-patron` bumped
+to the same floor since an adapter gem is inert without its host.
+
+`curb` has the equivalent floor for an unrelated reason: its C extension
+references `CURL_SSLVERSION_MAX_*` constants absent before libcurl 7.54.0.
+The official `ruby:2.2.10`/`ruby:2.3.8` Docker Hub images' system
+`libcurl-dev` predates that (confirmed hard compile failure, not a
+graceful skip); `ruby:2.4.10`'s image is new enough.
+
+CI had a latent bug matching both of these: `bundle config set with "..."`
+is Bundler 2.x-only syntax. Bundler 1.17.3 (what every Ruby < 3.2 falls
+back to) has no `set` verb and silently creates a bogus config key
+literally named `"set"` -- meaning curb/faraday were never actually
+exercised in CI below Ruby 3.2. Fixed with the `BUNDLE_WITH="group1:group2"`
+env var instead (confirmed identical behavior on both Bundler
+generations), exported (not just prefixed on the `bundle install` line)
+since `bundle exec`'s own `Bundler.setup` re-checks group inclusion
+independently for that invocation too. Both `ci.yml` steps also needed
+explicit skip-and-exit-0 logic below their respective floors --
+previously a Ruby version below the floor would hard-fail the whole job
+(`RuntimeError: HTTP client backend not found`) rather than skip cleanly.
+
+### Ox pin split three ways (dropping htmlentities where possible)
+
+The Gemfile pins Ox differently depending on Ruby version because
+compatibility and entity-decoding behavior don't move together, or even
+monotonically, across releases -- confirmed by installing every Ox
+release from 2.4.5 through 2.14.28 across Ruby 1.8.7, 1.9.3, 2.0.0, and
+2.1.10 and testing both (a) whether it loads and (b) whether it decodes
+named HTML entities (`…`, `—`) natively via
+`:convert_special => true`:
+
+- Entity decoding never improves anywhere in 2.4.5-2.8.0 (identical,
+ undecoded output throughout every version tested). It first improves at
+ **2.13.4** (2020-09-12), which decodes the same named-entity set
+ htmlentities does, byte-identical on direct comparison.
+- **2.14.7** is the first release to call `rb_utf8_str_new`, a symbol not
+ exported by Ruby's shared lib before 2.2 -- `undefined symbol:
+ rb_utf8_str_new` at native-ext load time on Ruby 2.0.0/2.1.10. Oddly,
+ this does *not* reproduce on Ruby 1.9.3 even with the same Ox version --
+ Ox's extconf must feature-detect the symbol differently depending on
+ which Ruby's headers are present at compile time, so the boundary isn't
+ a simple "older Ruby is safer" gradient; each version needs testing
+ directly rather than assumed from a neighbor.
+- **2.14.6** is therefore the newest release confirmed to work, with
+ correct entity decoding, on 1.9.3, 2.0.0, *and* 2.1.10 uniformly. Pinned
+ exactly (`= 2.14.6`), since the very next patch release breaks it.
+- **Ruby 1.8.7** can't use 2.14.6 at all: its `extconf.rb` fails outright
+ with `undefined method 'slice!' for nil:NilClass`, an mkmf/extconf
+ incompatibility with 1.8.7's build tooling, unrelated to the
+ `rb_utf8_str_new` issue above. Falls back to the older `= 2.4.5` pin,
+ which does compile and load there but only decodes the basic 5 XML
+ entities -- htmlentities is still required, and only there.
+ - Pinned exactly (`= 2.4.5`, not `~>`): later 2.4.x patches (confirmed
+ with 2.4.13) hit a *second*, different unresolvable-symbol crash
+ (`RSTRUCT_GET`) on Ruby 2.0.0/2.1.10 -- though notably *not* on
+ 1.8.7/1.9.3, the same non-monotonic pattern as the 2.14.x boundary
+ above. Moot now that 1.8.7 is the only Ruby left in this branch, but a
+ floating patch-level constraint would silently regress this the
+ moment that changes.
+ - `htmlentities 4.3.1` pinned exactly there too: 4.3.3+ hard-fails on
+ 1.8.7 with `NameError: uninitialized constant
+ HTMLEntities::Decoder::Encoding` (references Ruby's `Encoding` class,
+ introduced in 1.9, absent on 1.8.7).
+
+A CRLF regression was found and fixed along the way, incidental to this
+investigation: `lib/xsd/xmlparser/oxparser.rb`'s no-decoder branch used
+`:skip => :skip_return`, which discards `\r` -- unrelated to entity
+decoding, but broke CRLF round-tripping (`test_string_crlf`) for anyone
+running without htmlentities installed. Changed to `:skip_none`
+unconditionally.
+
+Regression matrix re-run after these changes: 1.9.3, 2.0.0, and 2.1.10 all
+green; 1.8.7 shows only its pre-existing, already-documented exceptions
+(see "Known Test Suite Exceptions" below) -- no new failures introduced.
+**This re-run did not include Ruby >= 2.2** (its `gem 'ox'` branch wasn't
+touched by this change, so it seemed out of scope at the time) -- see the
+correction directly below, found days later while reordering these same
+Gemfile branches for an unrelated reason.
+
+### Correction: htmlentities still required on Ruby 2.2.x - 2.6.x
+
+The "htmlentities is dead weight above 1.9.3" conclusion above was wrong
+for part of that range. `gem 'ox'` unconstrained (the `>= 2.2` branch)
+resolves whatever the newest Ox release is for the running Ruby -- and Ox
+added a hard `Ruby >= 2.7.0` requirement to its own gemspec starting at
+2.14.15. That means Ruby 2.2.x-2.6.x are stuck resolving **2.14.14** (the
+last release before that floor), while Ruby >= 2.7 gets 2.14.15+.
+
+2.14.14 segfaults inside `Ox.sax_parse`'s `:convert_special => true` path
+on complex documents -- confirmed via a real crash in `test_mapping.rb`
+(`[BUG] Segmentation fault` at `oxparser.rb:33`), reproduced identically on
+Ruby 2.2.10, 2.3.8, 2.4.10, and 2.6.10, while a bare, simple XML string
+through the same code path does *not* crash (so it's data/complexity
+dependent, not a trivial always-fails bug -- a quick smoke test wouldn't
+have caught it). Ruby 2.7.8 (resolving Ox 2.14.28) and Ruby 3.0.7/3.4.10/
+4.0.5 all run the same test cleanly.
+
+`htmlentities` avoids the bug entirely by design, not by luck:
+`oxparser.rb` only passes `:convert_special => true` when no decoder is
+present; with htmlentities installed, Ox is called without that option and
+never enters the buggy path. So for Ruby 2.2.x-2.6.x, htmlentities is a
+required workaround, not an optional speed boost -- `gem 'htmlentities'`
+was restored there (Gemfile), unconstrained Ox unconstrained-and-safe only
+from Ruby >= 2.7.
+
+This was only caught because a later, unrelated task (reordering the
+Gemfile's version-gate branches newest-first) prompted a fresh full-matrix
+re-run that happened to include Ruby 2.2.10 for the first time since the
+original htmlentities change. The lesson: a version-gate boundary that
+touches one branch's *dependency count* (adding/removing a gem) can change
+behavior in a sibling branch that looks untouched, if both branches share
+downstream code (`oxparser.rb`) that behaves differently based on which
+gems happen to be present -- worth re-running the full matrix after any
+Gemfile change that touches a shared code path, not just the Ruby
+versions whose own branch changed.
+
+### CI: Docker-per-version architecture, not ruby/setup-ruby
+
+`ci.yml`'s `test` job runs each Ruby version via a plain `docker run` of
+the exact same official `ruby:X.Y.Z` image used for local validation,
+rather than a `ruby/setup-ruby`-based matrix on the GitHub-hosted runner
+VM directly, and rather than the job-level `container:` key.
+
+- The earlier `ruby/setup-ruby` approach kept drifting from local
+ validation in ways that cost real debugging time: `ubuntu-latest`
+ rolling forward under a pinned Ruby version, `ruby-builder` not always
+ publishing a binary for every Ruby x Ubuntu combination, and Ubuntu
+ 22.04's ICU-enabled system libxml2 colliding with `libxml-ruby`'s C
+ extension in a way that never reproduced locally against Debian. Running
+ the identical container makes "works locally" and "works in CI" the same
+ claim by construction.
+- `container:` was tried and rejected: GitHub Actions injects its own
+ Node.js runtime into container jobs to run JS-based steps like
+ `actions/checkout`, and most of these official `ruby` images are old
+ enough (Debian jessie/stretch-era glibc) that the injected Node 24
+ binary can't execute (`version 'GLIBC_2.27' not found`), failing
+ checkout before a single test runs. Confirmed empirically: every job up
+ through Ruby 2.7 failed this way. Checkout instead runs on the runner VM
+ as normal, and a plain `docker run` does the actual `bundle install`/test
+ run inside the target image.
+- Exact patch-level tags (e.g. `2.4.10`, not `2.4`) are pinned to match
+ precisely what was locally validated, even though these are all
+ EOL/frozen lines where the floating tag would resolve identically.
+
+### CI: BUNDLE_WITH bug (curb/faraday silently never tested below Ruby 3.2)
+
+`bundle config set with "http_curb"` is Bundler 2.x-only syntax. Every
+Ruby below 3.2 in this matrix falls back to Bundler 1.17.3 (the last
+release installable there), whose `config` CLI has no `set` verb at all --
+under 1.17.3 that command silently creates a bogus config key literally
+named `"set"` (value `"with http_curb"`) instead of configuring the `with`
+group list. Confirmed empirically: `bundle config` afterward showed the
+bogus key, and `SOAP4R_HTTP_CLIENTS=curb` then failed with "HTTP client
+backend not found" against a gem that was never installed. Meaning: every
+curb/faraday CI step below Ruby 3.2 had been silently testing nothing.
+
+Fixed with the `BUNDLE_WITH="group1:group2"` env var instead -- the same
+underlying mechanism a persisted `bundle config` writes to, confirmed
+working identically on both Bundler generations. It must be `export`ed
+(not just prefixed on the `bundle install` line): `bundle install` alone
+happily installs an optional group's gems to disk even without
+`BUNDLE_WITH` set for a later command, but `bundle exec`'s own
+`Bundler.setup` excludes `:optional => true` groups from the load path by
+default unless told to include them for that invocation too -- confirmed
+empirically that a `bundle exec` immediately after a successful,
+curb-installing `bundle install` still raised `LoadError` on `require
+'curb'` without `BUNDLE_WITH` set again.
+
+Both the curb and faraday CI steps also needed explicit skip-and-exit-0
+logic below their respective Gemfile floors (Ruby >= 2.4 / >= 2.6): without
+it, a matrix entry below the floor would hard-fail the whole job
+(`RuntimeError: HTTP client backend not found`) under `set -e` with no
+`continue-on-error` at that granularity, rather than skip cleanly.
+
+### CI: port 17171 clearing between parser runs
+
+All parsers/backends run as separate `rake test:deep` invocations
+sequentially in one long-lived container, sharing a hardcoded port (17171)
+used throughout the test suite. Confirmed via CI logs (Ruby 2.7.8, run
+28854082576) that something can stay bound to that port across a process
+boundary for minutes, well past the widened retry budget in
+`test/testutil.rb` (60 tries/1s). Never reproduced locally, and not fully
+root-caused (suspected a WEBrick/CGI-handler subprocess not fully
+released) -- `fuser -k -n tcp 17171` runs before every iteration
+regardless of cause rather than keep guessing at timing.
+
+Relatedly, `set -e` must not wrap the parser loop itself (only the setup
+steps before it): `rake test:deep` exits non-zero on any test failure, and
+several Ruby versions have exactly one guaranteed failure on every parser
+(see "Known Test Suite Exceptions" below) -- under `set -e` that killed the
+script after the first parser alone, silently skipping the rest without
+anyone noticing (confirmed: CI logs showed only one parser ever ran for
+those versions). Failures are tracked in a variable and the loop always
+runs to completion instead.
+
+### CI: summary job's GFM table-adjacency gotcha
+
+The `summary` job uses `core.summary.addTable()` (an actual HTML
+`
`) rather than hand-rolled markdown pipe syntax, because a
+markdown table placed immediately after `addHeading()`'s `` HTML with
+no blank line in between doesn't get parsed as a table at all (GitHub
+Flavored Markdown requires a fresh block boundary before table syntax) --
+confirmed on a live run, where the whole thing rendered as one
+literal-pipe-character paragraph instead of a table.
+
+### CI: legacy Ruby build strategy (1.9.3, 2.0.0, 1.8.7)
+
+1.9.3 and 2.0.0 have official Docker Hub images, but every tag in both
+lines was published in the legacy Docker manifest v1 schema and is no
+longer pullable at all (confirmed empirically: `not implemented: media
+type ... manifest.v1+prettyjws ... no longer supported since containerd
+v2.1`, against every tag tried). GitHub-hosted runners hit the same wall.
+Built from source instead via rbenv/ruby-build on Debian bullseye
+(`Dockerfile.legacy`) -- confirmed clean across all 5 parsers for both
+versions, including libxmlparser (bullseye's libxml2-dev doesn't have
+Ubuntu 22.04's ICU/`bool`-collision problem) and ogaparser (oga's ruby-ll
+dependency installs and works fine on both, unlike 1.8.7 below).
+
+1.8.7 has no official Docker Hub image at all (the `ruby` library starts
+at 1.9) and no ruby-build definition with OpenSSL handling, so
+rbenv/ruby-build can't get it for free. `Dockerfile.legacy187` builds it
+from source against a vendored OpenSSL 1.0.2u instead (confirmed working:
+`openssl.so` builds, `OpenSSL::SSL::SSLContext.new` instantiates
+correctly). `oga` is intentionally excluded from its parser loop: its
+`ruby-ll` dependency genuinely needs Ruby >= 2.1 on this version (unlike
+1.9.3 above), so it's correctly never installed there.
+
+### HTTP client backend rollout (curb/faraday, wiredump parity, TLS trust)
+
+`http-access2` (httpclient's old name, same author) is no longer published
+on RubyGems.org at all; retired from the backend cascade for the same
+reason. See git history for the adapter that used to sit here.
+
+Before `SOAP4R_HTTP_CLIENTS` existed as an independently selectable env
+var, nothing in the test suite could ever actually reach the `net_http`
+fallback -- this project's Gemfile always installs `httpclient`, so the
+cascade never had a reason to fall through to it. Making backends
+independently selectable surfaced (and fixed) a real bug in the process:
+`SOAP::NetHttpClient`'s wiredump output duplicated request/response bodies
+and was missing the raw request-line/header block entirely, corrupting
+any test or tool parsing `wiredump_dev` output.
+
+Several tests (WSDL-driven codegen round-trips, request-envelope
+assertions, ASP.NET-handler interop) used to be gated to `httpclient` only
+because they parsed `wiredump_dev` output assuming its specific block
+layout. That layout turned out to already be backend-neutral by design
+once `NetHttpClient`/`CurbClient`/`FaradayClient` were all built to mirror
+it -- confirmed by simply removing the gates and running the suite
+unmodified under every backend. The duplicated parsing logic across those
+test files is now consolidated into
+`TestUtil.parse_wiredump_request_body`/`parse_wiredump_response_body`
+(`test/testutil.rb`).
+
+`test/soap/ssl/test_ssl.rb` runs its SSL config-loading coverage against
+every SSL-capable backend now, not just httpclient: `test_ca_verification`
+and `test_ciphers` (rewritten to expect each backend's own real exception
+class -- `OpenSSL::SSL::SSLError` for httpclient, `Curl::Err::CurlError`
+for curb, `Faraday::Error` for Faraday; confirmed empirically, since
+test-unit's `assert_raise` wants an exact class match).
+`test_options`/`test_verification`/`test_property` stay httpclient-only:
+they're built around `ssl_config.verify_callback`, and neither
+libcurl-based backend (curb, or Faraday on typhoeus/patron) exposes a
+per-certificate Ruby callback hook at all -- confirmed against both
+libraries' public APIs. Faraday's own adapter list is deliberately not
+swept exhaustively in CI -- `lib/soap/faradayClient.rb` is what's under
+test, and sweeping every adapter Faraday supports would mostly re-test
+Faraday's own correctness. `:typhoeus` was chosen as the one CI adapter
+because it's also the one people actually reach for in practice (28
+reverse dependencies on RubyGems vs. `faraday-patron`'s 3, per [Ruby
+Toolbox](https://www.ruby-toolbox.com/projects/faraday-typhoeus)); Faraday's
+own `faraday-typhoeus` adapter was confirmed to silently drop the
+`ciphers` option (never forwards it to `ethon`'s `ssl_cipher_list`) -- a
+real gap in that third-party adapter, not this bridge.
+
+`httpclient`'s `SSLConfig` doesn't trust the system CA bundle unless told
+to -- left unconfigured, it lazily falls back to its own gem-vendored
+`cacert.pem` snapshot, which can go stale relative to a real server's
+certificate chain as CAs rotate intermediates (confirmed directly: a real
+Let's Encrypt-signed endpoint failed verification against an older bundled
+snapshot while verifying fine against the host's own CA bundle).
+`lib/soap/httpbackend/httpclient.rb` now calls `set_default_paths` on
+every connection's `ssl_config` by default, deferring to whatever CA store
+OpenSSL was built to trust on the platform, layered before any
+`ssl_config.ca_file`/`ca_path`/`cert_store` set explicitly. `NetHttpClient`
+was never affected -- it has no SSL config surface of its own and defers
+to stdlib `Net::HTTP`/OpenSSL defaults directly.
+
+## Known Test Suite Exceptions (full detail)
+
+Short summary lives in README.md; full root-cause narrative here.
+
+- **Ruby 1.8.7**:
+ - 3 errors (`test_time`, `test_time_ivar`, `test_time_subclass` in
+ `marshaltestlib.rb`): `Kernel#singleton_class` doesn't exist until Ruby
+ 1.9.2. CANTFIX -- the method is absent on that Ruby, full stop.
+ - CGI-based tests (`test_calc_cgi`, `test_authheader_cgi`) fail with
+ `SOAP::ResponseFormatError: ... Internal Server Error`. Root-caused
+ partway: WEBrick's `CGIHandler` wipes the spawned CGI subprocess's
+ entire environment before exec'ing it, so `logger-application` and
+ `webrick` need forwarding via a raw `-I` load-path flag (already fixed,
+ in both test files). That forwarding alone wasn't enough on 1.8.7
+ specifically: the spawned subprocess still hits `NameError: undefined
+ local variable or method 'logger'` inside
+ `lib/soap/rpc/cgistub.rb#run`, even though `Logger::Application#logger`
+ is a real public method and a minimal reproduction of the same class
+ hierarchy works fine in isolation. Not fully root-caused -- something
+ specific to the real `CGIStub`/`AuthHeaderPortServer`/`CalcServer`
+ class graph, only reproducible via the actual spawned-CGI-subprocess
+ path. Narrow enough (2 of ~330 tests, both CGI smoke tests) that it
+ wasn't worth further chasing.
+ - Collateral damage from the CGI issue above: `test_nil_attribute` and
+ `test_wsdl_with_map` (`test/wsdl/document/test_rpc.rb`) intermittently
+ receive a garbage `dateTime` value (`XSD::ValueSpaceError`) when run as
+ part of the full suite, but pass cleanly in isolation
+ (`SCOPE=wsdl/document`). Confirmed state leaking from a still-lingering
+ CGI subprocess/WEBrick thread earlier in the same run, not a genuine
+ 1.8.7 `Date`/`DateTime` bug -- `XSDDateTimeImpl#screen_data`'s `Time`
+ branch produces a correct result in isolation. Not independently
+ fixable -- same underlying CGI/WEBrick fragility, different test.
+- **Ruby 2.4.10, 2.5.9, `SOAP4R_HTTP_CLIENTS=curb`**: `test_ca_verification`
+ and `test_ciphers` (`test/soap/ssl/test_ssl.rb`) fail with
+ `Curl::Err::SSLPeerCertificateError: ... unable to get issuer
+ certificate`, even with a correct, complete CA chain supplied. CANTFIX --
+ confirmed environment-specific: the official `ruby:2.4.10`/`ruby:2.5.9`
+ images ship libcurl 7.64.0/OpenSSL 1.1.1d, while `ruby:2.6.10`+ ship
+ libcurl 7.74.0/OpenSSL 1.1.1n; the same chain validates cleanly under the
+ newer pair. httpclient/net_http (unaffected by libcurl version) pass
+ this same test cleanly everywhere, confirming this is that older libcurl
+ build, not this bridge's CA-file wiring.
+- **Ruby 3.0.7, 3.1.7, 3.2.11**: 1 failure in `test_exception`
+ (`marshaltestlib.rb`), which marshals an exception whose `.message`
+ embeds a live `#inspect` dump of the entire running
+ `Test::Unit::TestCase` (memory addresses, internal framework state and
+ all). WONTFIX -- confirmed via minimal reproduction that soap4r-ng's own
+ exception-marshaling round-trips correctly; the test itself compares
+ volatile process/framework internals that render differently across this
+ narrow patch-version range.
+- **JRuby** (9.4.15.0 and 10.1.0.0, identical on both) -- 13 confirmed
+ environment-specific items:
+ - 7 `XSD::ValueSpaceError` tests (`test_SOAPInteger`, `test_XSDInteger`,
+ etc.): JRuby's `Kernel#Integer()` silently stops validating trailing
+ garbage once the digit count gets long enough, where MRI always raises
+ `ArgumentError`. CANTFIX -- JRuby core-method behavior difference.
+ - `test_singleton` (`marshaltestlib.rb`): expects marshaling `ENV` to
+ raise `TypeError`. JRuby's `ENV` is backed by a `Hash`-flavored object
+ rather than MRI's anonymous-object singleton, so it never trips the
+ check. CANTFIX -- JRuby object-representation difference.
+ - `test_ciphers`, `test_property`, `test_verification`
+ (`test/soap/ssl/test_ssl.rb`): `TypeError: failed to coerce
+ java.lang.String to [Ljava.lang.String;`, confirmed entirely inside
+ `httpclient`'s own JRuby SSL socket bridge
+ (`httpclient/jruby_ssl_socket.rb`). CANTFIX (upstream dependency).
+ - `test_nestedexception` (both variants): JRuby's backtrace formatting
+ differs from every MRI format already branched on. CANTFIX -- the
+ engine differs, not the Ruby version.
+ - A handful of other JRuby-only failures (`test_calc`, `test_calc2`,
+ `test_calc_cgi`, `test_authfailure` x2, `test_mu`) turned out to be a
+ real, fixable bug: `SOAP::Mapping.fault2exception` assumed a
+ reconstructed fault exception always has a non-nil `.backtrace`, false
+ on JRuby for a programmatically-constructed exception. Fixed with a
+ nil-guard.
+- **`SOAP4R_HTTP_CLIENTS=faraday SOAP4R_FARADAY_ADAPTER=patron`** -- not run
+ in CI, but documented for anyone who reaches for it: CGI-based tests
+ fail with `Patron::Aborted: Callback aborted`. Root-caused to `patron`
+ itself: reproduced with a bare `Patron::Session#post` against the exact
+ same WEBrick CGI-subprocess server, no soap4r-ng or Faraday code in the
+ path. A raw TCP-level dump ruled out a missing `Content-Length`. Most
+ likely explanation, unconfirmed without reading patron's C extension
+ directly: patron implements its own request timeouts via a libcurl
+ progress callback, and the CGI handler's per-request subprocess spawn (a
+ few hundred ms before the first byte, unlike every other test's
+ in-process handler) is the one thing distinguishing failing requests
+ from passing ones, including under `curb` (also libcurl-based) and both
+ adapters CI actually runs. CANTFIX without a patron-side fix.
diff --git a/Gemfile b/Gemfile
index 86a4cd823..a470ad1ed 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,42 +1,151 @@
source 'http://rubygems.org'
-if RUBY_VERSION.to_f <= 1.8
- gem 'htmlentities', '4.3.1' # Require this if OxParser's built-in "Special Character" conversion isn't sufficient for your needs.
- gem 'nokogiri', '~> 1.5.11' # nokogiriparser ; Uses libxml2, libxslt, and zlib
+if RUBY_VERSION.to_f > 1.8
+ gem 'httpclient' # 2.1.5.2
+else
gem 'httpclient', '~> 2.7.0.1'
+end
+
+# ---------------------------------------------------------------------------
+# Additional (opt-in) HTTP client backends -- see "HTTP Client Backends" in
+# README.md and lib/soap/httpbackend.rb. Not installed by default; requires
+# curb needs a system libcurl-dev at compile time. Install with:
+# bundle install --with http_curb http_faraday
+# Neither has a JRuby port (no native-extension support there).
+group :http_curb, :optional => true do
+ # >= 2.4: curb.c needs CURL_SSLVERSION_MAX_* (libcurl >= 7.54.0), which
+ # predates the system libcurl-dev on ruby:2.2.10/2.3.8's Docker images.
+ # See CHANGELOG.md.
+ gem 'curb' if RUBY_PLATFORM !~ /java/ && RUBY_VERSION.to_f >= 2.4
+end
+group :http_faraday, :optional => true do
+ # >= 2.6: an enhancement for modern Rubies, not a legacy capability --
+ # older Faraday releases don't match lib/soap/faradayClient.rb's adapter
+ # architecture. See CHANGELOG.md.
+ gem 'faraday' if RUBY_VERSION.to_f >= 2.6
+ # faraday-typhoeus is the real second adapter this project tests against
+ # (see SOAP4R_FARADAY_ADAPTER in lib/soap/faradayClient.rb).
+ gem 'faraday-typhoeus' if RUBY_PLATFORM !~ /java/ && RUBY_VERSION.to_f >= 2.6
+ gem 'typhoeus' if RUBY_PLATFORM !~ /java/ && RUBY_VERSION.to_f >= 2.6
+ # ethon (typhoeus's dep) pulls in an unconstrained ffi needing Ruby >= 3.0;
+ # pin the newest release that still installs below that.
+ gem 'ffi', '~> 1.12.2' if RUBY_PLATFORM !~ /java/ && RUBY_VERSION.to_f >= 2.6 && RUBY_VERSION.to_f < 3.0
+ # faraday-patron: third, optional spot-check adapter (see
+ # SOAP4R_FARADAY_ADAPTER=patron).
+ gem 'faraday-patron' if RUBY_PLATFORM !~ /java/ && RUBY_VERSION.to_f >= 2.6
+ gem 'patron' if RUBY_PLATFORM !~ /java/ && RUBY_VERSION.to_f >= 2.6
+ # faradayClient.rb's manual Basic-auth header encoding needs this from
+ # Ruby >= 3.4 (see logger/getoptlong below for the same demotion pattern).
+ gem 'base64' if RUBY_VERSION.to_f >= 3.4
+end
+
+# ---------------------------------------------------------------------------
+# XML parser backends, declared in the same precedence order xsd/xmlparser.rb
+# tries them in (see its parser_list): oxparser, nokogiriparser,
+# libxmlparser, ogaparser, rexmlparser.
+# ---------------------------------------------------------------------------
+
+if RUBY_PLATFORM =~ /java/
+ # ox has no JRuby port at all -- nothing to require here; oxparser will
+ # gracefully report unavailable via xsd/xmlparser.rb's parser cascade.
+elsif RUBY_VERSION.to_f >= 2.7
+ gem 'ox' # oxparser ; Uses its own custom C-library
+elsif RUBY_VERSION.to_f >= 2.2
+ # Ruby 2.2.x - 2.6.x: unconstrained resolves Ox 2.14.14, the newest release
+ # before Ox's own gemspec required Ruby >= 2.7. 2.14.14 segfaults inside
+ # Ox.sax_parse's :convert_special => true path on complex documents
+ # (confirmed via test_mapping.rb on 2.2.10/2.3.8/2.4.10/2.6.10) --
+ # htmlentities avoids that path entirely (see oxparser.rb), so it's a
+ # required workaround here, not an optional speed boost.
+ gem 'ox'
+ gem 'htmlentities', '~> 4.3.3'
+elsif RUBY_VERSION.to_f > 1.8
+ # Ruby 1.9.3 - 2.1.x: newest Ox that loads without the rb_utf8_str_new
+ # crash here and decodes named entities natively (htmlentities not
+ # needed). Confirmed empirically -- 2.14.7 introduces the crash. Pin
+ # exact: patch releases matter here.
+ gem 'ox', '= 2.14.6'
else
- gem 'httpclient' # 2.1.5.2
- gem 'htmlentities', '~> 4.3.3' # Require this if OxParser's built-in "Special Character" conversion isn't sufficient for your needs.
- gem 'nokogiri', '~> 1.6.6' # nokogiriparser ; Uses libxml2, libxslt, and zlib
- gem 'oga' # ogaparser ; Pure-Ruby Alternative ; Ruby 1.9 and above only.
- gem 'logger-application', :require=>'logger-application'
+ # Ruby 1.8.7: 2.14.6's extconf.rb fails to build here (unrelated build
+ # tooling issue), so fall back to 2.4.5. Pin exact: 2.4.13 hits a
+ # different crash (RSTRUCT_GET) elsewhere in this range.
+ gem 'ox', '= 2.4.5'
+ # Only needed here: 2.4.5 decodes just the 5 basic XML entities, not the
+ # full named set. 4.3.1 pinned -- 4.3.3+ needs Encoding, absent on 1.8.7.
+ gem 'htmlentities', '4.3.1'
+end
+
+if RUBY_VERSION.to_f > 2.2
+ gem 'nokogiri' # let Bundler pick a version compatible with the running Ruby
+elsif RUBY_VERSION.to_f > 1.8
+ gem 'nokogiri', '~> 1.6.6' # nokogiriparser ; Uses libxml2, libxslt, and zlib
+else
+ gem 'nokogiri', '~> 1.5.11' # nokogiriparser ; Uses libxml2, libxslt, and zlib
end
if RUBY_PLATFORM =~ /java/
- gem 'libxml-jruby' # libxmlparser (Java Equivalent)
+ # libxml-jruby's only release (2010) calls a since-removed JRuby API
+ # (NoMethodError at require time) -- no replacement exists, so libxmlparser
+ # is simply unavailable here, same as ox above.
+elsif RUBY_VERSION.to_f > 1.9
+ gem 'libxml-ruby', '~> 3.1.0'
else
- platform :ruby_18, :ruby_19 do
- gem 'libxml-ruby', '~> 2.8.0'
- end
- gem 'ox' # oxparser ; Uses its own custom C-library
- gem 'curb'
+ gem 'libxml-ruby', '~> 2.8.0'
+end
+
+if RUBY_VERSION.to_f > 1.8
+ # oga's own ruby-ll dependency (~> 2.1) can drift to 2.2.0, whose C ext
+ # needs RUBY_TYPED_FREE_IMMEDIATELY (Ruby >= 2.1). Pin the last ruby-ll
+ # release that still supports Ruby >= 1.9.3.
+ gem 'ruby-ll', '~> 2.1.2' if RUBY_VERSION.to_f < 2.1
+ gem 'oga' # ogaparser ; Pure-Ruby Alternative
+end
+
+if RUBY_VERSION.to_f > 1.8
+ # rexml/webrick/logger were implicit default gems until demoted to bundled
+ # gems (rexml/webrick: Ruby 3.0, logger: 4.0). Below that floor they're
+ # still built in and must NOT get a Gemfile entry: current releases on
+ # rubygems.org use syntax predates-Ruby can't parse.
+ gem 'rexml' if RUBY_VERSION.to_f >= 3.0
+ gem 'webrick' if RUBY_VERSION.to_f >= 3.0 # needed by lib/soap/rpc/{httpserver,cgistub,soaplet}.rb
+ # logger's deprecation warning starts firing a version early (3.4, not
+ # 4.0) on a plain `require` with no pinning at all; gated here to silence
+ # it, matching getoptlong below.
+ gem 'logger' if RUBY_VERSION.to_f >= 3.4
+ gem 'getoptlong' if RUBY_VERSION.to_f >= 3.4 # bin/{xsd2ruby,wsdl2ruby}.rb need it unconditionally
+ gem 'logger-application', :require=>'logger-application'
end
## # Testing Support ###
group :test do
- if RUBY_VERSION.to_f <= 1.8
- gem 'test-unit', '~> 1.2.3'
- gem 'rake', '~> 10.4.2'
- else
+ if RUBY_VERSION.to_f > 1.9
gem 'test-unit'
gem 'rake'
+ elsif RUBY_VERSION.to_f > 1.8
+ # current test-unit uses Ruby >= 2.0 keyword-argument syntax (SyntaxError
+ # on 1.9.3); pin an old-enough release.
+ gem 'test-unit', '~> 3.0.5'
+ gem 'rake'
+ else
+ gem 'test-unit', '~> 1.2.3'
+ gem 'rake', '~> 10.4.2'
+ # test-unit 1.2.3 lists hoe as a runtime dependency; current hoe needs
+ # Ruby >= 2.7, so pin the oldest release satisfying test-unit's floor
+ # (>= 1.5.1). hoe pulls in rubyforge -> json_pure; pin that too.
+ gem 'hoe', '1.5.1'
+ gem 'json_pure', '~> 1.7.6'
end
- platform :ruby_18, :ruby_19 do
- gem 'json', '~> 1.8' # Mostly for Code Climate's benefit if running on Ruby 1.9 or less.
- end
+ # test-unit -> power_assert -> ansi; ansi 1.6.0 needs Ruby >= 3.1 and
+ # Bundler's resolver doesn't always avoid it below that. Pin directly.
+ gem 'ansi', '~> 1.5.0' if RUBY_VERSION.to_f < 3.1
+ # Not used directly -- simplecov (below) depends on unconstrained 'json',
+ # whose current releases use **opts syntax (SyntaxError on Ruby <= 1.9).
+ gem 'json', '~> 1.8' if RUBY_VERSION.to_f <= 1.9
gem 'rubyjedi-testunitxml', :git=>'https://github.com/rubyjedi/testunitxml.git', :branch=>'master'
- gem "codeclimate-test-reporter", :require=>nil if RUBY_VERSION.to_f >= 1.9
-
+ # test/helper.rb requires this directly; pinned to the version this
+ # project's test matrix has actually been verified against.
+ gem 'simplecov', '0.13.0' if RUBY_VERSION.to_f >= 1.9
+
### Misc Debugging Aids ###
# gem 'awesome_print'
# gem 'rcov' # Coverage Test scoring, for more confidence. Do a 'rake rcov:rcov' to yield coverage results.
@@ -44,6 +153,11 @@ group :test do
# gem 'ruby-termios' # Unroller requires this . . .
# gem 'unroller', :git=>'https://github.com/jayjlawrence/unroller.git', :branch=>'master'
- gem 'byebug' if RUBY_VERSION.to_f >= 2.0
+ # byebug's C ext needs MRI's ruby.h, which doesn't exist on JRuby -- these
+ # are just debugging conveniences, not required for tests to run.
+ if RUBY_VERSION.to_f >= 2.0 && RUBY_PLATFORM !~ /java/
+ gem 'pry-byebug'
+ gem 'byebug'
+ end
gem 'soap4r-ng', :path=>'.' # Make our development copy (this directory) available as a Gem via Bundler. Useful for running tests.
end
diff --git a/README.md b/README.md
index 75fd084ed..c6837e2a3 100644
--- a/README.md
+++ b/README.md
@@ -1,50 +1,182 @@
# soap4r-ng
[](http://badge.fury.io/rb/soap4r-ng)
[](http://badge.fury.io/gh/rubyjedi%2Fsoap4r)
-[](https://codeclimate.com/github/rubyjedi/soap4r)
-[](https://travis-ci.org/rubyjedi/soap4r)
+[](https://github.com/rubyjedi/soap4r/actions/workflows/ci.yml)
#### Soap4R (as maintained by RubyJedi)
-* Unit Tested to work under MRI Ruby **1.8.7** thru **2.2**
-* ***NEW CODE! Added Support for newer, faster XML Parsers***
- * **[Ox](https://github.com/ohler55/ox)** (Fully Functional),
- * **[Nokogiri](https://github.com/sparklemotion/nokogiri)** (Fully Functional)
- * **[Oga](https://github.com/YorickPeterse/oga)** (Fully Functional)
+* Unit Tested to work under MRI Ruby **1.8.7** thru **4.0** -- yes, still 1.8.7! Every minor release in between (1.9.3, 2.0-2.7, 3.0-3.4) passes the full test suite, with a small, understood set of exceptions -- see "Known Test Suite Exceptions" below.
+* Also runs under **JRuby** (9.4 and 10.1): 3 of the 5 XML parsers (Nokogiri, Oga, REXML) work correctly, with a handful of JRuby-engine-specific test exceptions (also documented below). `Ox` and `LibXML` have no viable JRuby-compatible gem available at all -- `ox` has never had a JRuby port, and the one and only `libxml-jruby` release is from 2010 and calls a JRuby API removed long ago.
+* ***Five interchangeable XML Parser backends -- all fully functional***
+ * **[Ox](https://github.com/ohler55/ox)**
+ * **[Nokogiri](https://github.com/sparklemotion/nokogiri)**
+ * **[Oga](https://github.com/YorickPeterse/oga)**
+ * **[LibXML](https://github.com/xml4r/libxml-ruby)**
+ * **REXML** (the built-in fallback, bundled with Ruby)
* ***Fully Operational Unit Test Suite***. NaHi's Unit Tests are astonishingly thorough, and have been instrumental in discovering issues that each new Ruby version brings up. Thanks to those Unit Tests, I'm **very** confident in the code quality of this fork.
-* ***Roadmap and Future Plans***
- * Much improved [GitHub-Pages Website](http://rubyjedi.github.io/soap4r/) for documentation and presentation purposes.
- * Support for newer, faster HTTP Clients like [Curb](https://github.com/taf2/curb)
- * Support for Ruby 2.3, JRuby, and (?) coming soon - depending on demand. (File an Issue, +1 to chime in and add support).
- * ***More to come soon*** - I'm hammering on getting Soap4R-ng working under Ruby 2.3 (As in "Regression Tests pass with Zero Errors or Warnings") before tackling the feature enhancements like **Oga** or **Curb**.
#### How to Install
##### (Bundler Gemfile / GitHub Hosted)
```
-## Performance Boosting Gems
-gem 'ox' # For faster XML Parsing, use Ox or Nokogiri. Ox has highest priority if available.
-gem 'nokogiri' # For faster XML Parsing. If neither Ox nor Nokogiri available, we'll fall back to REXML.
-gem 'httpclient' # Absolutely necessary for soap4r-ng. Net::HTTP Fallback is quite broken, so don't let that happen.
-#
+## Performance Boosting Gems -- soap4r-ng uses whichever of these are available,
+## in priority order: Ox, then Nokogiri, then LibXML, then Oga, then REXML.
+gem 'ox'
+gem 'nokogiri'
+gem 'libxml-ruby'
+gem 'oga'
+## REXML is the last-resort fallback -- it's bundled with Ruby itself, but
+## Ruby 3.0+ demoted it (and webrick) from the standard library to an
+## optional bundled gem, so add them explicitly or you'll hit a LoadError
+## at runtime:
+gem 'rexml'
+gem 'webrick'
+## Ruby 4.0+ did the same to logger:
+gem 'logger'
+
+gem 'httpclient' # Strongly recommended. See "HTTP Client Backends" below for the Net::HTTP fallback's limitations.
+## curb and faraday are additional, opt-in HTTP client backends -- not
+## needed unless you specifically want one of them; see "HTTP Client
+## Backends" below.
+
gem 'soap4r-ng', :git=>'https://github.com/rubyjedi/soap4r.git', :branch=>"master"
```
##### Standard Ruby Gem
```
gem install soap4r-ng
```
+
+#### Gem Version Pinning by Ruby Version
+If you're on a legacy Ruby and updating soap4r-ng, here's exactly what each of
+the 5 parsers needs. This table only covers what changes between Ruby
+versions -- the *why* behind each pin (usually "the current release of this
+gem needs a newer Ruby than this, and there's no older release to fall back
+on") is documented alongside the real thing, in this project's own
+`Gemfile`.
+
+There's an asymmetry worth knowing up front: **older Rubies need parser gems
+capped to an old version**, while **Ruby 3.0+ needs a couple of gems added
+explicitly that used to come for free**. It's not simply "newer Ruby needs
+less" -- it needs *different* things.
+
+| Ruby version | nokogiri | ox | oga (via ruby-ll) | libxml-ruby | test-unit | rexml / webrick | logger |
+|---|---|---|---|---|---|---|---|
+| ≤ 1.8 (1.8.7) | `~> 1.5.11` | `~> 2.4.5` | **not available** -- never installed, gracefully falls through to the next parser | `~> 2.8.0` | `~> 1.2.3` (also needs `hoe '1.5.1'` and `json_pure '~> 1.7.6'` pinned, or `bundle install` itself fails) | implicit, no gem needed | implicit |
+| 1.9.x | `~> 1.6.6` | `~> 2.4.5` | `ruby-ll '~> 2.1.2'` pinned | `~> 2.8.0` | `~> 3.0.5` | implicit | implicit |
+| 2.0.x | `~> 1.6.6` | `~> 2.4.5` | `ruby-ll '~> 2.1.2'` pinned | `~> 3.1.0` | unconstrained | implicit | implicit |
+| 2.1.x | `~> 1.6.6` | `~> 2.4.5` | unconstrained | `~> 3.1.0` | unconstrained | implicit | implicit |
+| 2.2.x | `~> 1.6.6` | unconstrained | unconstrained | `~> 3.1.0` | unconstrained | implicit | implicit |
+| 2.3.x -- 2.9.x | unconstrained | unconstrained | unconstrained | `~> 3.1.0` | unconstrained | implicit | implicit |
+| 3.0.x -- 3.9.x | unconstrained | unconstrained | unconstrained | `~> 3.1.0` | unconstrained | **must add explicitly** | implicit |
+| 4.0.x and up | unconstrained | unconstrained | unconstrained | `~> 3.1.0` | unconstrained | must add explicitly | **must add explicitly** |
+
+"Unconstrained" means just `gem 'nokogiri'` with no version -- Bundler picks
+whatever release actually supports the Ruby you're running. "Must add
+explicitly" means the gem is a real runtime requirement on that Ruby version
+(`rexml`/`webrick` were demoted from the standard library to optional bundled
+gems in Ruby 3.0; `logger` followed in Ruby 4.0) -- without it you'll hit a
+`LoadError` the first time that code path runs, not at `bundle install` time.
+
+**JRuby** is a separate axis from the table above. `ox` and `libxml-ruby`
+have no working JRuby-compatible gem at all (`ox` has never had a JRuby port;
+`libxml-jruby`'s one and only release is from 2010 and calls a since-removed
+JRuby API) -- both parsers gracefully report unavailable rather than
+crashing. `nokogiri`, `oga`, and `rexml` all work normally. `byebug` and
+`pry-byebug` (dev-only debugging aids, not required to run anything) are
+skipped entirely on JRuby since their C extension needs MRI's `ruby.h`.
+
+#### HTTP Client Backends
+Like the XML parsers above, the HTTP client is pluggable: `lib/soap/streamHandler.rb`
+picks one at load time via `lib/soap/httpbackend.rb`, using the exact same
+pattern as `SOAP4R_PARSERS` (`lib/xsd/xmlparser.rb`) -- a hardcoded
+preference order, overridable with an environment variable, so a new backend
+is a drop-in file rather than a change to library code.
+
+* **[httpclient](https://github.com/nahi/httpclient)** -- the default and
+ strongly recommended backend. Fully featured: proxying, basic/digest auth,
+ cookies, SSL/TLS configuration, request/response filters. (Formerly named
+ `http-access2`; see CHANGELOG.md if you need that old adapter.)
+* **[curb](https://github.com/taf2/curb)** -- libcurl bindings. Opt-in (see
+ below), needs system `libcurl-dev` at compile time. Supports proxying,
+ SSL/TLS config (ca_file, client cert/key, cipher-list restriction), and
+ basic/digest WWW-Authenticate auth. No cookies or request/response
+ filters, and no `ssl_config.verify_callback` (libcurl's C API exposes no
+ per-certificate Ruby callback hook). No JRuby port; gated to Ruby >= 2.4.
+* **[faraday](https://github.com/lostisland/faraday)** -- gated to Ruby
+ >= 2.6 (see CHANGELOG.md for why). Has its own adapter system underneath
+ our backend selection, defaulting to `:net_http` and overridable with
+ `SOAP4R_FARADAY_ADAPTER` (e.g. `SOAP4R_FARADAY_ADAPTER=typhoeus`) -- a
+ second, independent knob layered under `SOAP4R_HTTP_CLIENTS`. Opt-in (see
+ below). Supports proxying, basic auth (sent proactively via a
+ manually-built header), and SSL/TLS config (ca_file, client cert/key as
+ file paths). No challenge-response auth, cookies, request/response
+ filters, or `verify_callback`. Cipher-list restriction passes through to
+ the active adapter, but `:typhoeus` silently ignores it (an upstream gap,
+ not this bridge). `SOAP4R_FARADAY_ADAPTER=patron` also works for ordinary
+ requests but isn't part of the automated test matrix (see "Known Test
+ Suite Exceptions" below).
+* **`SOAP::NetHttpClient`** -- this project's own wrapper around stdlib
+ `Net::HTTP`, used only when none of the above are installed. No
+ basic/digest auth, cookies, or request/response filters. A reasonable
+ fallback for simple unauthenticated SOAP calls when you can't add a gem
+ dependency, but `httpclient` is what most of this library's real-world
+ testing assumes.
+
+Force a specific backend with:
+```
+SOAP4R_HTTP_CLIENTS=net_http bundle exec rake test:deep
+```
+Valid names are `httpclient`, `curb`, `faraday`, and `net_http`, matching
+the files under `lib/soap/httpbackend/`. `curb` and `faraday` are opt-in
+Bundler groups: `BUNDLE_WITH="http_curb:http_faraday" bundle install`
+(an env var, not `bundle config set with "..."` -- see CHANGELOG.md).
+
+CI runs the full suite against `net_http`, `curb`, and `faraday` too
+(single-parser each -- this is about the HTTP layer, not XML parsing).
+Faraday runs with the `:typhoeus` adapter specifically (real correctness
+check plus the most-used adapter in practice); `:patron` was tried and
+dropped from CI (real upstream bug, not ours -- see "Known Test Suite
+Exceptions" below). Full backstory on the backend rollout (wiredump-parity
+work, TLS-trust defaults, per-backend test coverage decisions) is in
+CHANGELOG.md.
+
+#### Known Test Suite Exceptions
+Running `rake test:deep` across the full version matrix surfaces a small,
+understood set of failures that aren't soap4r-ng bugs -- either something
+the target Ruby/engine genuinely can't do, or a test checking something too
+environment-specific to be fair. Documented here so they don't get mistaken
+for regressions; full root-cause writeups are in CHANGELOG.md.
+
+* **Ruby 1.8.7** -- 3 errors from `Kernel#singleton_class` not existing
+ until Ruby 1.9.2 (CANTFIX), plus the CGI-based tests
+ (`test_calc_cgi`, `test_authheader_cgi`) and two collateral WSDL tests
+ failing from a not-fully-root-caused WEBrick/CGI-subprocess environment
+ issue.
+* **Ruby 2.4.10, 2.5.9, `SOAP4R_HTTP_CLIENTS=curb`** -- `test_ca_verification`/
+ `test_ciphers` fail from those Docker images' older libcurl/OpenSSL
+ (CANTFIX, environment-specific, not a soap4r-ng/curb bug).
+* **Ruby 3.0.7, 3.1.7, 3.2.11** -- `test_exception` fails because it compares
+ a live `Test::Unit::TestCase#inspect` dump that renders differently across
+ this patch-version range (WONTFIX, not under this library's control).
+* **JRuby** (9.4.15.0, 10.1.0.0) -- 13 confirmed environment/engine
+ differences (integer parsing, `ENV` object model, SSL socket bridge,
+ backtrace formatting), all CANTFIX/upstream. A handful of other JRuby-only
+ failures were a real, fixable bug (`SOAP::Mapping.fault2exception` assumed
+ a non-nil `.backtrace`) -- fixed with a nil-guard.
+* **`SOAP4R_HTTP_CLIENTS=faraday SOAP4R_FARADAY_ADAPTER=patron`** -- not run
+ in CI; the CGI-based tests fail with `Patron::Aborted: Callback aborted`,
+ root-caused to patron itself, not this project's code (CANTFIX).
+
#### How to Use
* [NaHi's Original documentation](https://web.archive.org/web/20101212040735/http://dev.ctor.org/soap4r/wiki/) -- the authoritative reference material is still available through the Wayback Machine, thankfully!
* [Soap4R-NG Website](http://rubyjedi.github.io/soap4r/) -- My own attempt at incorporating and modernizing the above into GitHub Pages -- still a work in progress at this time.
#### How to Get a Speed Boost : Use Nokogiri or Ox, not REXML
-Be sure to have Nokogiri or Ox available in your Gemset. Soap4R-ng will find and use what's available (Ox has highest precedence, then Nokogiri, falling back to REXML as the last-resort if needed.
+Be sure to have Nokogiri or Ox available in your Gemset. Soap4R-ng will find and use what's available (Ox has highest precedence, then Nokogiri, then LibXML, then Oga, falling back to REXML as the last resort if needed).
-I personally recommend **Nokogiri** as the best performing, most flexible parser at this time, as it handles "special characters" like HTML ampersand-escaped characters internally. Ox doesn't handle such an extensive set of special-characters natively, so to get things up to par, I added **htmlentities** support if it's available when using the Ox parser. Using **htmlentities** with **Ox** in this manner adds a bit of a performance penalty, however.
+I personally recommend **Nokogiri** as the best performing, most flexible parser at this time, as it handles "special characters" like HTML ampersand-escaped characters internally. Ox needs **htmlentities** as a fallback on Ruby 1.8.7 (stuck on the old Ox 2.4.5, which only decodes the 5 basic XML entities) and on Ruby 2.2.x-2.6.x (stuck on Ox 2.14.14, which segfaults on complex documents without it -- see CHANGELOG.md). Ruby 1.9.3-2.1.x and Ruby >= 2.7 both resolve an Ox release that decodes entities natively and safely, so htmlentities is dead weight there.
If you know your incoming XML is "clean", Ox is a really great alternative.
-**LibXML** is somewhat broken at this time. It's low-priority on the task list, as **Nokogiri** and **Ox** are more readily available. In fact, I may drop support for the **LibXML** parser in a future release.
-
-***More to come soon*** I'm hammering on getting Soap4R-ng working under Ruby 2.3 (As in "Regression Tests pass with Zero Errors or Warnings") before tackling feature enhancements like **Oga** or **Curb**
+**LibXML** used to have a real bug: its SAX binding silently drops namespace prefixes on attributes, which broke type-casting for anything relying on `xsi:type`, `xsi:nil`, or `xml:lang`. That's been fixed by switching the parser over to libxml-ruby's `XML::Reader` API instead, and it now passes the exact same test suite as the other four parsers.
#### Project Motivation
@@ -54,7 +186,7 @@ Soap4R has received a less-than-stellar reputation amongst the Ruby Community fo
IMHO, NaHi did a freaking brilliant job with **Soap4R**. The code is tight, the Unit Tests are astonishingly comprehensive, and -- aside from finding someone willing to invest time to carry **Soap4R** forward -- there's really no good reason why **Soap4R** should be so neglected.
-In fact, I'd much prefer spending time forward-porting **Soap4R** to keep this known-good foundation library going, versus taking on the risky task of migrating already-written applications to a completely new SOAP implementation. Along the way in this journey, I'm adding support for newer XML Parsers like **[Ox](https://github.com/ohler55/ox)** (which is screaming fast, btw!) and **[Nokogiri](https://github.com/sparklemotion/nokogiri)**. I also have future plans to add support for newer HTTP Clients such as **[Curb](https://github.com/taf2/curb)**.
+In fact, I'd much prefer spending time forward-porting **Soap4R** to keep this known-good foundation library going, versus taking on the risky task of migrating already-written applications to a completely new SOAP implementation. Along the way in this journey, I'm adding support for newer XML Parsers like **[Ox](https://github.com/ohler55/ox)** (which is screaming fast, btw!) and **[Nokogiri](https://github.com/sparklemotion/nokogiri)**.
#### Why Name This "Soap4R-ng" ?
As **[felipec/soap4r](https://github.com/felipec/soap4r)** (now **[soap2r](https://github.com/felipec/soap4r)**) pointed out upon renaming his fork to **soap2r** , there is a LOT of competition to uniquely name the a "successor" to the original Soap4R. **soap2r** came into being because **"[Soap5R](https://github.com/aforward/soap4r)"** had already been claimed. :-)
diff --git a/gemfiles/httpclient.gemfile b/gemfiles/httpclient.gemfile
deleted file mode 100644
index 7f3b2e8ef..000000000
--- a/gemfiles/httpclient.gemfile
+++ /dev/null
@@ -1,38 +0,0 @@
-source 'http://rubygems.org'
-if RUBY_VERSION.to_f <= 1.8
- gem 'htmlentities', '4.3.1' # Require this if OxParser's built-in "Special Character" conversion isn't sufficient for your needs.
- gem 'nokogiri', '~> 1.5.11' # nokogiriparser ; Uses libxml2, libxslt, and zlib
- gem 'httpclient', '~> 2.7.0.1'
-else
- gem 'httpclient' # 2.1.5.2
- gem 'htmlentities', '~> 4.3.3' # Require this if OxParser's built-in "Special Character" conversion isn't sufficient for your needs.
- gem 'nokogiri', '~> 1.6.6' # nokogiriparser ; Uses libxml2, libxslt, and zlib
- gem 'oga' # ogaparser ; Pure-Ruby Alternative ; Ruby 1.9 and above only.
- gem 'logger-application', :require=>'logger-application'
-end
-
-if RUBY_PLATFORM =~ /java/
- gem 'libxml-jruby' # libxmlparser (Java Equivalent)
-else
- platform :ruby_18, :ruby_19 do
- gem 'libxml-ruby', '~> 2.8.0'
- end
- gem 'ox' # oxparser ; Uses its own custom C-library
-end
-
-### Testing Support ###
-group :test do
- if RUBY_VERSION.to_f <= 1.8
- gem 'test-unit', '~> 1.2.3'
- gem 'rake', '~> 10.4.2'
- else
- gem 'test-unit'
- gem 'rake'
- end
-
- platform :ruby_18, :ruby_19 do
- gem 'json', '~> 1.8' # Mostly for Code Climate's benefit if running on Ruby 1.9 or less.
- end
- gem 'rubyjedi-testunitxml', :git=>'https://github.com/rubyjedi/testunitxml.git', :branch=>'master'
- gem "codeclimate-test-reporter", :require=>nil if RUBY_VERSION.to_f >= 1.9
-end
diff --git a/install.rb b/install.rb
deleted file mode 100644
index 37e742a25..000000000
--- a/install.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-#!/usr/bin/env ruby
-# encoding: UTF-8
-
-require 'getoptlong'
-require 'rbconfig'
-require 'ftools'
-
-OptSet = [
- ['--prefix','-p', GetoptLong::REQUIRED_ARGUMENT],
-]
-prefix = nil
-GetoptLong.new(*OptSet).each do |name, arg|
- case name
- when "--prefix"
- prefix = arg
- else
- raise ArgumentError.new("Unknown type #{ arg }")
- end
-end
-
-include Config
-RV = CONFIG["MAJOR"] + "." + CONFIG["MINOR"]
-ORG_PREFIX = CONFIG["prefix"]
-SRCPATH = File.join(File.dirname($0), 'lib')
-
-RUBYLIBDIR = CONFIG["rubylibdir"]
-SITELIBDIR = CONFIG["sitedir"] + "/" + RV
-
-if prefix
- RUBYLIBDIR.sub!(/^#{Regexp.quote(ORG_PREFIX)}/, prefix)
- SITELIBDIR.sub!(/^#{Regexp.quote(ORG_PREFIX)}/, prefix)
-end
-
-def install(from, to)
- to_path = File.catname(from, to)
- unless FileTest.exist?(to_path) and File.compare(from, to_path)
- File.install(from, to_path, 0644, true)
- end
-end
-
-def install_dir(srcbase, *path)
- from_path = File.join(srcbase, *path)
- unless FileTest.directory?(from_path)
- raise RuntimeError.new("'#{ from_path }' not found.")
- end
- to_path_rubylib = File.join(RUBYLIBDIR, *path)
- to_path_sitelib = File.join(SITELIBDIR, *path)
- Dir[File.join(from_path, '*.rb')].each do |from_file|
- basename = File.basename(from_file)
- to_file_rubylib = File.join(to_path_rubylib, basename)
- to_file_sitelib = File.join(to_path_sitelib, basename)
- if File.exist?(to_file_rubylib)
- if File.exist?(to_file_sitelib)
- raise RuntimeError.new(
- "trying to install '#{ to_file_rubylib }' but '#{ to_file_sitelib }' exists. please remove '#{ to_file_sitelib }' first to avoid versioning problem and run installer again.")
- end
- install(from_file, to_path_rubylib)
- else
- File.mkpath(to_path_sitelib, true)
- install(from_file, to_path_sitelib)
- end
- end
-end
-
-begin
- install_dir(SRCPATH, 'soap')
- install_dir(SRCPATH, 'soap', 'rpc')
- install_dir(SRCPATH, 'soap', 'mapping')
- install_dir(SRCPATH, 'soap', 'encodingstyle')
- install_dir(SRCPATH, 'soap', 'header')
- install_dir(SRCPATH, 'soap', 'filter')
- install_dir(SRCPATH, 'wsdl')
- install_dir(SRCPATH, 'wsdl', 'xmlSchema')
- install_dir(SRCPATH, 'wsdl', 'soap')
- install_dir(SRCPATH, 'xsd')
- install_dir(SRCPATH, 'xsd', 'codegen')
- install_dir(SRCPATH, 'xsd', 'xmlparser')
-
- # xmlscan
- xmlscansrcdir = File.join('redist', 'xmlscan', 'xmlscan-20050522', 'lib')
- if File.exist?(xmlscansrcdir)
- install_dir(xmlscansrcdir, 'xmlscan')
- end
-
- puts "install succeed!"
-
-rescue
- puts "install failed!"
- puts $!
-
-end
diff --git a/lib/soap/baseData.rb b/lib/soap/baseData.rb
index d6e83e915..8c23d9509 100644
--- a/lib/soap/baseData.rb
+++ b/lib/soap/baseData.rb
@@ -542,7 +542,7 @@ def initialize(type = nil)
end
def to_s
- str = ''
+ str = String.new
self.each do |key, data|
str << "#{key}: #{data}\n"
end
@@ -1080,7 +1080,11 @@ def self.create_arytype(typename, rank)
"#{typename}[" << ',' * (rank - 1) << ']'
end
- TypeParseRegexp = Regexp.new('^(.+)\[([\d,]*)\]$', nil, 'n')
+ # Regexp::NOENCODING doesn't exist pre-1.9 (part of Ruby's M17N overhaul);
+ # 0 (no special options) is the equivalent on 1.8.7, which has no
+ # per-string encoding concept for this flag to apply to in the first place.
+ TypeParseRegexp = Regexp.new('^(.+)\[([\d,]*)\]$',
+ defined?(Regexp::NOENCODING) ? Regexp::NOENCODING : 0)
def self.parse_type(string)
TypeParseRegexp =~ string
diff --git a/lib/soap/curbClient.rb b/lib/soap/curbClient.rb
new file mode 100644
index 000000000..2824484f8
--- /dev/null
+++ b/lib/soap/curbClient.rb
@@ -0,0 +1,286 @@
+# encoding: UTF-8
+# SOAP4R - curb (libcurl) wrapper.
+
+require 'curb'
+require 'tempfile'
+require 'soap/filter/filterchain'
+
+
+module SOAP
+
+
+class CurbClient
+ attr_reader :proxy
+ attr_accessor :no_proxy
+ attr_accessor :debug_dev
+ attr_reader :ssl_config
+ attr_accessor :protocol_version # ignored -- libcurl negotiates this itself.
+ attr_accessor :connect_timeout
+ attr_accessor :send_timeout # ignored -- libcurl has one overall #timeout, not separate send/receive phases.
+ attr_accessor :receive_timeout
+ attr_reader :test_loopback_response
+ attr_reader :request_filter # ignored for now, same as SOAP::NetHttpClient.
+
+ def initialize(proxy = nil, agent = nil)
+ @proxy = proxy
+ @no_proxy = nil
+ @agent = agent
+ @debug_dev = nil
+ @ssl_config = SSLConfig.new
+ @connect_timeout = @receive_timeout = @send_timeout = nil
+ @basic_auth = nil # [user, pass], set via #set_basic_auth
+ @challenge_auth = nil # [user, pass], set via #set_auth
+ @cookie_store = nil
+ @test_loopback_response = []
+ @request_filter = Filter::FilterChain.new
+ end
+
+ def proxy=(value)
+ if value
+ uri = value.is_a?(URI) ? value : URI.parse(value)
+ if uri.scheme.nil? or uri.scheme.downcase != 'http' or uri.host.nil? or uri.port.nil?
+ raise ArgumentError.new("unsupported proxy `#{value}'")
+ end
+ end
+ @proxy = value
+ end
+
+ def set_basic_auth(uri, user_id, passwd)
+ @basic_auth = [user_id, passwd]
+ end
+
+ # soap4r's "auth" (as opposed to "basic_auth") is fed by a server
+ # challenge (WWW-Authenticate: Basic or Digest) rather than sent
+ # proactively -- :any lets libcurl negotiate whichever the server
+ # actually asks for, covering both test/soap/auth/test_basic.rb and
+ # test/soap/auth/test_digest.rb with the same code path.
+ def set_auth(uri, user_id, passwd)
+ @challenge_auth = [user_id, passwd]
+ end
+
+ def set_cookie_store(filename)
+ @cookie_store = filename
+ end
+
+ def save_cookie_store
+ # curb's cookiejar is written incrementally as requests complete;
+ # nothing to flush here.
+ end
+
+ def reset(url)
+ # no persistent connection state kept between requests; ignored.
+ end
+
+ def reset_all
+ # ditto.
+ end
+
+ def post(url, req_body, header = {})
+ if str = @test_loopback_response.shift
+ if @debug_dev
+ @debug_dev << "= Request\n\n"
+ @debug_dev << req_body
+ @debug_dev << "\n\n= Response\n\n"
+ @debug_dev << str
+ end
+ return Response.new(200, nil, 'text/xml', Hash.new { |h, k| h[k] = [] }, str)
+ end
+ curl = build_curl(url, header)
+ curl.http_post(req_body)
+ dump_wiredump(curl, req_body) if @debug_dev
+ Response.from_curl(curl)
+ end
+
+private
+
+ def build_curl(url, header)
+ # Curl::Easy.new needs a String -- given a URI object instead (the
+ # driver's endpoint can be constructed from either, see test_uri in
+ # test/soap/test_streamhandler.rb), curb's C extension doesn't coerce
+ # it and silently stores nil for the URL internally, surfacing much
+ # later as "TypeError: no implicit conversion of nil into String"
+ # inside curl/multi.rb's own perform machinery rather than failing
+ # here where the actual mistake is.
+ curl = Curl::Easy.new(url.to_s)
+ curl.headers = header.dup
+ curl.headers['User-Agent'] = @agent if @agent
+ curl.follow_location = false # streamHandler.rb's own send_post loop handles redirects.
+ curl.connect_timeout = @connect_timeout if @connect_timeout
+ curl.timeout = @receive_timeout if @receive_timeout
+ unless no_proxy?(url.is_a?(URI) ? url : URI.parse(url))
+ curl.proxy_url = @proxy
+ end
+ if @challenge_auth
+ curl.http_auth_types = :any
+ curl.username, curl.password = @challenge_auth
+ elsif @basic_auth
+ curl.http_auth_types = :basic
+ curl.username, curl.password = @basic_auth
+ end
+ curl.cookiejar = @cookie_store if @cookie_store
+ apply_ssl_config(curl)
+ curl
+ end
+
+ def apply_ssl_config(curl)
+ cfg = @ssl_config
+ return unless cfg
+ curl.ssl_verify_peer = (cfg.verify_mode != OpenSSL::SSL::VERIFY_NONE)
+ curl.ssl_verify_host = (cfg.verify_mode == OpenSSL::SSL::VERIFY_NONE) ? 0 : 2
+ curl.cacert = cfg.ca_file if cfg.ca_file
+ # Curl::Easy has no dedicated ciphers=/cipher_list= method at all
+ # (confirmed empirically -- NoMethodError) despite libcurl itself
+ # supporting CURLOPT_SSL_CIPHER_LIST; #setopt with the raw constant is
+ # curb's only way to reach it.
+ curl.setopt(Curl::CURLOPT_SSL_CIPHER_LIST, cfg.ciphers) if cfg.ciphers
+ # Curl::Easy#cert=/#cert_key= want file paths, but
+ # HTTPConfigLoader#cert_from_file/#key_from_file (lib/soap/httpconfigloader.rb)
+ # already parsed the configured files into OpenSSL::X509::Certificate/
+ # OpenSSL::PKey::RSA objects (that's what httpclient's SSLConfig wants) --
+ # round-trip them back out to PEM tempfiles for curb's sake.
+ curl.cert = write_pem_tempfile(cfg.client_cert, 'cert') if cfg.client_cert
+ curl.cert_key = write_pem_tempfile(cfg.client_key, 'key') if cfg.client_key
+ end
+
+ def write_pem_tempfile(openssl_obj, basename)
+ f = Tempfile.new(["soap4r-curb-#{basename}-", '.pem'])
+ f.write(openssl_obj.to_pem)
+ f.close
+ # The finalizer proc must not be created in an instance-method context
+ # closing over self -- that makes self itself unreachable-but-not-quite
+ # (reachable only via its own finalizer), so it never actually becomes
+ # eligible for GC and the finalizer never runs (confirmed: Ruby warns
+ # "finalizer references object to be finalized" and the tempfile never
+ # got cleaned up). Building the proc in a class method keeps the
+ # closure's only capture to f, not self.
+ ObjectSpace.define_finalizer(self, self.class.tempfile_unlinker(f))
+ f.path
+ end
+
+ def self.tempfile_unlinker(file)
+ proc { file.unlink rescue nil }
+ end
+
+ def dump_wiredump(curl, req_body)
+ # Mirrors httpclient's wiredump block layout (marker line, blank line,
+ # raw request-line + headers, blank line, body) -- callers that parse
+ # wiredump_dev output by block position or by scanning for a "POST ..."
+ # line (e.g. test/soap/test_streamhandler.rb's parse_req_header) depend
+ # on that exact shape regardless of which backend produced it.
+ uri = URI.parse(curl.url)
+ request_line = (curl.proxy_url ? curl.url : uri.request_uri)
+ @debug_dev << "= Request\n\n"
+ @debug_dev << "POST #{request_line} HTTP/1.1\n"
+ curl.headers.each { |k, v| @debug_dev << "#{k}: #{v}\n" }
+ @debug_dev << "\n"
+ @debug_dev << req_body
+ @debug_dev << "\n\n= Response\n\n"
+ @debug_dev << "HTTP/1.1 #{curl.response_code} #{Response.reason_from_header_str(curl.header_str)}\n"
+ @debug_dev << "Content-Type: #{curl.content_type}\n" if curl.content_type
+ @debug_dev << "\n"
+ @debug_dev << curl.body
+ end
+
+ NO_PROXY_HOSTS = ['localhost']
+
+ def no_proxy?(uri)
+ if !@proxy or NO_PROXY_HOSTS.include?(uri.host)
+ return true
+ end
+ unless @no_proxy
+ return false
+ end
+ @no_proxy.scan(/([^:,]+)(?::(\d+))?/) do |host, port|
+ if /(\A|\.)#{Regexp.quote(host)}\z/i =~ uri.host &&
+ (!port || uri.port == port.to_i)
+ return true
+ end
+ end
+ false
+ end
+
+ class SSLConfig
+ attr_accessor :client_cert
+ attr_accessor :client_key
+ attr_accessor :client_ca
+ attr_accessor :verify_mode
+ attr_accessor :verify_depth # not directly settable via libcurl's simpler peer/host verify model; stored only.
+ attr_accessor :options # ditto -- no libcurl equivalent to OpenSSL::SSL::SSLContext#options.
+ attr_accessor :ciphers
+ attr_accessor :verify_callback # not supported -- libcurl has no per-certificate Ruby callback hook.
+ attr_accessor :cert_store # not supported -- libcurl manages its own trust store internally.
+ attr_reader :ca_file
+
+ def set_trust_ca(value)
+ @ca_file = value
+ end
+
+ def set_crl(value)
+ @crl = value # not applied -- libcurl has no direct CRL-file option exposed via curb.
+ end
+
+ # curb/libcurl never bundles its own CA snapshot the way httpclient
+ # does (see lib/soap/httpconfigloader.rb) -- unless #set_trust_ca
+ # overrides it, it always defers to whatever CA bundle libcurl itself
+ # was built against (the system's, on every mainstream Linux
+ # distribution), so there's nothing to override here.
+ def set_default_paths
+ end
+ end
+
+ class Response
+ attr_reader :status
+ attr_reader :reason
+ attr_reader :contenttype
+ attr_reader :header
+ attr_reader :content
+
+ def initialize(status, reason, contenttype, header, content)
+ @status = status
+ @reason = reason
+ @contenttype = contenttype
+ @header = header
+ @content = content
+ end
+
+ def self.from_curl(curl)
+ header = parse_headers(curl.header_str)
+ reason = reason_from_header_str(curl.header_str)
+ new(curl.response_code, reason, curl.content_type, header, curl.body)
+ end
+
+ # curb has no accessor for the HTTP reason phrase (e.g. "OK", "Not
+ # Found") separate from the numeric status -- pull it off the first
+ # line of the raw header block ourselves ("HTTP/1.1 200 OK" -> "OK").
+ # With redirects (multiple status lines in header_str, one per hop)
+ # this reads the LAST one, matching #parse_headers below which also
+ # only reflects the final response's headers.
+ def self.reason_from_header_str(header_str)
+ return nil unless header_str
+ line = header_str.each_line.select { |l| l.start_with?('HTTP/') }.last
+ return nil unless line
+ line.strip.split(' ', 3)[2]
+ end
+
+ # curb only exposes the raw response header block (#header_str), not a
+ # parsed hash -- build one ourselves, keyed lowercase like
+ # streamHandler.rb expects (it reads header['content-encoding'][0] and
+ # header['location'][0] directly), with an Array of values per key to
+ # match the other backends' Hash-of-Arrays shape.
+ def self.parse_headers(header_str)
+ result = Hash.new { |h, k| h[k] = [] }
+ return result unless header_str
+ header_str.each_line do |line|
+ line = line.strip
+ next if line.empty? || line.start_with?('HTTP/')
+ key, value = line.split(':', 2)
+ next unless key && value
+ result[key.strip.downcase] << value.strip
+ end
+ result
+ end
+ end
+end
+
+
+end
diff --git a/lib/soap/encodingstyle/aspDotNetHandler.rb b/lib/soap/encodingstyle/aspDotNetHandler.rb
index 8cbb56dcc..b91a29d35 100644
--- a/lib/soap/encodingstyle/aspDotNetHandler.rb
+++ b/lib/soap/encodingstyle/aspDotNetHandler.rb
@@ -20,7 +20,7 @@ class ASPDotNetHandler < Handler
def initialize(charset = nil)
super(charset)
- @textbuf = ''
+ @textbuf = String.new
@decode_typemap = nil
end
@@ -108,7 +108,7 @@ def as_string
end
def decode_tag(ns, elename, attrs, parent)
- @textbuf = ''
+ @textbuf = String.new
o = SOAPUnknown.new(self, elename)
o.parent = parent
o
@@ -128,7 +128,7 @@ def decode_tag_end(ns, node)
end
decode_textbuf(o)
- @textbuf = ''
+ @textbuf = String.new
end
def decode_text(ns, text)
diff --git a/lib/soap/faradayClient.rb b/lib/soap/faradayClient.rb
new file mode 100644
index 000000000..f10e3a5eb
--- /dev/null
+++ b/lib/soap/faradayClient.rb
@@ -0,0 +1,290 @@
+# encoding: UTF-8
+# SOAP4R - Faraday wrapper.
+#
+# Faraday is itself pluggable (it has its own adapter selection, on top of
+# ours) -- default to its own default adapter (:net_http, bundled via the
+# faraday-net_http gem that ships as a runtime dependency of faraday itself)
+# and let SOAP4R_FARADAY_ADAPTER override it for anyone who wants Faraday to
+# ride on a different underlying transport (e.g. :patron for a libcurl-based
+# one). This is a second, independent config knob layered under our own
+# SOAP4R_HTTP_CLIENTS -- see lib/soap/httpbackend.rb -- not a replacement
+# for it.
+
+require 'faraday'
+require 'base64'
+require 'tempfile'
+require 'soap/filter/filterchain'
+
+
+module SOAP
+
+
+class FaradayClient
+ attr_reader :proxy
+ attr_accessor :no_proxy
+ attr_accessor :debug_dev
+ attr_reader :ssl_config
+ attr_accessor :protocol_version # ignored -- the underlying Faraday adapter negotiates this itself.
+ attr_accessor :connect_timeout
+ attr_accessor :send_timeout # ignored -- mapped onto Faraday's single #timeout option instead (see #receive_timeout).
+ attr_accessor :receive_timeout
+ attr_reader :test_loopback_response
+ attr_reader :request_filter # ignored for now, same as SOAP::NetHttpClient.
+
+ ADAPTER = (ENV['SOAP4R_FARADAY_ADAPTER'] || 'net_http').to_sym
+
+ def initialize(proxy = nil, agent = nil)
+ @proxy = proxy
+ @no_proxy = nil
+ @agent = agent
+ @debug_dev = nil
+ @ssl_config = SSLConfig.new
+ @connect_timeout = @receive_timeout = @send_timeout = nil
+ @basic_auth = nil # [user, pass]
+ @cookie_store = nil
+ @test_loopback_response = []
+ @request_filter = Filter::FilterChain.new
+ require_adapter!
+ end
+
+ def proxy=(value)
+ if value
+ uri = value.is_a?(URI) ? value : URI.parse(value)
+ if uri.scheme.nil? or uri.scheme.downcase != 'http' or uri.host.nil? or uri.port.nil?
+ raise ArgumentError.new("unsupported proxy `#{value}'")
+ end
+ end
+ @proxy = value
+ end
+
+ def set_basic_auth(uri, user_id, passwd)
+ @basic_auth = [user_id, passwd]
+ end
+
+ def set_auth(uri, user_id, passwd)
+ # Faraday's core has no bundled challenge-response (WWW-Authenticate)
+ # negotiation -- that lives in third-party middleware, not something
+ # this bridge can assume is installed. Digest auth in particular has no
+ # core equivalent at all.
+ raise NotImplementedError.new("challenge-response auth is not supported under soap4r + faraday.")
+ end
+
+ def set_cookie_store(filename)
+ raise NotImplementedError.new("cookie persistence is not supported under soap4r + faraday.")
+ end
+
+ def save_cookie_store
+ raise NotImplementedError.new("cookie persistence is not supported under soap4r + faraday.")
+ end
+
+ def reset(url)
+ # no persistent connection state kept between requests; ignored.
+ end
+
+ def reset_all
+ # ditto.
+ end
+
+ def post(url, req_body, header = {})
+ if str = @test_loopback_response.shift
+ if @debug_dev
+ @debug_dev << "= Request\n\n"
+ @debug_dev << req_body
+ @debug_dev << "\n\n= Response\n\n"
+ @debug_dev << str
+ end
+ return Response.new(200, nil, 'text/xml', Hash.new { |h, k| h[k] = [] }, str)
+ end
+ response = build_connection(url).post do |req|
+ header.each { |k, v| req.headers[k] = v }
+ req.headers['User-Agent'] = @agent if @agent
+ if @basic_auth
+ req.headers['Authorization'] =
+ "Basic #{Base64.strict_encode64(@basic_auth.join(':'))}"
+ end
+ req.body = req_body
+ end
+ dump_wiredump(url, header, req_body, response) if @debug_dev
+ Response.from_faraday(response)
+ end
+
+private
+
+ def require_adapter!
+ require "faraday/#{ADAPTER}"
+ rescue LoadError
+ raise LoadError.new(
+ "Faraday adapter #{ADAPTER.inspect} could not be loaded -- is its " \
+ "gem (e.g. \"faraday-#{ADAPTER}\" and whatever it wraps) installed?")
+ end
+
+ def build_connection(url)
+ cfg = @ssl_config
+ ssl_opts = {
+ :ca_file => cfg && cfg.ca_file,
+ :verify => !(cfg && cfg.verify_mode == OpenSSL::SSL::VERIFY_NONE),
+ # Faraday::SSLOptions documents client_cert/client_key as accepting
+ # OpenSSL objects directly, but confirmed empirically that the
+ # :typhoeus adapter rejects them ("Problem with the local SSL
+ # certificate") and only accepts file paths -- same requirement as
+ # curb's C binding (see curbClient.rb), so round-trip the same way
+ # here for whichever adapter is actually active.
+ :client_cert => cfg && cfg.client_cert && write_pem_tempfile(cfg.client_cert, 'cert'),
+ :client_key => cfg && cfg.client_key && write_pem_tempfile(cfg.client_key, 'key'),
+ :verify_depth => cfg && cfg.verify_depth,
+ :cert_store => cfg && cfg.cert_store,
+ }
+ # Faraday::SSLOptions is a Struct, and Faraday's own connection setup
+ # merges this hash into it via []= -- which raises NameError for any
+ # key that isn't a struct member, rather than just ignoring it the way
+ # a Hash would. :ciphers was dropped from that struct for a stretch of
+ # faraday 2.x releases and later restored (confirmed: present on
+ # 2.14.3, absent on 2.8.1 -- exactly what Ruby 3.3+ vs. 2.6/2.7
+ # resolve to respectively) -- confirmed crashing every single request
+ # on the versions missing it. Only include the key at all when this
+ # faraday actually has it; it's passed through for adapters that honor
+ # SSLOptions#ciphers, but confirmed empirically that :typhoeus silently
+ # ignores it either way (Faraday's own typhoeus adapter doesn't
+ # forward it to ethon's ssl_cipher_list at all) -- a real, external gap
+ # in that adapter, not something this bridge can paper over.
+ # verify_depth/cert_store have the same no-guarantee-every-adapter-
+ # honors-them caveat.
+ ssl_opts[:ciphers] = cfg && cfg.ciphers if Faraday::SSLOptions.members.include?(:ciphers)
+ Faraday.new(
+ :url => url,
+ :proxy => no_proxy?(url.is_a?(URI) ? url : URI.parse(url)) ? nil : @proxy,
+ :ssl => ssl_opts
+ ) do |f|
+ f.adapter ADAPTER
+ end.tap do |conn|
+ conn.options.open_timeout = @connect_timeout if @connect_timeout
+ conn.options.timeout = @receive_timeout if @receive_timeout
+ end
+ end
+
+ def write_pem_tempfile(openssl_obj, basename)
+ f = Tempfile.new(["soap4r-faraday-#{basename}-", '.pem'])
+ f.write(openssl_obj.to_pem)
+ f.close
+ # The finalizer proc must not be created in an instance-method context
+ # closing over self -- see the identical comment in curbClient.rb's own
+ # write_pem_tempfile for why (confirmed: Ruby warns "finalizer
+ # references object to be finalized" and the tempfile never actually
+ # got cleaned up otherwise).
+ ObjectSpace.define_finalizer(self, self.class.tempfile_unlinker(f))
+ f.path
+ end
+
+ def self.tempfile_unlinker(file)
+ proc { file.unlink rescue nil }
+ end
+
+ def dump_wiredump(url, header, req_body, response)
+ # Mirrors httpclient's wiredump block layout (marker line, blank line,
+ # raw request-line + headers, blank line, body) -- callers that parse
+ # wiredump_dev output by block position or by scanning for a "POST ..."
+ # line (e.g. test/soap/test_streamhandler.rb's parse_req_header) depend
+ # on that exact shape regardless of which backend produced it.
+ uri = url.is_a?(URI) ? url : URI.parse(url)
+ request_line = (@proxy && !no_proxy?(uri)) ? url : uri.request_uri
+ @debug_dev << "= Request\n\n"
+ @debug_dev << "POST #{request_line} HTTP/1.1\n"
+ header.each { |k, v| @debug_dev << "#{k}: #{v}\n" }
+ @debug_dev << "\n"
+ @debug_dev << req_body
+ @debug_dev << "\n\n= Response\n\n"
+ @debug_dev << "HTTP/1.1 #{response.status} #{response.reason_phrase}\n"
+ contenttype = response.headers['content-type']
+ @debug_dev << "Content-Type: #{contenttype}\n" if contenttype
+ @debug_dev << "\n"
+ @debug_dev << response.body
+ end
+
+ NO_PROXY_HOSTS = ['localhost']
+
+ def no_proxy?(uri)
+ if !@proxy or NO_PROXY_HOSTS.include?(uri.host)
+ return true
+ end
+ unless @no_proxy
+ return false
+ end
+ @no_proxy.scan(/([^:,]+)(?::(\d+))?/) do |host, port|
+ if /(\A|\.)#{Regexp.quote(host)}\z/i =~ uri.host &&
+ (!port || uri.port == port.to_i)
+ return true
+ end
+ end
+ false
+ end
+
+ class SSLConfig
+ # client_cert/client_key/ciphers/verify_depth/cert_store are all applied
+ # via Faraday::SSLOptions in build_connection -- Faraday's options
+ # struct accepts the same OpenSSL::X509::Certificate/OpenSSL::PKey::RSA
+ # objects HTTPConfigLoader already builds for httpclient's sake. Whether
+ # each one is actually *honored* still depends on which concrete adapter
+ # Faraday is riding on (SOAP4R_FARADAY_ADAPTER) -- confirmed working for
+ # :net_http and :typhoeus, not verified against every adapter Faraday
+ # supports.
+ attr_accessor :client_cert
+ attr_accessor :client_key
+ attr_accessor :client_ca
+ attr_accessor :verify_mode
+ attr_accessor :verify_depth
+ attr_accessor :options # no unified Faraday equivalent across adapters (OpenSSL::SSL::SSLContext#options bitmask); stored only.
+ attr_accessor :ciphers
+ attr_accessor :verify_callback # NOT supported -- no adapter Faraday supports exposes a per-certificate Ruby callback hook (same underlying limitation as libcurl-based backends generally).
+ attr_accessor :cert_store
+ attr_reader :ca_file
+
+ def set_trust_ca(value)
+ @ca_file = value
+ end
+
+ def set_crl(value)
+ @crl = value # not applied -- no unified Faraday equivalent.
+ end
+
+ # Whichever concrete adapter Faraday is riding on (net_http, patron,
+ # etc.) determines the real default CA trust behavior; none of them
+ # bundle their own snapshot the way httpclient does, so there's nothing
+ # to override here.
+ def set_default_paths
+ end
+ end
+
+ class Response
+ attr_reader :status
+ attr_reader :reason
+ attr_reader :contenttype
+ attr_reader :header
+ attr_reader :content
+
+ def initialize(status, reason, contenttype, header, content)
+ @status = status
+ @reason = reason
+ @contenttype = contenttype
+ @header = header
+ @content = content
+ end
+
+ def self.from_faraday(response)
+ # Faraday::Utils::Headers keeps one value per key (last write wins for
+ # anything the adapter received more than once, e.g. repeated
+ # Set-Cookie lines) -- wrap each in a single-element Array so callers
+ # get the same Hash-of-Arrays shape the other backends provide
+ # (streamHandler.rb reads header['content-encoding'][0] and
+ # header['location'][0]). A known, accepted limitation: multi-valued
+ # response headers only reflect the last occurrence under this
+ # backend.
+ header = Hash.new { |h, k| h[k] = [] }
+ response.headers.each { |k, v| header[k.downcase] = [v] }
+ new(response.status, response.reason_phrase, response.headers['content-type'],
+ header, response.body)
+ end
+ end
+end
+
+
+end
diff --git a/lib/soap/generator.rb b/lib/soap/generator.rb
index a52f911bd..e6af6df29 100644
--- a/lib/soap/generator.rb
+++ b/lib/soap/generator.rb
@@ -51,7 +51,7 @@ def initialize(opt = {})
end
def generate(obj, io = nil)
- @buf = io || ''
+ @buf = io || String.new
@indent = ''
@encode_char_regexp = get_encode_char_regexp()
@@ -270,7 +270,15 @@ def get_encoded(str)
end
def get_encode_char_regexp
- ENCODE_CHAR_REGEXP[XSD::Charset.encoding] ||= Regexp.new("[#{EncodeMap.keys.join}]", nil, (RUBY_VERSION.to_f <= 1.8) ? XSD::Charset.encoding : nil) # RubyJedi: compatible with Ruby 1.8.6 and above
+ ENCODE_CHAR_REGEXP[XSD::Charset.encoding] ||= begin
+ if RUBY_VERSION.to_f <= 1.8
+ Regexp.new("[#{EncodeMap.keys.join}]", nil, XSD::Charset.encoding)
+ else
+ # the deprecated 3-arg form's kcode argument was already nil here
+ # (a no-op since Ruby 1.9), so there's nothing lost by dropping it.
+ Regexp.new("[#{EncodeMap.keys.join}]")
+ end
+ end
end
def find_handler(encodingstyle)
diff --git a/lib/soap/httpbackend.rb b/lib/soap/httpbackend.rb
new file mode 100644
index 000000000..58b39ed3a
--- /dev/null
+++ b/lib/soap/httpbackend.rb
@@ -0,0 +1,44 @@
+# encoding: UTF-8
+# SOAP4R - HTTP client backend loader.
+
+require 'soap/httpbackend/registry'
+
+# Provide a means to CHOOSE a preferred HTTP client backend and
+# loading-order, if multiple backend gems are available -- mirrors
+# xsd/xmlparser.rb's SOAP4R_PARSERS mechanism (same problem: several
+# interchangeable backends, hardcoded fallback order, and no way to force a
+# specific one without editing library code). This is what lets CI (and
+# anyone debugging a backend-specific issue) actually exercise
+# SOAP::NetHttpClient end-to-end instead of it only ever being reachable
+# when every other backend happens to fail to load.
+# http-access2 (httpclient's own predecessor, by the same author) used to
+# sit in this cascade too. Removed: the gem was renamed to httpclient years
+# ago and is no longer published on RubyGems.org at all, so that entry
+# could never actually load -- confirmed empirically (`gem install
+# http-access2` fails outright). Anyone still vendoring the old gem
+# directly (e.g. via a git ref) can select it by placing a matching
+# lib/soap/httpbackend/http_access2.rb adapter back on their own
+# $LOAD_PATH; see git history for the version that shipped here.
+if ENV.has_key?('SOAP4R_HTTP_CLIENTS')
+ backend_list = ENV['SOAP4R_HTTP_CLIENTS'].to_s.split(',')
+else
+ backend_list = [
+ 'httpclient', ## Uses the httpclient gem
+ 'curb', ## Uses the curb gem (libcurl bindings) ; not installed by default, opt-in
+ 'faraday', ## Uses the faraday gem (itself pluggable -- see soap/faradayClient.rb) ; not installed by default, opt-in
+ 'net_http', ## Falls back to this project's own wrapper around stdlib Net::HTTP
+ ]
+end
+
+loaded = false
+backend_list.each do |name|
+ begin
+ require "soap/httpbackend/#{name}"
+ loaded = true
+ break
+ rescue LoadError
+ end
+end
+unless loaded
+ raise RuntimeError.new("HTTP client backend not found.")
+end
diff --git a/lib/soap/httpbackend/curb.rb b/lib/soap/httpbackend/curb.rb
new file mode 100644
index 000000000..0ade1dfa5
--- /dev/null
+++ b/lib/soap/httpbackend/curb.rb
@@ -0,0 +1,7 @@
+# encoding: UTF-8
+# SOAP4R - HTTP client backend: the curb gem (libcurl bindings).
+
+require 'soap/httpbackend/registry'
+require 'soap/curbClient'
+
+SOAP::HTTPBackend.register(SOAP::CurbClient, true)
diff --git a/lib/soap/httpbackend/faraday.rb b/lib/soap/httpbackend/faraday.rb
new file mode 100644
index 000000000..d0a1644ae
--- /dev/null
+++ b/lib/soap/httpbackend/faraday.rb
@@ -0,0 +1,7 @@
+# encoding: UTF-8
+# SOAP4R - HTTP client backend: the faraday gem.
+
+require 'soap/httpbackend/registry'
+require 'soap/faradayClient'
+
+SOAP::HTTPBackend.register(SOAP::FaradayClient, true)
diff --git a/lib/soap/httpbackend/httpclient.rb b/lib/soap/httpbackend/httpclient.rb
new file mode 100644
index 000000000..d0a5e062d
--- /dev/null
+++ b/lib/soap/httpbackend/httpclient.rb
@@ -0,0 +1,7 @@
+# encoding: UTF-8
+# SOAP4R - HTTP client backend: the httpclient gem.
+
+require 'soap/httpbackend/registry'
+require 'httpclient'
+
+SOAP::HTTPBackend.register(HTTPClient, true)
diff --git a/lib/soap/httpbackend/net_http.rb b/lib/soap/httpbackend/net_http.rb
new file mode 100644
index 000000000..54c557ea9
--- /dev/null
+++ b/lib/soap/httpbackend/net_http.rb
@@ -0,0 +1,9 @@
+# encoding: UTF-8
+# SOAP4R - HTTP client backend: this project's own wrapper around stdlib
+# Net::HTTP. Final fallback when no third-party HTTP client gem is
+# available.
+
+require 'soap/httpbackend/registry'
+require 'soap/netHttpClient'
+
+SOAP::HTTPBackend.register(SOAP::NetHttpClient, false)
diff --git a/lib/soap/httpbackend/registry.rb b/lib/soap/httpbackend/registry.rb
new file mode 100644
index 000000000..e4b578ac5
--- /dev/null
+++ b/lib/soap/httpbackend/registry.rb
@@ -0,0 +1,32 @@
+# encoding: UTF-8
+# SOAP4R - HTTP client backend registry.
+
+module SOAP
+module HTTPBackend
+
+ @@client_class = nil
+ @@retryable = false
+
+ def self.client_class
+ @@client_class
+ end
+
+ def self.retryable?
+ @@retryable
+ end
+
+ # Called by each backend adapter (lib/soap/httpbackend/*.rb) once it has
+ # successfully required its underlying library. Mirrors
+ # XSD::XMLParser::Parser.add_factory's self-registration pattern
+ # (lib/xsd/xmlparser/parser.rb) -- same shape of problem, one process-wide
+ # choice made once at load time.
+ def self.register(client_class, retryable)
+ if $DEBUG
+ puts "Set #{ client_class } as HTTP client backend."
+ end
+ @@client_class = client_class
+ @@retryable = retryable
+ end
+
+end
+end
diff --git a/lib/soap/httpconfigloader.rb b/lib/soap/httpconfigloader.rb
index 17dd25688..200b289df 100644
--- a/lib/soap/httpconfigloader.rb
+++ b/lib/soap/httpconfigloader.rb
@@ -31,6 +31,26 @@ def set_options(client, options)
client.protocol_version = value
end
end
+ # httpclient's SSLConfig doesn't trust the system's CA bundle by
+ # default -- left unconfigured, it lazily falls back to its own
+ # gem-vendored cacert.pem snapshot (ssl_config.rb's
+ # `load_trust_ca unless @cacerts_loaded`), which can go stale
+ # relative to a real server's cert chain as CAs rotate intermediates
+ # (confirmed directly: a real Let's Encrypt-signed endpoint failed
+ # verification against httpclient 2.9.0's bundled snapshot while
+ # verifying fine against the host's own, actively-maintained CA
+ # bundle). set_default_paths defers to whatever OpenSSL was actually
+ # built to trust on this platform -- no hardcoded path, so it stays
+ # portable across the Debian/RHEL/Alpine/etc. layouts this project's
+ # Ruby-version matrix runs on. Called unconditionally, before any
+ # user-supplied ssl_config options are applied below, so it's just
+ # the baseline: an explicit ca_file/ca_path/cert_store still layers
+ # on top exactly as before (add_trust_ca merely adds to whatever's
+ # already in the store; cert_store= replaces it outright for anyone
+ # who wants full manual control).
+ if (cfg = client.ssl_config) && cfg.respond_to?(:set_default_paths)
+ cfg.set_default_paths
+ end
ssl_config = options["ssl_config"] ||= ::SOAP::Property.new
set_ssl_config(client, ssl_config)
ssl_config.add_hook(true) do |key, value|
diff --git a/lib/soap/mapping/encodedregistry.rb b/lib/soap/mapping/encodedregistry.rb
index c948e15c8..c698ab92e 100644
--- a/lib/soap/mapping/encodedregistry.rb
+++ b/lib/soap/mapping/encodedregistry.rb
@@ -6,6 +6,47 @@
# redistribute it and/or modify it under the same terms of Ruby's license;
# either the dual license version in 2003, or any later version.
+# in 2.4, 2.5 Fixnum/Bignum aliased to 'Integer'
+FixnumShim = 1.class
+FIXNUM_PRESENT = FixnumShim.name == 'Fixnum'
+BignumShim = (10**20).class
+BIGNUM_PRESENT = BignumShim.name == 'Bignum'
+
+if RUBY_VERSION.to_f >= 3.4 && defined?(Warning) && Warning.respond_to?(:warn)
+ # Ruby 3.4+'s chilled-string deprecation notice fires every time
+ # addextend2soap (below) opens a string's singleton class to check for a
+ # prior #extend, even when it turns out not to be extended -- unavoidable
+ # without breaking test/soap/marshal/marshaltestlib.rb#test_extend_string's
+ # real, tested round-trip behavior (see the NOTE on addextend2soap).
+ # Installed once, permanently, rather than swapped in/out per call: this
+ # method can run concurrently from multiple WEBrick request threads, and a
+ # temporary swap-and-restore around each call would race between threads.
+ # Matched on both the message text AND this file's own path, so no other
+ # chilled-string warning -- from elsewhere in this library, a dependency
+ # like simplecov, or the embedding application -- is ever affected.
+ #
+ # The override itself is built via class_eval on a *string* rather than
+ # literal keyword-argument syntax (`category:`) in this file: this file
+ # must parse cleanly on every supported Ruby back to 1.8.7, and Ruby
+ # parses an entire file up front regardless of the RUBY_VERSION guard
+ # above being a runtime check -- literal keyword-arg syntax here would be
+ # a SyntaxError on any pre-2.0 Ruby long before that guard ever runs
+ # (confirmed: broke the whole file, and everything requiring it, on
+ # 1.8.7). A string is just inert data to the parser; it's only parsed as
+ # code, by class_eval, once we're already inside the version-gated branch.
+ class << Warning
+ alias_method :__soap4r_encodedregistry_original_warn, :warn
+ end
+ Warning.singleton_class.class_eval(<<-'RUBY', __FILE__, __LINE__ + 1)
+ def warn(message, category: nil)
+ if message.include?("literal string will be frozen in the future") &&
+ message.include?("soap/mapping/encodedregistry.rb")
+ return
+ end
+ __soap4r_encodedregistry_original_warn(message, category: category)
+ end
+ RUBY
+end
require 'soap/baseData'
require 'soap/mapping/mapping'
@@ -122,7 +163,7 @@ def find_mapped_obj_class(target_soap_class)
StringFactory = StringFactory_.new
BasetypeFactory = BasetypeFactory_.new
- FixnumFactory = FixnumFactory_.new
+ FixnumFactory = FixnumFactory_.new if FIXNUM_PRESENT
DateTimeFactory = DateTimeFactory_.new
ArrayFactory = ArrayFactory_.new
Base64Factory = Base64Factory_.new
@@ -146,7 +187,6 @@ def find_mapped_obj_class(target_soap_class)
{:derived_class => true}],
[::Float, ::SOAP::SOAPFloat, BasetypeFactory,
{:derived_class => true}],
- [::Fixnum, ::SOAP::SOAPInt, FixnumFactory],
[::Integer, ::SOAP::SOAPInt, BasetypeFactory,
{:derived_class => true}],
[::Integer, ::SOAP::SOAPLong, BasetypeFactory,
@@ -199,6 +239,8 @@ def find_mapped_obj_class(target_soap_class)
{:type => XSD::QName.new(RubyCustomTypeNamespace, "SOAPException")}],
]
+ SOAPBaseMap << [FixnumShim, ::SOAP::SOAPInt, FixnumFactory] if FIXNUM_PRESENT
+
RubyOriginalMap = [
[::NilClass, ::SOAP::SOAPNil, BasetypeFactory],
[::TrueClass, ::SOAP::SOAPBoolean, BasetypeFactory],
@@ -212,7 +254,6 @@ def find_mapped_obj_class(target_soap_class)
{:derived_class => true}],
[::Float, ::SOAP::SOAPFloat, BasetypeFactory,
{:derived_class => true}],
- [::Fixnum, ::SOAP::SOAPInt, FixnumFactory],
[::Integer, ::SOAP::SOAPInt, BasetypeFactory,
{:derived_class => true}],
[::Integer, ::SOAP::SOAPLong, BasetypeFactory,
@@ -263,6 +304,8 @@ def find_mapped_obj_class(target_soap_class)
{:type => XSD::QName.new(RubyCustomTypeNamespace, "SOAPException")}],
]
+ RubyOriginalMap << [FixnumShim, ::SOAP::SOAPInt, FixnumFactory] if FIXNUM_PRESENT
+
attr_accessor :default_factory
attr_accessor :excn_handler_obj2soap
attr_accessor :excn_handler_soap2obj
@@ -411,7 +454,18 @@ def addextend2obj(obj, attr)
end
def addextend2soap(node, obj)
- return if [Symbol, Fixnum, Bignum, Float].any?{ |c| obj.is_a?(c) }
+ return if [Symbol, Integer, Float].any?{ |c| obj.is_a?(c) }
+ return if FIXNUM_PRESENT && obj.is_a?(FixnumShim)
+ return if BIGNUM_PRESENT && obj.is_a?(BignumShim)
+ # NOTE: test/soap/marshal/marshaltestlib.rb#test_extend_string
+ # deliberately extends a String value with a module and expects that
+ # extension to round-trip through SOAP marshal -- so plain (non-frozen)
+ # strings can't be skipped outright here, even though opening a
+ # singleton class on one (below) trips Ruby 3.4+'s chilled-string
+ # warning for any ordinary string literal that happens to pass through
+ # unextended. Not cleanly fixable without breaking that real behavior;
+ # left as a known, harmless, forward-looking warning.
+ return if obj.is_a?(String) && obj.frozen?
list = (class << obj; self; end).ancestors - obj.class.ancestors
list = list.reject{|c| c.class == Class } ## As of Ruby 2.1 Singleton Classes are now included in the ancestry. Need to filter those out here.
diff --git a/lib/soap/mapping/literalregistry.rb b/lib/soap/mapping/literalregistry.rb
index 0a0fad5e3..16cd6c663 100644
--- a/lib/soap/mapping/literalregistry.rb
+++ b/lib/soap/mapping/literalregistry.rb
@@ -357,10 +357,12 @@ def add_attributes2obj(node, obj)
# much memory for each singleton Object. just instance_eval instead of it.
def define_xmlattr_accessor(obj, qname)
# untaint depends GenSupport.safemethodname
- name = Mapping.safemethodname('xmlattr_' + qname.name).untaint
+ name = Mapping.safemethodname('xmlattr_' + qname.name)
+ name.untaint if RUBY_VERSION < '2.7'
unless obj.respond_to?(name)
# untaint depends QName#dump
- qnamedump = qname.dump.untaint
+ qnamedump = qname.dump
+ qnamedump.untaint if RUBY_VERSION < '2.7'
obj.instance_eval <<-EOS
def #{name}
@__xmlattr[#{qnamedump}]
diff --git a/lib/soap/mapping/mapping.rb b/lib/soap/mapping/mapping.rb
index bf3785787..4a1a5a601 100644
--- a/lib/soap/mapping/mapping.rb
+++ b/lib/soap/mapping/mapping.rb
@@ -108,7 +108,7 @@ def self.fault2exception(fault, registry = nil)
e.set_backtrace(nil)
raise e # ruby sets current caller as local backtrace of e => e2.
rescue Exception => e
- e.set_backtrace(remote_backtrace + e.backtrace[1..-1])
+ e.set_backtrace((remote_backtrace || []) + ((e.backtrace || [])[1..-1] || []))
raise
end
else
@@ -167,7 +167,7 @@ def self.create_empty_object(klass)
#
def self.name2elename(name)
name.to_s.gsub(/([^a-zA-Z0-9:_\-]+)/n) {
- '.' << $1.unpack('H2' * $1.size).join('.')
+ '.' + $1.unpack('H2' * $1.size).join('.')
}.gsub(/::/n, '..')
end
@@ -333,7 +333,8 @@ def self.set_attributes(obj, values)
else
values.each do |attr_name, value|
# untaint depends GenSupport.safevarname
- name = Mapping.safevarname(attr_name).untaint
+ name = Mapping.safevarname(attr_name)
+ name.untaint if RUBY_VERSION < '2.7'
setter = name + "="
if obj.respond_to?(setter)
obj.__send__(setter, value)
diff --git a/lib/soap/mapping/registry.rb b/lib/soap/mapping/registry.rb
index 1b2942b68..e425129b8 100644
--- a/lib/soap/mapping/registry.rb
+++ b/lib/soap/mapping/registry.rb
@@ -107,9 +107,11 @@ def marshal_load(dumpobj)
# much memory for each singleton Object. just instance_eval instead of it.
def __define_attr_accessor(qname)
# untaint depends GenSupport.safemethodname
- name = Mapping.safemethodname(qname.name).untaint
+ name = Mapping.safemethodname(qname.name)
+ name.untaint if RUBY_VERSION < '2.7'
# untaint depends on QName#dump
- qnamedump = qname.dump.untaint
+ qnamedump = qname.dump
+ qnamedump.untaint if RUBY_VERSION < '2.7'
singleton = false
unless self.respond_to?(name)
singleton = true
diff --git a/lib/soap/mapping/rubytypeFactory.rb b/lib/soap/mapping/rubytypeFactory.rb
index e6b04947b..643b6307b 100644
--- a/lib/soap/mapping/rubytypeFactory.rb
+++ b/lib/soap/mapping/rubytypeFactory.rb
@@ -6,6 +6,15 @@
# redistribute it and/or modify it under the same terms of Ruby's license;
# either the dual license version in 2003, or any later version.
+old_verbose, $VERBOSE = $VERBOSE, nil # silence warnings
+DATA_PRESENT = defined?(Data)
+# DataShim must always be a defined class, since it's referenced unconditionally
+# in a `case/when` below. Ruby's old C-extension Data class was removed in 3.1,
+# then Ruby 3.2 introduced an unrelated Data.define -- so DATA_PRESENT is false
+# only on the 3.1.x line. Fall back to an anonymous class nothing will ever be
+# an instance of, so the `when` comparison is always valid but never matches.
+DataShim = DATA_PRESENT ? Kernel.const_get('Data') : Class.new
+$VERBOSE = old_verbose
module SOAP
module Mapping
@@ -38,6 +47,7 @@ def initialize(config = {})
def obj2soap(soap_class, obj, info, map)
param = nil
+
case obj
when ::String
unless @allow_original_mapping
@@ -193,7 +203,7 @@ def obj2soap(soap_class, obj, info, map)
param.add('member', ele_member)
addiv2soapattr(param, obj, map)
end
- when ::IO, ::Binding, ::Data, ::Dir, ::File::Stat,
+ when ::IO, ::Binding, DataShim, ::Dir, ::File::Stat,
::MatchData, Method, ::Proc, ::Process::Status, ::Thread,
::ThreadGroup, ::UnboundMethod
return nil
diff --git a/lib/soap/mimemessage.rb b/lib/soap/mimemessage.rb
index a59783025..79516b4c3 100644
--- a/lib/soap/mimemessage.rb
+++ b/lib/soap/mimemessage.rb
@@ -225,7 +225,7 @@ def headers_str
end
def content_str
- str = ''
+ str = String.new
@parts.each do |prt|
str << "--" + boundary + "\r\n"
str << prt.to_s + "\r\n"
diff --git a/lib/soap/netHttpClient.rb b/lib/soap/netHttpClient.rb
index 95a9d6aed..445eb5d46 100644
--- a/lib/soap/netHttpClient.rb
+++ b/lib/soap/netHttpClient.rb
@@ -132,8 +132,22 @@ def post_redirect(url, req_body, header, redirect_count)
extra['User-Agent'] = @agent if @agent
res = start(url) { |http|
if @debug_dev
+ # Mirrors httpclient's wiredump block layout (marker line, blank
+ # line, raw request-line + headers, blank line, body) -- callers
+ # that parse wiredump_dev output by block position or by scanning
+ # for a "POST ..." line (e.g. test/soap/test_streamhandler.rb's
+ # parse_req_header) depend on that exact shape regardless of which
+ # backend produced it.
+ # Real proxied requests go out in absolute-form (Net::HTTP handles
+ # this itself at the socket level once the connection is built
+ # with Net::HTTP::Proxy) -- match that here too, or the dump shows
+ # origin-form even when a proxy is actually in use.
+ request_line = http.proxy? ? url.to_s : url.request_uri
@debug_dev << "= Request\n\n"
- @debug_dev << req_body << "\n"
+ @debug_dev << "POST #{request_line} HTTP/1.1\n"
+ extra.each { |k, v| @debug_dev << "#{k}: #{v}\n" }
+ @debug_dev << "\n"
+ @debug_dev << req_body
end
http.post(url.request_uri, req_body, extra)
}
@@ -158,7 +172,14 @@ def start(url)
worker.finish
}
if @debug_dev
+ # response here is the raw Net::HTTPResponse yielded by http.start,
+ # not yet wrapped into our own Response class (that happens back in
+ # post_redirect/get_content) -- so this uses Net::HTTPResponse's own
+ # #code/#message/#[] rather than #status/#reason/#contenttype.
@debug_dev << "\n\n= Response\n\n"
+ @debug_dev << "HTTP/1.1 #{response.code} #{response.message}\n"
+ @debug_dev << "Content-Type: #{response['content-type']}\n" if response['content-type']
+ @debug_dev << "\n"
@debug_dev << response.body << "\n"
end
response
@@ -169,11 +190,21 @@ def create_connection(url)
unless no_proxy?(url)
proxy_host = @proxy.host
proxy_port = @proxy.port
+ proxy_user = @proxy.user
+ proxy_password = @proxy.password
end
- http = Net::HTTP::Proxy(proxy_host, proxy_port).new(url.host, url.port)
- if http.respond_to?(:set_debug_output)
- http.set_debug_output(@debug_dev)
- end
+ http = Net::HTTP::Proxy(proxy_host, proxy_port, proxy_user, proxy_password).new(url.host, url.port)
+ # Deliberately NOT wiring Net::HTTP's own set_debug_output here: this
+ # class already writes its own structured "= Request"/"= Response"
+ # dump to @debug_dev in #post_redirect/#start (matching the other
+ # backends' wiredump shape, which callers/tests parse by splitting on
+ # blank lines). Net::HTTP's raw wire-level trace uses a different
+ # format and writes the same request/response bodies to the same
+ # stream a second time -- confirmed this was corrupting every
+ # blank-line-block-indexed wiredump parse in the suite (e.g. a request
+ # body substring counted twice, or a parse landing on a "= Response"
+ # marker line instead of XML) the first time this backend was actually
+ # exercised end-to-end (SOAP4R_HTTP_CLIENTS=net_http).
http.open_timeout = @connect_timeout if @connect_timeout
http.read_timeout = @receive_timeout if @receive_timeout
case url
diff --git a/lib/soap/property.rb b/lib/soap/property.rb
index 6cf745779..dc10d1de8 100644
--- a/lib/soap/property.rb
+++ b/lib/soap/property.rb
@@ -33,7 +33,9 @@ module SOAP
# aaa.hhh = iii
#
class Property
- FrozenError = (RUBY_VERSION >= "1.9.0") ? RuntimeError : TypeError
+ unless defined?(FrozenError) # defined since 2.5
+ FrozenError = (RUBY_VERSION >= "1.9.0") ? RuntimeError : TypeError
+ end
include Enumerable
@@ -327,4 +329,4 @@ def loadstr(str)
end
-end
\ No newline at end of file
+end
diff --git a/lib/soap/rpc/cgistub.rb b/lib/soap/rpc/cgistub.rb
index 89dbda143..51938fdb9 100644
--- a/lib/soap/rpc/cgistub.rb
+++ b/lib/soap/rpc/cgistub.rb
@@ -202,7 +202,7 @@ def set_fcgi_request(request)
HTTPVersion = WEBrick::HTTPVersion.new('1.0') # dummy; ignored
def run
- res = WEBrick::HTTPResponse.new({:HTTPVersion => HTTPVersion})
+ res = WEBrick::HTTPResponse.new({:HTTPVersion => HTTPVersion, :Logger => logger})
begin
@log.info { "received a request from '#{ @remote_host }'" }
if @fcgi
@@ -227,9 +227,12 @@ def run
r.send_http_header
buf = res.body
else
- buf = ''
+ # since 2.5 it doesn't work with empty string in WEBRICK::HTTPResponse#send_header(socket)
+ # https://github.com/ruby/ruby/commit/c44978b99f0454b8f00674f2f407893c8c47248e
+ buf = StringIO.new
res.send_response(buf)
- buf.sub!(/^[^\r]+\r\n/, '') # Trim status line.
+
+ buf = buf.string.sub(/^[^\r]+\r\n/, '') # Trim status line.
end
@log.debug { "SOAP CGI Response:\n#{ buf }" }
if @fcgi
diff --git a/lib/soap/rpc/httpserver.rb b/lib/soap/rpc/httpserver.rb
index 37d6390ba..b5123a38e 100644
--- a/lib/soap/rpc/httpserver.rb
+++ b/lib/soap/rpc/httpserver.rb
@@ -42,7 +42,7 @@ def initialize(config)
@soaplet = ::SOAP::RPC::SOAPlet.new(@router)
on_init
- @server = WEBrick::HTTPServer.new(@webrick_config)
+ @server = new_webrick_server(@webrick_config)
@server.mount('/soaprouter', @soaplet)
if wsdldir = config[:WSDLDocumentDirectory]
@server.mount('/wsdl', WEBrick::HTTPServlet::FileHandler, wsdldir)
@@ -137,6 +137,35 @@ def add_document_request_operation(factory, soapaction, name, param_def, opt = {
private
+ # A short retry-on-EADDRINUSE, matching test/testutil.rb's existing
+ # TestUtil.webrick_server helper, applied here so it covers every caller
+ # (not just tests that happen to go through that helper). Note: the mass
+ # EADDRINUSE cascade seen across the test suite (most test files share a
+ # single fixed port) turned out to be caused by a genuinely orphaned
+ # server -- test/soap/header/test_authheader_cgi.rb's teardown could
+ # raise before reaching teardown_server, permanently leaking that test's
+ # listener for the rest of the run -- not by transient TIME_WAIT. This
+ # retry is still worth keeping as a real defensive measure, just don't
+ # rely on it alone to mask a genuine leak elsewhere.
+ def new_webrick_server(config)
+ try = 0
+ begin
+ WEBrick::HTTPServer.new(config)
+ rescue Errno::EADDRINUSE => e
+ sleep 1
+ # See test/testutil.rb's webrick_server for the full history and why
+ # this was pulled back down from 120 to 20 -- the real leak (Thread#kill
+ # racing WEBrick's own async shutdown cleanup in test teardown) is
+ # fixed now, so this only needs to cover transient scheduling delay,
+ # not a real leak. A large budget here actively hurts: sslsvr.rb (test
+ # SSL support script) calls into this same path, and its parent process
+ # blocks on a timeout-less read waiting for it to report a PID --
+ # confirmed a stuck retry here manifests as a multi-minute *silent
+ # hang* in CI (run 28892185757), not just a slow test.
+ ((try += 1) < 20) ? retry : raise(e)
+ end
+ end
+
def attrproxy
@router
end
diff --git a/lib/soap/streamHandler.rb b/lib/soap/streamHandler.rb
index f23e6a2ec..7684e3c87 100644
--- a/lib/soap/streamHandler.rb
+++ b/lib/soap/streamHandler.rb
@@ -9,6 +9,7 @@
require 'soap/soap'
require 'soap/httpconfigloader'
+require 'soap/httpbackend'
require 'soap/filter/filterchain'
begin
require 'stringio'
@@ -92,25 +93,8 @@ def test_loopback_response
class HTTPStreamHandler < StreamHandler
include SOAP
- begin
- require 'httpclient'
- Client = HTTPClient
- RETRYABLE = true
- rescue LoadError
- begin
- require 'http-access2'
- if HTTPAccess2::VERSION < "2.0"
- raise LoadError.new("http-access/2.0 or later is required.")
- end
- Client = HTTPAccess2::Client
- RETRYABLE = true
- rescue LoadError
- warn("Loading http-access2 failed. Net/http is used.") if $DEBUG
- require 'soap/netHttpClient'
- Client = SOAP::NetHttpClient
- RETRYABLE = false
- end
- end
+ Client = SOAP::HTTPBackend.client_class
+ RETRYABLE = SOAP::HTTPBackend.retryable?
class HttpPostRequestFilter
def initialize(filterchain)
diff --git a/lib/soap/version.rb b/lib/soap/version.rb
index e70cc6792..97f35d49f 100644
--- a/lib/soap/version.rb
+++ b/lib/soap/version.rb
@@ -2,8 +2,8 @@
module SOAP
module VERSION #:nodoc:
MAJOR = 2
- MINOR = 0
- TINY = 3
+ MINOR = 1
+ TINY = 1
STRING = [MAJOR, MINOR, TINY].join('.')
FORK = "SOAP4R-NG"
diff --git a/lib/wsdl/parser.rb b/lib/wsdl/parser.rb
index 2cf2cb2e7..00b836165 100644
--- a/lib/wsdl/parser.rb
+++ b/lib/wsdl/parser.rb
@@ -62,7 +62,7 @@ def initialize(opt = {})
def parse(string_or_readable)
@parsestack = []
@lastnode = nil
- @textbuf = ''
+ @textbuf = String.new
@parser.do_parse(string_or_readable)
@lastnode
end
diff --git a/lib/wsdl/soap/classDefCreator.rb b/lib/wsdl/soap/classDefCreator.rb
index aaa11d4a2..7f48f9afa 100644
--- a/lib/wsdl/soap/classDefCreator.rb
+++ b/lib/wsdl/soap/classDefCreator.rb
@@ -43,7 +43,7 @@ def initialize(definitions, name_creator, modulepath = nil)
end
def dump(type = nil)
- result = "require 'xsd/qname'\n"
+ result = "require 'xsd/qname'\n".dup
# cannot use @modulepath because of multiple classes
if @modulepath
result << "\n"
@@ -274,11 +274,17 @@ def create_structdef(mpath, qname, typedef, qualified = false)
c.comment = "#{qname}"
c.comment << "\nabstract" if typedef.abstract
parentmodule = mapped_class_name(qname, mpath)
+ # Shared across both parse_elements calls below (and all of their
+ # recursive descendants) so that a name reachable through two
+ # independent paths -- e.g. two group refs each declaring an element
+ # called "shared" -- is detected as a collision no matter which branch
+ # of the content model it comes from.
+ varnames = {}
init_lines, init_params =
- parse_elements(c, typedef.elements, qname.namespace, parentmodule)
+ parse_elements(c, typedef.elements, qname.namespace, parentmodule, false, varnames)
if typedef.content && (WSDL::XMLSchema::Group === typedef.content)
g_init_lines, g_init_params =
- parse_elements(c, typedef.content.refelement.elements, qname.namespace, parentmodule)
+ parse_elements(c, typedef.content.refelement.elements, qname.namespace, parentmodule, false, varnames)
init_lines = (g_init_lines + init_lines)
init_params = (g_init_params + init_params)
end
@@ -292,7 +298,20 @@ def create_structdef(mpath, qname, typedef, qualified = false)
c
end
- def parse_elements(c, elements, base_namespace, mpath, as_array = false)
+ # varnames tracks every attribute/param name assigned so far across the
+ # whole recursive walk for one class (threaded through by the caller, and
+ # passed on unchanged to every recursive call below), so that a name
+ # reachable through two independent paths -- e.g. two group refs each
+ # declaring an element called "shared" -- collides no matter which branch
+ # of the content model it comes from. Without this, the second "shared"
+ # would emit an `attr_accessor :shared` and `def initialize(shared = nil)`
+ # entry identical to the first, silently wiring both onto the very same
+ # @shared ivar (a plain Array#uniq on init_params alone would only paper
+ # over the resulting SyntaxError, not this data loss: the two occurrences
+ # would remain last-write-wins instead of independently addressable). We
+ # instead suffix the repeat, the same way define_attribute already
+ # disambiguates colliding attribute constants below.
+ def parse_elements(c, elements, base_namespace, mpath, as_array = false, varnames = {})
init_lines = []
init_params = []
any = false
@@ -323,6 +342,12 @@ def parse_elements(c, elements, base_namespace, mpath, as_array = false)
unless as_array
attrname = safemethodname(name)
varname = safevarname(name)
+ varnames[varname] ||= 0
+ if (varnames[varname] += 1) > 1
+ suffix = "_#{varnames[varname]}"
+ attrname += suffix
+ varname += suffix
+ end
c.def_attr(attrname, true, varname)
init_lines << "@#{varname} = #{varname}"
if element.map_as_array?
@@ -334,12 +359,12 @@ def parse_elements(c, elements, base_namespace, mpath, as_array = false)
end
when WSDL::XMLSchema::Sequence
child_init_lines, child_init_params =
- parse_elements(c, element.elements, base_namespace, mpath, as_array)
+ parse_elements(c, element.elements, base_namespace, mpath, as_array, varnames)
init_lines.concat(child_init_lines)
init_params.concat(child_init_params)
when WSDL::XMLSchema::Choice
child_init_lines, child_init_params =
- parse_elements(c, element.elements, base_namespace, mpath, as_array)
+ parse_elements(c, element.elements, base_namespace, mpath, as_array, varnames)
init_lines.concat(child_init_lines)
init_params.concat(child_init_params)
when WSDL::XMLSchema::Group
@@ -351,7 +376,7 @@ def parse_elements(c, elements, base_namespace, mpath, as_array = false)
next
end
child_init_lines, child_init_params =
- parse_elements(c, element.content.elements, base_namespace, mpath, as_array)
+ parse_elements(c, element.content.elements, base_namespace, mpath, as_array, varnames)
init_lines.concat(child_init_lines)
init_params.concat(child_init_params)
else
@@ -363,7 +388,10 @@ def parse_elements(c, elements, base_namespace, mpath, as_array = false)
def define_attribute(c, attributes)
const = {}
- unless attributes.empty?
+ # No __xmlattr-backed storage is needed when every attribute is fixed:
+ # their getters return the fixed literal directly and none of them get
+ # a setter (see below), so there's nothing left to read the hash for.
+ unless attributes.empty? || attributes.all? { |attribute| attribute.fixed }
c.def_method("__xmlattr") do <<-__EOD__
@__xmlattr ||= {}
__EOD__
@@ -378,18 +406,37 @@ def define_attribute(c, attributes)
constname += "_#{const[constname]}"
end
c.def_const(constname, dqname(name))
- c.def_method(methodname) do <<-__EOD__
- __xmlattr[#{constname}]
- __EOD__
+ c.def_method(methodname) do
+ define_attribute_reader_body(attribute, constname)
end
- c.def_method(methodname + '=', 'value') do <<-__EOD__
- __xmlattr[#{constname}] = value
- __EOD__
+ # XSD's `fixed` value constraint means any value other than the fixed
+ # one is invalid, so there's no legitimate value a setter could ever
+ # assign; omit it entirely rather than generate a footgun.
+ unless attribute.fixed
+ c.def_method(methodname + '=', 'value') do <<-__EOD__
+ __xmlattr[#{constname}] = value
+ __EOD__
+ end
end
c.comment << "\n #{methodname} - #{attribute_basetype(attribute) || '(any)'}"
end
end
+ # Per XML Schema Part 1 S3.2.1, a `fixed` or `default` value constraint
+ # means an omitted attribute is treated as if it were present with that
+ # value -- this was previously discarded at codegen time, silently
+ # dropping that XSD semantic. `fixed` wins if both are somehow set, since
+ # WSDL::XMLSchema::Attribute itself only ever populates one or the other.
+ def define_attribute_reader_body(attribute, constname)
+ if f = attribute.fixed
+ %Q{"#{f}"}
+ elsif d = attribute.default
+ %Q{__xmlattr[#{constname}] || "#{d}"}
+ else
+ "__xmlattr[#{constname}]"
+ end
+ end
+
def create_arraydef(mpath, qname, typedef)
classname = mapped_class_basename(qname, mpath)
c = ClassDef.new(classname, '::Array')
diff --git a/lib/wsdl/soap/classDefCreatorSupport.rb b/lib/wsdl/soap/classDefCreatorSupport.rb
index dcaf7df16..0627b9b62 100644
--- a/lib/wsdl/soap/classDefCreatorSupport.rb
+++ b/lib/wsdl/soap/classDefCreatorSupport.rb
@@ -130,7 +130,7 @@ def create_type_name(modulepath, element)
def dump_inout_type(param, element_definitions)
if param
message = param.find_message
- params = ""
+ params = String.new
message.parts.each do |part|
name = safevarname(part.name)
if part.type
@@ -158,7 +158,7 @@ def dump_inout_type(param, element_definitions)
def dump_inputparam(input)
message = input.find_message
- params = ""
+ params = String.new
message.parts.each do |part|
params << ", " unless params.empty?
params << safevarname(part.name)
diff --git a/lib/wsdl/soap/clientSkeltonCreator.rb b/lib/wsdl/soap/clientSkeltonCreator.rb
index 76cb60180..5d8af865b 100644
--- a/lib/wsdl/soap/clientSkeltonCreator.rb
+++ b/lib/wsdl/soap/clientSkeltonCreator.rb
@@ -31,7 +31,7 @@ def dump(service_name)
unless services
raise RuntimeError.new("service not defined: #{service_name}")
end
- result = ""
+ result = String.new
if @modulepath
result << "\n"
modulepath = @modulepath.respond_to?(:lines) ? @modulepath.lines : @modulepath # RubyJedi: compatible with Ruby 1.8.6 and above
@@ -57,7 +57,7 @@ def dump_porttype(porttype)
assigned_method = collect_assigned_method(@definitions, porttype.name, @modulepath)
drv_name = mapped_class_basename(porttype.name, @modulepath)
- result = ""
+ result = String.new
result << <<__EOD__
endpoint_url = ARGV.shift
obj = #{ drv_name }.new(endpoint_url)
diff --git a/lib/wsdl/soap/driverCreator.rb b/lib/wsdl/soap/driverCreator.rb
index c97ca7d8f..ca300bc4c 100644
--- a/lib/wsdl/soap/driverCreator.rb
+++ b/lib/wsdl/soap/driverCreator.rb
@@ -32,7 +32,7 @@ def initialize(definitions, name_creator, modulepath = nil)
end
def dump(porttype = nil)
- result = "require 'soap/rpc/driver'\n\n"
+ result = String.new("require 'soap/rpc/driver'\n\n")
if @modulepath
modulepath = @modulepath.respond_to?(:lines) ? @modulepath.lines : @modulepath # RubyJedi: compatible with Ruby 1.8.6 and above
modulepath.each do |name|
diff --git a/lib/wsdl/soap/encodedMappingRegistryCreator.rb b/lib/wsdl/soap/encodedMappingRegistryCreator.rb
index e46218aa3..0826934b2 100644
--- a/lib/wsdl/soap/encodedMappingRegistryCreator.rb
+++ b/lib/wsdl/soap/encodedMappingRegistryCreator.rb
@@ -34,7 +34,7 @@ def initialize(definitions, name_creator, modulepath, defined_const)
def dump(varname)
@varname = varname
- result = ''
+ result = String.new
str = dump_complextype
unless str.empty?
result << "\n" unless result.empty?
diff --git a/lib/wsdl/soap/literalMappingRegistryCreator.rb b/lib/wsdl/soap/literalMappingRegistryCreator.rb
index 9f5a0667b..bfd13b05a 100644
--- a/lib/wsdl/soap/literalMappingRegistryCreator.rb
+++ b/lib/wsdl/soap/literalMappingRegistryCreator.rb
@@ -36,7 +36,7 @@ def initialize(definitions, name_creator, modulepath, defined_const)
def dump(varname)
@varname = varname
- result = ''
+ result = String.new
str = dump_complextype
unless str.empty?
result << "\n" unless result.empty?
diff --git a/lib/wsdl/soap/methodDefCreator.rb b/lib/wsdl/soap/methodDefCreator.rb
index 3dc8ca9d1..3b9f9b1db 100644
--- a/lib/wsdl/soap/methodDefCreator.rb
+++ b/lib/wsdl/soap/methodDefCreator.rb
@@ -37,7 +37,7 @@ def initialize(definitions, name_creator, modulepath, defined_const)
end
def dump(name)
- methoddef = ""
+ methoddef = String.new
porttype = @definitions.porttype(name)
binding = porttype.find_binding
if binding
@@ -94,7 +94,7 @@ def dump_method(mdef)
if paramstr.empty?
paramstr = '[]'
else
- paramstr = "[ " << paramstr.split(/\r?\n/).join("\n ") << " ]"
+ paramstr = "[ " + paramstr.split(/\r?\n/).join("\n ") + " ]"
end
definitions = <<__EOD__
#{ndq(mdef.soapaction)},
diff --git a/lib/wsdl/soap/servantSkeltonCreator.rb b/lib/wsdl/soap/servantSkeltonCreator.rb
index 9f4ddd959..eb85d4e79 100644
--- a/lib/wsdl/soap/servantSkeltonCreator.rb
+++ b/lib/wsdl/soap/servantSkeltonCreator.rb
@@ -29,7 +29,7 @@ def initialize(definitions, name_creator, modulepath = nil)
end
def dump(porttype = nil)
- result = ""
+ result = String.new
if @modulepath
result << "\n"
modulepath = @modulepath.respond_to?(:lines) ? @modulepath.lines : @modulepath # RubyJedi: compatible with Ruby 1.8.6 and above
diff --git a/lib/wsdl/xmlSchema/group.rb b/lib/wsdl/xmlSchema/group.rb
index 57ee65d1c..02d782041 100644
--- a/lib/wsdl/xmlSchema/group.rb
+++ b/lib/wsdl/xmlSchema/group.rb
@@ -90,8 +90,9 @@ def parse_attr(attr, value)
end
end
-#private
-
+ # No longer private: classDefCreator.rb and mappingRegistryCreatorSupport.rb
+ # both need to resolve a group ref's actual definition when generating code
+ # for a complexType whose sole content is a .
def refelement
@refelement ||= (@ref ? root.collect_modelgroups[@ref] : nil)
end
diff --git a/lib/wsdl/xmlSchema/importer.rb b/lib/wsdl/xmlSchema/importer.rb
index 2f4743957..34a6214cb 100644
--- a/lib/wsdl/xmlSchema/importer.rb
+++ b/lib/wsdl/xmlSchema/importer.rb
@@ -82,17 +82,9 @@ def web_client
require 'httpclient'
@web_client = HTTPClient
rescue LoadError
- begin
- require 'http-access2'
- if HTTPAccess2::VERSION < "2.0"
- raise LoadError.new("http-access/2.0 or later is required.")
- end
- @web_client = HTTPAccess2::Client
- rescue LoadError
- warn("Loading http-access2 failed. Net/http is used.") if $DEBUG
- require 'soap/netHttpClient'
- @web_client = ::SOAP::NetHttpClient
- end
+ warn("Loading httpclient failed. Net/http is used.") if $DEBUG
+ require 'soap/netHttpClient'
+ @web_client = ::SOAP::NetHttpClient
end
@web_client
end
diff --git a/lib/wsdl/xmlSchema/parser.rb b/lib/wsdl/xmlSchema/parser.rb
index 652564b61..1e8dd14ef 100644
--- a/lib/wsdl/xmlSchema/parser.rb
+++ b/lib/wsdl/xmlSchema/parser.rb
@@ -60,7 +60,7 @@ def initialize(opt = {})
def parse(string_or_readable)
@parsestack = []
@lastnode = nil
- @textbuf = ''
+ @textbuf = String.new
@parser.do_parse(string_or_readable)
@lastnode
end
diff --git a/lib/xsd/charset.rb b/lib/xsd/charset.rb
index e2be28c5b..7a0f88679 100644
--- a/lib/xsd/charset.rb
+++ b/lib/xsd/charset.rb
@@ -128,20 +128,27 @@ def Charset.charset_str(label)
end
end
+ # Regexp::NOENCODING doesn't exist pre-1.9 (part of Ruby's M17N overhaul);
+ # these regexes match raw byte patterns regardless of string encoding, so
+ # on 1.8.7 -- which has no per-string encoding concept at all -- byte
+ # matching is just the default behavior and 0 (no special options) is the
+ # equivalent.
+ NOENCODING_OPT = defined?(Regexp::NOENCODING) ? Regexp::NOENCODING : 0
+
# us_ascii = '[\x00-\x7F]'
us_ascii = '[\x9\xa\xd\x20-\x7F]' # XML 1.0 restricted.
- USASCIIRegexp = Regexp.new("\\A#{us_ascii}*\\z", nil, 'n')
+ USASCIIRegexp = Regexp.new("\\A#{us_ascii}*\\z", NOENCODING_OPT)
twobytes_euc = '(?:[\x8E\xA1-\xFE][\xA1-\xFE])'
threebytes_euc = '(?:\x8F[\xA1-\xFE][\xA1-\xFE])'
character_euc = "(?:#{us_ascii}|#{twobytes_euc}|#{threebytes_euc})"
- EUCRegexp = Regexp.new("\\A#{character_euc}*\\z", nil, 'n')
+ EUCRegexp = Regexp.new("\\A#{character_euc}*\\z", NOENCODING_OPT)
# onebyte_sjis = '[\x00-\x7F\xA1-\xDF]'
onebyte_sjis = '[\x9\xa\xd\x20-\x7F\xA1-\xDF]' # XML 1.0 restricted.
twobytes_sjis = '(?:[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])'
character_sjis = "(?:#{onebyte_sjis}|#{twobytes_sjis})"
- SJISRegexp = Regexp.new("\\A#{character_sjis}*\\z", nil, 'n')
+ SJISRegexp = Regexp.new("\\A#{character_sjis}*\\z", NOENCODING_OPT)
# 0xxxxxxx
# 110yyyyy 10xxxxxx
@@ -152,7 +159,7 @@ def Charset.charset_str(label)
fourbytes_utf8 = '(?:[\xF0-\xF7][\x80-\xBF][\x80-\xBF][\x80-\xBF])'
character_utf8 =
"(?:#{us_ascii}|#{twobytes_utf8}|#{threebytes_utf8}|#{fourbytes_utf8})"
- UTF8Regexp = Regexp.new("\\A#{character_utf8}*\\z", nil, 'n')
+ UTF8Regexp = Regexp.new("\\A#{character_utf8}*\\z", NOENCODING_OPT)
def Charset.is_us_ascii(str)
USASCIIRegexp =~ str
diff --git a/lib/xsd/codegen/classdef.rb b/lib/xsd/codegen/classdef.rb
index ec89d9575..f1be32cdb 100644
--- a/lib/xsd/codegen/classdef.rb
+++ b/lib/xsd/codegen/classdef.rb
@@ -42,7 +42,7 @@ def def_attr(attrname, writable = true, varname = nil)
end
def dump
- buf = ""
+ buf = String.new
unless @requirepath.empty?
buf << dump_requirepath
end
@@ -110,7 +110,7 @@ def dump_classvar
end
def dump_attributes
- str = ""
+ str = String.new
@attrdef.each do |attrname, writable, varname|
varname ||= attrname
if attrname == varname
diff --git a/lib/xsd/codegen/methoddef.rb b/lib/xsd/codegen/methoddef.rb
index eed815ee7..31de6eff6 100644
--- a/lib/xsd/codegen/methoddef.rb
+++ b/lib/xsd/codegen/methoddef.rb
@@ -39,7 +39,7 @@ def initialize(name, *params)
end
def dump
- buf = ""
+ buf = String.new
buf << dump_comment if @comment
buf << dump_method_def
buf << dump_definition if @definition and !@definition.empty?
diff --git a/lib/xsd/codegen/moduledef.rb b/lib/xsd/codegen/moduledef.rb
index be994bd93..4284c0062 100644
--- a/lib/xsd/codegen/moduledef.rb
+++ b/lib/xsd/codegen/moduledef.rb
@@ -67,7 +67,7 @@ def add_method(m, visibility = :public)
end
def dump
- buf = ""
+ buf = String.new
unless @requirepath.empty?
buf << dump_requirepath
end
@@ -141,7 +141,7 @@ def dump_methods
@methoddef.each do |visibility, method|
(methods[visibility] ||= []) << method
end
- str = ""
+ str = String.new
[:public, :protected, :private].each do |visibility|
if methods[visibility]
str << "\n" unless str.empty?
diff --git a/lib/xsd/datatypes.rb b/lib/xsd/datatypes.rb
index a645e7171..85c487dcd 100644
--- a/lib/xsd/datatypes.rb
+++ b/lib/xsd/datatypes.rb
@@ -256,7 +256,7 @@ def nonzero?
def screen_data(d)
if d.is_a?(String)
# Integer("00012") => 10 in Ruby.
- d.sub!(/^([+\-]?)0*(?=\d)/, "\\1")
+ d = d.sub(/^([+\-]?)0*(?=\d)/, "\\1")
end
screen_data_str(d)
end
@@ -480,14 +480,14 @@ def _set(data)
end
def _to_s
- str = ''
+ str = String.new
str << @sign if @sign
str << 'P'
- l = ''
+ l = String.new
l << "#{ @year }Y" if @year.nonzero?
l << "#{ @month }M" if @month.nonzero?
l << "#{ @day }D" if @day.nonzero?
- r = ''
+ r = String.new
r << "#{ @hour }H" if @hour.nonzero?
r << "#{ @min }M" if @min.nonzero?
r << "#{ @sec }S" if @sec.nonzero?
@@ -568,7 +568,7 @@ def of2tz(offset)
if diffmin.zero?
'Z'
else
- ((diffmin < 0) ? '-' : '+') << format('%02d:%02d',
+ ((diffmin < 0) ? '-' : '+') + format('%02d:%02d',
(diffmin.abs / 60.0).to_i, (diffmin.abs % 60.0).to_i)
end
end
@@ -584,10 +584,18 @@ def screen_data(t)
t <<= 12 if t.year < 0
t
elsif t.is_a?(Time)
- jd = DateTime.send(:civil_to_jd, t.year, t.mon, t.mday, DateTime::ITALY)
- fr = DateTime.send(:time_to_day_fraction, t.hour, t.min, [t.sec, 59].min) + t.usec.to_r / DayInMicro
+ # Reached only on Rubies without Time#to_datetime (in practice, 1.8.7
+ # only, since the to_datetime branch above wins on every later
+ # version). The civil_to_jd/jd_to_ajd route below built a Rational
+ # day-fraction and hit the exact same Ruby 1.8.7 DateTime/Rational
+ # arithmetic bug documented in screen_data_str's secfrac handling
+ # below (confirmed: produced a bogus ~4714 BC date). Build via
+ # DateTime.civil directly with the sub-second fraction baked into
+ # the seconds argument instead, avoiding the buggy code path
+ # entirely, same fix as that other call site.
of = t.utc_offset.to_r / DayInSec
- DateTime.new!(DateTime.send(:jd_to_ajd, jd, fr, of), of, DateTime::ITALY)
+ secfrac = t.usec.to_r / SecInMicro
+ DateTime.civil(t.year, t.mon, t.mday, t.hour, t.min, t.sec + secfrac, of)
else
screen_data_str(t)
end
@@ -629,8 +637,18 @@ def screen_data_str(t)
zonestr = $8
data = DateTime.civil(year, mon, mday, hour, min, sec, tz2of(zonestr))
if secfrac
- diffday = secfrac.to_i.to_r / (10 ** secfrac.size) / DayInSec
- data += diffday
+ if RUBY_VERSION.to_f >= 1.9
+ diffday = secfrac.to_i.to_r / (10 ** secfrac.size) / DayInSec
+ data += diffday
+ else
+ # DateTime#+ produces wildly wrong results for certain Rational
+ # values on Ruby 1.8.7's date library (confirmed directly: e.g.
+ # adding Rational(1, 86400000) sends the date to 4763 BC). Baking
+ # the fraction into DateTime.civil's own seconds argument instead
+ # of adding it afterwards avoids the buggy code path entirely.
+ fracval = secfrac.to_i.to_r / (10 ** secfrac.size)
+ data = DateTime.civil(year, mon, mday, hour, min, sec + fracval, tz2of(zonestr))
+ end
# FYI: new! and jd_to_rjd are not necessary to use if you don't have
# exceptional reason.
end
@@ -683,8 +701,18 @@ def screen_data_str(t)
zonestr = $5
data = DateTime.civil(1, 1, 1, hour, min, sec, tz2of(zonestr))
if secfrac
- diffday = secfrac.to_i.to_r / (10 ** secfrac.size) / DayInSec
- data += diffday
+ if RUBY_VERSION.to_f >= 1.9
+ diffday = secfrac.to_i.to_r / (10 ** secfrac.size) / DayInSec
+ data += diffday
+ else
+ # DateTime#+ produces wildly wrong results for certain Rational
+ # values on Ruby 1.8.7's date library (confirmed directly: e.g.
+ # adding Rational(1, 86400000) sends the date to 4763 BC). Baking
+ # the fraction into DateTime.civil's own seconds argument instead
+ # of adding it afterwards avoids the buggy code path entirely.
+ fracval = secfrac.to_i.to_r / (10 ** secfrac.size)
+ data = DateTime.civil(1, 1, 1, hour, min, sec + fracval, tz2of(zonestr))
+ end
end
[data, secfrac]
end
diff --git a/lib/xsd/mapping.rb b/lib/xsd/mapping.rb
index e975303f9..ae0caefb3 100644
--- a/lib/xsd/mapping.rb
+++ b/lib/xsd/mapping.rb
@@ -20,12 +20,12 @@ module XSD
module Mapping
MappingRegistry = SOAP::Mapping::LiteralRegistry.new
- def self.obj2xml(obj, elename = nil, io = nil)
- Mapper.new(MappingRegistry).obj2xml(obj, elename, io)
+ def self.obj2xml(obj, elename = nil, io = nil, options = {})
+ Mapper.new(MappingRegistry).obj2xml(obj, elename, io, options)
end
- def self.xml2obj(stream, klass = nil)
- Mapper.new(MappingRegistry).xml2obj(stream, klass)
+ def self.xml2obj(stream, klass = nil, options = {})
+ Mapper.new(MappingRegistry).xml2obj(stream, klass, options)
end
class Mapper
@@ -39,8 +39,8 @@ def initialize(registry)
@registry = registry
end
- def obj2xml(obj, elename = nil, io = nil)
- opt = MAPPING_OPT.dup
+ def obj2xml(obj, elename = nil, io = nil, options = {})
+ opt = MAPPING_OPT.dup.merge(options)
unless elename
if definition = @registry.elename_schema_definition_from_class(obj.class)
elename = definition.elename
@@ -57,8 +57,9 @@ def obj2xml(obj, elename = nil, io = nil)
generator.generate(soap, io)
end
- def xml2obj(stream, klass = nil)
- parser = SOAP::Parser.new(MAPPING_OPT)
+ def xml2obj(stream, klass = nil, options = {})
+ opt = MAPPING_OPT.dup.merge(options)
+ parser = SOAP::Parser.new(opt)
soap = parser.parse(stream)
SOAP::Mapping.soap2obj(soap, @registry, klass)
end
diff --git a/lib/xsd/qname.rb b/lib/xsd/qname.rb
index 147e66a0f..c29a30abd 100644
--- a/lib/xsd/qname.rb
+++ b/lib/xsd/qname.rb
@@ -33,6 +33,7 @@ def dump(predefined_ns = nil)
end
def match(rhs)
+ return false unless rhs.is_a?(XSD::QName)
if rhs.namespace and (rhs.namespace != @namespace)
return false
end
@@ -43,7 +44,7 @@ def match(rhs)
end
def ==(rhs)
- !rhs.nil? and @namespace == rhs.namespace and @name == rhs.name
+ rhs.is_a?(XSD::QName) and @namespace == rhs.namespace and @name == rhs.name
end
def ===(rhs)
diff --git a/lib/xsd/xmlparser/libxmlparser.rb b/lib/xsd/xmlparser/libxmlparser.rb
index 4716cd7a1..e0c707444 100644
--- a/lib/xsd/xmlparser/libxmlparser.rb
+++ b/lib/xsd/xmlparser/libxmlparser.rb
@@ -6,15 +6,6 @@
# redistribute it and/or modify it under the same terms of Ruby's license;
# either the dual license version in 2003, or any later version.
-### WIP, 2015-June-13:
-###
-### LibXML drops namespaces on Elements *AND* Attributes, which makes it impossible
-### to correctly associate Namespaces on Namespace-Declared Element Attributes when
-### more than one namespace exists.
-###
-### This issue is evident when you run test/soap/test_cookie.rb
-###
-
require 'libxml'
module XSD
@@ -22,113 +13,69 @@ module XMLParser
class LibXMLParser < XSD::XMLParser::Parser
- include ::LibXML::XML::SaxParser::Callbacks
-
+ # Parses via XML::Reader rather than the SaxParser callbacks used before.
+ # libxml-ruby's SAX2 callback (on_start_element_ns) never surfaces an
+ # attribute's own namespace prefix to Ruby: the C extension
+ # (ruby_xml_sax2_handler.c's start_element_ns_callback) only reads an
+ # attribute's local name and value, discarding the prefix/URI slots that
+ # libxml2 itself provides per attribute. That made it impossible to
+ # recognize namespace-qualified attributes such as xsi:type, xsi:nil, and
+ # xml:lang, which SOAP4R's demarshaller depends on to pick the right Ruby
+ # type -- values silently fell back to an untyped SOAP::Mapping::Object,
+ # or stayed raw strings instead of being cast to Integer/nil/etc. This gap
+ # is still present as of libxml-ruby 6.0.0 (2026); it isn't a matter of
+ # being on an old release.
+ #
+ # XML::Reader's per-attribute #name *does* return the full "prefix:local"
+ # form (confirmed against libxml-ruby 2.8.0, the version pinned for Ruby
+ # 1.9.3, through the current 3.x/6.x releases) -- matching exactly what
+ # REXML's tag_start and Ox's attr callbacks already hand back, so this
+ # mirrors their convention rather than trying to resolve namespace URIs
+ # itself.
def do_parse(string_or_readable)
- $stderr.puts "XSD::XMLParser::LibXMLParser.do_parse" if $DEBUG
- # string = string_or_readable.respond_to?(:read) ? string_or_readable.read : string_or_readable
-
+ $stderr.puts "XSD::XMLParser::LibXMLParser.do_parse" if $DEBUG
@charset = 'utf-8'
- string = StringIO.new(string_or_readable)
- parser = LibXML::XML::SaxParser.io(string)
- parser.callbacks = self
- parser.parse
- end
-
- ENTITY_REF_MAP = {
- 'lt' => '<',
- 'gt' => '>',
- 'amp' => '&',
- 'quot' => '"',
- 'apos' => '\''
- }
-
- #def on_internal_subset(name, external_id, system_id)
- # nil
- #end
-
- #def on_is_standalone()
- # nil
- #end
-
- #def on_has_internal_subset()
- # nil
- #end
-
- #def on_has_external_subset()
- # nil
- #end
-
- #def on_start_document()
- # nil
- #end
-
- #def on_end_document()
- # nil
- #end
-
- def on_start_element_ns (name, attr_hash, prefix, uri, namespaces)
- prefixed_ns = attr_hash.merge(Hash[namespaces.map{|k,v| ["xmlns:#{k}",v]}])
- if prefix.nil?
- start_element(name, prefixed_ns)
- else
- start_element("#{prefix}:#{name}", prefixed_ns)
+ string = string_or_readable.respond_to?(:read) ? string_or_readable.read : string_or_readable
+ reader = ::LibXML::XML::Reader.string(string)
+ while reader.read
+ case reader.node_type
+ when ::LibXML::XML::Reader::TYPE_ELEMENT
+ name = reader.name
+ attrs = read_attributes(reader)
+ empty = reader.empty_element?
+ start_element(name, attrs)
+ end_element(name) if empty
+ when ::LibXML::XML::Reader::TYPE_END_ELEMENT
+ end_element(reader.name)
+ when ::LibXML::XML::Reader::TYPE_TEXT,
+ ::LibXML::XML::Reader::TYPE_CDATA,
+ ::LibXML::XML::Reader::TYPE_SIGNIFICANT_WHITESPACE
+ characters(reader.value)
+ end
end
+ rescue ::LibXML::XML::Error => e
+ raise ParseError.new(e.message)
end
- def on_end_element_ns (name, prefix, uri)
- if prefix.nil?
- end_element(name)
- else
- end_element("#{prefix}:#{name}")
- end
- end
-
- def on_start_element (name, attr_hash)
- # start_element(name, attr_hash)
- end
-
- def on_end_element(name)
- # end_element(name)
- end
-
- def on_reference(name)
- characters(ENTITY_REF_MAP[name])
- end
-
- def on_characters(chars)
- characters(chars)
- end
-
- #def on_processing_instruction(target, data)
- # nil
- #end
-
- #def on_comment(msg)
- # nil
- #end
-
- def on_parser_warning(msg)
- warn(msg)
- end
-
- def on_parser_error(msg)
- raise ParseError.new(msg)
- end
-
- def on_parser_fatal_error(msg)
- raise ParseError.new(msg)
- end
-
- def on_cdata_block(cdata)
- characters(cdata)
- end
+ add_factory(self)
- def on_external_subset(name, external_id, system_id)
- nil
+ private
+
+ # Attribute names come back in literal "prefix:local" form already (see
+ # do_parse comment above), including xmlns:* declarations -- which are
+ # themselves just attributes as far as the reader is concerned, so no
+ # separate namespace-merging step is needed.
+ def read_attributes(reader)
+ attrs = {}
+ return attrs unless reader.has_attributes?
+ if reader.move_to_first_attribute == 1
+ begin
+ attrs[reader.name] = reader.value
+ end while reader.move_to_next_attribute == 1
+ reader.move_to_element
+ end
+ attrs
end
-
- add_factory(self)
end
diff --git a/lib/xsd/xmlparser/oxparser.rb b/lib/xsd/xmlparser/oxparser.rb
index 79cc384b0..ad03d3908 100644
--- a/lib/xsd/xmlparser/oxparser.rb
+++ b/lib/xsd/xmlparser/oxparser.rb
@@ -23,12 +23,18 @@ def do_parse(string_or_readable)
handler = OxDocHandler.new(self, @decoder)
string = string_or_readable.respond_to?(:read) ? string_or_readable.read : StringIO.new(string_or_readable)
+ # :skip_none regardless of decoder: the old no-decoder default
+ # (:skip_return) discarded \r entirely, breaking CRLF round-tripping
+ # (see test_string_crlf) whenever htmlentities wasn't installed.
if @decoder.nil?
- # Use the built-in conversion with Ox.
- ::Ox.sax_parse(handler, string, {:symbolize=> false, :convert_special=> true, :skip=> :skip_return} )
+ # Ox's own :convert_special decodes the full named-entity set, but this
+ # path segfaults on complex documents under Ox 2.14.14 (the version
+ # Ruby 2.2.x-2.6.x are stuck on) -- htmlentities is required there
+ # specifically to avoid this branch entirely (see Gemfile).
+ ::Ox.sax_parse(handler, string, {:symbolize=> false, :convert_special=> true, :skip=> :skip_none} )
else
- # Use HTMLEntities Decoder. Leave the special-character conversion alone and let HTMLEntities decode it for us.
- ::Ox.sax_parse(handler, string, {})
+ # Let HTMLEntities decode instead; leave Ox's own conversion off.
+ ::Ox.sax_parse(handler, string, {:skip=> :skip_none})
end
end
diff --git a/soap4r-ng.gemspec b/soap4r-ng.gemspec
index 7ceebb297..36d8936d6 100644
--- a/soap4r-ng.gemspec
+++ b/soap4r-ng.gemspec
@@ -10,16 +10,36 @@ Gem::Specification.new do |s|
s.homepage = "http://rubyjedi.github.io/soap4r/"
s.license = "Ruby"
- s.summary = "Soap4R-ng - Soap4R (as maintained by RubyJedi) for Ruby 1.8 thru 2.1 and beyond"
- s.description = "Soap4R NextGen (as maintained by RubyJedi) for Ruby 1.8 thru 2.1 and beyond"
+ s.summary = "Soap4R-ng - Soap4R (as maintained by RubyJedi) for Ruby 1.8.7 thru 4.0 and beyond"
+ s.description = "Soap4R NextGen (as maintained by RubyJedi) for Ruby 1.8.7 thru 4.0 and beyond"
- s.add_dependency("httpclient", "~> 2.6")
- s.add_dependency("logger-application", "~> 0.0.2")
-
- s.has_rdoc = false # disable rdoc generation until we've got more
s.requirements << 'none'
s.require_path = 'lib'
- s.files = `git ls-files lib bin`.split("\n")
+ # Not `git ls-files` -- that silently returns empty (with only a stderr
+ # warning) when built outside a git checkout (vendored copy, downloaded
+ # tarball/zip, `bundle package`), which drops the entire lib/ directory
+ # from the packaged gem with no build failure to notice it by.
+ s.files = Dir.glob('{lib,bin}/**/*')
s.executables = [ "wsdl2ruby.rb", "xsd2ruby.rb" ]
+
+ # Required unconditionally by the shipped bin/ executables (both
+ # httpclient and logger-application) -- without these declared, `gem
+ # install soap4r-ng` (which never reads this project's own Gemfile)
+ # installs a gem whose executables immediately LoadError. This was
+ # reported and reproduced live against current master: GH #23.
+ #
+ # rexml/webrick/logger are deliberately NOT declared here even though
+ # lib/soap/rpc/*.rb needs them on Ruby >= 3.0/4.0: this gemspec's
+ # dependency list is static (baked in once at gem-build time), so it
+ # can't replicate the Gemfile's `if RUBY_VERSION.to_f >= 3.0` gating.
+ # Confirmed empirically that adding them unconditionally breaks Ruby
+ # 1.9.3 -- Bundler merges this gemspec's deps into any Gemfile using
+ # `gem 'soap4r-ng', :path=>'.'`, bypassing the Gemfile's own version
+ # gate entirely and resolving current rexml/webrick/logger releases
+ # (**kwargs syntax) that are a SyntaxError on that old a Ruby. Bundler
+ # users on Ruby >= 3.0/4.0 must add rexml/webrick/logger to their own
+ # Gemfile, exactly as documented in this project's README.
+ s.add_runtime_dependency 'httpclient'
+ s.add_runtime_dependency 'logger-application'
end
diff --git a/test/helper.rb b/test/helper.rb
index 499a4b02c..c0e259422 100644
--- a/test/helper.rb
+++ b/test/helper.rb
@@ -1,12 +1,31 @@
# encoding: UTF-8
+
+# On Ruby < 2.3, Thread::Mutex is not a real, cached constant -- every
+# reference to it re-runs an internal deprecated-constant bridge (resolving
+# via the real toplevel ::Mutex) and prints a warning, every single time,
+# not just once. httpclient depends on the standalone 'mutex_m' gem (unlike
+# Ruby's own bundled mutex_m.rb, which uses bare Mutex and never hits this),
+# and that gem's Mutex_m#mu_initialize references Thread::Mutex on every
+# single object construction (e.g. every Logger.new) -- confirmed this was
+# responsible for ~2900 warning lines in one CI run across just these 4
+# Ruby versions. Defining it for real, once, before anything else runs,
+# makes every later reference an ordinary constant lookup with no bridge
+# and no warning.
+if RUBY_VERSION.to_f > 1.8 && defined?(::Mutex) && !Thread.const_defined?(:Mutex, false)
+ Thread.const_set(:Mutex, ::Mutex)
+end
+
require 'test/unit'
require 'test/unit/xml' ## RubyJedi
if RUBY_VERSION.to_f >= 1.9
- require "codeclimate-test-reporter"
- CodeClimate::TestReporter.start
+ require 'simplecov'
+ SimpleCov.start
end
ENV['DEBUG_SOAP4R'] = 'true' ## Needed to force wsdl2ruby.rb and xsd2ruby.rb to use DEVELOPMENT soap4r libs instead of installed soap4r libs
$DEBUG = !!ENV['WIREDUMPS']
+# see https://bugs.ruby-lang.org/issues/13181 & https://github.com/ruby/ruby/commit/86bfcc2da0
+RUBY_GEM_VERSION = Gem::Version.new(RUBY_VERSION.dup) # .dup: RUBY_VERSION is frozen, and old RubyGems' Version#initialize mutates its argument in place
+RESCUE_LINE_NUMBERS_FIXED = (RUBY_GEM_VERSION >= Gem::Version.new('2.4.3')) || (RUBY_GEM_VERSION >= Gem::Version.new('2.3.6') && RUBY_GEM_VERSION < Gem::Version.new('2.4.0'))
diff --git a/test/soap/asp.net/test_aspdotnet.rb b/test/soap/asp.net/test_aspdotnet.rb
index 8c4491c6e..5451b25e2 100644
--- a/test/soap/asp.net/test_aspdotnet.rb
+++ b/test/soap/asp.net/test_aspdotnet.rb
@@ -49,8 +49,14 @@ def setup_server
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def test_document_method
@@ -69,7 +75,7 @@ def test_xml
XSD::QName.new(Server::Namespace, 'SayHello'),
XSD::QName.new(Server::Namespace, 'SayHelloResponse'))
require 'rexml/document'
- xml = <<__XML__
+ xml = (<<__XML__).dup
Mike
@@ -88,10 +94,8 @@ def test_aspdotnethandler
assert_equal("Hello Mike", @client.sayHello("Mike"))
end
- if defined?(HTTPClient)
-
- # qualified!
- REQUEST_ASPDOTNETHANDLER =
+ # qualified!
+ REQUEST_ASPDOTNETHANDLER =
%q[
]
- def test_aspdotnethandler_envelope
- @client = SOAP::RPC::Driver.new(Endpoint, Server::Namespace)
- @client.wiredump_dev = str = ''
- @client.add_method_with_soapaction('sayHello', Server::Namespace + 'SayHello', 'name')
- @client.default_encodingstyle = SOAP::EncodingStyle::ASPDotNetHandler::Namespace
- assert_equal("Hello Mike", @client.sayHello("Mike"))
- assert_xml_equal(REQUEST_ASPDOTNETHANDLER, parse_requestxml(str),
- [REQUEST_ASPDOTNETHANDLER, parse_requestxml(str)].join("\n\n"))
- end
-
- def parse_requestxml(str)
- str.split(/\r?\n\r?\n/)[3]
- end
+ def test_aspdotnethandler_envelope
+ @client = SOAP::RPC::Driver.new(Endpoint, Server::Namespace)
+ @client.wiredump_dev = str = String.new
+ @client.add_method_with_soapaction('sayHello', Server::Namespace + 'SayHello', 'name')
+ @client.default_encodingstyle = SOAP::EncodingStyle::ASPDotNetHandler::Namespace
+ assert_equal("Hello Mike", @client.sayHello("Mike"))
+ assert_xml_equal(REQUEST_ASPDOTNETHANDLER, parse_requestxml(str),
+ [REQUEST_ASPDOTNETHANDLER, parse_requestxml(str)].join("\n\n"))
+ end
+ def parse_requestxml(str)
+ TestUtil.parse_wiredump_request_body(str)
end
end
diff --git a/test/soap/auth/test_basic.rb b/test/soap/auth/test_basic.rb
index e98bde229..a61ebc874 100644
--- a/test/soap/auth/test_basic.rb
+++ b/test/soap/auth/test_basic.rb
@@ -26,13 +26,13 @@ def setup
end
def teardown
- teardown_client if @server
+ teardown_client if @client
teardown_proxyserver if @proxyserver
- teardown_server if @client
+ teardown_server if @server
end
def setup_server
- @server = WEBrick::HTTPServer.new(
+ @server = TestUtil.webrick_http_server(
:BindAddress => "0.0.0.0",
:Logger => @logger,
:Port => Port,
@@ -50,11 +50,12 @@ def setup_server
'/',
WEBrick::HTTPServlet::ProcHandler.new(method(:do_server_proc).to_proc)
)
+
@server_thread = TestUtil.start_server_thread(@server)
end
def setup_proxyserver
- @proxyserver = WEBrick::HTTPProxyServer.new(
+ @proxyserver = TestUtil.webrick_http_server(
:BindAddress => "0.0.0.0",
:Logger => @logger,
:Port => ProxyPort,
@@ -70,14 +71,26 @@ def setup_client
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def teardown_proxyserver
@proxyserver.shutdown
- @proxyserver_thread.kill
- @proxyserver_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @proxyserver_thread.join(10)
+ @proxyserver_thread.kill
+ @proxyserver_thread.join
+ end
end
def teardown_client
@@ -100,18 +113,37 @@ def do_server_proc(req, res)
end
def test_direct
+ return unless auth_supported?
@client.wiredump_dev = STDOUT if $DEBUG
@client.options["protocol.http.auth"] << [@url, "admin", "admin"]
assert_equal("OK", @client.do_server_proc)
end
def test_proxy
+ return unless auth_supported?
setup_proxyserver
@client.wiredump_dev = STDOUT if $DEBUG
@client.options["protocol.http.proxy"] = @proxyurl
@client.options["protocol.http.auth"] << [@url, "guest", "guest"]
assert_equal("OK", @client.do_server_proc)
end
+
+ private
+
+ # SOAP::NetHttpClient#set_auth and SOAP::FaradayClient#set_auth both
+ # explicitly raise NotImplementedError -- neither soap4r + net/http nor
+ # soap4r + faraday (whose core has no bundled challenge-response
+ # middleware) support WWW-Authenticate-style auth. This is a real,
+ # permanent limitation for those backends, not a bug, so skip cleanly
+ # rather than fail (see lib/soap/httpbackend.rb;
+ # test_streamhandler.rb's test_basic_auth uses the same pattern).
+ # SOAP::CurbClient DOES support this (libcurl negotiates the challenge
+ # itself), so it's deliberately absent from this list.
+ AUTH_UNSUPPORTED_BACKENDS = %w[SOAP::NetHttpClient SOAP::FaradayClient]
+
+ def auth_supported?
+ !AUTH_UNSUPPORTED_BACKENDS.include?(SOAP::HTTPStreamHandler::Client.name)
+ end
end
diff --git a/test/soap/auth/test_digest.rb b/test/soap/auth/test_digest.rb
index 335af870e..e73ee6262 100644
--- a/test/soap/auth/test_digest.rb
+++ b/test/soap/auth/test_digest.rb
@@ -32,7 +32,7 @@ def teardown
end
def setup_server
- @server = WEBrick::HTTPServer.new(
+ @server = TestUtil.webrick_http_server(
:BindAddress => "0.0.0.0",
:Logger => @logger,
:Port => Port,
@@ -55,7 +55,7 @@ def setup_server
end
def setup_proxyserver
- @proxyserver = WEBrick::HTTPProxyServer.new(
+ @proxyserver = TestUtil.webrick_proxy_server(
:BindAddress => "0.0.0.0",
:Logger => @logger,
:Port => ProxyPort,
@@ -71,14 +71,26 @@ def setup_client
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def teardown_proxyserver
@proxyserver.shutdown
- @proxyserver_thread.kill
- @proxyserver_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @proxyserver_thread.join(10)
+ @proxyserver_thread.kill
+ @proxyserver_thread.join
+ end
end
def teardown_client
@@ -101,18 +113,37 @@ def do_server_proc(req, res)
end
def test_direct
+ return unless auth_supported?
@client.wiredump_dev = STDOUT if $DEBUG
@client.options["protocol.http.auth"] << [@url, "admin", "admin"]
assert_equal("OK", @client.do_server_proc)
end
def test_proxy
+ return unless auth_supported?
setup_proxyserver
@client.wiredump_dev = STDOUT if $DEBUG
@client.options["protocol.http.proxy"] = @proxyurl
@client.options["protocol.http.auth"] << [@url, "guest", "guest"]
assert_equal("OK", @client.do_server_proc)
end
+
+ private
+
+ # SOAP::NetHttpClient#set_auth and SOAP::FaradayClient#set_auth both
+ # explicitly raise NotImplementedError -- neither soap4r + net/http nor
+ # soap4r + faraday (whose core has no bundled challenge-response
+ # middleware) support WWW-Authenticate-style auth. This is a real,
+ # permanent limitation for those backends, not a bug, so skip cleanly
+ # rather than fail (see lib/soap/httpbackend.rb;
+ # test_streamhandler.rb's test_basic_auth uses the same pattern).
+ # SOAP::CurbClient DOES support this (libcurl negotiates the challenge
+ # itself), so it's deliberately absent from this list.
+ AUTH_UNSUPPORTED_BACKENDS = %w[SOAP::NetHttpClient SOAP::FaradayClient]
+
+ def auth_supported?
+ !AUTH_UNSUPPORTED_BACKENDS.include?(SOAP::HTTPStreamHandler::Client.name)
+ end
end
diff --git a/test/soap/calc/test_calc.rb b/test/soap/calc/test_calc.rb
index a6a777961..a806bd3f9 100644
--- a/test/soap/calc/test_calc.rb
+++ b/test/soap/calc/test_calc.rb
@@ -29,8 +29,14 @@ def setup
def teardown
@server.shutdown if @server
if @t
- @t.kill
- @t.join
+ # join with a bound, falling back to kill only if genuinely
+ # stuck (see git history: unconditional immediate kill raced
+ # WEBrick's own async listener cleanup and occasionally leaked
+ # the port).
+ unless @t.join(10)
+ @t.kill
+ @t.join
+ end
end
@calc.reset_stream if @calc
end
diff --git a/test/soap/calc/test_calc2.rb b/test/soap/calc/test_calc2.rb
index 784863eed..5fe2bd52c 100644
--- a/test/soap/calc/test_calc2.rb
+++ b/test/soap/calc/test_calc2.rb
@@ -15,10 +15,7 @@ class TestCalc2 < Test::Unit::TestCase
def setup
@server = CalcServer2.new('CalcServer', 'http://tempuri.org/calcService', '0.0.0.0', Port)
@server.level = Logger::Severity::ERROR
- @t = Thread.new {
- Thread.current.abort_on_exception = true
- @server.start
- }
+ @t = TestUtil.start_server_thread(@server)
@endpoint = "http://localhost:#{Port}/"
@var = SOAP::RPC::Driver.new(@endpoint, 'http://tempuri.org/calcService')
@var.wiredump_dev = STDERR if $DEBUG
@@ -33,8 +30,14 @@ def setup
def teardown
@server.shutdown if @server
if @t
- @t.kill
- @t.join
+ # join with a bound, falling back to kill only if genuinely
+ # stuck (see git history: unconditional immediate kill raced
+ # WEBrick's own async listener cleanup and occasionally leaked
+ # the port).
+ unless @t.join(10)
+ @t.kill
+ @t.join
+ end
end
@var.reset_stream if @var
end
diff --git a/test/soap/calc/test_calc_cgi.rb b/test/soap/calc/test_calc_cgi.rb
index 646819935..c45524e49 100644
--- a/test/soap/calc/test_calc_cgi.rb
+++ b/test/soap/calc/test_calc_cgi.rb
@@ -18,13 +18,16 @@ class TestCalcCGI < Test::Unit::TestCase
)
RUBYBIN << " -d" if $DEBUG
- if RUBY_VERSION.to_f >= 2.2
- logger_gem = Gem::Specification.find { |s| s.name == 'logger-application' }
- if logger_gem
- logger_gem.load_paths.each do |path|
- RUBYBIN << " -I #{path}"
- end
- end
+ # See test/soap/header/test_authheader_cgi.rb for why this needs to run
+ # unconditionally (Ruby 1.8.7's ancient RubyGems has no
+ # Gem::Specification.find, hence the $LOAD_PATH-based lookup instead).
+ # 'logger' added alongside webrick/logger-application for the same reason:
+ # lib/soap/rpc/cgistub.rb requires it too, and on Ruby >= 4.0 it's also
+ # been demoted from stdlib to a real gem the CGI child can't find once its
+ # ENV is wiped.
+ ['logger-application', 'webrick', 'logger'].each do |feature|
+ dir = $LOAD_PATH.find { |path| File.exist?(File.join(path, "#{feature}.rb")) }
+ RUBYBIN << " -I #{dir}" if dir
end
Port = 17171
@@ -32,7 +35,7 @@ class TestCalcCGI < Test::Unit::TestCase
def setup
logger = Logger.new(STDERR)
logger.level = Logger::Severity::ERROR
- @server = WEBrick::HTTPServer.new(
+ @server = TestUtil.webrick_http_server(
:BindAddress => "0.0.0.0",
:Logger => logger,
:Port => Port,
@@ -41,10 +44,7 @@ def setup
:CGIPathEnv => ENV['PATH'],
:CGIInterpreter => RUBYBIN
)
- @t = Thread.new {
- Thread.current.abort_on_exception = true
- @server.start
- }
+ @t = TestUtil.start_server_thread(@server)
@endpoint = "http://localhost:#{Port}/server.cgi"
@calc = SOAP::RPC::Driver.new(@endpoint, 'http://tempuri.org/calcService')
@calc.wiredump_dev = STDERR if $DEBUG
@@ -57,8 +57,14 @@ def setup
def teardown
@server.shutdown if @server
if @t
- @t.kill
- @t.join
+ # join with a bound, falling back to kill only if genuinely
+ # stuck (see git history: unconditional immediate kill raced
+ # WEBrick's own async listener cleanup and occasionally leaked
+ # the port).
+ unless @t.join(10)
+ @t.kill
+ @t.join
+ end
end
@calc.reset_stream if @calc
end
diff --git a/test/soap/fault/test_customfault.rb b/test/soap/fault/test_customfault.rb
index e59f0d9d3..3c006da6f 100644
--- a/test/soap/fault/test_customfault.rb
+++ b/test/soap/fault/test_customfault.rb
@@ -26,10 +26,7 @@ def fault(msg)
def setup
@server = CustomFaultServer.new('customfault', 'urn:customfault', '0.0.0.0', Port)
@server.level = Logger::Severity::ERROR
- @t = Thread.new {
- Thread.current.abort_on_exception = true
- @server.start
- }
+ @t = TestUtil.start_server_thread(@server)
@endpoint = "http://localhost:#{Port}/"
@client = SOAP::RPC::Driver.new(@endpoint, 'urn:customfault')
@client.wiredump_dev = STDERR if $DEBUG
@@ -39,8 +36,14 @@ def setup
def teardown
@server.shutdown if @server
if @t
- @t.kill
- @t.join
+ # join with a bound, falling back to kill only if genuinely
+ # stuck (see git history: unconditional immediate kill raced
+ # WEBrick's own async listener cleanup and occasionally leaked
+ # the port).
+ unless @t.join(10)
+ @t.kill
+ @t.join
+ end
end
@client.reset_stream if @client
end
diff --git a/test/soap/filter/test_filter.rb b/test/soap/filter/test_filter.rb
index 0b89c018c..11d424760 100644
--- a/test/soap/filter/test_filter.rb
+++ b/test/soap/filter/test_filter.rb
@@ -95,8 +95,14 @@ def teardown
def teardown_server
@server.shutdown
- @t.kill
- @t.join
+ # join with a bound, falling back to kill only if genuinely
+ # stuck (see git history: unconditional immediate kill raced
+ # WEBrick's own async listener cleanup and occasionally leaked
+ # the port).
+ unless @t.join(10)
+ @t.kill
+ @t.join
+ end
end
def teardown_client
diff --git a/test/soap/header/test_authheader.rb b/test/soap/header/test_authheader.rb
index 50c39ca92..a26170b29 100644
--- a/test/soap/header/test_authheader.rb
+++ b/test/soap/header/test_authheader.rb
@@ -179,8 +179,14 @@ def teardown
def teardown_server
@server.shutdown
- @t.kill
- @t.join
+ # join with a bound, falling back to kill only if genuinely
+ # stuck (see git history: unconditional immediate kill raced
+ # WEBrick's own async listener cleanup and occasionally leaked
+ # the port).
+ unless @t.join(10)
+ @t.kill
+ @t.join
+ end
end
def teardown_client
diff --git a/test/soap/header/test_authheader_cgi.rb b/test/soap/header/test_authheader_cgi.rb
index b55f77b61..c78bc5cb9 100644
--- a/test/soap/header/test_authheader_cgi.rb
+++ b/test/soap/header/test_authheader_cgi.rb
@@ -20,13 +20,24 @@ class TestAuthHeaderCGI < Test::Unit::TestCase
)
RUBYBIN << " -d" if $DEBUG
- if RUBY_VERSION.to_f >= 2.2
- logger_gem = Gem::Specification.find { |s| s.name == 'logger-application' }
- if logger_gem
- logger_gem.load_paths.each do |path|
- RUBYBIN << " -I #{path}"
- end
- end
+ # WEBrick::HTTPServlet::CGIHandler wipes the CGI child's entire ENV
+ # before exec'ing it (cgi_runner.rb: `ENV.keys.each{|name| ENV.delete(name)}`,
+ # standard CGI hygiene) -- so GEM_HOME/GEM_PATH/BUNDLE_GEMFILE never
+ # reach the spawned script, and any gem it needs must be forwarded via
+ # a raw -I load-path flag instead (bypasses RubyGems activation
+ # entirely, so it's immune to the ENV wipe). logger-application always
+ # needed this; webrick needs the same treatment now that Ruby 3.0+
+ # demoted it from stdlib to a real gem, and logger needs it too now that
+ # Ruby 4.0+ has done the same.
+ #
+ # Uses $LOAD_PATH (not Gem::Specification.find, which doesn't exist on
+ # Ruby 1.8.7's ancient bundled RubyGems, and not $LOADED_FEATURES, which
+ # stores bare relative filenames like "webrick.rb" on 1.8.7 instead of
+ # absolute paths) to find each feature's actual directory -- this works
+ # identically on every supported Ruby version, gem or stdlib alike.
+ ['logger-application', 'webrick', 'logger'].each do |feature|
+ dir = $LOAD_PATH.find { |path| File.exist?(File.join(path, "#{feature}.rb")) }
+ RUBYBIN << " -I #{dir}" if dir
end
Port = 17171
@@ -69,7 +80,7 @@ def setup_server
@endpoint = "http://localhost:#{Port}/server.cgi"
logger = Logger.new(STDERR)
logger.level = Logger::Severity::ERROR
- @server = WEBrick::HTTPServer.new(
+ @server = TestUtil.webrick_http_server(
:BindAddress => "0.0.0.0",
:Logger => logger,
:Port => Port,
@@ -78,10 +89,7 @@ def setup_server
:CGIPathEnv => ENV['PATH'],
:CGIInterpreter => RUBYBIN
)
- @t = Thread.new {
- Thread.current.abort_on_exception = true
- @server.start
- }
+ @t = TestUtil.start_server_thread(@server)
end
def setup_client
@@ -95,14 +103,25 @@ def setup_client
def teardown
@supportclient.delete_sessiondb if @supportclient
+ ensure
+ # Must run even if delete_sessiondb above raises (e.g. the CGI child
+ # process failing to start) -- otherwise @server is orphaned still
+ # listening on Port, and every later test file sharing that same fixed
+ # port fails with EADDRINUSE for the rest of the run.
teardown_server if @server
teardown_client if @client
end
def teardown_server
@server.shutdown
- @t.kill
- @t.join
+ # join with a bound, falling back to kill only if genuinely
+ # stuck (see git history: unconditional immediate kill raced
+ # WEBrick's own async listener cleanup and occasionally leaked
+ # the port).
+ unless @t.join(10)
+ @t.kill
+ @t.join
+ end
end
def teardown_client
diff --git a/test/soap/header/test_simplehandler.rb b/test/soap/header/test_simplehandler.rb
index 8c8b1bf63..cf717fb27 100644
--- a/test/soap/header/test_simplehandler.rb
+++ b/test/soap/header/test_simplehandler.rb
@@ -96,8 +96,14 @@ def teardown
def teardown_server
@server.shutdown
- @t.kill
- @t.join
+ # join with a bound, falling back to kill only if genuinely
+ # stuck (see git history: unconditional immediate kill raced
+ # WEBrick's own async listener cleanup and occasionally leaked
+ # the port).
+ unless @t.join(10)
+ @t.kill
+ @t.join
+ end
end
def teardown_client
diff --git a/test/soap/helloworld/test_helloworld.rb b/test/soap/helloworld/test_helloworld.rb
index d8e98788b..bf7d80ba8 100644
--- a/test/soap/helloworld/test_helloworld.rb
+++ b/test/soap/helloworld/test_helloworld.rb
@@ -15,10 +15,7 @@ class TestHelloWorld < Test::Unit::TestCase
def setup
@server = HelloWorldServer.new('hws', 'urn:hws', '0.0.0.0', Port)
@server.level = Logger::Severity::ERROR
- @t = Thread.new {
- Thread.current.abort_on_exception = true
- @server.start
- }
+ @t = TestUtil.start_server_thread(@server)
@endpoint = "http://localhost:#{Port}/"
@client = SOAP::RPC::Driver.new(@endpoint, 'urn:hws')
@client.wiredump_dev = STDERR if $DEBUG
@@ -28,8 +25,14 @@ def setup
def teardown
@server.shutdown if @server
if @t
- @t.kill
- @t.join
+ # join with a bound, falling back to kill only if genuinely
+ # stuck (see git history: unconditional immediate kill raced
+ # WEBrick's own async listener cleanup and occasionally leaked
+ # the port).
+ unless @t.join(10)
+ @t.kill
+ @t.join
+ end
end
@client.reset_stream if @client
end
diff --git a/test/soap/marshal/marshaltestlib.rb b/test/soap/marshal/marshaltestlib.rb
index d0493b31c..ea6c10d8d 100644
--- a/test/soap/marshal/marshaltestlib.rb
+++ b/test/soap/marshal/marshaltestlib.rb
@@ -16,6 +16,34 @@ module MarshalTestLib
module Mod1; end
module Mod2; end
+ def freezing_time(t=Time.now)
+ t.class.singleton_class.send :alias_method, :orig_now, :now
+ t.class.define_singleton_method(:now) { t }
+
+ yield(t)
+
+ t.class.singleton_class.send :alias_method, :now, :orig_now
+ t.class.singleton_class.send :undef_method, :orig_now
+ end
+
+ def without_did_you_mean!(exception)
+ # cannot map DidYouMean::* classes (with Proc)
+ if exception.instance_variable_defined? :@spell_checker
+ exception.instance_eval { remove_instance_variable(:@spell_checker) }
+ end
+ if exception.respond_to?(:corrections)
+ exception.singleton_class.send :alias_method, :orig_corrections, :corrections
+ exception.singleton_class.send :undef_method, :corrections
+ end
+
+ yield(exception)
+
+ if exception.respond_to?(:orig_corrections)
+ exception.singleton_class.send :alias_method, :corrections, :orig_corrections
+ exception.singleton_class.send :undef_method, :orig_corrections
+ end
+ end
+
def marshaltest(o1)
str = encode(o1)
print str, "\n" if $DEBUG
@@ -110,7 +138,10 @@ def test_array_ivar
class MyException < Exception; def initialize(v, *args) super(*args); @v = v; end; attr_reader :v; end
def test_exception
marshal_equal(Exception.new('foo')) {|o| o.message}
- marshal_equal(assert_raise(NoMethodError) {no_such_method()}) {|o| o.message}
+
+ without_did_you_mean!(assert_raise(NoMethodError) {no_such_method()}) do |exception|
+ marshal_equal(exception) {|o| o.message}
+ end
end
def test_exception_subclass
@@ -284,7 +315,7 @@ def test_string
end
def test_string_ivar
- o1 = ""
+ o1 = String.new
o1.instance_eval { @iv = 1 }
marshal_equal(o1) {|o| o.instance_eval { @iv }}
end
@@ -303,7 +334,7 @@ def test_string_subclass_cycle
end
def test_string_subclass_extend
- o = "abc"
+ o = String.new("abc")
o.extend(Mod1)
str = MyString.new(o, "c")
marshal_equal(str) { |o|
@@ -393,21 +424,22 @@ def test_symbol
class MyTime < Time; def initialize(v, *args) super(*args); @v = v; end end
def test_time
- # once there was a bug caused by usec overflow. try a little harder.
- 10.times do
- t = Time.now
+ freezing_time do |t|
marshal_equal(t,t.usec.to_s) {|t| t.tv_usec }
end
end
def test_time_subclass
- marshal_equal(MyTime.new(10)) {|t| t.tv_usec }
+ freezing_time(MyTime.new(10)) do |t|
+ marshal_equal(t) {|t| t.tv_usec }
+ end
end
def test_time_ivar
- o1 = Time.now
- o1.instance_eval { @iv = 1 }
- marshal_equal(o1) {|o| o.instance_eval { @iv }}
+ freezing_time do |t|
+ t.instance_eval { @iv = 1 }
+ marshal_equal(t) {|o| o.instance_eval { @iv }}
+ end
end
def test_true
@@ -470,10 +502,10 @@ def test_extend
end
def test_extend_string
- o = ""
+ o = String.new
o.extend Mod1
marshal_equal(o) { |obj| obj.kind_of? Mod1 }
- o = ""
+ o = String.new
o.extend Mod1
o.extend Mod2
if RUBY_VERSION.to_f >= 2.1
@@ -481,7 +513,7 @@ def test_extend_string
else
marshal_equal(o) {|obj| class << obj; ancestors end}
end
- o = ""
+ o = String.new
o.extend Module.new
assert_raise(TypeError) { marshaltest(o) }
end
diff --git a/test/soap/ssl/README b/test/soap/ssl/README
index 927c738f2..aff5a2fcc 100644
--- a/test/soap/ssl/README
+++ b/test/soap/ssl/README
@@ -1 +1,9 @@
-* certificates and keys in this directory is copied from httpclient test.
+* certificates and keys in this directory were originally copied from httpclient's test suite.
+* regenerated 2026-07 with 2048-bit RSA keys and SHA-256 signatures: the
+ originals were 1024-bit RSA with SHA-1 signatures (created 2004), both
+ rejected outright by modern OpenSSL's default security level
+ ("SSL_CTX_use_certificate: ee key too small"). The CA -> SubCA -> server
+ chain topology, Subject DNs, and client cert issued directly by the root
+ CA (not via SubCA) are preserved exactly, since test_ssl.rb's assertions
+ depend on that specific chain structure to get expected pass/fail
+ behavior for its certificate-verification test cases.
diff --git a/test/soap/ssl/ca.cert b/test/soap/ssl/ca.cert
index bcabbee4a..a22497c65 100644
--- a/test/soap/ssl/ca.cert
+++ b/test/soap/ssl/ca.cert
@@ -1,23 +1,20 @@
-----BEGIN CERTIFICATE-----
-MIID0DCCArigAwIBAgIBADANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGDAJKUDES
-MBAGA1UECgwJSklOLkdSLkpQMQwwCgYDVQQLDANSUlIxCzAJBgNVBAMMAkNBMB4X
-DTA0MDEzMDAwNDIzMloXDTM2MDEyMjAwNDIzMlowPDELMAkGA1UEBgwCSlAxEjAQ
-BgNVBAoMCUpJTi5HUi5KUDEMMAoGA1UECwwDUlJSMQswCQYDVQQDDAJDQTCCASIw
-DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANbv0x42BTKFEQOE+KJ2XmiSdZpR
-wjzQLAkPLRnLB98tlzs4xo+y4RyY/rd5TT9UzBJTIhP8CJi5GbS1oXEerQXB3P0d
-L5oSSMwGGyuIzgZe5+vZ1kgzQxMEKMMKlzA73rbMd4Jx3u5+jdbP0EDrPYfXSvLY
-bS04n2aX7zrN3x5KdDrNBfwBio2/qeaaj4+9OxnwRvYP3WOvqdW0h329eMfHw0pi
-JI0drIVdsEqClUV4pebT/F+CPUPkEh/weySgo9wANockkYu5ujw2GbLFcO5LXxxm
-dEfcVr3r6t6zOA4bJwL0W/e6LBcrwiG/qPDFErhwtgTLYf6Er67SzLyA66UCAwEA
-AaOB3DCB2TAPBgNVHRMBAf8EBTADAQH/MDEGCWCGSAGG+EIBDQQkFiJSdWJ5L09w
-ZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBRJ7Xd380KzBV7f
-USKIQ+O/vKbhDzAOBgNVHQ8BAf8EBAMCAQYwZAYDVR0jBF0wW4AUSe13d/NCswVe
-31EiiEPjv7ym4Q+hQKQ+MDwxCzAJBgNVBAYMAkpQMRIwEAYDVQQKDAlKSU4uR1Iu
-SlAxDDAKBgNVBAsMA1JSUjELMAkGA1UEAwwCQ0GCAQAwDQYJKoZIhvcNAQEFBQAD
-ggEBAIu/mfiez5XN5tn2jScgShPgHEFJBR0BTJBZF6xCk0jyqNx/g9HMj2ELCuK+
-r/Y7KFW5c5M3AQ+xWW0ZSc4kvzyTcV7yTVIwj2jZ9ddYMN3nupZFgBK1GB4Y05GY
-MJJFRkSu6d/Ph5ypzBVw2YMT/nsOo5VwMUGLgS7YVjU+u/HNWz80J3oO17mNZllj
-PvORJcnjwlroDnS58KoJ7GDgejv3ESWADvX1OHLE4cRkiQGeLoEU4pxdCxXRqX0U
-PbwIkZN9mXVcrmPHq8MWi4eC/V7hnbZETMHuWhUoiNdOEfsAXr3iP4KjyyRdwc7a
-d/xgcK06UVQRL/HbEYGiQL056mc=
+MIIDWzCCAkOgAwIBAgIUNRRGpXXqK5T97WwHvVlWsAo1h+IwDQYJKoZIhvcNAQEL
+BQAwPDELMAkGA1UEBhMCSlAxEjAQBgNVBAoMCUpJTi5HUi5KUDEMMAoGA1UECwwD
+UlJSMQswCQYDVQQDDAJDQTAgFw0yNjA3MDYyMTQyNTZaGA8yMDU2MDYyODIxNDI1
+NlowPDELMAkGA1UEBhMCSlAxEjAQBgNVBAoMCUpJTi5HUi5KUDEMMAoGA1UECwwD
+UlJSMQswCQYDVQQDDAJDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AIWObofhwZoo+u0GLjjcNEW9EjAapmYY+X6G/Gm17WAoB7klU8U3ohMEgc45mDPk
+/6JmCnktKLP+NdwM8SScOdIGFz/uaV/z9/+F+YTbcOwBcXB9fZUIfE/3sIH2CwKS
+jvb2CyCzwHmxFYv6hvKNIoCz8fEe+rV3ADxaDIRljGuPv6FnzU7LjIfJYWXSEagr
+DmqzOBMqEquCKeq/wC4LjCF5VEVADIzsLTNCACdDLEglraoHR4lKOB5nTGE+bAee
+vpdQTWbTbo97NwRgT2bsEeG6DVtWJ7mFkqyXsrRjRVsAC8/BRljFvRC7YlBokYXd
+1Wi5Zys8+vnDkd4YZkEHd+8CAwEAAaNTMFEwHQYDVR0OBBYEFHbFnl9yNS5yqPIV
+7gZ76qeCBddWMB8GA1UdIwQYMBaAFHbFnl9yNS5yqPIV7gZ76qeCBddWMA8GA1Ud
+EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFJbaTyuyVbDfzEZA/yYEf4T
+CnzvoIWfiaQeU+8fZ2nsZ4HICV25LfccQbRclVUKKpiAUAI1k69hwW2fCSmzsXnH
+x9DSjXIkqLt8Co7QQ15EJrwK4Mx7XLq3dZ/pgzLPFauujstixlqR+71XLjNOG2jG
+8iXFFDydQNgKEDr6hZSEkjvy5NAcVPwD41BU+p2khJ6VZdQHRa7Fp8PSiL3jlE6j
+ouszKjESVr7aJC7Wazqw0L6Ah1P2Jep5B2+kZcovj8CZF69LryJULYoDgoPRoyB5
+M7dkU3abgc0Tsiy44tmraMSXyJnMnGLioHBcxJpdODDLa1KgHLj34iOZyhjOpho=
-----END CERTIFICATE-----
diff --git a/test/soap/ssl/client.cert b/test/soap/ssl/client.cert
index ad13c4b73..b2cc67e49 100644
--- a/test/soap/ssl/client.cert
+++ b/test/soap/ssl/client.cert
@@ -1,19 +1,22 @@
-----BEGIN CERTIFICATE-----
-MIIDKDCCAhCgAwIBAgIBAjANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGDAJKUDES
-MBAGA1UECgwJSklOLkdSLkpQMQwwCgYDVQQLDANSUlIxCzAJBgNVBAMMAkNBMB4X
-DTA0MDEzMTAzMTQ1OFoXDTM1MDEyMzAzMTQ1OFowZTELMAkGA1UEBgwCSlAxEjAQ
-BgNVBAoMCUpJTi5HUi5KUDEMMAoGA1UECwwDUlJSMRAwDgYDVQQDDAdleGFtcGxl
-MSIwIAYJKoZIhvcNAQkBDBNleGFtcGxlQGV4YW1wbGUub3JnMIGfMA0GCSqGSIb3
-DQEBAQUAA4GNADCBiQKBgQDRWssrK8Gyr+500hpLjCGR3+AHL8/hEJM5zKi/MgLW
-jTkvsgOwbYwXOiNtAbR9y4/ucDq7EY+cMUMHES4uFaPTcOaAV0aZRmk8AgslN1tQ
-gNS6ew7/Luq3DcVeWkX8PYgR9VG0mD1MPfJ6+IFA5d3vKpdBkBgN4l46jjO0/2Xf
-ewIDAQABo4GPMIGMMAwGA1UdEwEB/wQCMAAwMQYJYIZIAYb4QgENBCQWIlJ1Ynkv
-T3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFOFvay0H7lr2
-xUx6waYEV2bVDYQhMAsGA1UdDwQEAwIF4DAdBgNVHSUEFjAUBggrBgEFBQcDAgYI
-KwYBBQUHAwQwDQYJKoZIhvcNAQEFBQADggEBABd2dYWqbDIWf5sWFvslezxJv8gI
-w64KCJBuyJAiDuf+oazr3016kMzAlt97KecLZDusGNagPrq02UX7YMoQFsWJBans
-cDtHrkM0al5r6/WGexNMgtYbNTYzt/IwodISGBgZ6dsOuhznwms+IBsTNDAvWeLP
-lt2tOqD8kEmjwMgn0GDRuKjs4EoboA3kMULb1p9akDV9ZESU3eOtpS5/G5J5msLI
-9WXbYBjcjvkLuJH9VsJhb+R58Vl0ViemvAHhPilSl1SPWVunGhv6FcIkdBEi1k9F
-e8BNMmsEjFiANiIRvpdLRbiGBt0KrKTndVfsmoKCvY48oCOvnzxtahFxfs8=
+MIIDozCCAougAwIBAgIUB3h7ZvrzHmZKWfrRVqMGZLIBJQswDQYJKoZIhvcNAQEL
+BQAwPDELMAkGA1UEBhMCSlAxEjAQBgNVBAoMCUpJTi5HUi5KUDEMMAoGA1UECwwD
+UlJSMQswCQYDVQQDDAJDQTAgFw0yNjA3MDYyMTQyNTZaGA8yMDU2MDYyODIxNDI1
+NlowZTELMAkGA1UEBhMCSlAxEjAQBgNVBAoMCUpJTi5HUi5KUDEMMAoGA1UECwwD
+UlJSMRAwDgYDVQQDDAdleGFtcGxlMSIwIAYJKoZIhvcNAQkBFhNleGFtcGxlQGV4
+YW1wbGUub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0dFF299o
++1PjgzZ8baPjezWHouGguvdmR+8ZG/OKL5Rs1obE5xlPFHvY4U9+1CokuRgdgoGv
+p4ife/JXjjWZRWg7mP4SxHbdKf4jI+Y2u/F58Wylqg20smCIxMqVCwQw/GFUih9O
+UNYgKmOA8dT4iM+VaeOovrqIipo9evuNk6dnwvz/y7zxSLneB01B56SwXdq4oKA+
+6lw9ntK1rG8txLK4A/hjIM+8Ha9LnN6NTsnQg3sJ3DE20GbRKe6a5hHo1cgUamfu
+9RmGQ3CvOoggs0gNidM1R4CN1bysX8/24hOcfapTtRhQx2ugBHTlrVDqaIi0tRn0
+iPfb6tyoQwfEpQIDAQABo3IwcDAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIFoDAT
+BgNVHSUEDDAKBggrBgEFBQcDAjAdBgNVHQ4EFgQULRC0bx25mYp+RnAP3WJiyA4p
+ebkwHwYDVR0jBBgwFoAUdsWeX3I1LnKo8hXuBnvqp4IF11YwDQYJKoZIhvcNAQEL
+BQADggEBAE2C4/FvEQFNqNWVbRPst87glGIPYjxK0riXw1bYXjfZP4N5PL8AOiie
+50XyeJ1LySXPT6l722GBz7pik07p1o40p2THTH4igmxXKaEmG59k0IClVjVi/ZJs
+Ptk2lQ3srRLzRwXIEQvK3ItffQ01AmFnpgL4cckNFAKMN/y7TUD85MX/fsNrdjj6
+zXwGrMzu/Mw3RVrsFIF5RGHIigVu9zmv7nYo2rTpD07cF0K9mrCvhaGPFvcEmA6V
+xlledeaF7Kab5ASv8C3I8s1MPzN8Pw4N9lVE9ajgOVxTV/57ZvrI9QLbFKF+kG+g
+B7MOVCNAlv4ee1aXvzYi+5cN1t80Sh4=
-----END CERTIFICATE-----
diff --git a/test/soap/ssl/client.key b/test/soap/ssl/client.key
index 37bc62f25..76e621225 100644
--- a/test/soap/ssl/client.key
+++ b/test/soap/ssl/client.key
@@ -1,15 +1,28 @@
------BEGIN RSA PRIVATE KEY-----
-MIICWwIBAAKBgQDRWssrK8Gyr+500hpLjCGR3+AHL8/hEJM5zKi/MgLWjTkvsgOw
-bYwXOiNtAbR9y4/ucDq7EY+cMUMHES4uFaPTcOaAV0aZRmk8AgslN1tQgNS6ew7/
-Luq3DcVeWkX8PYgR9VG0mD1MPfJ6+IFA5d3vKpdBkBgN4l46jjO0/2XfewIDAQAB
-AoGAZcz8llWErtsV3QB9gNb3S/PNADGjqBFjReva8n3jG2k4sZSibpwWTwUaTNtT
-ZQgjSRKRvH1hk9XwffNAvXAQZNNkuj/16gO2oO45nyLj4dO365ujLptWnVIWDHOE
-uN0GeiZO+VzcCisT0WCq4tvtLeH8svrxzA8cbXIEyOK7NiECQQDwo2zPFyKAZ/Cu
-lDJ6zKT+RjfWwW7DgWzirAlTrt4ViMaW+IaDH29TmQpb4V4NuR3Xi+2Xl4oicu6S
-36TW9+/FAkEA3rgfOQJuLlWSnw1RTGwvnC816a/W7iYYY7B+0U4cDbfWl7IoXT4y
-M8nV/HESooviZLqBwzAYSoj3fFKYBKpGPwJAUO8GN5iWWA2dW3ooiDiv/X1sZmRk
-dojfMFWgRW747tEzya8Ivq0h6kH8w+5GjeMG8Gn1nRiwsulo6Ckj7dEx6QJACyui
-7UIQ8qP6GZ4aYMHgVW4Mvy7Bkeo5OO7GPYs0Xv/EdJFL8vlGnVBXOjUVoS9w6Gpu
-TbLg1QQvnX2rADjmEwJANxZO2GUkaWGsEif8aGW0x5g/IdaMGG27pTWk5zqix7P3
-1UDrdo/JOXhptovhRi06EppIxAxYmbh9vd9VN8Itlw==
------END RSA PRIVATE KEY-----
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDR0UXb32j7U+OD
+Nnxto+N7NYei4aC692ZH7xkb84ovlGzWhsTnGU8Ue9jhT37UKiS5GB2Cga+niJ97
+8leONZlFaDuY/hLEdt0p/iMj5ja78XnxbKWqDbSyYIjEypULBDD8YVSKH05Q1iAq
+Y4Dx1PiIz5Vp46i+uoiKmj16+42Tp2fC/P/LvPFIud4HTUHnpLBd2rigoD7qXD2e
+0rWsby3EsrgD+GMgz7wdr0uc3o1OydCDewncMTbQZtEp7prmEejVyBRqZ+71GYZD
+cK86iCCzSA2J0zVHgI3VvKxfz/biE5x9qlO1GFDHa6AEdOWtUOpoiLS1GfSI99vq
+3KhDB8SlAgMBAAECggEACQXNEYK2tsIZgUm9SZC35lM7uTUnLosXhqXhSEqtiVTC
+ZmpGuuJjfG/D+JJs045ZZG1FTZiql1OAejnGapri87FjCLX6VCEBaz4ewDs05Nmi
+Qw/HQvwSZm5Qy1ej16SvP5cm5capXUAMA46iW6PrKST2Gabc2ZDXcNBnrut03+HJ
+xT/l9GQPoF/x9C/Fwkv3ndHNfhGBCpVWJxVQk+oEg1EYqpIxe9ULBZ3xga53nZRp
+eB0pT6SHNS68b43gNvL4kPNaF63u8c98+Lnfuqlm2iR3cbBUUPYHIiA5wmLzinpI
+mf6wfsaLoG40tDmAfM1rMKciC3yb+ahjqm4W/KSb0QKBgQD/cu0nxHdwStghczL8
+8x2fVbORHZYaDZV4qvkyN1aKwnpVcS4m3mmfoB9vjN3ANwxVErDyRPh/IjNtSyCb
+wWgvXu2HlL3bUQf+g56FDry6RASAR+V9xWiY00M9x2wrXyIuc539ex9YN3vLJ7i/
+xonQiKptmHBBlClfYiUxRaNrVQKBgQDSRSVr871/USwf5XLodGHU2WzGX7os2RvQ
++RNiauQjzqNNGRrByWb2+7EMlKB2+bdYpdlKlysDyEai/4AOpZDbSQi8kP1W/ykw
+tV23ZVmr66iCOsAX/t8nKQB4jeyrYnCGkAFv9YYGtlSaNfxfdtSLqdYs5Q6Tkhox
+J/mrYZ8UEQKBgCjbRtcfc0MAlYT0GemfMNO1L108cC+GkKuWHPlNJIhZBpv32Dvb
+QuWHBMAgta7ynwRUUvzMzzn2TGdkpriCvJknA7K1tZAIa9DnWElLzB+2lUm3Nhth
+oZMf5xdQeBqPwrXPRCuwr5i6dxBNMB1sMcqqsSeKRBoZCmz5E53H15VpAoGAbQ0J
+dGxL2iz6CmzQZHh/iIle3Z8mCj5rK70R7ZfsTBnOHZ0ogFp4uR0d3J10f1/RU/tn
+r0SpPWPwJncGLhJ2BxRgoJM9oJHjBez59rRshjpJAYmAuLEpgMDGCr/of5YlL9Sd
+ujgG05JaXEQlaElTwD9L4arGHYjHZrkJxnVxsFECgYB5lBl3r6proU/9IzMtP/8J
+StDUxZ8/yzFCgrOniCwAL3hutVnOmHAbZLBoDQPdG1O8dF39pvJXliL4E023pT0D
+Wf3oXkaEc7WE8DwqOZTD/10wRtTY2Ty6TpMVbbi7/S5MY/+dIPK0psh08ldkOfnX
+QcR60xhQc7H53A8F0DnLBg==
+-----END PRIVATE KEY-----
diff --git a/test/soap/ssl/server.cert b/test/soap/ssl/server.cert
index 998ccc589..dd5efa1c4 100644
--- a/test/soap/ssl/server.cert
+++ b/test/soap/ssl/server.cert
@@ -1,19 +1,22 @@
-----BEGIN CERTIFICATE-----
-MIIC/zCCAeegAwIBAgIBATANBgkqhkiG9w0BAQUFADA/MQswCQYDVQQGDAJKUDES
-MBAGA1UECgwJSklOLkdSLkpQMQwwCgYDVQQLDANSUlIxDjAMBgNVBAMMBVN1YkNB
-MB4XDTA0MDEzMTAzMTMxNloXDTMzMDEyMzAzMTMxNlowQzELMAkGA1UEBgwCSlAx
-EjAQBgNVBAoMCUpJTi5HUi5KUDEMMAoGA1UECwwDUlJSMRIwEAYDVQQDDAlsb2Nh
-bGhvc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANFJTxWqup3nV9dsJAku
-p+WaXnPNIzcpAA3qMGZDJTJsfa8Du7ZxTP0XJK5mETttBrn711cJxAuP3KjqnW9S
-vtZ9lY2sXJ6Zj62sN5LwG3VVe25dI28yR1EsbHjJ5Zjf9tmggMC6am52dxuHbt5/
-vHo4ngJuKE/U+eeGRivMn6gFAgMBAAGjgYUwgYIwDAYDVR0TAQH/BAIwADAxBglg
-hkgBhvhCAQ0EJBYiUnVieS9PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAd
-BgNVHQ4EFgQUpZIyygD9JxFYHHOTEuWOLbCKfckwCwYDVR0PBAQDAgWgMBMGA1Ud
-JQQMMAoGCCsGAQUFBwMBMA0GCSqGSIb3DQEBBQUAA4IBAQBwAIj5SaBHaA5X31IP
-CFCJiep96awfp7RANO0cuUj+ZpGoFn9d6FXY0g+Eg5wAkCNIzZU5NHN9xsdOpnUo
-zIBbyTfQEPrge1CMWMvL6uGaoEXytq84VTitF/xBTky4KtTn6+es4/e7jrrzeUXQ
-RC46gkHObmDT91RkOEGjHLyld2328jo3DIN/VTHIryDeVHDWjY5dENwpwdkhhm60
-DR9IrNBbXWEe9emtguNXeN0iu1ux0lG1Hc6pWGQxMlRKNvGh0yZB9u5EVe38tOV0
-jQaoNyL7qzcQoXD3Dmbi1p0iRmg/+HngISsz8K7k7MBNVsSclztwgCzTZOBiVtkM
-rRlQ
+MIIDnDCCAoSgAwIBAgIUV0HxatIun9Bz8t0+PgQgHjrUm0kwDQYJKoZIhvcNAQEL
+BQAwPzELMAkGA1UEBhMCSlAxEjAQBgNVBAoMCUpJTi5HUi5KUDEMMAoGA1UECwwD
+UlJSMQ4wDAYDVQQDDAVTdWJDQTAgFw0yNjA3MDYyMTQyNTZaGA8yMDU2MDYyODIx
+NDI1NlowQzELMAkGA1UEBhMCSlAxEjAQBgNVBAoMCUpJTi5HUi5KUDEMMAoGA1UE
+CwwDUlJSMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IB
+DwAwggEKAoIBAQD046la6a9oW7mEeNxUYCw02oAhikUvLrEs+ZP1wEXqy0dngp9j
+vkJWNL74HM7jxlW7jFnt2QEBwOeCEzudluCxgoPfoqJQsiapzufaC7O3fvyz8rnq
+IfezGAan+y/EcD6PWwsB5JtMi4dyXf1i3NGLhuTR44b9WYhs7PVo86GxCgvVPbV+
+pka7o+YWC+8sxZ/PBz3/ipdkbxRU4a8Rl779zFJxB655jpFIIraV6HNZrif032xU
+6v0WiM2Ccuoq29LT/Z7CO5UKt/ktD0KGTMO3JhGEeEdlJnVT+6tonPkWpezty6ip
+mDvxxJdIm8q07HrQWMKAAd8ZTl0djSIQztzDAgMBAAGjgYkwgYYwCQYDVR0TBAIw
+ADAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwFAYDVR0RBA0w
+C4IJbG9jYWxob3N0MB0GA1UdDgQWBBTVzVSkwocIQmaoa0NhZkabz5s22zAfBgNV
+HSMEGDAWgBS6auKGVppowpJ4AdI9rC9xhbLdZTANBgkqhkiG9w0BAQsFAAOCAQEA
+NsXhZGeG1uEbyFPNuMyTeQvXLJmGYqbeUx1287Dnn/z65fZBQ8O6MGq/yLsBOaIi
+acpJsUih6ApB14810Gwu1zSJ35QgJ9ciAP4FwaXnodRE4cZfuu+O/FxS+YuAXE/P
+j60nBvrbK0ZIYaTJjevZT48H6sE8NaKB/lobb7QgEjOrYzF+D7hLOemB4wXdjzHR
+ew+mYEBsS/NaAYiF8t+qPI5gAPBuJ/MyiV/kNW9yN5jAZ3qAEHEBiK45gAZmoxo6
+R5i4YlbPB8Oli9DSizCLKLozaDwiP8QKjJf4r+PI2rtDJB4R820hzlHxa0Ysb1yR
+QV0OMQxyGDA2x3FOvVUmLg==
-----END CERTIFICATE-----
diff --git a/test/soap/ssl/server.key b/test/soap/ssl/server.key
index 9ba2218a0..a3e9cde08 100644
--- a/test/soap/ssl/server.key
+++ b/test/soap/ssl/server.key
@@ -1,15 +1,28 @@
------BEGIN RSA PRIVATE KEY-----
-MIICXQIBAAKBgQDRSU8Vqrqd51fXbCQJLqflml5zzSM3KQAN6jBmQyUybH2vA7u2
-cUz9FySuZhE7bQa5+9dXCcQLj9yo6p1vUr7WfZWNrFyemY+trDeS8Bt1VXtuXSNv
-MkdRLGx4yeWY3/bZoIDAumpudncbh27ef7x6OJ4CbihP1PnnhkYrzJ+oBQIDAQAB
-AoGBAIf4CstW2ltQO7+XYGoex7Hh8s9lTSW/G2vu5Hbr1LTHy3fzAvdq8MvVR12O
-rk9fa+lU9vhzPc0NMB0GIDZ9GcHuhW5hD1Wg9OSCbTOkZDoH3CAFqonjh4Qfwv5W
-IPAFn9KHukdqGXkwEMdErsUaPTy9A1V/aROVEaAY+HJgq/eZAkEA/BP1QMV04WEZ
-Oynzz7/lLizJGGxp2AOvEVtqMoycA/Qk+zdKP8ufE0wbmCE3Qd6GoynavsHb6aGK
-gQobb8zDZwJBANSK6MrXlrZTtEaeZuyOB4mAmRzGzOUVkUyULUjEx2GDT93ujAma
-qm/2d3E+wXAkNSeRpjUmlQXy/2oSqnGvYbMCQQDRM+cYyEcGPUVpWpnj0shrF/QU
-9vSot/X1G775EMTyaw6+BtbyNxVgOIu2J+rqGbn3c+b85XqTXOPL0A2RLYkFAkAm
-syhSDtE9X55aoWsCNZY/vi+i4rvaFoQ/WleogVQAeGVpdo7/DK9t9YWoFBIqth0L
-mGSYFu9ZhvZkvQNV8eYrAkBJ+rOIaLDsmbrgkeDruH+B/9yrm4McDtQ/rgnOGYnH
-LjLpLLOrgUxqpzLWe++EwSLwK2//dHO+SPsQJ4xsyQJy
------END RSA PRIVATE KEY-----
+-----BEGIN PRIVATE KEY-----
+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQD046la6a9oW7mE
+eNxUYCw02oAhikUvLrEs+ZP1wEXqy0dngp9jvkJWNL74HM7jxlW7jFnt2QEBwOeC
+EzudluCxgoPfoqJQsiapzufaC7O3fvyz8rnqIfezGAan+y/EcD6PWwsB5JtMi4dy
+Xf1i3NGLhuTR44b9WYhs7PVo86GxCgvVPbV+pka7o+YWC+8sxZ/PBz3/ipdkbxRU
+4a8Rl779zFJxB655jpFIIraV6HNZrif032xU6v0WiM2Ccuoq29LT/Z7CO5UKt/kt
+D0KGTMO3JhGEeEdlJnVT+6tonPkWpezty6ipmDvxxJdIm8q07HrQWMKAAd8ZTl0d
+jSIQztzDAgMBAAECggEABm7zerpx5VcN8bkVHLhY4PVhviY6RanyqKC3FN+XI24L
+Ga2/HALNdLzpzKEN6sKOnqDQyf4hJL3zHW7KhAwIw7d1WT4vonJ6FoyJ31revuU8
+pH0t1vgYE3uAAHcjKZ3c3uCm7BO1ffAA2aEDfTUZrvjKJFA9/x2t7YkKGqNv4RoH
+ATxf6TF1fxaY30qp2qJtMWjuUNLNK0kJv9GLVJNJOLL9fsTUH8fEwX1QSQN/d1BM
++lywVvTN5lKjJ3J+CD38+rO16GAFCrCIWExsRhKk9J8e950y8IsE+hw/hVvjqjAR
+snqbjw7IYJXY2ysNBraHUjHS8lZZRQr5NSxH05AEjQKBgQD/bjZA6WQxAFUSWaZp
+JHj57oZQn3u9eDF/cgChuYJ9yybE+xmYQyB43cj59EEBJIxHFUSXLqbnvGGAXXo3
+2Cw3UrBQvEJ6XtPKlSNE/3w+49fwE0+f5B1GH0ONX/kh7W6MXzN9fQd0Qfm22hpq
+eK1ZwGqpZh5XTIrsRptrfcDntQKBgQD1b27kavIESIUeqaBl6maTUKRqcgRqEKsp
+QX3eU0ckBupHLk1pwqVjTVSrlmaXJ8UgbeBnjtztvK9fCL4/3vjg+YEyPDnfA/qw
+RsV/oZdIpAKkawXeIGh96woZuH6TPiHKIoLAa2z0kO+FzXzXebT9p9cW6890zGo/
+tJk2fHoNlwKBgQDX7el7UlHagrdn/dWJyMVkM1nkSg4nC7Z8UHlsPhCsGtK5brQi
+XYzT4FmHxA430xeq97W2QD/kYwHhrCQnlV07n3FhruRb4lIUTaM1Lu1vlNj1IL4N
+IqAEqWVH3DwVjEwJ6mjyyoFErIlXJGV0YHPbDaidb7ByiEhgmQfGBNbQiQKBgQCY
+d0jkDKubjZFkoutJZHukOAxrM7kaSpCJaG3Qltsyvjj7TwA4Gvqy3W8jyKKic8o5
+gwhMTKth5DztRHkrJFBnMZfYpSEuMUKiDtTRnIhmT7x0dyeF/Yvr7P6xS7MVtRs4
+fofLEPD2XLLu4+AR20Fb8c/kJUkSvYmjWYV0G4+WsQKBgQD3dypHrBPVZ2aZUzpT
+UR0k1CiiqM2832TNFzVfR8oBsgzrM7xVPxcHK5Ngif0NkEtP/IkIfPUYQvNVlmS8
+l5auNXMQ3X+F897Qdahx4qTmr5+e6aJSKantX9Z+aCBOVjt9HpqUlBTu0us73uFA
+fTe78kYfbfhQAm35Xrh/bd4/ZA==
+-----END PRIVATE KEY-----
diff --git a/test/soap/ssl/sslsvr.rb b/test/soap/ssl/sslsvr.rb
index 9f299d55d..3bd95b492 100644
--- a/test/soap/ssl/sslsvr.rb
+++ b/test/soap/ssl/sslsvr.rb
@@ -1,5 +1,15 @@
# encoding: UTF-8
-$:.unshift File.expand_path( File.dirname(__FILE__) + '../../../../lib')
+$:.unshift File.expand_path( File.dirname(__FILE__) + '../../../../lib')
+
+# Spawned via a bare `ruby sslsvr.rb` (test_ssl.rb#setup_server), not
+# `bundle exec`, so without this it falls through to whatever RubyGems
+# activates by default -- the true standard-library copies of logger/webrick
+# on Ruby >= 3.0/4.0, which print a "will no longer be part of the default
+# gems" notice. This is a private test fixture that always runs inside this
+# repo's own checkout, so a Gemfile is always present; activating it here
+# picks up the same pinned gem versions the parent `bundle exec rake
+# test:deep` process already uses, silencing that notice.
+require 'bundler/setup'
require 'webrick/https'
require 'logger'
diff --git a/test/soap/ssl/subca.cert b/test/soap/ssl/subca.cert
index 1e471851b..7e0458c15 100644
--- a/test/soap/ssl/subca.cert
+++ b/test/soap/ssl/subca.cert
@@ -1,21 +1,21 @@
-----BEGIN CERTIFICATE-----
-MIIDaDCCAlCgAwIBAgIBATANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGDAJKUDES
-MBAGA1UECgwJSklOLkdSLkpQMQwwCgYDVQQLDANSUlIxCzAJBgNVBAMMAkNBMB4X
-DTA0MDEzMDAwNDMyN1oXDTM1MDEyMjAwNDMyN1owPzELMAkGA1UEBgwCSlAxEjAQ
-BgNVBAoMCUpJTi5HUi5KUDEMMAoGA1UECwwDUlJSMQ4wDAYDVQQDDAVTdWJDQTCC
-ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ0Ou7AyRcRXnB/kVHv/6kwe
-ANzgg/DyJfsAUqW90m7Lu1nqyug8gK0RBd77yU0w5HOAMHTVSdpjZK0g2sgx4Mb1
-d/213eL9TTl5MRVEChTvQr8q5DVG/8fxPPE7fMI8eOAzd98/NOAChk+80r4Sx7fC
-kGVEE1bKwY1MrUsUNjOY2d6t3M4HHV3HX1V8ShuKfsHxgCmLzdI8U+5CnQedFgkm
-3e+8tr8IX5RR1wA1Ifw9VadF7OdI/bGMzog/Q8XCLf+WPFjnK7Gcx6JFtzF6Gi4x
-4dp1Xl45JYiVvi9zQ132wu8A1pDHhiNgQviyzbP+UjcB/tsOpzBQF8abYzgEkWEC
-AwEAAaNyMHAwDwYDVR0TAQH/BAUwAwEB/zAxBglghkgBhvhCAQ0EJBYiUnVieS9P
-cGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUlCjXWLsReYzH
-LzsxwVnCXmKoB/owCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQCJ/OyN
-rT8Cq2Y+G2yA/L1EMRvvxwFBqxavqaqHl/6rwsIBFlB3zbqGA/0oec6MAVnYynq4
-c4AcHTjx3bQ/S4r2sNTZq0DH4SYbQzIobx/YW8PjQUJt8KQdKMcwwi7arHP7A/Ha
-LKu8eIC2nsUBnP4NhkYSGhbmpJK+PFD0FVtD0ZIRlY/wsnaZNjWWcnWF1/FNuQ4H
-ySjIblqVQkPuzebv3Ror6ZnVDukn96Mg7kP4u6zgxOeqlJGRe1M949SS9Vudjl8X
-SF4aZUUB9pQGhsqQJVqaz2OlhGOp9D0q54xko/rekjAIcuDjl1mdX4F2WRrzpUmZ
-uY/bPeOBYiVsOYVe
+MIIDcTCCAlmgAwIBAgIUB3h7ZvrzHmZKWfrRVqMGZLIBJQowDQYJKoZIhvcNAQEL
+BQAwPDELMAkGA1UEBhMCSlAxEjAQBgNVBAoMCUpJTi5HUi5KUDEMMAoGA1UECwwD
+UlJSMQswCQYDVQQDDAJDQTAgFw0yNjA3MDYyMTQyNTZaGA8yMDU2MDYyODIxNDI1
+NlowPzELMAkGA1UEBhMCSlAxEjAQBgNVBAoMCUpJTi5HUi5KUDEMMAoGA1UECwwD
+UlJSMQ4wDAYDVQQDDAVTdWJDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBALFyfK/emuyV2s8sVTrZfH50cEu/hvwykcm7DMNRXjJGG9oUXoIuYi20JY5w
+QXIN0+JC6B8ZVvKzYlLb8TMslB5pJr0w8B4KLmiQ+Z7bbzOzRdWtESUafKlIl11O
+G0fKAdVQEp1EUTm/YdJN0v5ztcKbqQ+ezBoO4icePswIwrVOkl2hPXGYQ4AxBPni
+9HJpWdPw8P5OdQrJqFSkoPadC9XNyRAEdKm1dF/u3fILymuIPR19mrYVKG3nMJbu
+p6ddS1wst2MeJioctlMWyCWaDl1D0+s2IWbsLFemtYue525Lto5hBravYoJHH12n
+6cioUGLNOS7uN4ZE3n2c8l3QBFECAwEAAaNmMGQwEgYDVR0TAQH/BAgwBgEB/wIB
+ADAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLpq4oZWmmjCkngB0j2sL3GFst1l
+MB8GA1UdIwQYMBaAFHbFnl9yNS5yqPIV7gZ76qeCBddWMA0GCSqGSIb3DQEBCwUA
+A4IBAQBKXT+Nsn0yjVQVwwk7Oy3OCkntok3AxdTEevmEuqj6+Xcj87jaS0Ah9NjJ
+h5uyTmqkRhuIveimHtdkx24HwnXFyQBf8kVh+k/BtgC1hGWa9F1yhGP8kwlkDOxS
+e93VYvqdlDU03k5+YL6bUmtIi7AIXKnROBKM8Tv7fd1Q1aq58OLx6jt0KA7G44FB
+qs12yN66DkDJmL7robzhVvrq9WjD9Pdx7XaD1G9r0fRq5Ypo+cYl3K6y6FCe7mPk
+BXJT6uXLYOVP8OQLr9ew/kIcE5l+02I1k9QEMd9mdsBKW8p9PiEGNKLnIIJA+GE0
+dChxve2zVR2JvPuldNfu7r4sylwa
-----END CERTIFICATE-----
diff --git a/test/soap/ssl/test_ssl.rb b/test/soap/ssl/test_ssl.rb
index 444f2b9e0..f750e0c84 100644
--- a/test/soap/ssl/test_ssl.rb
+++ b/test/soap/ssl/test_ssl.rb
@@ -1,12 +1,34 @@
# encoding: UTF-8
require 'helper'
+require 'timeout'
begin
require 'httpclient'
rescue LoadError
end
+begin
+ require 'curb'
+rescue LoadError
+end
+begin
+ require 'faraday'
+rescue LoadError
+end
require 'soap/rpc/driver'
-if defined?(HTTPClient) and defined?(OpenSSL)
+# Checking defined?(HTTPClient) alone isn't enough now that the HTTP client
+# backend is independently selectable (SOAP4R_HTTP_CLIENTS -- see
+# lib/soap/httpbackend.rb): the requires above pull in these gems
+# regardless of which backend SOAP::HTTPStreamHandler actually picked, so
+# e.g. HTTPClient can be defined while SOAP::NetHttpClient (whose
+# #ssl_config is always nil) is the active one. This must check the ACTIVE
+# backend, not merely whether a gem happens to be loaded.
+#
+# httpclient, curb, and faraday (any SOAP4R_FARADAY_ADAPTER) all get real
+# SSL config plumbing exercised here -- SOAP::NetHttpClient is excluded
+# since it has no SSL configuration surface of its own at all (see
+# lib/soap/netHttpClient.rb).
+SSL_TESTABLE_BACKENDS = %w[HTTPClient SOAP::CurbClient SOAP::FaradayClient]
+if SSL_TESTABLE_BACKENDS.include?(SOAP::HTTPStreamHandler::Client.name) and defined?(OpenSSL)
module SOAP; module SSL
@@ -34,7 +56,38 @@ def teardown
teardown_server
end
+ def httpclient_backend?
+ SOAP::HTTPStreamHandler::Client == HTTPClient
+ end
+
+ def faraday_backend?
+ SOAP::HTTPStreamHandler::Client.name == 'SOAP::FaradayClient'
+ end
+
+ # Each backend surfaces a TLS verification/handshake failure as a
+ # different exception class -- confirmed empirically against the real
+ # server this file spins up, not guessed from documentation:
+ # httpclient: OpenSSL::SSL::SSLError
+ # curb: Curl::Err::CurlError (base class for its whole SSL error
+ # family -- SSLPeerCertificateError, SSLCACertificateError,
+ # SSLCypherError, etc. -- rather than pinning to one)
+ # faraday: Faraday::Error (SSLError and ConnectionFailed are BOTH
+ # direct subclasses of it, and different Faraday adapters
+ # raise different ones for the same failure -- confirmed
+ # :typhoeus raises ConnectionFailed, not SSLError)
+ def expected_ssl_error_class
+ case SOAP::HTTPStreamHandler::Client.name
+ when 'SOAP::CurbClient'
+ Curl::Err::CurlError
+ when 'SOAP::FaradayClient'
+ Faraday::Error
+ else
+ OpenSSL::SSL::SSLError
+ end
+ end
+
def test_options
+ return unless httpclient_backend?
cfg = @client.streamhandler.client.ssl_config
assert_nil(cfg.client_cert)
assert_nil(cfg.client_key)
@@ -61,7 +114,14 @@ def test_options
end
end
+ # verify_callback is not portable across backends: no libcurl-based
+ # backend (curb, or Faraday riding on typhoeus/patron/etc.) exposes a
+ # per-certificate Ruby callback hook the way OpenSSL::SSL::SSLContext
+ # does -- confirmed there's no equivalent in either library's public API.
+ # See test_ca_verification below for the backend-neutral equivalent of
+ # what this test covers, minus the callback-specific assertions.
def test_verification
+ return unless httpclient_backend?
cfg = @client.options
cfg["protocol.http.ssl_config.verify_callback"] = method(:verify_callback).to_proc
begin
@@ -99,7 +159,7 @@ def test_verification
assert_equal("Hello World, from ssl client", @client.hello_world("ssl client"))
assert(@verify_callback_called)
#
- cfg["protocol.http.ssl_config.verify_depth"] = "1"
+ cfg["protocol.http.ssl_config.verify_depth"] = "0"
@verify_callback_called = false
begin
@client.hello_world("ssl client")
@@ -123,13 +183,14 @@ def test_verification
assert_equal("Hello World, from ssl client", @client.hello_world("ssl client"))
end
+ # Also verify_callback-dependent throughout -- see test_verification above.
def test_property
+ return unless httpclient_backend?
testpropertyname = File.join(DIR, 'soapclient.properties')
File.open(testpropertyname, "w") do |f|
f<<<<__EOP__
protocol.http.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_PEER
-# depth: 1 causes an error (intentional)
-protocol.http.ssl_config.verify_depth = 1
+protocol.http.ssl_config.verify_depth = 0
protocol.http.ssl_config.client_cert = #{File.join(DIR, 'client.cert')}
protocol.http.ssl_config.client_key = #{File.join(DIR, 'client.key')}
protocol.http.ssl_config.ca_file = #{File.join(DIR, 'ca.cert')}
@@ -192,29 +253,108 @@ def test_ciphers
#cfg.timeout = 123
cfg["protocol.http.ssl_config.ciphers"] = "!ALL"
#
- begin
- @client.hello_world("ssl client")
- assert(false)
- rescue OpenSSL::SSL::SSLError => ssle
- # depends on OpenSSL version. (?:0.9.8|0.9.7)
- assert_match(/\A(?:SSL_CTX_set_cipher_list:+ no cipher match|no ciphers available)\z/, ssle.message)
+ if faraday_backend?
+ # Confirmed empirically: Faraday's own :typhoeus adapter never
+ # forwards Faraday::SSLOptions#ciphers to ethon's ssl_cipher_list at
+ # all, so "!ALL" is silently ignored rather than rejected -- a real
+ # gap in that third-party adapter (not this bridge, and not
+ # something soap4r-ng's own code can fix). The handshake succeeds
+ # here regardless of the (unhonored) cipher restriction.
+ assert_equal("Hello World, from ssl client", @client.hello_world("ssl client"))
+ else
+ begin
+ @client.hello_world("ssl client")
+ assert(false)
+ rescue expected_ssl_error_class => ssle
+ if httpclient_backend?
+ # depends on OpenSSL version. (?:0.9.8|0.9.7)
+ assert_match(/\A(?:SSL_CTX_set_cipher_list:+ no cipher match|no ciphers available)\z/, ssle.message)
+ end
+ # curb's own message ("Could not use specified SSL cipher: failed
+ # setting cipher list: !ALL") is asserted only by class above --
+ # its exact wording isn't pinned since it's libcurl/OpenSSL-version
+ # dependent the same way httpclient's is.
+ end
end
#
cfg["protocol.http.ssl_config.ciphers"] = "ALL"
assert_equal("Hello World, from ssl client", @client.hello_world("ssl client"))
end
-private
+ # Backend-neutral equivalent of test_verification/test_property, minus
+ # the verify_callback-specific assertions those can't support on
+ # anything but httpclient (see the comment on test_verification). This
+ # is what actually matters for "does soap4r-ng's own config-loading
+ # bridge correctly plumb ca_file/client_cert/client_key through to a real
+ # TLS handshake" -- runs identically for every SSL-testable backend.
+ def test_ca_verification
+ cfg = @client.options
+ cfg["protocol.http.ssl_config.client_cert"] = File.join(DIR, "client.cert")
+ cfg["protocol.http.ssl_config.client_key"] = File.join(DIR, "client.key")
+ #
+ # No ca_file configured at all -- client has no reason to trust this
+ # private test CA, so verification must fail regardless of backend.
+ assert_ssl_verification_fails { @client.hello_world("ssl client") }
+ #
+ # Wrong CA (the root, not the intermediate that actually signed the
+ # server's cert) -- still fails.
+ cfg["protocol.http.ssl_config.ca_file"] = File.join(DIR, "ca.cert")
+ assert_ssl_verification_fails { @client.hello_world("ssl client") }
+ #
+ # Correct CA (the signing intermediate) -- succeeds.
+ cfg["protocol.http.ssl_config.ca_file"] = File.join(DIR, "subca.cert")
+ assert_equal("Hello World, from ssl client", @client.hello_world("ssl client"))
+ end
- def q(str)
- %Q["#{str}"]
+ # test-unit's assert_raise wants an EXACT class match, not
+ # kind_of?/subclass-inclusive like plain Ruby rescue -- confirmed
+ # empirically (curb/Faraday both raise a specific subclass of the base
+ # class expected_ssl_error_class returns, e.g. Curl::Err::SSLPeerCertificateError
+ # rather than bare Curl::Err::CurlError, and assert_raise rejected it).
+ # Every other SSL-exception check in this file already routes around
+ # that via begin/rescue instead of assert_raise; this just gives
+ # test_ca_verification the same treatment.
+ def assert_ssl_verification_fails
+ yield
+ assert(false, "expected an SSL verification failure")
+ rescue expected_ssl_error_class
end
+private
+
def setup_server
- svrcmd = "#{q(RUBY)} "
+ # No quoting around RUBY here: on POSIX, IO.popen only spawns an
+ # intermediary /bin/sh -c when the command string contains shell
+ # metacharacters. Quoting was doing exactly that (unnecessarily, since
+ # RbConfig's bindir path never has embedded whitespace), which made
+ # svrout.pid the *shell's* pid rather than sslsvr.rb's -- so the pid
+ # sslsvr.rb reports over stdout (its own $$) was a grandchild, not a
+ # direct child, and teardown_server's Process.waitpid below always
+ # failed with Errno::ECHILD (100% of the time, on every run). Dropping
+ # the quotes lets Ruby exec directly with no shell hop, so the reported
+ # pid is a real, waitable child again.
+ svrcmd = "#{RUBY} "
svrcmd << File.join(DIR, "sslsvr.rb")
svrout = IO.popen(svrcmd)
- @serverpid = Integer(svrout.gets.chomp)
+ # sslsvr.rb only prints its PID once its own WEBrick server has bound
+ # successfully (which retries internally on EADDRINUSE -- see
+ # lib/soap/rpc/httpserver.rb#new_webrick_server). Without a timeout here,
+ # a stuck child means this blocking read hangs indefinitely with zero
+ # console output -- confirmed via a local repro (pre-occupying port 17171
+ # made this block for the full retry window with nothing printed at all,
+ # looking exactly like the silent CI hangs seen in run 28892185757).
+ # Bounded slightly above that retry window so it only fires as a genuine
+ # backstop, not under normal contention.
+ line = nil
+ begin
+ Timeout.timeout(130) { line = svrout.gets }
+ rescue Timeout::Error
+ Process.kill('KILL', svrout.pid) rescue nil
+ Process.waitpid(svrout.pid) rescue nil
+ raise "sslsvr.rb did not report its PID within 130s -- likely stuck retrying its own port bind"
+ end
+ raise "sslsvr.rb exited without printing a PID (crashed before starting?)" if line.nil?
+ @serverpid = Integer(line.chomp)
end
def setup_client
diff --git a/test/soap/struct/test_struct.rb b/test/soap/struct/test_struct.rb
index b61d852e4..39b789c47 100644
--- a/test/soap/struct/test_struct.rb
+++ b/test/soap/struct/test_struct.rb
@@ -53,8 +53,14 @@ def teardown
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def teardown_client
diff --git a/test/soap/swa/test_file.rb b/test/soap/swa/test_file.rb
index 6ec62cba9..4457823c3 100644
--- a/test/soap/swa/test_file.rb
+++ b/test/soap/swa/test_file.rb
@@ -44,8 +44,14 @@ def setup
def teardown
@server.shutdown if @server
if @t
- @t.kill
- @t.join
+ # join with a bound, falling back to kill only if genuinely
+ # stuck (see git history: unconditional immediate kill raced
+ # WEBrick's own async listener cleanup and occasionally leaked
+ # the port).
+ unless @t.join(10)
+ @t.kill
+ @t.join
+ end
end
@client.reset_stream if @client
end
diff --git a/test/soap/test_cookie.rb b/test/soap/test_cookie.rb
index 3f7363499..5463bdf7f 100644
--- a/test/soap/test_cookie.rb
+++ b/test/soap/test_cookie.rb
@@ -50,7 +50,7 @@ def teardown
end
def setup_server
- @server = WEBrick::HTTPServer.new(
+ @server = TestUtil.webrick_http_server(
:BindAddress => "0.0.0.0",
:Logger => @logger,
:Port => Port,
@@ -71,8 +71,14 @@ def setup_client
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def teardown_client
@@ -97,7 +103,19 @@ def do_server_proc(req, res)
end
+ # SOAP::NetHttpClient/CurbClient/FaradayClient all expose #request_filter
+ # (so streamHandler.rb's respond_to?(:request_filter) check passes and
+ # wires this filter in), but none of them actually invoke it anywhere in
+ # their own request/response code -- it's a genuinely inert accessor on
+ # all three, like #ssl_config is for NetHttpClient. Only httpclient (which
+ # has its own native filter-chain support) really honors it.
+ FILTER_CHAIN_INERT_BACKENDS = %w[SOAP::NetHttpClient SOAP::CurbClient SOAP::FaradayClient]
+
def test_normal
+ # Skip cleanly under a backend that never wired this up, rather than
+ # fail on a feature it doesn't support (see lib/soap/httpbackend.rb for
+ # how the active backend is selected/forced).
+ return if FILTER_CHAIN_INERT_BACKENDS.include?(SOAP::HTTPStreamHandler::Client.name)
@client.wiredump_dev = STDOUT if $DEBUG
filter = CookieFilter.new
@client.streamhandler.filterchain << filter
diff --git a/test/soap/test_custommap.rb b/test/soap/test_custommap.rb
index 7837e533a..a57b8f95c 100644
--- a/test/soap/test_custommap.rb
+++ b/test/soap/test_custommap.rb
@@ -70,13 +70,19 @@ def setup
def teardown
@server.shutdown if @server
if @t
- @t.kill
- @t.join
+ # join with a bound, falling back to kill only if genuinely
+ # stuck (see git history: unconditional immediate kill raced
+ # WEBrick's own async listener cleanup and occasionally leaked
+ # the port).
+ unless @t.join(10)
+ @t.kill
+ @t.join
+ end
end
@client.reset_stream if @client
end
- def test_map
+ def test_map_with_frozen_literals
h = {'a' => 1, 'b' => 2}
soap = SOAP::Marshal.marshal(h)
puts soap if $DEBUG
@@ -89,19 +95,32 @@ def test_map
assert_equal(h, obj)
end
+ def test_map
+ h = {String.new('a') => 1, String.new('b') => 2}
+ soap = SOAP::Marshal.marshal(h)
+ puts soap if $DEBUG
+ obj = SOAP::Marshal.unmarshal(soap)
+ assert_equal(h, obj)
+ #
+ soap = SOAP::Marshal.marshal(h, Map)
+ puts soap if $DEBUG
+ obj = SOAP::Marshal.unmarshal(soap, Map)
+ assert_equal(h, obj)
+ end
+
def test_rpc
h = {'a' => 1, 'b' => 2}
- @client.wiredump_dev = str = ''
+ @client.wiredump_dev = str = String.new
assert_equal(h, @client.echo(h))
assert_equal(0, str.scan(/customname/).size)
#
@client.setmap
- @client.wiredump_dev = str = ''
+ @client.wiredump_dev = str = String.new
assert_equal(h, @client.echo(h))
assert_equal(4, str.scan(/customname/).size)
#
@client.mapping_registry = Map
- @client.wiredump_dev = str = ''
+ @client.wiredump_dev = str = String.new
assert_equal(h, @client.echo(h))
assert_equal(8, str.scan(/customname/).size)
end
diff --git a/test/soap/test_empty.rb b/test/soap/test_empty.rb
index d79cd69b1..9cd18d086 100644
--- a/test/soap/test_empty.rb
+++ b/test/soap/test_empty.rb
@@ -51,8 +51,14 @@ def setup
def teardown
@server.shutdown if @server
if @t
- @t.kill
- @t.join
+ # join with a bound, falling back to kill only if genuinely
+ # stuck (see git history: unconditional immediate kill raced
+ # WEBrick's own async listener cleanup and occasionally leaked
+ # the port).
+ unless @t.join(10)
+ @t.kill
+ @t.join
+ end
end
@client.reset_stream if @client
end
@@ -73,14 +79,14 @@ def teardown
]
def test_nop
- @client.wiredump_dev = str = ''
+ @client.wiredump_dev = str = String.new
@client.nop
assert_xml_equal(EMPTY_XML, parse_requestxml(str))
assert_xml_equal(EMPTY_XML, parse_responsexml(str))
end
def test_nop_nil
- @client.wiredump_dev = str = ''
+ @client.wiredump_dev = str = String.new
@client.nop_nil
assert_xml_equal(EMPTY_XML, parse_requestxml(str))
assert_xml_equal(EMPTY_XML, parse_responsexml(str))
@@ -88,7 +94,7 @@ def test_nop_nil
def test_empty_header
@client.headerhandler << EmptyHeaderHandler.new(nil)
- @client.wiredump_dev = str = ''
+ @client.wiredump_dev = str = String.new
@client.nop
assert_xml_equal(EMPTY_HEADER_XML, parse_requestxml(str))
end
diff --git a/test/soap/test_envelopenamespace.rb b/test/soap/test_envelopenamespace.rb
index d84b1a2ab..fda75ccc0 100644
--- a/test/soap/test_envelopenamespace.rb
+++ b/test/soap/test_envelopenamespace.rb
@@ -29,7 +29,7 @@ def teardown
end
def setup_server
- @server = WEBrick::HTTPServer.new(
+ @server = TestUtil.webrick_http_server(
:BindAddress => "0.0.0.0",
:Logger => @logger,
:Port => Port,
@@ -50,8 +50,14 @@ def setup_client
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
sleep 1
end
diff --git a/test/soap/test_httpconfigloader.rb b/test/soap/test_httpconfigloader.rb
index e2ec148b7..687786d3b 100644
--- a/test/soap/test_httpconfigloader.rb
+++ b/test/soap/test_httpconfigloader.rb
@@ -3,8 +3,6 @@
require 'soap/httpconfigloader'
require 'soap/rpc/driver'
-if defined?(HTTPClient)
-
module SOAP
@@ -30,6 +28,17 @@ def initialize(request_uri)
end
def test_property
+ # Assertions below (h.www_auth.basic_auth) assume httpclient's specific
+ # client shape, so this must check the ACTIVE backend (SOAP4R_HTTP_CLIENTS
+ # can force a different one -- see lib/soap/httpbackend.rb), not merely
+ # whether the gem happens to be loaded in this process (e.g.
+ # test/soap/ssl/test_ssl.rb requires it unconditionally regardless of the
+ # active backend). Unlike this class's other tests (which exercise
+ # HTTPConfigLoader.set_options against plain fakes and don't care which
+ # backend is active), this one drives a real SOAP::RPC::Driver end to end.
+ unless defined?(HTTPClient) and SOAP::HTTPStreamHandler::Client == HTTPClient
+ return
+ end
testpropertyname = File.join(DIR, 'soapclient.properties')
File.open(testpropertyname, "w") do |f|
f<<<<__EOP__
@@ -64,9 +73,55 @@ def test_property
File.unlink(testpropertyname) if File.file?(testpropertyname)
end
end
-end
+ # Regression test for the stale-bundled-CA-snapshot issue: httpclient's
+ # SSLConfig doesn't trust the system CA bundle unless told to, and its
+ # own lazy fallback (its gem-vendored cacert.pem) can go stale relative
+ # to a real server's cert chain. HTTPConfigLoader.set_options must call
+ # set_default_paths on every client's ssl_config by default.
+ class FakeSSLConfig
+ attr_reader :default_paths_called
+ attr_reader :trusted
+
+ def initialize
+ @default_paths_called = false
+ @trusted = []
+ end
+
+ def set_default_paths
+ @default_paths_called = true
+ end
+
+ def set_trust_ca(value)
+ @trusted << value
+ end
+ end
+
+ class FakeClient
+ attr_accessor :proxy
+ attr_accessor :no_proxy
+ attr_reader :ssl_config
+
+ def initialize
+ @ssl_config = FakeSSLConfig.new
+ end
+ end
+ def test_set_options_defaults_ssl_config_to_system_trust
+ client = FakeClient.new
+ SOAP::HTTPConfigLoader.set_options(client, ::SOAP::Property.new)
+ assert_equal(true, client.ssl_config.default_paths_called)
+ end
+
+ def test_set_options_still_layers_explicit_ca_file_on_top_of_default
+ client = FakeClient.new
+ options = ::SOAP::Property.new
+ options["ssl_config.ca_file"] = '/some/custom/ca.pem'
+ SOAP::HTTPConfigLoader.set_options(client, options)
+ assert_equal(true, client.ssl_config.default_paths_called)
+ assert_equal(['/some/custom/ca.pem'], client.ssl_config.trusted)
+ end
end
+
end
diff --git a/test/soap/test_nestedexception.rb b/test/soap/test_nestedexception.rb
index 7d99ad7d7..3e35aba4c 100644
--- a/test/soap/test_nestedexception.rb
+++ b/test/soap/test_nestedexception.rb
@@ -35,24 +35,45 @@ def test_nestedexception
rescue MyError => e
trace = e.backtrace.find_all { |line| /test\/unit/ !~ line && /\d\z/ !~ line }
trace = trace.map { |line| line.sub(/\A[^:]*/, '') }
- assert_equal(TOBE, trace)
+ # Ruby 3.4 changed backtrace formatting from `foo' to 'foo' (single
+ # quotes instead of a backtick+quote pair). Normalize both sides so
+ # this one detail doesn't need yet another version branch below.
+ normalize = lambda { |line| line.tr('`', "'") }
+ assert_equal(TOBE.map(&normalize), trace.map(&normalize))
end
end
- if (RUBY_VERSION.to_f >= 1.9)
+ if (RUBY_VERSION.to_f >= 3.4)
+ # Ruby 3.4 also stopped emitting a separate "rescue in X" pseudo-frame
+ # for a rescue clause inside the method itself -- that frame and the
+ # method's own frame collapse into one (same shape the < 1.9 backtraces
+ # below already had), and frame labels became fully qualified
+ # "Class#method" instead of the bare method name.
+ TOBE = [
+ ":16:in `SOAP::TestNestedException#foo'",
+ ":34:in `SOAP::TestNestedException#test_nestedexception'",
+ ":24:in `SOAP::TestNestedException#bar': bar (SOAP::TestNestedException::MyError) [NESTED]",
+ ":14:in `SOAP::TestNestedException#foo'",
+ ":34:in `SOAP::TestNestedException#test_nestedexception'",
+ ":29:in `SOAP::TestNestedException#baz': baz (SOAP::TestNestedException::MyError) [NESTED]",
+ ":22:in `SOAP::TestNestedException#bar'",
+ ":14:in `SOAP::TestNestedException#foo'",
+ ":34:in `SOAP::TestNestedException#test_nestedexception'"
+ ]
+ elsif (RUBY_VERSION.to_f >= 1.9)
TOBE = [
":16:in `rescue in foo'",
- ":13:in `foo'",
+ ":#{RESCUE_LINE_NUMBERS_FIXED ? 12 : 13}:in `foo'",
":34:in `test_nestedexception'",
":24:in `rescue in bar': bar (SOAP::TestNestedException::MyError) [NESTED]",
- ":21:in `bar'",
+ ":#{RESCUE_LINE_NUMBERS_FIXED ? 20 : 21}:in `bar'",
":14:in `foo'",
":34:in `test_nestedexception'",
":29:in `baz': baz (SOAP::TestNestedException::MyError) [NESTED]",
":22:in `bar'",
":14:in `foo'",
":34:in `test_nestedexception'"
- ]
+ ]
else
TOBE = [
":16:in `foo'",
diff --git a/test/soap/test_nethttpclient.rb b/test/soap/test_nethttpclient.rb
new file mode 100644
index 000000000..f8733b943
--- /dev/null
+++ b/test/soap/test_nethttpclient.rb
@@ -0,0 +1,49 @@
+# encoding: UTF-8
+require 'helper'
+require 'soap/netHttpClient'
+
+module SOAP
+
+
+# Covers SOAP::NetHttpClient#create_connection directly (via #send, since
+# it's private) rather than through the full driver/streamHandler stack:
+# by default HTTPStreamHandler only falls back to NetHttpClient when
+# httpclient fails to load, and this project's Gemfile always installs
+# httpclient, so nothing else in the suite exercises this file under the
+# default backend order. create_connection only builds and configures a
+# Net::HTTP object (no #start call), so this is safe to test without a real
+# network call or proxy server.
+#
+# For real end-to-end coverage of this backend (live WEBrick round trips,
+# proxying, basic auth, etc.), force it via SOAP4R_HTTP_CLIENTS=net_http --
+# see lib/soap/httpbackend.rb -- which is what CI's HTTP-backend sweep does.
+class TestNetHttpClient < Test::Unit::TestCase
+ TARGET = URI.parse("http://target.example.com/")
+
+ def test_proxy_with_credentials
+ client = NetHttpClient.new("http://myuser:mypass@myproxy.example.com:8080")
+ conn = client.send(:create_connection, TARGET)
+ assert_equal("myproxy.example.com", conn.proxy_address)
+ assert_equal(8080, conn.proxy_port)
+ assert_equal("myuser", conn.proxy_user)
+ assert_equal("mypass", conn.proxy_pass)
+ end
+
+ def test_proxy_without_credentials
+ client = NetHttpClient.new("http://myproxy.example.com:8080")
+ conn = client.send(:create_connection, TARGET)
+ assert_equal("myproxy.example.com", conn.proxy_address)
+ assert_equal(8080, conn.proxy_port)
+ assert_nil(conn.proxy_user)
+ assert_nil(conn.proxy_pass)
+ end
+
+ def test_no_proxy
+ client = NetHttpClient.new
+ conn = client.send(:create_connection, TARGET)
+ assert_nil(conn.proxy_address)
+ end
+end
+
+
+end
diff --git a/test/soap/test_nil.rb b/test/soap/test_nil.rb
index 87c85f42f..eb307b8c7 100644
--- a/test/soap/test_nil.rb
+++ b/test/soap/test_nil.rb
@@ -36,8 +36,14 @@ def setup
def teardown
@server.shutdown if @server
if @t
- @t.kill
- @t.join
+ # join with a bound, falling back to kill only if genuinely
+ # stuck (see git history: unconditional immediate kill raced
+ # WEBrick's own async listener cleanup and occasionally leaked
+ # the port).
+ unless @t.join(10)
+ @t.kill
+ @t.join
+ end
end
@client.reset_stream if @client
end
diff --git a/test/soap/test_no_indent.rb b/test/soap/test_no_indent.rb
index 222d41f13..3fab57d88 100644
--- a/test/soap/test_no_indent.rb
+++ b/test/soap/test_no_indent.rb
@@ -1,10 +1,9 @@
# encoding: UTF-8
require 'helper'
+require 'testutil'
require 'soap/rpc/standaloneServer'
require 'soap/rpc/driver'
-if defined?(HTTPClient)
-
module SOAP
@@ -36,8 +35,14 @@ def setup
def teardown
@server.shutdown if @server
if @t
- @t.kill
- @t.join
+ # join with a bound, falling back to kill only if genuinely
+ # stuck (see git history: unconditional immediate kill raced
+ # WEBrick's own async listener cleanup and occasionally leaked
+ # the port).
+ unless @t.join(10)
+ @t.kill
+ @t.join
+ end
end
@client.reset_stream if @client
end
@@ -65,25 +70,23 @@ def teardown
]
def test_indent
- @client.wiredump_dev = str = ''
+ @client.wiredump_dev = str = String.new
@client.options["soap.envelope.no_indent"] = false
@client.nop
assert_xml_equal(INDENT_XML, parse_requestxml(str))
end
def test_no_indent
- @client.wiredump_dev = str = ''
+ @client.wiredump_dev = str = String.new
@client.options["soap.envelope.no_indent"] = true
@client.nop
assert_xml_equal(NO_INDENT_XML, parse_requestxml(str))
end
def parse_requestxml(str)
- str.split(/\r?\n\r?\n/)[3]
+ TestUtil.parse_wiredump_request_body(str)
end
end
-end
-
end
diff --git a/test/soap/test_property.rb b/test/soap/test_property.rb
index 3f6952639..0a42ea81f 100644
--- a/test/soap/test_property.rb
+++ b/test/soap/test_property.rb
@@ -8,7 +8,9 @@ module SOAP
class TestProperty < Test::Unit::TestCase
- FrozenError = (RUBY_VERSION >= "1.9.0") ? RuntimeError : TypeError
+ unless defined?(FrozenError) # defined since 2.5
+ FrozenError = (RUBY_VERSION >= "1.9.0") ? RuntimeError : TypeError
+ end
def setup
@prop = ::SOAP::Property.new
diff --git a/test/soap/test_response_as_xml.rb b/test/soap/test_response_as_xml.rb
index f242576c6..8ae0ee965 100644
--- a/test/soap/test_response_as_xml.rb
+++ b/test/soap/test_response_as_xml.rb
@@ -53,8 +53,14 @@ def teardown
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def teardown_client
@@ -76,7 +82,10 @@ def teardown_client
__XML__
def test_hello
- assert_xml_equal("hello world", @client.hello("world"))
+ # return_response_as_xml is still false here, so the client returns plain
+ # text, not XML -- assert_xml_equal would try to parse "hello world" as
+ # an XML document and raise REXML::ParseException.
+ assert_equal("hello world", @client.hello("world"))
@client.return_response_as_xml = true
xml = @client.hello("world")
assert_xml_equal(RESPONSE_AS_XML, xml, [RESPONSE_AS_XML, xml].join("\n\n"))
diff --git a/test/soap/test_streamhandler.rb b/test/soap/test_streamhandler.rb
index d571486f8..e81050872 100644
--- a/test/soap/test_streamhandler.rb
+++ b/test/soap/test_streamhandler.rb
@@ -32,7 +32,7 @@ def teardown
end
def setup_server
- @server = WEBrick::HTTPServer.new(
+ @server = TestUtil.webrick_http_server(
:BindAddress => "0.0.0.0",
:Logger => @logger,
:Port => Port,
@@ -58,7 +58,7 @@ def setup_server
end
def setup_proxyserver
- @proxyserver = WEBrick::HTTPProxyServer.new(
+ @proxyserver = TestUtil.webrick_proxy_server(
:BindAddress => "0.0.0.0",
:Logger => @logger,
:Port => ProxyPort,
@@ -75,14 +75,26 @@ def setup_client
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def teardown_proxyserver
@proxyserver.shutdown
- @proxyserver_thread.kill
- @proxyserver_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @proxyserver_thread.join(10)
+ @proxyserver_thread.kill
+ @proxyserver_thread.join
+ end
end
def teardown_client
@@ -134,7 +146,7 @@ def parse_req_header_http_access2(str)
end
def test_normal
- str = ""
+ str = String.new
@client.wiredump_dev = str
assert_nil(@client.do_server_proc)
r, h = parse_req_header(str)
@@ -147,7 +159,7 @@ def test_uri
@client = SOAP::RPC::Driver.new(URI.parse(@url), '')
@client.add_method("do_server_proc")
# same as test_normal
- str = ""
+ str = String.new
@client.wiredump_dev = str
assert_nil(@client.do_server_proc)
r, h = parse_req_header(str)
@@ -156,14 +168,20 @@ def test_uri
end
def test_basic_auth
- unless Object.const_defined?('HTTPClient')
+ # Checking Object.const_defined?('HTTPClient') alone isn't enough now
+ # that the HTTP client backend is independently selectable
+ # (SOAP4R_HTTP_CLIENTS -- see lib/soap/httpbackend.rb):
+ # test/soap/ssl/test_ssl.rb requires the httpclient gem unconditionally
+ # regardless of the active backend, so this must check the ACTIVE
+ # backend instead of merely whether the gem happens to be loaded.
+ unless defined?(HTTPClient) and SOAP::HTTPStreamHandler::Client == HTTPClient
# soap4r + net/http + basic_auth is not supported.
# use httpclient instead.
assert(true)
return
end
@client.endpoint_url = @url + 'basic_auth'
- str = ""
+ str = String.new
@client.wiredump_dev = str
@client.options['protocol.http.basic_auth']['0'] = [@url, "admin", "admin"]
assert_nil(@client.do_server_proc_basic_auth)
@@ -171,16 +189,28 @@ def test_basic_auth
assert_nil(@client.do_server_proc_basic_auth)
end
+ # Every backend that owns a NO_PROXY_HOSTS constant treats 'localhost' as
+ # never-proxied by default (see e.g. lib/soap/netHttpClient.rb,
+ # lib/soap/curbClient.rb, lib/soap/faradayClient.rb) -- this test needs
+ # the opposite for its own assertions, so it clears whichever class is
+ # ACTIVE, not a hardcoded HTTPClient-or-NetHttpClient binary choice (that
+ # binary check predates SOAP4R_HTTP_CLIENTS making more than two backends
+ # reachable -- see lib/soap/httpbackend.rb).
+ def no_proxy_hosts_holder
+ # const_defined?'s 2-arg (inherit) form is Ruby 1.9+ only -- the 1-arg
+ # form works identically here across every supported Ruby version since
+ # SOAP::HTTPStreamHandler::Client itself (not some ancestor) is always
+ # what actually defines NO_PROXY_HOSTS on every backend that has one.
+ SOAP::HTTPStreamHandler::Client.const_defined?(:NO_PROXY_HOSTS) ?
+ SOAP::HTTPStreamHandler::Client : nil
+ end
+
def test_proxy
- if Object.const_defined?('HTTPClient')
- backup = HTTPClient::NO_PROXY_HOSTS.dup
- HTTPClient::NO_PROXY_HOSTS.clear
- else
- backup = SOAP::NetHttpClient::NO_PROXY_HOSTS.dup
- SOAP::NetHttpClient::NO_PROXY_HOSTS.clear
- end
+ holder = no_proxy_hosts_holder
+ backup = holder ? holder::NO_PROXY_HOSTS.dup : nil
+ holder::NO_PROXY_HOSTS.clear if holder
setup_proxyserver
- str = ""
+ str = String.new
@client.wiredump_dev = str
@client.options["protocol.http.proxy"] = @proxyurl
assert_nil(@client.do_server_proc)
@@ -191,15 +221,12 @@ def test_proxy
@client.options["protocol.http.proxy"] = 'ftp://foo:8080'
end
ensure
- if Object.const_defined?('HTTPClient')
- HTTPClient::NO_PROXY_HOSTS.replace(backup)
- else
- SOAP::NetHttpClient::NO_PROXY_HOSTS.replace(backup)
- end
+ holder = no_proxy_hosts_holder
+ holder::NO_PROXY_HOSTS.replace(backup) if holder && backup
end
def test_charset
- str = ""
+ str = String.new
@client.wiredump_dev = str
@client.options["protocol.http.charset"] = "iso-8859-8"
assert_nil(@client.do_server_proc)
diff --git a/test/soap/test_styleuse.rb b/test/soap/test_styleuse.rb
index 05645388e..022fa66cf 100644
--- a/test/soap/test_styleuse.rb
+++ b/test/soap/test_styleuse.rb
@@ -238,8 +238,14 @@ def teardown
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def teardown_client
@@ -309,7 +315,11 @@ def test_doc_lit_doc_enc
def test_doc_enc_doc_lit
ret1, ret2 = @client.doc_enc_doc_lit('a', 1)
# literal Array
- assert_equal(['String', 'Fixnum'], ret1['obj1']['klass'])
+ if (RUBY_VERSION.to_f <= 2.3)
+ assert_equal(['String', 'Fixnum'], ret1['obj1']['klass'])
+ else
+ assert_equal(['String', 'Integer'], ret1['obj1']['klass'])
+ end
# same value
assert_equal(ret1['obj1']['klass'], ret2['obj2']['klass'])
# not the same object (not encoded)
diff --git a/test/soap/wsdlDriver/test_calc.rb b/test/soap/wsdlDriver/test_calc.rb
index 40076dd67..86347c34f 100644
--- a/test/soap/wsdlDriver/test_calc.rb
+++ b/test/soap/wsdlDriver/test_calc.rb
@@ -49,8 +49,14 @@ def teardown
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def teardown_client
diff --git a/test/soap/wsdlDriver/test_document.rb b/test/soap/wsdlDriver/test_document.rb
index f0234409c..1f10ce9c7 100644
--- a/test/soap/wsdlDriver/test_document.rb
+++ b/test/soap/wsdlDriver/test_document.rb
@@ -50,8 +50,14 @@ def teardown
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def teardown_client
diff --git a/test/soap/wsdlDriver/test_simpletype.rb b/test/soap/wsdlDriver/test_simpletype.rb
index 6af6cb903..1187e1331 100644
--- a/test/soap/wsdlDriver/test_simpletype.rb
+++ b/test/soap/wsdlDriver/test_simpletype.rb
@@ -57,8 +57,14 @@ def teardown
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def teardown_client
diff --git a/test/testutil.rb b/test/testutil.rb
index c0238b2b8..369e8c5c6 100644
--- a/test/testutil.rb
+++ b/test/testutil.rb
@@ -52,4 +52,56 @@ def self.start_server_thread(server)
}
t
end
+
+ def self.webrick_http_server(options)
+ webrick_server(WEBrick::HTTPServer, options)
+ end
+
+ def self.webrick_proxy_server(options)
+ webrick_server(WEBrick::HTTPProxyServer, options)
+ end
+
+ # Extracts the request body XML out of a wiredump_dev capture. Every HTTP
+ # client backend (see lib/soap/httpbackend.rb) writes the same block
+ # layout -- "Wire dump:", "= Request", the raw request-line+headers, the
+ # body, "= Response", the raw response-line+headers, the response body --
+ # separated by blank lines, so the body is always the 4th block (index 3)
+ # regardless of which backend produced the dump. This was confirmed by
+ # design, not by accident: SOAP::NetHttpClient/CurbClient/FaradayClient
+ # were all built to match httpclient's own block layout exactly (see the
+ # "Mirrors httpclient's wiredump block layout" comments in
+ # lib/soap/netHttpClient.rb, curbClient.rb, and faradayClient.rb) precisely
+ # so callers like this one don't need to special-case any of them.
+ def self.parse_wiredump_request_body(str)
+ str.split(/\r?\n\r?\n/)[3]
+ end
+
+ # Same deal, for the response body (block index 6 -- see
+ # parse_wiredump_request_body above for the full block layout).
+ def self.parse_wiredump_response_body(str)
+ str.split(/\r?\n\r?\n/)[6]
+ end
+
+ def self.webrick_server(klass, options)
+ try = 0
+ begin
+ klass.new(options)
+ rescue Errno::EADDRINUSE => e
+ STDERR.puts "Wait for available port for #{klass.name} (#{e.message}) [#{Thread.list.inspect}]"
+ sleep 1
+ # This budget got widened all the way to 120 (2min) chasing what
+ # turned out to be a real leak: several test teardown methods called
+ # Thread#kill immediately after WEBrick's own shutdown, racing its
+ # async listener cleanup and occasionally leaving the port bound (see
+ # the removed .kill calls across 36 test files). With that fixed, this
+ # only needs to cover genuine transient scheduling delay on a busy
+ # runner, not a real leak -- and a large budget here is actively
+ # harmful now: compounded across several tests in one run it produces
+ # multi-minute *hangs* rather than fast, visible failures (confirmed:
+ # run 28892185757 stalled 20+ minutes with several retries stacking).
+ # Pulled back down to 20s, comfortably above what transient delay
+ # needs, far below what turns a rare miss into a CI-wrecking stall.
+ ((try += 1) < 20) ? retry : raise(e)
+ end
+ end
end
diff --git a/test/wsdl/abstract/test_abstract.rb b/test/wsdl/abstract/test_abstract.rb
index 61bb70ca4..a0de1856f 100644
--- a/test/wsdl/abstract/test_abstract.rb
+++ b/test/wsdl/abstract/test_abstract.rb
@@ -84,8 +84,14 @@ def setup_classdef
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def pathname(filename)
diff --git a/test/wsdl/anonymous/test_anonymous.rb b/test/wsdl/anonymous/test_anonymous.rb
index 991c30a26..28810ceff 100644
--- a/test/wsdl/anonymous/test_anonymous.rb
+++ b/test/wsdl/anonymous/test_anonymous.rb
@@ -6,8 +6,6 @@
require 'soap/wsdlDriver'
-if defined?(HTTPClient)
-
module WSDL; module Anonymous
@@ -87,8 +85,14 @@ def setup_clientdef
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def pathname(filename)
@@ -127,5 +131,3 @@ def test_anonymous_mapping
end; end
-
-end
diff --git a/test/wsdl/any/test_any.rb b/test/wsdl/any/test_any.rb
index 6f18aa13a..83042eb2f 100644
--- a/test/wsdl/any/test_any.rb
+++ b/test/wsdl/any/test_any.rb
@@ -108,8 +108,14 @@ def setup_classdef
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def pathname(filename)
diff --git a/test/wsdl/attrdefault/attrdefault.wsdl b/test/wsdl/attrdefault/attrdefault.wsdl
new file mode 100644
index 000000000..018fb9229
--- /dev/null
+++ b/test/wsdl/attrdefault/attrdefault.wsdl
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/wsdl/attrdefault/test_attrdefault.rb b/test/wsdl/attrdefault/test_attrdefault.rb
new file mode 100644
index 000000000..cfeca03bf
--- /dev/null
+++ b/test/wsdl/attrdefault/test_attrdefault.rb
@@ -0,0 +1,83 @@
+# encoding: UTF-8
+require 'helper'
+require 'testutil'
+require 'wsdl/parser'
+require 'wsdl/soap/wsdl2ruby'
+
+
+module WSDL; module Attrdefault
+
+
+# Regression test for a fork-sourced fix, not yet merged upstream: see
+# attrdefault.wsdl for the schema this exercises. Today, `fixed` and
+# `default` XSD attribute constraints are parsed but silently discarded
+# by classDefCreator.rb's define_attribute, so every assertion below that
+# depends on them will FAIL (the class itself loads fine, unlike the
+# dupinitparams case -- this is a missing feature, not a crash). It will
+# pass once define_attribute honors attribute.fixed/attribute.default
+# (nedap/soap4r commit 0739995).
+class TestAttrDefault < Test::Unit::TestCase
+ DIR = File.dirname(File.expand_path(__FILE__))
+
+ def teardown
+ unless $DEBUG
+ File.unlink(pathname('attrdefault.rb')) if File.file?(pathname('attrdefault.rb'))
+ end
+ end
+
+ def pathname(filename)
+ File.join(DIR, filename)
+ end
+
+ def setup_classdef
+ gen = WSDL::SOAP::WSDL2Ruby.new
+ gen.location = pathname("attrdefault.wsdl")
+ gen.basedir = DIR
+ gen.logger.level = Logger::FATAL
+ gen.opt['classdef'] = nil
+ gen.opt['module_path'] = self.class.to_s.sub(/::[^:]+$/, '')
+ gen.opt['force'] = true
+ gen.run
+ end
+
+ def test_fixed_attribute_always_returns_fixed_value
+ setup_classdef
+ TestUtil.require(DIR, 'attrdefault.rb')
+ obj = Attrdefault_type.new
+ assert_equal("1.0", obj.xmlattr_version)
+ end
+
+ def test_fixed_attribute_has_no_setter
+ setup_classdef
+ TestUtil.require(DIR, 'attrdefault.rb')
+ obj = Attrdefault_type.new
+ assert_equal(false, obj.respond_to?(:xmlattr_version=))
+ end
+
+ def test_default_attribute_falls_back_when_unset
+ setup_classdef
+ TestUtil.require(DIR, 'attrdefault.rb')
+ obj = Attrdefault_type.new
+ assert_equal("en", obj.xmlattr_lang)
+ end
+
+ def test_default_attribute_setter_still_overrides
+ setup_classdef
+ TestUtil.require(DIR, 'attrdefault.rb')
+ obj = Attrdefault_type.new
+ obj.xmlattr_lang = "fr"
+ assert_equal("fr", obj.xmlattr_lang)
+ end
+
+ def test_plain_attribute_unaffected
+ setup_classdef
+ TestUtil.require(DIR, 'attrdefault.rb')
+ obj = Attrdefault_type.new
+ assert_nil(obj.xmlattr_plain)
+ obj.xmlattr_plain = "x"
+ assert_equal("x", obj.xmlattr_plain)
+ end
+end
+
+
+end; end
diff --git a/test/wsdl/choice/test_choice.rb b/test/wsdl/choice/test_choice.rb
index 47dee9f76..9b6d9b606 100644
--- a/test/wsdl/choice/test_choice.rb
+++ b/test/wsdl/choice/test_choice.rb
@@ -94,8 +94,14 @@ def setup_classdef
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def pathname(filename)
diff --git a/test/wsdl/complexcontent/test_echo.rb b/test/wsdl/complexcontent/test_echo.rb
index 1f549aba3..6de135967 100644
--- a/test/wsdl/complexcontent/test_echo.rb
+++ b/test/wsdl/complexcontent/test_echo.rb
@@ -67,8 +67,14 @@ def setup_classdef
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def pathname(filename)
diff --git a/test/wsdl/datetime/test_datetime.rb b/test/wsdl/datetime/test_datetime.rb
index 5987d71b7..d3bd6dddb 100644
--- a/test/wsdl/datetime/test_datetime.rb
+++ b/test/wsdl/datetime/test_datetime.rb
@@ -22,10 +22,7 @@ def setup
def setup_server
@server = DatetimePortTypeApp.new('Datetime server', nil, '0.0.0.0', Port)
@server.level = Logger::Severity::ERROR
- @t = Thread.new {
- Thread.current.abort_on_exception = true
- @server.start
- }
+ @t = TestUtil.start_server_thread(@server)
end
def setup_client
@@ -43,8 +40,14 @@ def teardown
def teardown_server
@server.shutdown
- @t.kill
- @t.join
+ # join with a bound, falling back to kill only if genuinely
+ # stuck (see git history: unconditional immediate kill raced
+ # WEBrick's own async listener cleanup and occasionally leaked
+ # the port).
+ unless @t.join(10)
+ @t.kill
+ @t.join
+ end
end
def teardown_client
diff --git a/test/wsdl/document/array/test_array.rb b/test/wsdl/document/array/test_array.rb
index 5335fd2e2..3c020c057 100644
--- a/test/wsdl/document/array/test_array.rb
+++ b/test/wsdl/document/array/test_array.rb
@@ -94,8 +94,14 @@ def setup_classdef
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def pathname(filename)
diff --git a/test/wsdl/document/test_nosoapaction.rb b/test/wsdl/document/test_nosoapaction.rb
index 7082d5708..a0dfd7867 100644
--- a/test/wsdl/document/test_nosoapaction.rb
+++ b/test/wsdl/document/test_nosoapaction.rb
@@ -68,8 +68,14 @@ def setup_server
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def test_with_soapaction
diff --git a/test/wsdl/document/test_number.rb b/test/wsdl/document/test_number.rb
index bef4b8249..0ec393baa 100644
--- a/test/wsdl/document/test_number.rb
+++ b/test/wsdl/document/test_number.rb
@@ -63,8 +63,14 @@ def setup_classdef
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def pathname(filename)
diff --git a/test/wsdl/document/test_rpc.rb b/test/wsdl/document/test_rpc.rb
index 3fc8701fe..0be5752e2 100644
--- a/test/wsdl/document/test_rpc.rb
+++ b/test/wsdl/document/test_rpc.rb
@@ -114,8 +114,14 @@ def setup_classdef
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def pathname(filename)
diff --git a/test/wsdl/dupinitparams/dupinitparams.wsdl b/test/wsdl/dupinitparams/dupinitparams.wsdl
new file mode 100644
index 000000000..365ad2fef
--- /dev/null
+++ b/test/wsdl/dupinitparams/dupinitparams.wsdl
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/wsdl/dupinitparams/test_dupinitparams.rb b/test/wsdl/dupinitparams/test_dupinitparams.rb
new file mode 100644
index 000000000..ce7804f0c
--- /dev/null
+++ b/test/wsdl/dupinitparams/test_dupinitparams.rb
@@ -0,0 +1,59 @@
+# encoding: UTF-8
+require 'helper'
+require 'testutil'
+require 'wsdl/parser'
+require 'wsdl/soap/wsdl2ruby'
+
+
+module WSDL; module Dupinitparams
+
+
+# Regression test for a fork-inspired fix: see dupinitparams.wsdl for the
+# shape of the bug (duplicate initialize params generated when the same
+# element name appears twice in one complexType's content). Before the
+# fix, this crashed with a SyntaxError while requiring the generated class
+# file, since `def initialize(shared = nil, shared = nil)` cannot be
+# parsed by Ruby (see krebbl/soap4r commit 381b9f1, "fix duplicate
+# initialize params issue"). Rather than adopting that fix as-is -- a
+# plain `init_params.uniq`, which would silently wire both occurrences of
+# "shared" onto a single @shared ivar, discarding one of the two values --
+# the second occurrence is instead renamed "shared_2", the same way
+# classDefCreator.rb already disambiguates colliding attribute constants
+# in define_attribute (see its own `const[constname] += 1` suffixing).
+# That keeps both values independently addressable instead of silently
+# merging them.
+class TestDupInitParams < Test::Unit::TestCase
+ DIR = File.dirname(File.expand_path(__FILE__))
+
+ def teardown
+ unless $DEBUG
+ File.unlink(pathname('dup.rb')) if File.file?(pathname('dup.rb'))
+ end
+ end
+
+ def pathname(filename)
+ File.join(DIR, filename)
+ end
+
+ def setup_classdef
+ gen = WSDL::SOAP::WSDL2Ruby.new
+ gen.location = pathname("dupinitparams.wsdl")
+ gen.basedir = DIR
+ gen.logger.level = Logger::FATAL
+ gen.opt['classdef'] = nil
+ gen.opt['module_path'] = self.class.to_s.sub(/::[^:]+$/, '')
+ gen.opt['force'] = true
+ gen.run
+ end
+
+ def test_generated_class_keeps_both_occurrences_independently_addressable
+ setup_classdef
+ TestUtil.require(DIR, 'dup.rb')
+ obj = Dup_type.new("hello", "world")
+ assert_equal("hello", obj.shared)
+ assert_equal("world", obj.shared_2)
+ end
+end
+
+
+end; end
diff --git a/test/wsdl/fault/test_fault.rb b/test/wsdl/fault/test_fault.rb
index 13049c584..5f8355594 100644
--- a/test/wsdl/fault/test_fault.rb
+++ b/test/wsdl/fault/test_fault.rb
@@ -65,8 +65,14 @@ def setup_classdef
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def pathname(filename)
diff --git a/test/wsdl/fault/test_multifault.rb b/test/wsdl/fault/test_multifault.rb
index 6669849e5..ba7f5f6b4 100644
--- a/test/wsdl/fault/test_multifault.rb
+++ b/test/wsdl/fault/test_multifault.rb
@@ -71,8 +71,14 @@ def setup_classdef
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def pathname(filename)
diff --git a/test/wsdl/group/expectedClassdef.rb b/test/wsdl/group/expectedClassdef.rb
index dd51b06d0..d2331dc57 100644
--- a/test/wsdl/group/expectedClassdef.rb
+++ b/test/wsdl/group/expectedClassdef.rb
@@ -30,7 +30,7 @@ def __xmlattr
end
def xmlattr_attr_min
- __xmlattr[AttrAttr_min]
+ __xmlattr[AttrAttr_min] || "0"
end
def xmlattr_attr_min=(value)
@@ -38,7 +38,7 @@ def xmlattr_attr_min=(value)
end
def xmlattr_attr_max
- __xmlattr[AttrAttr_max]
+ __xmlattr[AttrAttr_max] || "0"
end
def xmlattr_attr_max=(value)
@@ -55,5 +55,27 @@ def initialize(comment = nil, element = nil, eletype = nil, var = nil)
end
end
+# {urn:grouptype}groupdirect_type
+# comment - SOAP::SOAPString
+# element - SOAP::SOAPString
+# eletype - SOAP::SOAPString
+class Groupdirect_type
+ attr_accessor :comment
+ attr_reader :__xmlele_any
+ attr_accessor :element
+ attr_accessor :eletype
+
+ def set_any(elements)
+ @__xmlele_any = elements
+ end
+
+ def initialize(comment = nil, element = nil, eletype = nil)
+ @comment = comment
+ @__xmlele_any = nil
+ @element = element
+ @eletype = eletype
+ end
+end
+
end; end
diff --git a/test/wsdl/group/expectedMappingRegistry.rb b/test/wsdl/group/expectedMappingRegistry.rb
index 371a0c0b2..edd6e12f1 100644
--- a/test/wsdl/group/expectedMappingRegistry.rb
+++ b/test/wsdl/group/expectedMappingRegistry.rb
@@ -28,6 +28,19 @@ module EchoMappingRegistry
}
)
+ EncodedRegistry.register(
+ :class => WSDL::Group::Groupdirect_type,
+ :schema_type => XSD::QName.new(NsGrouptype, "groupdirect_type"),
+ :schema_element => [
+ ["comment", "SOAP::SOAPString", [0, 1]],
+ ["any", [nil, XSD::QName.new(NsXMLSchema, "anyType")]],
+ [ :choice,
+ ["element", ["SOAP::SOAPString", XSD::QName.new(nil, "element")]],
+ ["eletype", ["SOAP::SOAPString", XSD::QName.new(nil, "eletype")]]
+ ]
+ ]
+ )
+
LiteralRegistry.register(
:class => WSDL::Group::Groupele_type,
:schema_type => XSD::QName.new(NsGrouptype, "groupele_type"),
@@ -46,6 +59,19 @@ module EchoMappingRegistry
}
)
+ LiteralRegistry.register(
+ :class => WSDL::Group::Groupdirect_type,
+ :schema_type => XSD::QName.new(NsGrouptype, "groupdirect_type"),
+ :schema_element => [
+ ["comment", "SOAP::SOAPString", [0, 1]],
+ ["any", [nil, XSD::QName.new(NsXMLSchema, "anyType")]],
+ [ :choice,
+ ["element", ["SOAP::SOAPString", XSD::QName.new(nil, "element")]],
+ ["eletype", ["SOAP::SOAPString", XSD::QName.new(nil, "eletype")]]
+ ]
+ ]
+ )
+
LiteralRegistry.register(
:class => WSDL::Group::Groupele_type,
:schema_name => XSD::QName.new(NsGrouptype, "groupele"),
@@ -63,6 +89,19 @@ module EchoMappingRegistry
XSD::QName.new(nil, "attr_max") => "SOAP::SOAPDecimal"
}
)
+
+ LiteralRegistry.register(
+ :class => WSDL::Group::Groupdirect_type,
+ :schema_name => XSD::QName.new(NsGrouptype, "groupdirect"),
+ :schema_element => [
+ ["comment", "SOAP::SOAPString", [0, 1]],
+ ["any", [nil, XSD::QName.new(NsXMLSchema, "anyType")]],
+ [ :choice,
+ ["element", ["SOAP::SOAPString", XSD::QName.new(nil, "element")]],
+ ["eletype", ["SOAP::SOAPString", XSD::QName.new(nil, "eletype")]]
+ ]
+ ]
+ )
end
end; end
diff --git a/test/wsdl/group/group.wsdl b/test/wsdl/group/group.wsdl
index 4012ea6f2..435bef647 100644
--- a/test/wsdl/group/group.wsdl
+++ b/test/wsdl/group/group.wsdl
@@ -50,6 +50,23 @@
+
+
+
+
+
+
+
diff --git a/test/wsdl/group/test_rpc.rb b/test/wsdl/group/test_rpc.rb
index 5f53b960d..8659b670c 100644
--- a/test/wsdl/group/test_rpc.rb
+++ b/test/wsdl/group/test_rpc.rb
@@ -79,8 +79,14 @@ def setup_classdef
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def pathname(filename)
@@ -97,6 +103,29 @@ def test_generate
compare("expectedDriver.rb", "echoDriver.rb")
end
+ # Regression test for a used as a complexType's
+ # sole, direct content (groupdirect_type in group.wsdl), as opposed to
+ # groupele_type's group ref nested inside an explicit .
+ # Before this was fixed, generating a class definition for a complexType
+ # shaped this way crashed with "undefined method 'have_any?' for an
+ # instance of WSDL::XMLSchema::Group" (classDefCreator.rb calling
+ # ComplexType#elements, which delegates straight to the bare Group
+ # instance since it's the complexType's only content, but Group didn't
+ # yet have any of the ComplexType-derived methods that requires). This
+ # test only needs test_generate (above) to have already run without
+ # raising to prove the crash is fixed; it additionally confirms the
+ # generated class actually has the fields pulled in from the referenced
+ # group (comment/element/eletype, from common_element's own nested
+ # group ref to common plus a choice) rather than silently coming up
+ # empty.
+ def test_groupdirect_classdef
+ obj = Groupdirect_type.new("a comment", "an element", nil)
+ assert_equal("a comment", obj.comment)
+ assert_equal("an element", obj.element)
+ assert_nil(obj.eletype)
+ assert_respond_to(obj, :set_any)
+ end
+
def test_wsdl
wsdl = File.join(DIR, 'group.wsdl')
@client = ::SOAP::WSDLDriverFactory.new(wsdl).create_rpc_driver
diff --git a/test/wsdl/list/test_list.rb b/test/wsdl/list/test_list.rb
index bf188887c..2ba7dd4ef 100644
--- a/test/wsdl/list/test_list.rb
+++ b/test/wsdl/list/test_list.rb
@@ -71,8 +71,14 @@ def setup_classdef
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def pathname(filename)
diff --git a/test/wsdl/map/test_map.rb b/test/wsdl/map/test_map.rb
index b8e55c499..f26d8b9d0 100644
--- a/test/wsdl/map/test_map.rb
+++ b/test/wsdl/map/test_map.rb
@@ -39,10 +39,7 @@ def setup_server
:SOAPDefaultNamespace => "urn:map"
)
@server.level = Logger::Severity::ERROR
- @t = Thread.new {
- Thread.current.abort_on_exception = true
- @server.start
- }
+ @t = TestUtil.start_server_thread(@server)
end
def setup_client
@@ -60,8 +57,14 @@ def teardown
def teardown_server
@server.shutdown
- @t.kill
- @t.join
+ # join with a bound, falling back to kill only if genuinely
+ # stuck (see git history: unconditional immediate kill raced
+ # WEBrick's own async listener cleanup and occasionally leaked
+ # the port).
+ unless @t.join(10)
+ @t.kill
+ @t.join
+ end
end
def teardown_client
diff --git a/test/wsdl/oneway/test_oneway.rb b/test/wsdl/oneway/test_oneway.rb
index 3365fa332..e4c180a88 100644
--- a/test/wsdl/oneway/test_oneway.rb
+++ b/test/wsdl/oneway/test_oneway.rb
@@ -78,8 +78,14 @@ def setup_classdef
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def pathname(filename)
diff --git a/test/wsdl/overload/test_overload.rb b/test/wsdl/overload/test_overload.rb
index 44ea4bcea..a8a234ea2 100644
--- a/test/wsdl/overload/test_overload.rb
+++ b/test/wsdl/overload/test_overload.rb
@@ -90,8 +90,14 @@ def setup_classdef
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def pathname(filename)
diff --git a/test/wsdl/qualified/test_qualified.rb b/test/wsdl/qualified/test_qualified.rb
index 7be7a06e0..515882ad5 100644
--- a/test/wsdl/qualified/test_qualified.rb
+++ b/test/wsdl/qualified/test_qualified.rb
@@ -6,8 +6,6 @@
require 'soap/wsdlDriver'
-if defined?(HTTPClient)
-
module WSDL
@@ -82,8 +80,14 @@ def setup_clientdef
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def pathname(filename)
@@ -114,7 +118,7 @@ def test_wsdl
Dir.chdir(backupdir)
end
@client.endpoint_url = "http://localhost:#{Port}/"
- @client.wiredump_dev = str = ''
+ @client.wiredump_dev = str = String.new
@client.GetPrimeNumbers(:Min => 2, :Max => 10)
assert_xml_equal(LOGIN_REQUEST_QUALIFIED, parse_requestxml(str),
[LOGIN_REQUEST_QUALIFIED, parse_requestxml(str)].join("\n\n"))
@@ -125,18 +129,16 @@ def test_naive
TestUtil.require(DIR, 'defaultDriver.rb', 'defaultMappingRegistry.rb', 'default.rb')
@client = PnumSoap.new("http://localhost:#{Port}/")
- @client.wiredump_dev = str = ''
+ @client.wiredump_dev = str = String.new
@client.getPrimeNumbers(GetPrimeNumbers.new(2, 10))
assert_xml_equal(LOGIN_REQUEST_QUALIFIED, parse_requestxml(str),
[LOGIN_REQUEST_QUALIFIED, parse_requestxml(str)].join("\n\n"))
end
def parse_requestxml(str)
- str.split(/\r?\n\r?\n/)[3]
+ TestUtil.parse_wiredump_request_body(str)
end
end
-end
-
end
diff --git a/test/wsdl/qualified/test_unqualified.rb b/test/wsdl/qualified/test_unqualified.rb
index 3ce427030..cc87cabda 100644
--- a/test/wsdl/qualified/test_unqualified.rb
+++ b/test/wsdl/qualified/test_unqualified.rb
@@ -6,8 +6,6 @@
require 'soap/wsdlDriver'
-if defined?(HTTPClient)
-
module WSDL
@@ -82,8 +80,14 @@ def setup_clientdef
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def pathname(filename)
@@ -115,7 +119,7 @@ def test_wsdl
Dir.chdir(backupdir)
end
@client.endpoint_url = "http://localhost:#{Port}/"
- @client.wiredump_dev = str = ''
+ @client.wiredump_dev = str = String.new
@client.login(:timezone => 'JST', :password => 'passwd',
:username => 'NaHi')
# untyped because of passing a Hash
@@ -127,17 +131,15 @@ def test_naive
TestUtil.require(DIR, 'lpDriver.rb', 'lpMappingRegistry.rb', 'lp.rb')
@client = Lp_porttype.new("http://localhost:#{Port}/")
- @client.wiredump_dev = str = ''
+ @client.wiredump_dev = str = String.new
@client.login(Login.new('NaHi', 'passwd', 'JST'))
assert_xml_equal(LOGIN_REQUEST_QUALIFIED_UNTYPED, parse_requestxml(str))
end
def parse_requestxml(str)
- str.split(/\r?\n\r?\n/)[3]
+ TestUtil.parse_wiredump_request_body(str)
end
end
-end
-
end
diff --git a/test/wsdl/raa/test_raa.rb b/test/wsdl/raa/test_raa.rb
index 9220a0324..7992b82da 100644
--- a/test/wsdl/raa/test_raa.rb
+++ b/test/wsdl/raa/test_raa.rb
@@ -38,10 +38,7 @@ def setup_server
require pathname('RAAService.rb')
@server = RAABaseServicePortTypeApp.new('RAA server', nil, '0.0.0.0', Port)
@server.level = Logger::Severity::ERROR
- @t = Thread.new {
- Thread.current.abort_on_exception = true
- @server.start
- }
+ @t = TestUtil.start_server_thread(@server)
end
def setup_client
@@ -65,8 +62,14 @@ def teardown
def teardown_server
@server.shutdown
- @t.kill
- @t.join
+ # join with a bound, falling back to kill only if genuinely
+ # stuck (see git history: unconditional immediate kill raced
+ # WEBrick's own async listener cleanup and occasionally leaked
+ # the port).
+ unless @t.join(10)
+ @t.kill
+ @t.join
+ end
end
def teardown_client
diff --git a/test/wsdl/ref/expectedProduct.rb b/test/wsdl/ref/expectedProduct.rb
index 9c31948d5..076f48c0c 100644
--- a/test/wsdl/ref/expectedProduct.rb
+++ b/test/wsdl/ref/expectedProduct.rb
@@ -194,7 +194,7 @@ def xmlattr_version=(value)
end
def xmlattr_yesno
- __xmlattr[AttrYesno]
+ __xmlattr[AttrYesno] || "Y"
end
def xmlattr_yesno=(value)
diff --git a/test/wsdl/ref/test_ref.rb b/test/wsdl/ref/test_ref.rb
index 82f027f64..195194045 100644
--- a/test/wsdl/ref/test_ref.rb
+++ b/test/wsdl/ref/test_ref.rb
@@ -95,8 +95,14 @@ def setup_classdef
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def pathname(filename)
diff --git a/test/wsdl/rpc/test_rpc.rb b/test/wsdl/rpc/test_rpc.rb
index 42321a846..2a77101f8 100644
--- a/test/wsdl/rpc/test_rpc.rb
+++ b/test/wsdl/rpc/test_rpc.rb
@@ -103,8 +103,14 @@ def setup_classdef
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def pathname(filename)
diff --git a/test/wsdl/rpc/test_rpc_lit.rb b/test/wsdl/rpc/test_rpc_lit.rb
index 60888a89e..66995b1ee 100644
--- a/test/wsdl/rpc/test_rpc_lit.rb
+++ b/test/wsdl/rpc/test_rpc_lit.rb
@@ -6,8 +6,6 @@
require 'soap/wsdlDriver'
-if defined?(HTTPClient) and defined?(OpenSSL)
-
module WSDL; module RPC
@@ -136,8 +134,14 @@ def setup_classdef
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def pathname(filename)
@@ -188,7 +192,7 @@ def test_wsdl_echoStringArray
def test_stub_echoStringArray
drv = SoapTestPortTypeRpcLit.new("http://localhost:#{Port}/")
- drv.wiredump_dev = str = ''
+ drv.wiredump_dev = str = String.new
drv.generate_explicit_type = false
# response contains only 1 part.
result = drv.echoStringArray(ArrayOfstring["a", "b", "c"])[0]
@@ -233,7 +237,7 @@ def test_stub_echoStringArray
def test_stub_echoStringArrayInline
drv = SoapTestPortTypeRpcLit.new("http://localhost:#{Port}/")
- drv.wiredump_dev = str = ''
+ drv.wiredump_dev = str = String.new
drv.generate_explicit_type = false
# response contains only 1 part.
result = drv.echoStringArrayInline(ArrayOfstringInline["a", "b", "c"])[0]
@@ -289,7 +293,7 @@ def test_wsdl_echoNestedStruct
wsdl = pathname('test-rpc-lit.wsdl')
@client = ::SOAP::WSDLDriverFactory.new(wsdl).create_rpc_driver
@client.endpoint_url = "http://localhost:#{Port}/"
- @client.wiredump_dev = str = ''
+ @client.wiredump_dev = str = String.new
@client.generate_explicit_type = false
# response contains only 1 part.
result = @client.echoNestedStruct(SOAPStructStruct.new("str", 1, 1.0, SOAPStruct.new("str", 1, 1.0)))[0]
@@ -346,7 +350,7 @@ def test_wsdl_echoNestedStruct_nil
wsdl = pathname('test-rpc-lit.wsdl')
@client = ::SOAP::WSDLDriverFactory.new(wsdl).create_rpc_driver
@client.endpoint_url = "http://localhost:#{Port}/"
- @client.wiredump_dev = str = ''
+ @client.wiredump_dev = str = String.new
@client.generate_explicit_type = false
result = @client.echoNestedStruct(SOAPStructStruct.new("str", nil, 1.0, SOAPStruct.new("str", ::SOAP::SOAPNil.new, 1.0)))[0]
assert(!result.respond_to?(:varInt))
@@ -358,7 +362,7 @@ def test_wsdl_echoNestedStruct_nil
def test_stub_echoNestedStruct
drv = SoapTestPortTypeRpcLit.new("http://localhost:#{Port}/")
- drv.wiredump_dev = str = ''
+ drv.wiredump_dev = str = String.new
drv.generate_explicit_type = false
# response contains only 1 part.
result = drv.echoNestedStruct(SOAPStructStruct.new("str", 1, 1.0, SOAPStruct.new("str", 1, 1.0)))[0]
@@ -374,7 +378,7 @@ def test_stub_echoNestedStruct
def test_stub_echoNestedStruct_nil
drv = SoapTestPortTypeRpcLit.new("http://localhost:#{Port}/")
- drv.wiredump_dev = str = ''
+ drv.wiredump_dev = str = String.new
drv.generate_explicit_type = false
# response contains only 1 part.
result = drv.echoNestedStruct(SOAPStructStruct.new("str", nil, 1.0, SOAPStruct.new("str", ::SOAP::SOAPNil.new, 1.0)))[0]
@@ -435,7 +439,7 @@ def test_wsdl_echoStructArray
wsdl = pathname('test-rpc-lit.wsdl')
@client = ::SOAP::WSDLDriverFactory.new(wsdl).create_rpc_driver
@client.endpoint_url = "http://localhost:#{Port}/"
- @client.wiredump_dev = str = ''
+ @client.wiredump_dev = str = String.new
@client.generate_explicit_type = false
# response contains only 1 part.
e = SOAPStruct.new("str", 2, 2.1)
@@ -447,7 +451,7 @@ def test_wsdl_echoStructArray
def test_stub_echoStructArray
drv = SoapTestPortTypeRpcLit.new("http://localhost:#{Port}/")
- drv.wiredump_dev = str = ''
+ drv.wiredump_dev = str = String.new
drv.generate_explicit_type = false
# response contains only 1 part.
e = SOAPStruct.new("str", 2, 2.1)
@@ -457,15 +461,13 @@ def test_stub_echoStructArray
end
def parse_requestxml(str)
- str.split(/\r?\n\r?\n/)[3]
+ TestUtil.parse_wiredump_request_body(str)
end
def parse_responsexml(str)
- str.split(/\r?\n\r?\n/)[6]
+ TestUtil.parse_wiredump_response_body(str)
end
end
end; end
-
-end
diff --git a/test/wsdl/simplecontent/test_simplecontent.rb b/test/wsdl/simplecontent/test_simplecontent.rb
index 41683d05c..dd0e44dba 100644
--- a/test/wsdl/simplecontent/test_simplecontent.rb
+++ b/test/wsdl/simplecontent/test_simplecontent.rb
@@ -68,8 +68,14 @@ def setup_classdef
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def pathname(filename)
diff --git a/test/wsdl/simpletype/test_simpletype.rb b/test/wsdl/simpletype/test_simpletype.rb
index 539867726..99e44febc 100644
--- a/test/wsdl/simpletype/test_simpletype.rb
+++ b/test/wsdl/simpletype/test_simpletype.rb
@@ -61,8 +61,14 @@ def teardown
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def teardown_client
diff --git a/test/wsdl/soap/test_soapbodyparts.rb b/test/wsdl/soap/test_soapbodyparts.rb
index 3243e25fd..457e989e3 100644
--- a/test/wsdl/soap/test_soapbodyparts.rb
+++ b/test/wsdl/soap/test_soapbodyparts.rb
@@ -39,10 +39,7 @@ def setup
def setup_server
@server = Server.new('Test', "urn:www.example.com:soapbodyparts:v1", '0.0.0.0', Port)
@server.level = Logger::Severity::ERROR
- @t = Thread.new {
- Thread.current.abort_on_exception = true
- @server.start
- }
+ @t = TestUtil.start_server_thread(@server)
end
def setup_client
@@ -59,8 +56,14 @@ def teardown
def teardown_server
@server.shutdown
- @t.kill
- @t.join
+ # join with a bound, falling back to kill only if genuinely
+ # stuck (see git history: unconditional immediate kill raced
+ # WEBrick's own async listener cleanup and occasionally leaked
+ # the port).
+ unless @t.join(10)
+ @t.kill
+ @t.join
+ end
end
def teardown_client
diff --git a/test/wsdl/soap/wsdl2ruby/soapenc/test_soapenc.rb b/test/wsdl/soap/wsdl2ruby/soapenc/test_soapenc.rb
index 2ffd24754..f9b5e588d 100644
--- a/test/wsdl/soap/wsdl2ruby/soapenc/test_soapenc.rb
+++ b/test/wsdl/soap/wsdl2ruby/soapenc/test_soapenc.rb
@@ -63,8 +63,14 @@ def setup_classdef
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def pathname(filename)
diff --git a/test/wsdl/soaptype/test_soaptype.rb b/test/wsdl/soaptype/test_soaptype.rb
index f48070376..b26584405 100644
--- a/test/wsdl/soaptype/test_soaptype.rb
+++ b/test/wsdl/soaptype/test_soaptype.rb
@@ -72,8 +72,14 @@ def setup_classdef
def teardown_server
@server.shutdown
- @server_thread.kill
- @server_thread.join
+ # join with a bound, falling back to kill only if the thread
+ # is genuinely stuck (not as an unconditional first resort --
+ # that raced WEBrick's own async listener cleanup and
+ # occasionally leaked the port; see git history).
+ unless @server_thread.join(10)
+ @server_thread.kill
+ @server_thread.join
+ end
end
def pathname(filename)
@@ -117,7 +123,7 @@ def test_wsdl
wsdl = File.join(DIR, 'soaptype.wsdl')
@client = ::SOAP::WSDLDriverFactory.new(wsdl).create_rpc_driver
@client.endpoint_url = "http://localhost:#{Port}/"
- @client.wiredump_dev = str = ''
+ @client.wiredump_dev = str = String.new
arg = Wrapper.new
arg.short = 123
@@ -134,7 +140,7 @@ def test_wsdl
def test_stub
@client = WSDL::RPC::Echo_port_type.new("http://localhost:#{Port}/")
- @client.wiredump_dev = str = ''
+ @client.wiredump_dev = str = String.new
arg = WSDL::RPC::Wrapper.new
arg.short = 123
@@ -153,7 +159,7 @@ def test_native
@client = ::SOAP::RPC::Driver.new("http://localhost:#{Port}/", 'urn:soaptype')
@client.endpoint_url = "http://localhost:#{Port}/"
@client.add_method('echo_soaptype', 'arg')
- @client.wiredump_dev = str = ''
+ @client.wiredump_dev = str = String.new
@client.mapping_registry = WSDL::RPC::EchoMappingRegistry::EncodedRegistry
@client.literal_mapping_registry = WSDL::RPC::EchoMappingRegistry::LiteralRegistry