diff --git a/packages/packages.json b/packages/packages.json index b6acaaf72..decec1440 100644 --- a/packages/packages.json +++ b/packages/packages.json @@ -17,13 +17,13 @@ "contract/valory/relayer/0.1.0": "bafybeihzgjyvhtorugjw3yldznqsbwo3aqpxowm7k2nrvj6qtwpsc7jl7u", "skill/valory/market_manager_abci/0.1.0": "bafybeicymkyk7e5rohauml6xazai6mv2rqd3euz3ifbk2ftfkylhpgo4ra", "skill/valory/decision_maker_abci/0.1.0": "bafybeicr3o4z5ytp6njmsxlj3p6cbaxyad6tdv5kyoxaeon5da2wn6hxqy", - "skill/valory/trader_abci/0.1.0": "bafybeieya5jxda7vacxeaowaoyafqtyldzjsloxnj5gcnschxoicfmua3y", - "skill/valory/tx_settlement_multiplexer_abci/0.1.0": "bafybeic6w4wdyvicy3mvmvskp3fg2snthofb3z5kmqhfohqfiv65k7hlgq", + "skill/valory/trader_abci/0.1.0": "bafybeigzvqxr6koeatwohgthqz7jnuxfdf5wr47yidu6x275azmz546nxa", + "skill/valory/tx_settlement_multiplexer_abci/0.1.0": "bafybeigplcf3dsovuxvyfkvx5p7u3yipeludxgy6535s3tkkn2s246djqa", "skill/valory/staking_abci/0.1.0": "bafybeiegy5iny4x3jk2kjuykebxksoqhfl7irdcldbcwzrsvpopxooys2q", "skill/valory/check_stop_trading_abci/0.1.0": "bafybeif6w4uc3pzrbuuhyktgehvwsq2gxv6udeivhlhijlguhmctxokdqa", - "agent/valory/trader/0.1.0": "bafybeiepmomqpnpmzwe2ed5qexpdnkmeaxhthuia4iqk7v24d6il4mgrh4", - "service/valory/trader/0.1.0": "bafybeigwrd7kjfygxvaxegnfu4l7vmdxcbe7hlur642mskzfoflahjnjdm", - "service/valory/trader_pearl/0.1.0": "bafybeiaxpxlmg7q3tg5sarbzntx443sgjvmkmgfhb4rvxgw4bdxkfejwhe" + "agent/valory/trader/0.1.0": "bafybeial2oamo6d6ffywoml4u4mhn2725t42ifuokbljlf6beuoxjwkgvy", + "service/valory/trader/0.1.0": "bafybeif4retqhv7fwtzqoxa4k6jktu7bjntlfzys3dvpybhekzspnuuug4", + "service/valory/trader_pearl/0.1.0": "bafybeig23bhhc5abobbmqyfu4nygneuuaxcupzshwybdzc47hqbnljrcbu" }, "third_party": { "protocol/open_aea/signing/1.0.0": "bafybeihv62fim3wl2bayavfcg3u5e5cxu3b7brtu4cn5xoxd6lqwachasi", diff --git a/packages/valory/agents/trader/aea-config.yaml b/packages/valory/agents/trader/aea-config.yaml index f13912808..c2b0ae859 100644 --- a/packages/valory/agents/trader/aea-config.yaml +++ b/packages/valory/agents/trader/aea-config.yaml @@ -47,10 +47,10 @@ skills: - valory/reset_pause_abci:0.1.0:bafybeiameewywqigpupy3u2iwnkfczeiiucue74x2l5lbge74rmw6bgaie - valory/termination_abci:0.1.0:bafybeif2zim2de356eo3sipkmoev5emwadpqqzk3huwqarywh4tmqt3vzq - valory/transaction_settlement_abci:0.1.0:bafybeic3tccdjypuge2lewtlgprwkbb53lhgsgn7oiwzyrcrrptrbeyote -- valory/tx_settlement_multiplexer_abci:0.1.0:bafybeic6w4wdyvicy3mvmvskp3fg2snthofb3z5kmqhfohqfiv65k7hlgq +- valory/tx_settlement_multiplexer_abci:0.1.0:bafybeigplcf3dsovuxvyfkvx5p7u3yipeludxgy6535s3tkkn2s246djqa - valory/market_manager_abci:0.1.0:bafybeicymkyk7e5rohauml6xazai6mv2rqd3euz3ifbk2ftfkylhpgo4ra - valory/decision_maker_abci:0.1.0:bafybeicr3o4z5ytp6njmsxlj3p6cbaxyad6tdv5kyoxaeon5da2wn6hxqy -- valory/trader_abci:0.1.0:bafybeieya5jxda7vacxeaowaoyafqtyldzjsloxnj5gcnschxoicfmua3y +- valory/trader_abci:0.1.0:bafybeigzvqxr6koeatwohgthqz7jnuxfdf5wr47yidu6x275azmz546nxa - valory/staking_abci:0.1.0:bafybeiegy5iny4x3jk2kjuykebxksoqhfl7irdcldbcwzrsvpopxooys2q - valory/check_stop_trading_abci:0.1.0:bafybeif6w4uc3pzrbuuhyktgehvwsq2gxv6udeivhlhijlguhmctxokdqa - valory/mech_interact_abci:0.1.0:bafybeih2cck5xu6yaibomwtm5zbcp6llghr3ighdnk56fzwu3ihu5xx35e diff --git a/packages/valory/services/trader/service.yaml b/packages/valory/services/trader/service.yaml index 3aa07a502..77b896944 100644 --- a/packages/valory/services/trader/service.yaml +++ b/packages/valory/services/trader/service.yaml @@ -7,7 +7,7 @@ license: Apache-2.0 fingerprint: README.md: bafybeigtuothskwyvrhfosps2bu6suauycolj67dpuxqvnicdrdu7yhtvq fingerprint_ignore_patterns: [] -agent: valory/trader:0.1.0:bafybeiepmomqpnpmzwe2ed5qexpdnkmeaxhthuia4iqk7v24d6il4mgrh4 +agent: valory/trader:0.1.0:bafybeial2oamo6d6ffywoml4u4mhn2725t42ifuokbljlf6beuoxjwkgvy number_of_agents: 4 deployment: agent: diff --git a/packages/valory/services/trader_pearl/service.yaml b/packages/valory/services/trader_pearl/service.yaml index cf4ae4b6a..e83335869 100644 --- a/packages/valory/services/trader_pearl/service.yaml +++ b/packages/valory/services/trader_pearl/service.yaml @@ -8,7 +8,7 @@ license: Apache-2.0 fingerprint: README.md: bafybeibg7bdqpioh4lmvknw3ygnllfku32oca4eq5pqtvdrdsgw6buko7e fingerprint_ignore_patterns: [] -agent: valory/trader:0.1.0:bafybeiepmomqpnpmzwe2ed5qexpdnkmeaxhthuia4iqk7v24d6il4mgrh4 +agent: valory/trader:0.1.0:bafybeial2oamo6d6ffywoml4u4mhn2725t42ifuokbljlf6beuoxjwkgvy number_of_agents: 1 deployment: agent: diff --git a/packages/valory/skills/trader_abci/skill.yaml b/packages/valory/skills/trader_abci/skill.yaml index 10905010d..cc14925c0 100644 --- a/packages/valory/skills/trader_abci/skill.yaml +++ b/packages/valory/skills/trader_abci/skill.yaml @@ -28,7 +28,7 @@ skills: - valory/termination_abci:0.1.0:bafybeif2zim2de356eo3sipkmoev5emwadpqqzk3huwqarywh4tmqt3vzq - valory/market_manager_abci:0.1.0:bafybeicymkyk7e5rohauml6xazai6mv2rqd3euz3ifbk2ftfkylhpgo4ra - valory/decision_maker_abci:0.1.0:bafybeicr3o4z5ytp6njmsxlj3p6cbaxyad6tdv5kyoxaeon5da2wn6hxqy -- valory/tx_settlement_multiplexer_abci:0.1.0:bafybeic6w4wdyvicy3mvmvskp3fg2snthofb3z5kmqhfohqfiv65k7hlgq +- valory/tx_settlement_multiplexer_abci:0.1.0:bafybeigplcf3dsovuxvyfkvx5p7u3yipeludxgy6535s3tkkn2s246djqa - valory/staking_abci:0.1.0:bafybeiegy5iny4x3jk2kjuykebxksoqhfl7irdcldbcwzrsvpopxooys2q - valory/check_stop_trading_abci:0.1.0:bafybeif6w4uc3pzrbuuhyktgehvwsq2gxv6udeivhlhijlguhmctxokdqa - valory/mech_interact_abci:0.1.0:bafybeih2cck5xu6yaibomwtm5zbcp6llghr3ighdnk56fzwu3ihu5xx35e diff --git a/packages/valory/skills/tx_settlement_multiplexer_abci/skill.yaml b/packages/valory/skills/tx_settlement_multiplexer_abci/skill.yaml index 065737d9d..fbe930dbc 100644 --- a/packages/valory/skills/tx_settlement_multiplexer_abci/skill.yaml +++ b/packages/valory/skills/tx_settlement_multiplexer_abci/skill.yaml @@ -15,7 +15,9 @@ fingerprint: models.py: bafybeigtmxoecoow663hgqnyinxarlrttyyt5ghpbdamdv4tc4kikcfx3a rounds.py: bafybeig3dhhrf5tkj63b3bk2mqfprcwzk3galz2ukzvdenz4g2femaixku tests/__init__.py: bafybeiat74pbtmxvylsz7karp57qp2v7y6wtrsz572jkrghbcssoudgjay + tests/test_dialogues.py: bafybeidzr3xck5mi4ot3awq46ixkxlv37s6igk5zwh2bycx5bw25s7j54m tests/test_handlers.py: bafybeiayuktfupylm3p3ygufjb66swzxhpbmioqoffwuauakfgbkwrv7ma + tests/test_rounds.py: bafybeicag32wpr2ibgf6wramnupc4wtzt52itm727nuqpbmx4rwcll6nx4 fingerprint_ignore_patterns: [] connections: [] contracts: [] diff --git a/packages/valory/skills/tx_settlement_multiplexer_abci/tests/test_dialogues.py b/packages/valory/skills/tx_settlement_multiplexer_abci/tests/test_dialogues.py new file mode 100644 index 000000000..b462c3755 --- /dev/null +++ b/packages/valory/skills/tx_settlement_multiplexer_abci/tests/test_dialogues.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test the dialogues.py module of the skill.""" + +# pylint: skip-file + + +import packages.valory.skills.tx_settlement_multiplexer_abci.dialogues # noqa + + +def test_import() -> None: + """Test that the 'dialogues.py' Python module can be imported.""" diff --git a/packages/valory/skills/tx_settlement_multiplexer_abci/tests/test_rounds.py b/packages/valory/skills/tx_settlement_multiplexer_abci/tests/test_rounds.py new file mode 100644 index 000000000..a92e2d33f --- /dev/null +++ b/packages/valory/skills/tx_settlement_multiplexer_abci/tests/test_rounds.py @@ -0,0 +1,379 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2023-2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains the tests for the PreTxSettlementRound of TxSettlementMultiplexerAbciApp.""" + +import json +from dataclasses import dataclass, field +from typing import ( + Any, + Callable, + Dict, + FrozenSet, + Hashable, + List, + Mapping, + Optional, + Type, +) +from unittest.mock import MagicMock, patch + +import pytest + +from packages.valory.skills.abstract_round_abci.base import ( + CollectionRound, + VotingRound, + get_name, +) +from packages.valory.skills.abstract_round_abci.test_tools.rounds import ( + BaseVotingRoundTest, +) +from packages.valory.skills.decision_maker_abci.payloads import VotingPayload +from packages.valory.skills.tx_settlement_multiplexer_abci.rounds import ( + BetPlacementRound, + CallCheckpointRound, + ChecksPassedRound, + Event, + FailedMultiplexerRound, + FinishedBetPlacementTxRound, + FinishedMechRequestTxRound, + FinishedRedeemingTxRound, + FinishedStakingTxRound, + FinishedSubscriptionTxRound, + MechRequestRound, + PostTxSettlementRound, + PreTxSettlementRound, + RedeemRound, + SubscriptionRound, + SynchronizedData, + TxSettlementMultiplexerAbciApp, +) + + +DUMMY_PAYLOAD_DATA = {"example_key": "example_value"} + + +def get_participants() -> FrozenSet[str]: + """Participants.""" + return frozenset([f"agent_{i}" for i in range(MAX_PARTICIPANTS)]) + + +def get_participant_to_votes( + participants: FrozenSet[str], vote: bool +) -> Dict[str, VotingPayload]: + """participant_to_votes""" + + return { + participant: VotingPayload(sender=participant, vote=vote) + for participant in participants + } + + +def get_participant_to_votes_serialized( + participants: FrozenSet[str], vote: bool +) -> Dict[str, Dict[str, Any]]: + """participant_to_votes""" + + return CollectionRound.serialize_collection( + get_participant_to_votes(participants, vote) + ) + + +def get_payloads( + payload_cls: Type[VotingPayload], + data: Optional[str], +) -> Mapping[str, VotingPayload]: + """Get payloads.""" + return { + participant: payload_cls(participant, data is not None) + for participant in get_participants() + } + + +def get_dummy_tx_settlement_payload_serialized() -> str: + """Dummy payload serialization""" + return json.dumps(DUMMY_PAYLOAD_DATA, sort_keys=True) + + +@dataclass +class RoundTestCase: + """RoundTestCase""" + + name: str + initial_data: Dict[str, Hashable] + payloads: Mapping[str, VotingPayload] + final_data: Dict[str, Hashable] + event: Event + most_voted_payload: Any + synchronized_data_attr_checks: List[Callable] = field(default_factory=list) + + +MAX_PARTICIPANTS: int = 4 + + +class BasePreTxSettlementRoundTest(BaseVotingRoundTest): + """Base test class for TxSettlementMultiplexer rounds derived from VotingRound.""" + + test_class: Type[VotingRound] + test_payload: Type[VotingPayload] + + def _test_voting_round( + self, vote: bool, expected_event: Any, threshold_check: Callable + ) -> None: + """Helper method to test voting rounds with positive or negative votes.""" + + test_round = self.test_class( + synchronized_data=self.synchronized_data, context=MagicMock() + ) + + self._complete_run( + self._test_round( + test_round=test_round, + round_payloads=get_participant_to_votes(self.participants, vote=vote), + synchronized_data_update_fn=lambda _synchronized_data, _: _synchronized_data.update( + participant_to_votes=get_participant_to_votes_serialized( + self.participants, vote=vote + ) + ), + synchronized_data_attr_checks=[ + lambda _synchronized_data: _synchronized_data.participant_to_votes.keys() + ] + if vote + else [], + exit_event=expected_event, + threshold_check=threshold_check, + ) + ) + + def test_positive_votes(self) -> None: + """Test PreTxSettlementRound for positive votes.""" + self._test_voting_round( + vote=True, + expected_event=Event.CHECKS_PASSED, + threshold_check=lambda x: x.positive_vote_threshold_reached, + ) + + def test_negative_votes(self) -> None: + """Test PreTxSettlementRound for negative votes.""" + self._test_voting_round( + vote=False, + expected_event=Event.REFILL_REQUIRED, + threshold_check=lambda x: x.negative_vote_threshold_reached, + ) + + +class TestPreTxSettlementRound(BasePreTxSettlementRoundTest): + """Tests for PreTxSettlementRound.""" + + test_class = PreTxSettlementRound + _event_class = Event + _synchronized_data_class = SynchronizedData + + @pytest.mark.parametrize( + "test_case", + ( + RoundTestCase( + name="Happy path", + initial_data={}, + payloads=get_payloads( + payload_cls=VotingPayload, + data=get_dummy_tx_settlement_payload_serialized(), + ), + final_data={}, + event=Event.CHECKS_PASSED, + most_voted_payload=get_dummy_tx_settlement_payload_serialized(), + synchronized_data_attr_checks=[ + lambda sync_data: sync_data.db.get( + get_name(SynchronizedData.participant_to_votes) + ) + == CollectionRound.deserialize_collection( + json.loads(get_dummy_tx_settlement_payload_serialized()) + ) + ], + ), + RoundTestCase( + name="Negative votes", + initial_data={}, + payloads=get_payloads( + payload_cls=VotingPayload, + data=get_dummy_tx_settlement_payload_serialized(), + ), + final_data={}, + event=Event.REFILL_REQUIRED, + most_voted_payload=get_dummy_tx_settlement_payload_serialized(), + synchronized_data_attr_checks=[], + ), + ), + ) + def test_run(self, test_case: RoundTestCase) -> None: + """Run tests.""" + if test_case.event == Event.CHECKS_PASSED: + self.test_positive_votes() + elif test_case.event == Event.REFILL_REQUIRED: + self.test_negative_votes() + + +class TestPostTxSettlementRound: + """Tests for PostTxSettlementRound.""" + + def setup_method(self) -> None: + """Setup the synchronized_data for each test.""" + self.synchronized_data = MagicMock() + self.synchronized_data.db = MagicMock() + self.synchronized_data.policy = MagicMock() + self.synchronized_data.utilized_tools = {} + self.synchronized_data.mech_tool = "tool_2" + self.synchronized_data.final_tx_hash = "hash_123" + + def test_end_block_unknown(self) -> None: + """Test the end_block logic for unknown tx_submitter.""" + # Arrange + self.synchronized_data.tx_submitter = "unknown_submitter" + round_ = PostTxSettlementRound( + synchronized_data=self.synchronized_data, context=MagicMock() + ) + # Act + result = round_.end_block() + assert result is not None + _, event = result + # Assert + assert event == Event.UNRECOGNIZED + + def test_mech_requesting_done_event_updates_policy(self) -> None: + """Test the MECH_REQUESTING_DONE event updates policy.""" + # Arrange + event = Event.MECH_REQUESTING_DONE + + with patch.object(self.synchronized_data.policy, "tool_used") as mock_tool_used: + with patch.object( + self.synchronized_data.policy, "serialize" + ) as mock_serialize: + if event == Event.MECH_REQUESTING_DONE: + self.synchronized_data.policy.tool_used( + self.synchronized_data.mech_tool + ) + serialized_policy = self.synchronized_data.policy.serialize() + self.synchronized_data.update(policy=serialized_policy) + + mock_tool_used.assert_called_once_with(self.synchronized_data.mech_tool) + mock_serialize.assert_called_once() + + def test_bet_placement_done_event_updates_policy(self) -> None: + """Test the BET_PLACEMENT_DONE event updates policy.""" + # Arrange + event = Event.BET_PLACEMENT_DONE + + with patch.object(self.synchronized_data.policy, "tool_used") as mock_tool_used: + with patch.object( + self.synchronized_data.policy, "serialize" + ) as mock_serialize: + if event == Event.BET_PLACEMENT_DONE: + self.synchronized_data.policy.tool_used( + self.synchronized_data.mech_tool + ) + serialized_policy = self.synchronized_data.policy.serialize() + self.synchronized_data.update(policy=serialized_policy) + + mock_tool_used.assert_called_once_with(self.synchronized_data.mech_tool) + mock_serialize.assert_called_once() + + def test_end_block_mech_requesting_done(self) -> None: + """Test the end_block logic for MECH_REQUESTING_DONE event.""" + # Arrange + + self.synchronized_data.tx_submitter = MechRequestRound.auto_round_id() + round_ = PostTxSettlementRound( + synchronized_data=self.synchronized_data, context=MagicMock() + ) + # Act + result = round_.end_block() + assert result is not None + _, event = result + # Assert + if self.synchronized_data.tx_submitter in [ + MechRequestRound.auto_round_id(), + BetPlacementRound.auto_round_id(), + RedeemRound.auto_round_id(), + CallCheckpointRound.auto_round_id(), + SubscriptionRound.auto_round_id(), + ]: + assert event == Event.UNRECOGNIZED + + def test_end_block_bet_placement_done(self) -> None: + """Test the end_block logic for BET_PLACEMENT_DONE event.""" + # Arrange + self.synchronized_data.tx_submitter = BetPlacementRound.auto_round_id() + round_ = PostTxSettlementRound( + synchronized_data=self.synchronized_data, context=MagicMock() + ) + # Act + result = round_.end_block() + assert result is not None + _, event = result + # Assert + if self.synchronized_data.tx_submitter in [ + MechRequestRound.auto_round_id(), + BetPlacementRound.auto_round_id(), + RedeemRound.auto_round_id(), + CallCheckpointRound.auto_round_id(), + SubscriptionRound.auto_round_id(), + ]: + assert event == Event.UNRECOGNIZED + + +def test_tx_settlement_abci_app_initialization() -> None: + """Test the initialization of TxSettlementMultiplexerAbciApp.""" + abci_app = TxSettlementMultiplexerAbciApp( + synchronized_data=MagicMock(), logger=MagicMock(), context=MagicMock() + ) + assert abci_app.initial_round_cls is PreTxSettlementRound + assert abci_app.final_states == { + ChecksPassedRound, + FinishedMechRequestTxRound, + FinishedBetPlacementTxRound, + FinishedRedeemingTxRound, + FinishedStakingTxRound, + FinishedSubscriptionTxRound, + FailedMultiplexerRound, + } + assert abci_app.transition_function == { + PreTxSettlementRound: { + Event.CHECKS_PASSED: ChecksPassedRound, + Event.REFILL_REQUIRED: PreTxSettlementRound, + Event.NO_MAJORITY: PreTxSettlementRound, + Event.ROUND_TIMEOUT: PreTxSettlementRound, + }, + PostTxSettlementRound: { + Event.MECH_REQUESTING_DONE: FinishedMechRequestTxRound, + Event.BET_PLACEMENT_DONE: FinishedBetPlacementTxRound, + Event.REDEEMING_DONE: FinishedRedeemingTxRound, + Event.STAKING_DONE: FinishedStakingTxRound, + Event.SUBSCRIPTION_DONE: FinishedSubscriptionTxRound, + Event.ROUND_TIMEOUT: PostTxSettlementRound, + Event.UNRECOGNIZED: FailedMultiplexerRound, + }, + ChecksPassedRound: {}, + FinishedMechRequestTxRound: {}, + FinishedBetPlacementTxRound: {}, + FinishedRedeemingTxRound: {}, + FinishedStakingTxRound: {}, + FinishedSubscriptionTxRound: {}, + FailedMultiplexerRound: {}, + } + assert abci_app.event_to_timeout == {Event.ROUND_TIMEOUT: 30.0} diff --git a/tox.ini b/tox.ini index 8c569a52f..f95572f84 100644 --- a/tox.ini +++ b/tox.ini @@ -106,7 +106,7 @@ commands = pytest -rfE {env:SKILLS_PATHS}/check_stop_trading_abci/tests --cov={env:SKILLS_PATHS}/check_stop_trading_abci --cov-report=xml --cov-report=term --cov-report=term-missing --cov-config=.coveragerc {posargs} pytest -rfE {env:SKILLS_PATHS}/market_manager_abci/tests --cov={env:SKILLS_PATHS}/market_manager_abci --cov-report=xml --cov-report=term --cov-report=term-missing --cov-config=.coveragerc {posargs} pytest -rfE {env:SKILLS_PATHS}/staking_abci/tests --cov={env:SKILLS_PATHS}/staking_abci --cov-report=xml --cov-report=term --cov-report=term-missing --cov-config=.coveragerc {posargs} - + pytest -rfE {env:SKILLS_PATHS}/tx_settlement_multiplexer_abci/tests --cov={env:SKILLS_PATHS}/tx_settlement_multiplexer_abci --cov-report=xml --cov-report=term --cov-report=term-missing --cov-config=.coveragerc {posargs} [testenv:py3.8-linux] basepython = python3.8 platform=^linux$