diff --git a/api/Pipfile b/api/Pipfile index 219dd380cc0..2a085b4d06e 100755 --- a/api/Pipfile +++ b/api/Pipfile @@ -32,6 +32,7 @@ flake8-annotations = "~=2.4.1" flake8-docstrings = "~=1.5.0" decoy = "~=1.2.0" pytest-lazy-fixture = "==0.6.3" +pytest-timeout = "*" [packages] aionotify = "==0.2.0" diff --git a/api/Pipfile.lock b/api/Pipfile.lock index f1b873982d4..95c685f53d9 100644 --- a/api/Pipfile.lock +++ b/api/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "14bc9a9b98525ade82a5e7f48a69c84e3a255fa3803690ffe793acaad46bf77a" + "sha256": "9804881159b7846c91ae5596212ca98b4ace5fc0d37be2d96386fc9cfbf28925" }, "pipfile-spec": 6, "requires": { @@ -134,7 +134,7 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "systemd-python": { @@ -157,46 +157,46 @@ "develop": { "aiohttp": { "hashes": [ - "sha256:0b795072bb1bf87b8620120a6373a3c61bfcb8da7e5c2377f4bb23ff4f0b62c9", - "sha256:0d438c8ca703b1b714e82ed5b7a4412c82577040dadff479c08405e2a715564f", - "sha256:16a3cb5df5c56f696234ea9e65e227d1ebe9c18aa774d36ff42f532139066a5f", - "sha256:1edfd82a98c5161497bbb111b2b70c0813102ad7e0aa81cbeb34e64c93863005", - "sha256:2406dc1dda01c7f6060ab586e4601f18affb7a6b965c50a8c90ff07569cf782a", - "sha256:2858b2504c8697beb9357be01dc47ef86438cc1cb36ecb6991796d19475faa3e", - "sha256:2a7b7640167ab536c3cb90cfc3977c7094f1c5890d7eeede8b273c175c3910fd", - "sha256:3228b7a51e3ed533f5472f54f70fd0b0a64c48dc1649a0f0e809bec312934d7a", - "sha256:328b552513d4f95b0a2eea4c8573e112866107227661834652a8984766aa7656", - "sha256:39f4b0a6ae22a1c567cb0630c30dd082481f95c13ca528dc501a7766b9c718c0", - "sha256:3b0036c978cbcc4a4512278e98e3e6d9e6b834dc973206162eddf98b586ef1c6", - "sha256:3ea8c252d8df5e9166bcf3d9edced2af132f4ead8ac422eac723c5781063709a", - "sha256:41608c0acbe0899c852281978492f9ce2c6fbfaf60aff0cefc54a7c4516b822c", - "sha256:59d11674964b74a81b149d4ceaff2b674b3b0e4d0f10f0be1533e49c4a28408b", - "sha256:5e479df4b2d0f8f02133b7e4430098699450e1b2a826438af6bec9a400530957", - "sha256:684850fb1e3e55c9220aad007f8386d8e3e477c4ec9211ae54d968ecdca8c6f9", - "sha256:6ccc43d68b81c424e46192a778f97da94ee0630337c9bbe5b2ecc9b0c1c59001", - "sha256:6d42debaf55450643146fabe4b6817bb2a55b23698b0434107e892a43117285e", - "sha256:710376bf67d8ff4500a31d0c207b8941ff4fba5de6890a701d71680474fe2a60", - "sha256:756ae7efddd68d4ea7d89c636b703e14a0c686688d42f588b90778a3c2fc0564", - "sha256:77149002d9386fae303a4a162e6bce75cc2161347ad2ba06c2f0182561875d45", - "sha256:78e2f18a82b88cbc37d22365cf8d2b879a492faedb3f2975adb4ed8dfe994d3a", - "sha256:7d9b42127a6c0bdcc25c3dcf252bb3ddc70454fac593b1b6933ae091396deb13", - "sha256:8389d6044ee4e2037dca83e3f6994738550f6ee8cfb746762283fad9b932868f", - "sha256:9c1a81af067e72261c9cbe33ea792893e83bc6aa987bfbd6fdc1e5e7b22777c4", - "sha256:c1e0920909d916d3375c7a1fdb0b1c78e46170e8bb42792312b6eb6676b2f87f", - "sha256:c68fdf21c6f3573ae19c7ee65f9ff185649a060c9a06535e9c3a0ee0bbac9235", - "sha256:c733ef3bdcfe52a1a75564389bad4064352274036e7e234730526d155f04d914", - "sha256:c9c58b0b84055d8bc27b7df5a9d141df4ee6ff59821f922dd73155861282f6a3", - "sha256:d03abec50df423b026a5aa09656bd9d37f1e6a49271f123f31f9b8aed5dc3ea3", - "sha256:d2cfac21e31e841d60dc28c0ec7d4ec47a35c608cb8906435d47ef83ffb22150", - "sha256:dcc119db14757b0c7bce64042158307b9b1c76471e655751a61b57f5a0e4d78e", - "sha256:df3a7b258cc230a65245167a202dd07320a5af05f3d41da1488ba0fa05bc9347", - "sha256:df48a623c58180874d7407b4d9ec06a19b84ed47f60a3884345b1a5099c1818b", - "sha256:e1b95972a0ae3f248a899cdbac92ba2e01d731225f566569311043ce2226f5e7", - "sha256:f326b3c1bbfda5b9308252ee0dcb30b612ee92b0e105d4abec70335fab5b1245", - "sha256:f411cb22115cb15452d099fec0ee636b06cf81bfb40ed9c02d30c8dc2bc2e3d1" + "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe", + "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe", + "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5", + "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8", + "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd", + "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb", + "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c", + "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87", + "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0", + "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290", + "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5", + "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287", + "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde", + "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf", + "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8", + "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16", + "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf", + "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809", + "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213", + "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f", + "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013", + "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b", + "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9", + "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5", + "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb", + "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df", + "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4", + "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439", + "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f", + "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22", + "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f", + "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5", + "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970", + "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009", + "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc", + "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a", + "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95" ], "markers": "python_version >= '3.6'", - "version": "==3.7.3" + "version": "==3.7.4.post0" }, "aionotify": { "hashes": [ @@ -272,10 +272,11 @@ }, "chardet": { "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", + "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" ], - "version": "==3.0.4" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==4.0.0" }, "clint": { "hashes": [ @@ -404,11 +405,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:ace61d5fc652dc280e7b6b4ff732a9c2d40db2c0f92bc6cb74e07b73d53a1771", - "sha256:fa5daa4477a7414ae34e95942e4dd07f62adf589143c875c133c1e53c4eff38d" + "sha256:742add720a20d0467df2f444ae41704000f50e1234f46174b51f9c6031a1bd71", + "sha256:b74159469b464a99cb8cc3e21973e4d96e05d3024d337313fedb618a6e86e6f4" ], - "markers": "python_version < '3.8' and python_version < '3.8'", - "version": "==3.4.0" + "markers": "python_version < '3.8'", + "version": "==3.7.3" }, "iniconfig": { "hashes": [ @@ -694,11 +695,11 @@ }, "pydocstyle": { "hashes": [ - "sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325", - "sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678" + "sha256:164befb520d851dbcf0e029681b91f4f599c62c5cd8933fd54b1bfbd50e89e1f", + "sha256:d4449cf16d7e6709f63192146706933c7a334af7c0f083904799ccb851c50f6d" ], - "markers": "python_version >= '3.5'", - "version": "==5.1.1" + "markers": "python_version >= '3.6'", + "version": "==6.0.0" }, "pyflakes": { "hashes": [ @@ -721,7 +722,7 @@ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.4.7" }, "pyrsistent": { @@ -771,6 +772,14 @@ "index": "pypi", "version": "==0.6.3" }, + "pytest-timeout": { + "hashes": [ + "sha256:20b3113cf6e4e80ce2d403b6fb56e9e1b871b510259206d40ff8d609f48bda76", + "sha256:541d7aa19b9a6b4e475c759fd6073ef43d7cdc9a92d95644c260076eb257a063" + ], + "index": "pypi", + "version": "==1.4.2" + }, "pytest-watch": { "hashes": [ "sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9" @@ -787,10 +796,10 @@ }, "readme-renderer": { "hashes": [ - "sha256:267854ac3b1530633c2394ead828afcd060fc273217c42ac36b6be9c42cd9a9d", - "sha256:6b7e5aa59210a40de72eb79931491eaf46fefca2952b9181268bd7c7c65c260a" + "sha256:63b4075c6698fcfa78e584930f07f39e05d46f3ec97f65006e430b595ca6348c", + "sha256:92fd5ac2bf8677f310f3303aa4bce5b9d5f9f2094ab98c29f13791d7b805a3db" ], - "version": "==28.0" + "version": "==29.0" }, "requests": { "hashes": [ @@ -812,7 +821,7 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "snowballstemmer": { @@ -898,16 +907,16 @@ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, "tqdm": { "hashes": [ - "sha256:11d544652edbdfc9cc41aa4c8a5c166513e279f3f2d9f1a9e1c89935b51de6ff", - "sha256:a89be573bfddb81bb0b395a416d5e55e3ecc73ce95a368a4f6360bedea33195e" + "sha256:9fdf349068d047d4cfbe24862c425883af1db29bcddf4b0eeb2524f6fbdb23c7", + "sha256:d666ae29164da3e517fcf125e41d4fe96e5bb375cd87ff9763f6b38b5592fe33" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.56.2" + "version": "==4.59.0" }, "twine": { "hashes": [ @@ -972,34 +981,34 @@ }, "urllib3": { "hashes": [ - "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80", - "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73" + "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", + "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4.0'", - "version": "==1.26.3" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.26.4" }, "watchdog": { "hashes": [ - "sha256:0e3a14d5927dfc8381a45618d8373734a182e9747086a2ed7b4ee8c55e393d61", - "sha256:1e4af875a9cb0323945a6f3640721cbd75e5fb55d4fe9a122306ed6e3e81cb87", - "sha256:218b1fc22c4b60a28b45a3c1894f82e03cd15ab82e983f50d7edab323ba4dbdd", - "sha256:275305b701aa2157c21d48bbe0c29cc844f14ae3095e7352faa90d2664f5eb3c", - "sha256:7bda6200f409f5c5166cad6cdf122dc5988ee98d8b61f1d60e15386501bc52ce", - "sha256:7cec8807a4f0c48fc7edbaf57c8feaf033841bde700e7e3f75f80a68ea607b3c", - "sha256:865891481224e83baa8bfa3cbf188ef346498caa161b8998ef80f4204db4a8f4", - "sha256:900995c9a4842f081e1ca06fde1df0e36304b34d2e1b59712c2618a4e7d25e98", - "sha256:9371f7090cc7e648654b3cabe955f97105069c1b2a051158e4edd290ebfd4753", - "sha256:9a62a10ec580ca48751caa9f9346bdea0d652801b006a3856344f033bd1fe138", - "sha256:a7b8811e6dd23864e21b5acbfce3b9d3d37f9e0d5815253902c17a6739b84ce7", - "sha256:bce7e4caf491e753f3d554155e67fb2ae1aafeb8a5a11e8d2b3fcbe213dd3b55", - "sha256:bfa78c9f9a6371b0a61f3f0f3499a39118e0bf67e6d635837fc521f3fcb985a0", - "sha256:d28a933682d0a47d8d48e46ff9fb8b323e27cb33830d5aae9c4b9c59767b302c", - "sha256:f003a04c462720a03cb2332054a48a2453116b34145c1cd7287dccee0bb9eef4", - "sha256:fd4b56cfbe0d0d9c4fc431aceca75700a7f12d3737dc2e9cb2ec2ba649680425", - "sha256:fda5fc3586fc198c39d4a84ff04a785b95145ad8fc6538a8d8fe1fd37d67fdd7" + "sha256:035f4816daf3c62e03503c267620f3aa8fc7472df85ff3ef1e0c100ea1ed2744", + "sha256:0f7e9de9ba84af15e9e9fc29c3b13c972daa4d2b11de29aa86b26a26bc877c06", + "sha256:13c9ff58508dce55ba416eb0ef7af5aa5858558f2ec51112f099fd03503b670b", + "sha256:19675b8d1f00dabe74a0e66d87980623250d9360a21612e8c27b70a4b214ceeb", + "sha256:1cd715c4fb803581ded8943f39a51f21c17375d009ca9e3398d6b20638863a70", + "sha256:1f518a6940cde8720b8826a705c164e6b9bd6cf8c00f14269ffac51e017e06ec", + "sha256:3e933f3567c4521dd1a5d59fd54a522cae90bebcbeb8b74b84a2f33c90f08388", + "sha256:41b1a773f364f232b5bc184688e8d60451745d9e0971ac60c648bd47be8f4733", + "sha256:532fedd993e75554671faa36cd04c580ced3fae084254a779afbbd8aaf00566b", + "sha256:74528772516228f6a015a647027057939ff0b695a0b864cb3037e8e1aabc7ca0", + "sha256:89102465764e453609463cf620e744da1b0aa1f9f321b05961e2e7e15b3c9d8b", + "sha256:a412b1914e27f67b0a10e1ee19b5d035a9f7c115a062bbbd640653d9820ba4c8", + "sha256:ac6adbdf32e1d180574f9d0819e80259ae48e68727e80c3d950ed5a023714c3e", + "sha256:adda34bfe6db05485c1dfcd98232bdec385f991fe16358750c2163473eefb985", + "sha256:d2fcbc15772a82cd139c803a513c45b0fbc72a10a8a34dc2a8b429110b6f1236", + "sha256:d54e187b76053982180532cb7fd31152201c438b348c456f699929f8a89e786d", + "sha256:e0114e48ee981b38e328eaa0d5a625c7b4fc144b8dc7f7637749d6b5f7fefb0e" ], "markers": "python_version >= '3.6'", - "version": "==2.0.0" + "version": "==2.0.2" }, "webencodings": { "hashes": [ @@ -1061,11 +1070,11 @@ }, "zipp": { "hashes": [ - "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108", - "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb" + "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", + "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" ], "markers": "python_version >= '3.6'", - "version": "==3.4.0" + "version": "==3.4.1" } } } diff --git a/api/src/opentrons/protocols/advanced_control/transfers.py b/api/src/opentrons/protocols/advanced_control/transfers.py index c421483bb3e..291cd3efbf2 100644 --- a/api/src/opentrons/protocols/advanced_control/transfers.py +++ b/api/src/opentrons/protocols/advanced_control/transfers.py @@ -364,6 +364,9 @@ def __init__(self, volume, sources, dests, + # todo(mm, 2021-03-10): + # Refactor to not need an InstrumentContext, so we can more + # easily test this class's logic on its own. instr: 'InstrumentContext', max_volume: float, api_version: APIVersion, @@ -554,6 +557,8 @@ def _plan_distribute(self): # the other maintains consistency in default behaviors of all functions plan_iter = self._expand_for_volume_constraints( self._volumes, self._dests, + # todo(mm, 2021-03-09): Is this a bug? + # Should this be the *working volume*? self._instr.max_volume - self._strategy.disposal_volume - self._strategy.air_gap) @@ -600,6 +605,11 @@ def _expand_for_volume_constraints( """ Split a sequence of proposed transfers if necessary to keep each transfer under the given max volume. """ + + if max_volume <= 0: + raise ValueError( + f"max_volume must be greater than 0. (Got {max_volume}.)") + for volume, target in zip(volumes, targets): while volume > max_volume * 2: yield max_volume, target @@ -648,6 +658,8 @@ def _plan_consolidate(self): .. Aspirate -> .....* """ plan_iter = self._expand_for_volume_constraints( + # todo(mm, 2021-03-09): Is this right? Why don't we account for + # disposal volume? self._volumes, self._sources, self._instr.max_volume) current_xfer = next(plan_iter) if self._strategy.new_tip == types.TransferTipPolicy.ALWAYS: diff --git a/api/tests/opentrons/protocols/advanced_control/test_transfers.py b/api/tests/opentrons/protocols/advanced_control/test_transfers.py index 5b3fa57184b..7e90dd0db86 100644 --- a/api/tests/opentrons/protocols/advanced_control/test_transfers.py +++ b/api/tests/opentrons/protocols/advanced_control/test_transfers.py @@ -1321,3 +1321,120 @@ def test_blowout_to_dest(_instr_labware): {'method': 'drop_tip', 'args': [], 'kwargs': {}}] for step, expected in zip(dist_plan, exp): assert step == expected + + +# todo(mm, 2021-03-22): +# 1) Move reserved_volume_breakdowns from test code to these parametrizations, +# for finer-grained bug characterization. (TransferPlan might behave +# differently between breakdowns, all other parameters being equal, and that +# difference might need to be preserved.) Also consider testing what happens +# with 8-Channel pipettes. +# 2) Temporarily remove the fix and audit every parametrization of this test +# to see how TransferPlan fails it. Characterize the existing behavior. I've +# seen some parametrizations fail by infinite-looping (timing out), and some +# fail by returning a step list with nothing but a pick_up_tip. There may be +# other variants. +# 3) Port the characterization of the existing behavior to tests for the current +# apiLevel. For the cases where TransferPlan currently infinite-loops, if we +# change it to raising an explicit exception, we can count that as the same +# behavior. But if TransferPlan fails in some other way, that failure +# generally will have to be preserved. +# 4) Make TransferPlan raise an explicit error for all these cases for new +# apiLevels. +@pytest.mark.timeout(timeout=5) +@pytest.mark.parametrize( + 'transfer_mode, source_indexes, dest_indexes', + [ + ('transfer', 0, 1), + ('distribute', 0, slice(1, 8)), + ('consolidate', 0, slice(1, 8)) + ] +) +@pytest.mark.parametrize( + # todo(mm, 2021-03-22): Is it worth testing two different Single-Channel + # pipettes? Testing 8-Channel behavior would probably be more valuable. + 'pipette_name, tip_rack_name, tip_max_volume, reserved_volume', + [ + # reserved == pipette max == tip max + ('p20_single_gen2', 'opentrons_96_filtertiprack_20ul', 20, 20), + ('p300_single', 'opentrons_96_tiprack_300ul', 300, 300), + + # pipette max != tip max; reserved == tip max + ('p300_single', 'opentrons_96_filtertiprack_200ul', 200, 200), + ('p20_single_gen2', 'opentrons_96_filtertiprack_10ul', 10, 10), + + # pipette max != tip max; reserved == pipette max + ('p300_single', 'opentrons_96_filtertiprack_200ul', 200, 300), + ('p20_single_gen2', 'opentrons_96_filtertiprack_10ul', 10, 20), + + # reserved > both pipette max and tip max + ('p20_single_gen2', 'opentrons_96_filtertiprack_10ul', 10, 10000), + ('p20_single_gen2', 'opentrons_96_filtertiprack_20ul', 20, 10000), + ('p300_single', 'opentrons_96_filtertiprack_200ul', 200, 10000), + ('p300_single', 'opentrons_96_tiprack_300ul', 300, 10000), + ] +) +def test_error_if_reserved_volume_too_high( + transfer_mode, + source_indexes, + dest_indexes, + pipette_name, + tip_rack_name, + tip_max_volume, + reserved_volume): + + # TransferPlan should raise ValueError if we ask it to plan a transfer with + # a disposal_volume so high, there would be no room left in the tip to + # actually transfer liquid and make progress. + # + # (As opposed to getting stuck in an infinite loop, or returning an empty + # list of steps.) + + # todo(mm, 2021-03-22): pytest.mark.parametrize these for finer-grained + # characterization of these bugs. + reserved_volume_breakdowns = [ + (0, reserved_volume), + (reserved_volume, 0), + + (1, reserved_volume-1), + (reserved_volume-1, 1), + + (reserved_volume/2, reserved_volume/2) + ] + + for air_gap, disposal_volume in reserved_volume_breakdowns: + # No floating point rounding error allowed. + assert air_gap + disposal_volume == reserved_volume + + # Boilerplate: TransferPlan wants an InstrumentContext. + context = papi.ProtocolContext( + implementation=ProtocolContextImplementation(), + ) + labware = context.load_labware('nest_12_reservoir_15ml', 1) + tip_rack = context.load_labware(tip_rack_name, 2) + pipette = context.load_instrument(pipette_name, 'left', tip_racks=[tip_rack]) + + # Make sure, in this test, it's sensible to expect a volume as high as the + # tip's max volume to work as the transfer's max volume. + assert tip_rack.wells()[0].max_volume <= pipette.max_volume + + options = tx.TransferOptions(tx.Transfer( + air_gap=air_gap, + disposal_volume=disposal_volume + )) + + with pytest.raises(ValueError): + plan = tx.TransferPlan( + volume=1.2345, + sources=labware.wells()[source_indexes], + dests=labware.wells()[dest_indexes], + instr=pipette, + max_volume=tip_max_volume, + api_version=context.api_version, + options=options, + mode=transfer_mode) + # Exhaust the iterator in case it raises the expected exception + # lazily. + # fixme(mm, 2021-03-22): print() for debugging, remove before + # merging. + print(list(plan))