diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 78d4d05f..efee3ce9 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -1,9 +1,7 @@ name: Elixir CI env: - PROTOBUF_VERSION: "21.4" - PROTOBUF_LIB_VERSION_MAJOR: "32" - PROTOBUF_LIB_VERSION_MINOR: "0.4" + PROTOX_PROTOBUF_VERSION: "v29.2" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} MIX_ENV: test @@ -64,27 +62,8 @@ jobs: uses: actions/cache@v4 id: compile-conformance-test-runner with: - path: conformance-bin - key: ${{ runner.os }}-protobuf-${{ env.PROTOBUF_VERSION }} - - - name: Compile conformance-test-runner - if: steps.compile-conformance-test-runner.outputs.cache-hit != 'true' - run: | - mkdir -p ./conformance-bin/.libs - wget https://github.com/protocolbuffers/protobuf/archive/v${{ env.PROTOBUF_VERSION }}.tar.gz - tar xf v${{ env.PROTOBUF_VERSION }}.tar.gz - cd protobuf-${{ env.PROTOBUF_VERSION }} - ./autogen.sh && ./configure --disable-maintainer-mode --disable-dependency-tracking --disable-static - make -C ./src protoc - make -C conformance - cp ./conformance/.libs/conformance-test-runner ../conformance-bin - cp ./src/.libs/libprotobuf.so.${{ env.PROTOBUF_LIB_VERSION_MAJOR }}.${{ env.PROTOBUF_LIB_VERSION_MINOR }} ../conformance-bin/.libs/libprotobuf.so.${{ env.PROTOBUF_LIB_VERSION_MAJOR }} - - - name: Install protoc - run: | - wget https://github.com/protocolbuffers/protobuf/releases/download/v24.4/protoc-24.4-linux-x86_64.zip - unzip -d protoc protoc-24.4-linux-x86_64.zip - echo "${PWD}/protoc/bin" >> $GITHUB_PATH + path: conformance_test_runner + key: ${{ runner.os }}-protobuf-${{ env.PROTOX_PROTOBUF_VERSION }} - name: Unlock all dependencies run: mix deps.unlock --all @@ -96,13 +75,22 @@ jobs: mix local.hex --force mix deps.get + - name: Install protoc + run: | + wget https://github.com/protocolbuffers/protobuf/releases/download/v24.4/protoc-24.4-linux-x86_64.zip + unzip -d protoc protoc-24.4-linux-x86_64.zip + echo "${PWD}/protoc/bin" >> $GITHUB_PATH + - name: Compile prod with warnings as errors run: MIX_ENV=prod mix compile --warnings-as-errors + - name: Compile conformance-test-runner + if: steps.compile-conformance-test-runner.outputs.cache-hit != 'true' + run: | + mix protox.conformance --compile-only + cp ./deps/protobuf/bin/conformance_test_runner ./conformance_test_runner + - name: Run tests - env: - PROTOBUF_CONFORMANCE_RUNNER: conformance-bin/conformance-test-runner - LD_LIBRARY_PATH: conformance-bin/.libs run: mix coveralls.github --include conformance - name: Check formatting diff --git a/README.md b/README.md index 3f10867d..5d4cb58b 100644 --- a/README.md +++ b/README.md @@ -516,16 +516,10 @@ The protox library has been thoroughly tested using the conformance checker [pro Here's how to launch the conformance tests: -* Get conformance-test-runner [sources](https://github.com/protocolbuffers/protobuf/archive/refs/tags/v3.18.0.tar.gz). -* Compile conformance-test-runner ([macOS and Linux only](https://github.com/protocolbuffers/protobuf/tree/master/conformance#portability)): - ``` - tar xf protobuf-3.18.0.tar.gz && cd protobuf-3.18.0 && ./autogen.sh && ./configure && make -j && cd conformance && make -j - ``` +``` +mix protox.conformance +``` -* Launch the conformance tests: - ``` - mix protox.conformance --runner=/path/to/protobuf-3.18.0/conformance/conformance-test-runner - ``` * A report will be generated in the directory `conformance_report` and the following text should be displayed: ``` diff --git a/conformance/mix/tasks/protox/task.ex b/conformance/mix/tasks/protox/task.ex index 65f83df7..35cfb39e 100644 --- a/conformance/mix/tasks/protox/task.ex +++ b/conformance/mix/tasks/protox/task.ex @@ -6,33 +6,116 @@ defmodule Mix.Tasks.Protox.Conformance do @impl Mix.Task @spec run(any) :: any def run(args) do - with {options, _, []} <- OptionParser.parse(args, strict: [runner: :string, quiet: :boolean]), - {:ok, runner} <- Keyword.fetch(options, :runner), - quiet <- Keyword.get(options, :quiet, false), + with {options, _, []} <- + OptionParser.parse(args, + strict: [ + runner: :string, + quiet: :boolean, + force_runner_build: :boolean, + compile_only: :boolean + ] + ), + {:ok, runner} <- get_runner(options), :ok <- Mix.Tasks.Escript.Build.run([]), - :ok <- launch(runner, quiet) do + :ok <- launch_runner(options, runner) do {:ok, :conformance_successful} else + # We return :ok here because it means that the conformance test was launched, not necessarily successful. {:error, :runner_failure} -> {:ok, :conformance_failure} e -> e end end - defp launch(runner, quiet) do - shell = - case quiet do - true -> Mix.Shell.Quiet - false -> Mix.Shell.IO + defp launch_runner(options, runner) do + compile_only = Keyword.get(options, :compile_only, false) + + if compile_only do + :ok + else + shell = shell(options) + + cmd = + shell.cmd( + "#{runner} --enforce_recommended --failure_list ./conformance/failure_list.txt --output_dir . ./protox_conformance" + ) + + case cmd do + 0 -> :ok + 1 -> {:error, :runner_failure} + 126 -> {:error, :cannot_execute_runner} + 127 -> {:error, :no_such_file_or_directory} + code -> {:error, code} end + end + end + + defp get_runner(options) do + case Keyword.get(options, :runner) do + nil -> + runner_path = + Path.expand("#{Mix.Project.deps_paths().protobuf}/bin/conformance_test_runner") + + force_runner_build = Keyword.get(options, :force_runner_build, false) + + if File.exists?(runner_path) and not force_runner_build do + {:ok, runner_path} + else + with :ok <- configure_runner(options), + :ok <- build_runner(options) do + {:ok, runner_path} + end + end + + runner_path -> + {:ok, runner_path} + end + end + + defp configure_runner(options) do + shell = shell(options) + + configuration = + [ + {"CMAKE_CXX_STANDARD", "14"}, + {"protobuf_INSTALL", "OFF"}, + {"protobuf_BUILD_TESTS", "OFF"}, + {"protobuf_BUILD_CONFORMANCE", "ON"}, + {"protobuf_BUILD_EXAMPLES", "OFF"}, + {"protobuf_BUILD_PROTOBUF_BINARIES", "ON"}, + {"protobuf_BUILD_PROTOC_BINARIES", "OFF"}, + {"protobuf_BUILD_LIBPROTOC", "OFF"}, + {"protobuf_BUILD_LIBUPB", "OFF"} + ] + |> Enum.map(fn {key, value} -> "-D#{key}=#{value}" end) + |> Enum.join(" ") + + File.cd!(Mix.Project.deps_paths().protobuf, fn -> + cmd = shell.cmd("cmake . #{configuration}") + + case cmd do + 0 -> :ok + code -> {:error, code} + end + end) + end + + defp build_runner(options) do + shell = shell(options) + + File.cd!(Mix.Project.deps_paths().protobuf, fn -> + cmd = shell.cmd("cmake --build . --parallel") + + case cmd do + 0 -> :ok + code -> {:error, code} + end + end) + end - case shell.cmd( - "#{runner} --enforce_recommended --failure_list ./conformance/failure_list.txt ./protox_conformance" - ) do - 0 -> :ok - 1 -> {:error, :runner_failure} - 126 -> {:error, :cannot_execute_runner} - 127 -> {:error, :no_such_file_or_directory} - code -> {:error, code} + defp shell(options) do + case Keyword.get(options, :quiet, false) do + true -> Mix.Shell.Quiet + false -> Mix.Shell.IO end end end diff --git a/conformance/protox/conformance/conformance.proto b/conformance/protox/conformance/conformance.proto deleted file mode 100644 index 13a3081e..00000000 --- a/conformance/protox/conformance/conformance.proto +++ /dev/null @@ -1,175 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// https://developers.google.com/protocol-buffers/ -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -syntax = "proto3"; -package conformance; -option java_package = "com.google.protobuf.conformance"; - -// This defines the conformance testing protocol. This protocol exists between -// the conformance test suite itself and the code being tested. For each test, -// the suite will send a ConformanceRequest message and expect a -// ConformanceResponse message. -// -// You can either run the tests in two different ways: -// -// 1. in-process (using the interface in conformance_test.h). -// -// 2. as a sub-process communicating over a pipe. Information about how to -// do this is in conformance_test_runner.cc. -// -// Pros/cons of the two approaches: -// -// - running as a sub-process is much simpler for languages other than C/C++. -// -// - running as a sub-process may be more tricky in unusual environments like -// iOS apps, where fork/stdin/stdout are not available. - -enum WireFormat { - UNSPECIFIED = 0; - PROTOBUF = 1; - JSON = 2; - JSPB = 3; // Google internal only. Opensource testees just skip it. - TEXT_FORMAT = 4; -} - -enum TestCategory { - UNSPECIFIED_TEST = 0; - BINARY_TEST = 1; // Test binary wire format. - JSON_TEST = 2; // Test json wire format. - // Similar to JSON_TEST. However, during parsing json, testee should ignore - // unknown fields. This feature is optional. Each implementation can descide - // whether to support it. See - // https://developers.google.com/protocol-buffers/docs/proto3#json_options - // for more detail. - JSON_IGNORE_UNKNOWN_PARSING_TEST = 3; - // Test jspb wire format. Google internal only. Opensource testees just skip it. - JSPB_TEST = 4; - // Test text format. For cpp, java and python, testees can already deal with - // this type. Testees of other languages can simply skip it. - TEXT_FORMAT_TEST = 5; -} - -// The conformance runner will request a list of failures as the first request. -// This will be known by message_type == "conformance.FailureSet", a conformance -// test should return a serialized FailureSet in protobuf_payload. -message FailureSet { - repeated string failure = 1; -} - -// Represents a single test case's input. The testee should: -// -// 1. parse this proto (which should always succeed) -// 2. parse the protobuf or JSON payload in "payload" (which may fail) -// 3. if the parse succeeded, serialize the message in the requested format. -message ConformanceRequest { - // The payload (whether protobuf of JSON) is always for a - // protobuf_test_messages.proto3.TestAllTypes proto (as defined in - // src/google/protobuf/proto3_test_messages.proto). - // - // TODO(haberman): if/when we expand the conformance tests to support proto2, - // we will want to include a field that lets the payload/response be a - // protobuf_test_messages.proto2.TestAllTypes message instead. - oneof payload { - bytes protobuf_payload = 1; - string json_payload = 2; - // Google internal only. Opensource testees just skip it. - string jspb_payload = 7; - string text_payload = 8; - } - - // Which format should the testee serialize its message to? - WireFormat requested_output_format = 3; - - // The full name for the test message to use; for the moment, either: - // protobuf_test_messages.proto3.TestAllTypesProto3 or - // protobuf_test_messages.proto2.TestAllTypesProto2. - string message_type = 4; - - // Each test is given a specific test category. Some category may need - // spedific support in testee programs. Refer to the definition of TestCategory - // for more information. - TestCategory test_category = 5; - - // Specify details for how to encode jspb. - JspbEncodingConfig jspb_encoding_options = 6; - - // This can be used in json and text format. If true, testee should print - // unknown fields instead of ignore. This feature is optional. - bool print_unknown_fields = 9; -} - -// Represents a single test case's output. -message ConformanceResponse { - oneof result { - // This string should be set to indicate parsing failed. The string can - // provide more information about the parse error if it is available. - // - // Setting this string does not necessarily mean the testee failed the - // test. Some of the test cases are intentionally invalid input. - string parse_error = 1; - - // If the input was successfully parsed but errors occurred when - // serializing it to the requested output format, set the error message in - // this field. - string serialize_error = 6; - - // This should be set if some other error occurred. This will always - // indicate that the test failed. The string can provide more information - // about the failure. - string runtime_error = 2; - - // If the input was successfully parsed and the requested output was - // protobuf, serialize it to protobuf and set it in this field. - bytes protobuf_payload = 3; - - // If the input was successfully parsed and the requested output was JSON, - // serialize to JSON and set it in this field. - string json_payload = 4; - - // For when the testee skipped the test, likely because a certain feature - // wasn't supported, like JSON input/output. - string skipped = 5; - - // If the input was successfully parsed and the requested output was JSPB, - // serialize to JSPB and set it in this field. JSPB is google internal only - // format. Opensource testees can just skip it. - string jspb_payload = 7; - - // If the input was successfully parsed and the requested output was - // TEXT_FORMAT, serialize to TEXT_FORMAT and set it in this field. - string text_payload = 8; - } -} - -// Encoding options for jspb format. -message JspbEncodingConfig { - // Encode the value field of Any as jspb array if true, otherwise binary. - bool use_jspb_array_any_format = 1; -} diff --git a/mix.exs b/mix.exs index b4196c14..f553cee4 100644 --- a/mix.exs +++ b/mix.exs @@ -51,6 +51,7 @@ defmodule Protox.Mixfile do {:propcheck, github: "alfert/propcheck", ref: "c564e89d", only: [:test, :dev]} ] |> maybe_add_muzak_pro() + |> maybe_download_protobuf() end defp maybe_add_muzak_pro(deps) do @@ -67,6 +68,25 @@ defmodule Protox.Mixfile do end end + defp maybe_download_protobuf(deps) do + case System.get_env("PROTOX_PROTOBUF_VERSION") do + nil -> + deps + + version -> + protobuf = + {:protobuf, + github: "protocolbuffers/protobuf", + tag: version, + submodules: true, + app: false, + compile: false, + only: [:dev, :test]} + + [protobuf | deps] + end + end + defp description do """ A fast, easy to use and 100% conformant Elixir library for Google Protocol Buffers (aka protobuf) diff --git a/mix.lock b/mix.lock index 6022febb..e9230495 100644 --- a/mix.lock +++ b/mix.lock @@ -14,6 +14,7 @@ "ex_doc": {:hex, :ex_doc, "0.36.1", "4197d034f93e0b89ec79fac56e226107824adcce8d2dd0a26f5ed3a95efc36b1", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d7d26a7cf965dacadcd48f9fa7b5953d7d0cfa3b44fa7a65514427da44eafd89"}, "excoveralls": {:hex, :excoveralls, "0.18.3", "bca47a24d69a3179951f51f1db6d3ed63bca9017f476fe520eb78602d45f7756", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "746f404fcd09d5029f1b211739afb8fb8575d775b21f6a3908e7ce3e640724c6"}, "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, + "google_protobuf": {:git, "https://github.com/protocolbuffers/protobuf.git", "233098326bc268fc03b28725c941519fc77703e6", [tag: "v29.2", submodules: true]}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, @@ -23,5 +24,6 @@ "poison": {:hex, :poison, "6.0.0", "9bbe86722355e36ffb62c51a552719534257ba53f3271dacd20fbbd6621a583a", [:mix], [{:decimal, "~> 2.1", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "bb9064632b94775a3964642d6a78281c07b7be1319e0016e1643790704e739a2"}, "propcheck": {:git, "https://github.com/alfert/propcheck.git", "c564e89d3873caf9c6bf64a2af4bb3890e24ecf1", [ref: "c564e89d"]}, "proper": {:git, "https://github.com/proper-testing/proper.git", "a5ae5669f01143b0828fc21667d4f5e344aa760b", [ref: "a5ae5669f01143b0828fc21667d4f5e344aa760b"]}, + "protobuf": {:git, "https://github.com/protocolbuffers/protobuf.git", "233098326bc268fc03b28725c941519fc77703e6", [tag: "v29.2", submodules: true]}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, } diff --git a/test/conformance_test.exs b/test/conformance_test.exs index 61af3229..89b8079a 100644 --- a/test/conformance_test.exs +++ b/test/conformance_test.exs @@ -5,15 +5,14 @@ defmodule Protox.ConformanceTest do test "Launch conformance" do {:ok, _} = File.rm_rf("./failing_tests.txt") - runner = System.get_env("PROTOBUF_CONFORMANCE_RUNNER") - assert runner != nil, "PROTOBUF_CONFORMANCE_RUNNER not set" + runner = Path.expand("#{Mix.Project.deps_paths().protobuf}/bin/conformance_test_runner") assert File.exists?(runner) # {:ok, _} here just means that the runner could be launched, not that the conformance # test performed correctly. We'll check the absence of the "failing_tests.txt" file # to verify this. - assert {:ok, _} = Mix.Tasks.Protox.Conformance.run(["--runner=#{runner}", "--quiet"]) + assert {:ok, _} = Mix.Tasks.Protox.Conformance.run(["--quiet"]) # protobuf conformance runner produces this file only when some tests have failed refute File.exists?("./failing_tests.txt"),