From 9f68495fd679dc31bc7da45d64f2db8f2ef22f78 Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Tue, 7 Feb 2023 16:42:42 +0100 Subject: [PATCH] strict versionning (forbid version not handled) --- lib/archethic/contracts/interpreter.ex | 13 ++-- .../contracts/interpreter/version1.ex | 17 +++-- test/archethic/contracts/interpreter_test.exs | 63 ++++++++++++------- test/support/contract_factory.ex | 44 +++++++++++++ 4 files changed, 102 insertions(+), 35 deletions(-) create mode 100644 test/support/contract_factory.ex diff --git a/lib/archethic/contracts/interpreter.ex b/lib/archethic/contracts/interpreter.ex index b59fbfa63..9771faf50 100644 --- a/lib/archethic/contracts/interpreter.ex +++ b/lib/archethic/contracts/interpreter.ex @@ -20,11 +20,14 @@ defmodule Archethic.Contracts.Interpreter do @spec parse(code :: binary()) :: {:ok, Contract.t()} | {:error, String.t()} def parse(code) when is_binary(code) do case version(code) do - {{0, _, _}, code_without_version} -> + {{0, 0, 1}, code_without_version} -> Version0.parse(code_without_version) {version = {1, _, _}, code_without_version} -> Version1.parse(code_without_version, version) + + _ -> + {:error, "@version not supported"} end end @@ -44,7 +47,7 @@ defmodule Archethic.Contracts.Interpreter do Return the version & the code where the version has been removed. (should be private, but there are unit tests) """ - @spec version(String.t()) :: {version(), String.t()} + @spec version(String.t()) :: {version(), String.t()} | :error def version(code) do regex_opts = [capture: :all_but_first] @@ -54,17 +57,17 @@ defmodule Archethic.Contracts.Interpreter do case Regex.run(version_attr_regex, code, regex_opts) do nil -> # there is a @version but syntax is invalid (probably the quotes missing) - raise "invalid @version" + :error [capture] -> case Regex.run(semver_regex(), capture, regex_opts) do nil -> # there is a @version but semver syntax is wrong - raise "invalid @version" + :error ["0", "0", "0"] -> # there is a @version but it's 0.0.0 - raise "invalid @version" + :error [major, minor, patch] -> { diff --git a/lib/archethic/contracts/interpreter/version1.ex b/lib/archethic/contracts/interpreter/version1.ex index eb9abc1cd..71a1c9785 100644 --- a/lib/archethic/contracts/interpreter/version1.ex +++ b/lib/archethic/contracts/interpreter/version1.ex @@ -1,12 +1,21 @@ defmodule Archethic.Contracts.Interpreter.Version1 do @moduledoc false + alias Archethic.Contracts.Contract alias Archethic.Contracts.ContractConditions, as: Conditions alias Archethic.TransactionChain.Transaction - @spec parse(code :: binary(), {integer(), integer(), integer()}) :: {:error, binary()} - def parse(code, {1, _, _}), do: parse_v1(code) + @doc """ + Parse the code and return the parsed contract. + """ + @spec parse(binary(), {integer(), integer(), integer()}) :: + {:ok, Contract.t()} | {:error, String.t()} + def parse(code, {1, 0, 0}) when is_binary(code) do + {:ok, %Contract{version: {1, 0, 0}}} + end + + def parse(_, _), do: {:error, "@version not supported"} @doc """ Return true if the given conditions are valid on the given constants @@ -24,8 +33,4 @@ defmodule Archethic.Contracts.Interpreter.Version1 do def execute_trigger(_ast, _constants) do nil end - - defp parse_v1(code) when is_binary(code) do - {:error, "not implemented"} - end end diff --git a/test/archethic/contracts/interpreter_test.exs b/test/archethic/contracts/interpreter_test.exs index 8c913ef62..46650c3fd 100644 --- a/test/archethic/contracts/interpreter_test.exs +++ b/test/archethic/contracts/interpreter_test.exs @@ -3,9 +3,41 @@ defmodule Archethic.Contracts.InterpreterTest do use ArchethicCase alias Archethic.Contracts.Interpreter + alias Archethic.ContractFactory doctest Interpreter + describe "strict versionning" do + test "should return ok if version exists" do + assert {:ok, _} = Interpreter.parse(ContractFactory.valid_version1_contract()) + assert {:ok, _} = Interpreter.parse(ContractFactory.valid_version0_contract()) + end + + test "should return an error if version does not exist yet" do + code_v0 = ~s""" + @version "0.144.233" + #{ContractFactory.valid_version0_contract()} + """ + + code_v1 = ~s""" + @version "1.377.610" + #{ContractFactory.valid_version1_contract(version_attribute: false)} + """ + + assert {:error, "@version not supported"} = Interpreter.parse(code_v0) + assert {:error, "@version not supported"} = Interpreter.parse(code_v1) + end + + test "should return an error if version is invalid" do + code_v0 = ~s""" + @version 12 + #{ContractFactory.valid_version0_contract()} + """ + + assert {:error, "@version not supported"} = Interpreter.parse(code_v0) + end + end + describe "version/1" do test "should return 0.0.1 if there is no interpreter tag" do code = ~s(some code) @@ -28,30 +60,13 @@ defmodule Archethic.Contracts.InterpreterTest do assert {{3, 105, 0}, _} = Interpreter.version(~s(\n \n @version "3.105.0" \n \n)) end - test "should raise if version is not formatted as expected" do - assert_raise RuntimeError, fn -> - Interpreter.version(~s(@version "0")) - end - - assert_raise RuntimeError, fn -> - Interpreter.version(~s(@version "1")) - end - - assert_raise RuntimeError, fn -> - Interpreter.version(~s(@version "0.0")) - end - - assert_raise RuntimeError, fn -> - Interpreter.version(~s(@version "1.1")) - end - - assert_raise RuntimeError, fn -> - Interpreter.version(~s(@version "0.0.0")) - end - - assert_raise RuntimeError, fn -> - Interpreter.version(~s(@version 1.1.1)) - end + test "should return error if version is not formatted as expected" do + assert :error = Interpreter.version(~s(@version "0")) + assert :error = Interpreter.version(~s(@version "1")) + assert :error = Interpreter.version(~s(@version "0.0")) + assert :error = Interpreter.version(~s(@version "1.1")) + assert :error = Interpreter.version(~s(@version "0.0.0")) + assert :error = Interpreter.version(~s(@version 1.1.1)) end end end diff --git a/test/support/contract_factory.ex b/test/support/contract_factory.ex new file mode 100644 index 000000000..4f3c58105 --- /dev/null +++ b/test/support/contract_factory.ex @@ -0,0 +1,44 @@ +defmodule Archethic.ContractFactory do + @moduledoc false + + def valid_version1_contract(opts \\ []) do + code = ~S""" + condition inherit: [ + content: true + ] + + condition transaction: [ + uco_transfers: Map.size() > 0 + ] + + actions triggered_by: transaction do + Contract.set_content "hello" + end + """ + + if Keyword.get(opts, :version_attribute, true) do + """ + @version "1.0.0" + #{code} + """ + else + code + end + end + + def valid_version0_contract() do + ~S""" + condition inherit: [ + content: true + ] + + condition transaction: [ + uco_transfers: size() > 0 + ] + + actions triggered_by: transaction do + set_content "hello" + end + """ + end +end