diff --git a/pyproject.toml b/pyproject.toml index 426394fa..9a6f0348 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ packages = [ "scfw.configure", "scfw.loggers", "scfw.package_managers", + "scfw.package_managers.npm", "scfw.verifiers", "scfw.verifiers.dd_verifier", "scfw.verifiers.osv_verifier" diff --git a/requirements-dev.txt b/requirements-dev.txt index ab1c19a0..e1394546 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,96 +1,96 @@ -coverage==7.12.0 ; python_version >= "3.10" and python_version < "4" \ - --hash=sha256:01d24af36fedda51c2b1aca56e4330a3710f83b02a5ff3743a6b015ffa7c9384 \ - --hash=sha256:04a79245ab2b7a61688958f7a855275997134bc84f4a03bc240cf64ff132abf6 \ - --hash=sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60 \ - --hash=sha256:099d11698385d572ceafb3288a5b80fe1fc58bf665b3f9d362389de488361d3d \ - --hash=sha256:09a86acaaa8455f13d6a99221d9654df249b33937b4e212b4e5a822065f12aa7 \ - --hash=sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a \ - --hash=sha256:172cf3a34bfef42611963e2b661302a8931f44df31629e5b1050567d6b90287d \ - --hash=sha256:22a7aade354a72dff3b59c577bfd18d6945c61f97393bc5fb7bd293a4237024b \ - --hash=sha256:24cff9d1f5743f67db7ba46ff284018a6e9aeb649b67aa1e70c396aa1b7cb23c \ - --hash=sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647 \ - --hash=sha256:297bc2da28440f5ae51c845a47c8175a4db0553a53827886e4fb25c66633000c \ - --hash=sha256:2fd8354ed5d69775ac42986a691fbf68b4084278710cee9d7c3eaa0c28fa982a \ - --hash=sha256:313672140638b6ddb2c6455ddeda41c6a0b208298034544cfca138978c6baed6 \ - --hash=sha256:31b8b2e38391a56e3cea39d22a23faaa7c3fc911751756ef6d2621d2a9daf742 \ - --hash=sha256:32b75c2ba3f324ee37af3ccee5b30458038c50b349ad9b88cee85096132a575b \ - --hash=sha256:33baadc0efd5c7294f436a632566ccc1f72c867f82833eb59820ee37dc811c6f \ - --hash=sha256:3ff651dcd36d2fea66877cd4a82de478004c59b849945446acb5baf9379a1b64 \ - --hash=sha256:40c867af715f22592e0d0fb533a33a71ec9e0f73a6945f722a0c85c8c1cbe3a2 \ - --hash=sha256:42435d46d6461a3b305cdfcad7cdd3248787771f53fe18305548cba474e6523b \ - --hash=sha256:459443346509476170d553035e4a3eed7b860f4fe5242f02de1010501956ce87 \ - --hash=sha256:4648158fd8dd9381b5847622df1c90ff314efbfc1df4550092ab6013c238a5fc \ - --hash=sha256:47324fffca8d8eae7e185b5bb20c14645f23350f870c1649003618ea91a78941 \ - --hash=sha256:473dc45d69694069adb7680c405fb1e81f60b2aff42c81e2f2c3feaf544d878c \ - --hash=sha256:4b59b501455535e2e5dde5881739897967b272ba25988c89145c12d772810ccb \ - --hash=sha256:4c589361263ab2953e3c4cd2a94db94c4ad4a8e572776ecfbad2389c626e4507 \ - --hash=sha256:51777647a749abdf6f6fd8c7cffab12de68ab93aab15efc72fbbb83036c2a068 \ - --hash=sha256:52ca620260bd8cd6027317bdd8b8ba929be1d741764ee765b42c4d79a408601e \ - --hash=sha256:5560c7e0d82b42eb1951e4f68f071f8017c824ebfd5a6ebe42c60ac16c6c2434 \ - --hash=sha256:5734b5d913c3755e72f70bf6cc37a0518d4f4745cde760c5d8e12005e62f9832 \ - --hash=sha256:583f9adbefd278e9de33c33d6846aa8f5d164fa49b47144180a0e037f0688bb9 \ - --hash=sha256:58c1c6aa677f3a1411fe6fb28ec3a942e4f665df036a3608816e0847fad23296 \ - --hash=sha256:5b3c889c0b8b283a24d721a9eabc8ccafcfc3aebf167e4cd0d0e23bf8ec4e339 \ - --hash=sha256:5bcead88c8423e1855e64b8057d0544e33e4080b95b240c2a355334bb7ced937 \ - --hash=sha256:5ea5a9f7dc8877455b13dd1effd3202e0bca72f6f3ab09f9036b1bcf728f69ac \ - --hash=sha256:5f3738279524e988d9da2893f307c2093815c623f8d05a8f79e3eff3a7a9e553 \ - --hash=sha256:68b0d0a2d84f333de875666259dadf28cc67858bc8fd8b3f1eae84d3c2bec455 \ - --hash=sha256:6d907ddccbca819afa2cd014bc69983b146cca2735a0b1e6259b2a6c10be1e70 \ - --hash=sha256:6e1a8c066dabcde56d5d9fed6a66bc19a2883a3fe051f0c397a41fc42aedd4cc \ - --hash=sha256:6ff7651cc01a246908eac162a6a86fc0dbab6de1ad165dfb9a1e2ec660b44984 \ - --hash=sha256:737c3814903be30695b2de20d22bcc5428fdae305c61ba44cdc8b3252984c49c \ - --hash=sha256:73f9e7fbd51a221818fd11b7090eaa835a353ddd59c236c57b2199486b116c6d \ - --hash=sha256:76336c19a9ef4a94b2f8dc79f8ac2da3f193f625bb5d6f51a328cd19bfc19933 \ - --hash=sha256:7670d860e18b1e3ee5930b17a7d55ae6287ec6e55d9799982aa103a2cc1fa2ef \ - --hash=sha256:79a44421cd5fba96aa57b5e3b5a4d3274c449d4c622e8f76882d76635501fd13 \ - --hash=sha256:7c1059b600aec6ef090721f8f633f60ed70afaffe8ecab85b59df748f24b31fe \ - --hash=sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736 \ - --hash=sha256:874fe69a0785d96bd066059cd4368022cebbec1a8958f224f0016979183916e6 \ - --hash=sha256:8787b0f982e020adb732b9f051f3e49dd5054cebbc3f3432061278512a2b1360 \ - --hash=sha256:8bb5b894b3ec09dcd6d3743229dc7f2c42ef7787dc40596ae04c0edda487371e \ - --hash=sha256:907e0df1b71ba77463687a74149c6122c3f6aac56c2510a5d906b2f368208560 \ - --hash=sha256:90d58ac63bc85e0fb919f14d09d6caa63f35a5512a2205284b7816cafd21bb03 \ - --hash=sha256:9157a5e233c40ce6613dead4c131a006adfda70e557b6856b97aceed01b0e27a \ - --hash=sha256:91b810a163ccad2e43b1faa11d70d3cf4b6f3d83f9fd5f2df82a32d47b648e0d \ - --hash=sha256:950411f1eb5d579999c5f66c62a40961f126fc71e5e14419f004471957b51508 \ - --hash=sha256:99d5415c73ca12d558e07776bd957c4222c687b9f1d26fa0e1b57e3598bdcde8 \ - --hash=sha256:9b57e2d0ddd5f0582bae5437c04ee71c46cd908e7bc5d4d0391f9a41e812dd12 \ - --hash=sha256:9bb44c889fb68004e94cab71f6a021ec83eac9aeabdbb5a5a88821ec46e1da73 \ - --hash=sha256:a00594770eb715854fb1c57e0dea08cce6720cfbc531accdb9850d7c7770396c \ - --hash=sha256:a1783ed5bd0d5938d4435014626568dc7f93e3cb99bc59188cc18857c47aa3c4 \ - --hash=sha256:a1c59b7dc169809a88b21a936eccf71c3895a78f5592051b1af8f4d59c2b4f92 \ - --hash=sha256:aa124a3683d2af98bd9d9c2bfa7a5076ca7e5ab09fdb96b81fa7d89376ae928f \ - --hash=sha256:aa7d48520a32cb21c7a9b31f81799e8eaec7239db36c3b670be0fa2403828d1d \ - --hash=sha256:b1518ecbad4e6173f4c6e6c4a46e49555ea5679bf3feda5edb1b935c7c44e8a0 \ - --hash=sha256:b1aab7302a87bafebfe76b12af681b56ff446dc6f32ed178ff9c092ca776e6bc \ - --hash=sha256:b2089cc445f2dc0af6f801f0d1355c025b76c24481935303cf1af28f636688f0 \ - --hash=sha256:b365adc70a6936c6b0582dc38746b33b2454148c02349345412c6e743efb646d \ - --hash=sha256:b527a08cdf15753279b7afb2339a12073620b761d79b81cbe2cdebdb43d90daa \ - --hash=sha256:bc13baf85cd8a4cfcf4a35c7bc9d795837ad809775f782f697bf630b7e200211 \ - --hash=sha256:bcec6f47e4cb8a4c2dc91ce507f6eefc6a1b10f58df32cdc61dff65455031dfc \ - --hash=sha256:c406a71f544800ef7e9e0000af706b88465f3573ae8b8de37e5f96c59f689ad1 \ - --hash=sha256:c5a6f20bf48b8866095c6820641e7ffbe23f2ac84a2efc218d91235e404c7777 \ - --hash=sha256:c87395744f5c77c866d0f5a43d97cc39e17c7f1cb0115e54a2fe67ca75c5d14d \ - --hash=sha256:ca8ecfa283764fdda3eae1bdb6afe58bf78c2c3ec2b2edcb05a671f0bba7b3f9 \ - --hash=sha256:cb2a1b6ab9fe833714a483a915de350abc624a37149649297624c8d57add089c \ - --hash=sha256:ccf3b2ede91decd2fb53ec73c1f949c3e034129d1e0b07798ff1d02ea0c8fa4a \ - --hash=sha256:ce61969812d6a98a981d147d9ac583a36ac7db7766f2e64a9d4d059c2fe29d07 \ - --hash=sha256:d6c2e26b481c9159c2773a37947a9718cfdc58893029cdfb177531793e375cfc \ - --hash=sha256:d7e0d0303c13b54db495eb636bc2465b2fb8475d4c8bcec8fe4b5ca454dfbae8 \ - --hash=sha256:d8842f17095b9868a05837b7b1b73495293091bed870e099521ada176aa3e00e \ - --hash=sha256:d93fbf446c31c0140208dcd07c5d882029832e8ed7891a39d6d44bd65f2316c3 \ - --hash=sha256:dcbb630ab034e86d2a0f79aefd2be07e583202f41e037602d438c80044957baa \ - --hash=sha256:e0d68c1f7eabbc8abe582d11fa393ea483caf4f44b0af86881174769f185c94d \ - --hash=sha256:e0f483ab4f749039894abaf80c2f9e7ed77bbf3c737517fb88c8e8e305896a17 \ - --hash=sha256:e71bba6a40883b00c6d571599b4627f50c360b3d0d02bfc658168936be74027b \ - --hash=sha256:e84da3a0fd233aeec797b981c51af1cabac74f9bd67be42458365b30d11b5291 \ - --hash=sha256:e949ebf60c717c3df63adb4a1a366c096c8d7fd8472608cd09359e1bd48ef59f \ - --hash=sha256:f3433ffd541380f3a0e423cff0f4926d55b0cc8c1d160fdc3be24a4c03aa65f7 \ - --hash=sha256:f7ba9da4726e446d8dd8aae5a6cd872511184a5d861de80a86ef970b5dacce3e \ - --hash=sha256:f7bbb321d4adc9f65e402c677cd1c8e4c2d0105d3ce285b51b4d87f1d5db5245 \ - --hash=sha256:f999813dddeb2a56aab5841e687b68169da0d3f6fc78ccf50952fa2463746022 \ - --hash=sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c \ - --hash=sha256:fdba9f15849534594f60b47c9a30bc70409b54947319a7c4fd0e8e3d8d2f355d +coverage==7.13.0 ; python_version >= "3.10" and python_version < "4" \ + --hash=sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe \ + --hash=sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b \ + --hash=sha256:02d9fb9eccd48f6843c98a37bd6817462f130b86da8660461e8f5e54d4c06070 \ + --hash=sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e \ + --hash=sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053 \ + --hash=sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080 \ + --hash=sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc \ + --hash=sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb \ + --hash=sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf \ + --hash=sha256:0dfa3855031070058add1a59fdfda0192fd3e8f97e7c81de0596c145dea51820 \ + --hash=sha256:0f4872f5d6c54419c94c25dd6ae1d015deeb337d06e448cd890a1e89a8ee7f3b \ + --hash=sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232 \ + --hash=sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657 \ + --hash=sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef \ + --hash=sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd \ + --hash=sha256:1ed5630d946859de835a85e9a43b721123a8a44ec26e2830b296d478c7fd4259 \ + --hash=sha256:22486cdafba4f9e471c816a2a5745337742a617fef68e890d8baf9f3036d7833 \ + --hash=sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d \ + --hash=sha256:24e4e56304fdb56f96f80eabf840eab043b3afea9348b88be680ec5986780a0f \ + --hash=sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493 \ + --hash=sha256:263c3dbccc78e2e331e59e90115941b5f53e85cfcc6b3b2fbff1fd4e3d2c6ea8 \ + --hash=sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf \ + --hash=sha256:30a3a201a127ea57f7e14ba43c93c9c4be8b7d17a26e03bb49e6966d019eede9 \ + --hash=sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19 \ + --hash=sha256:367449cf07d33dc216c083f2036bb7d976c6e4903ab31be400ad74ad9f85ce98 \ + --hash=sha256:37eee4e552a65866f15dedd917d5e5f3d59805994260720821e2c1b51ac3248f \ + --hash=sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b \ + --hash=sha256:3a7b1cd820e1b6116f92c6128f1188e7afe421c7e1b35fa9836b11444e53ebd9 \ + --hash=sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b \ + --hash=sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e \ + --hash=sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc \ + --hash=sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256 \ + --hash=sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8 \ + --hash=sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927 \ + --hash=sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae \ + --hash=sha256:4fdb6f54f38e334db97f72fa0c701e66d8479af0bc3f9bfb5b90f1c30f54500f \ + --hash=sha256:51a202e0f80f241ccb68e3e26e19ab5b3bf0f813314f2c967642f13ebcf1ddfe \ + --hash=sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f \ + --hash=sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621 \ + --hash=sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1 \ + --hash=sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137 \ + --hash=sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9 \ + --hash=sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74 \ + --hash=sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46 \ + --hash=sha256:62d7c4f13102148c78d7353c6052af6d899a7f6df66a32bddcc0c0eb7c5326f8 \ + --hash=sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940 \ + --hash=sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39 \ + --hash=sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a \ + --hash=sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d \ + --hash=sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b \ + --hash=sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0 \ + --hash=sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a \ + --hash=sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2 \ + --hash=sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb \ + --hash=sha256:74c136e4093627cf04b26a35dab8cbfc9b37c647f0502fc313376e11726ba303 \ + --hash=sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971 \ + --hash=sha256:7a485ff48fbd231efa32d58f479befce52dcb6bfb2a88bb7bf9a0b89b1bc8030 \ + --hash=sha256:7e442c013447d1d8d195be62852270b78b6e255b79b8675bad8479641e21fd96 \ + --hash=sha256:7f15a931a668e58087bc39d05d2b4bf4b14ff2875b49c994bbdb1c2217a8daeb \ + --hash=sha256:7f88ae3e69df2ab62fb0bc5219a597cb890ba5c438190ffa87490b315190bb33 \ + --hash=sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8 \ + --hash=sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904 \ + --hash=sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d \ + --hash=sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28 \ + --hash=sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e \ + --hash=sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e \ + --hash=sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9 \ + --hash=sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74 \ + --hash=sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8 \ + --hash=sha256:a00d3a393207ae12f7c49bb1c113190883b500f48979abb118d8b72b8c95c032 \ + --hash=sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57 \ + --hash=sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be \ + --hash=sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936 \ + --hash=sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f \ + --hash=sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c \ + --hash=sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a \ + --hash=sha256:c4be718e51e86f553bcf515305a158a1cd180d23b72f07ae76d6017c3cc5d791 \ + --hash=sha256:cdb3c9f8fef0a954c632f64328a3935988d33a6604ce4bf67ec3e39670f12ae5 \ + --hash=sha256:d10fd186aac2316f9bbb46ef91977f9d394ded67050ad6d84d94ed6ea2e8e54e \ + --hash=sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a \ + --hash=sha256:d2a9d7f1c11487b1c69367ab3ac2d81b9b3721f097aa409a3191c3e90f8f3dd7 \ + --hash=sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a \ + --hash=sha256:e5330fa0cc1f5c3c4c3bb8e101b742025933e7848989370a1d4c8c5e401ea753 \ + --hash=sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3 \ + --hash=sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6 \ + --hash=sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e \ + --hash=sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071 \ + --hash=sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b \ + --hash=sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511 \ + --hash=sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff \ + --hash=sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7 \ + --hash=sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6 exceptiongroup==1.3.1 ; python_version == "3.10" \ --hash=sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219 \ --hash=sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598 @@ -103,83 +103,83 @@ iniconfig==2.3.0 ; python_version >= "3.10" and python_version < "4" \ jinja2==3.1.6 ; python_version >= "3.10" and python_version < "4" \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 -librt==0.6.3 ; python_version >= "3.10" and python_version < "4" \ - --hash=sha256:04f8ce401d4f6380cfc42af0f4e67342bf34c820dae01343f58f472dbac75dcf \ - --hash=sha256:05f385a414de3f950886ea0aad8f109650d4b712cf9cc14cc17f5f62a9ab240b \ - --hash=sha256:0765b0fe0927d189ee14b087cd595ae636bef04992e03fe6dfdaa383866c8a46 \ - --hash=sha256:078cd77064d1640cb7b0650871a772956066174d92c8aeda188a489b58495179 \ - --hash=sha256:09262cb2445b6f15d09141af20b95bb7030c6f13b00e876ad8fdd1a9045d6aa5 \ - --hash=sha256:0c74c26736008481c9f6d0adf1aedb5a52aff7361fea98276d1f965c0256ee70 \ - --hash=sha256:0e0f2b79993fec23a685b3e8107ba5f8675eeae286675a216da0b09574fa1e47 \ - --hash=sha256:10a95ad074e2a98c9e4abc7f5b7d40e5ecbfa84c04c6ab8a70fabf59bd429b88 \ - --hash=sha256:14b345eb7afb61b9fdcdfda6738946bd11b8e0f6be258666b0646af3b9bb5916 \ - --hash=sha256:17000df14f552e86877d67e4ab7966912224efc9368e998c96a6974a8d609bf9 \ - --hash=sha256:1b51ba7d9d5d9001494769eca8c0988adce25d0a970c3ba3f2eb9df9d08036fc \ - --hash=sha256:1ef42ff4edd369e84433ce9b188a64df0837f4f69e3d34d3b34d4955c599d03f \ - --hash=sha256:25b1b60cb059471c0c0c803e07d0dfdc79e41a0a122f288b819219ed162672a3 \ - --hash=sha256:26b8026393920320bb9a811b691d73c5981385d537ffc5b6e22e53f7b65d4122 \ - --hash=sha256:324462fe7e3896d592b967196512491ec60ca6e49c446fe59f40743d08c97917 \ - --hash=sha256:349b6873ebccfc24c9efd244e49da9f8a5c10f60f07575e248921aae2123fc42 \ - --hash=sha256:36a8e337461150b05ca2c7bdedb9e591dfc262c5230422cea398e89d0c746cdc \ - --hash=sha256:36b2ec8c15030002c7f688b4863e7be42820d7c62d9c6eece3db54a2400f0530 \ - --hash=sha256:38320386a48a15033da295df276aea93a92dfa94a862e06893f75ea1d8bbe89d \ - --hash=sha256:3ac2a7835434b31def8ed5355dd9b895bbf41642d61967522646d1d8b9681106 \ - --hash=sha256:3caa0634c02d5ff0b2ae4a28052e0d8c5f20d497623dc13f629bd4a9e2a6efad \ - --hash=sha256:3e84a4121a7ae360ca4da436548a9c1ca8ca134a5ced76c893cc5944426164bd \ - --hash=sha256:3f0e4bd9bcb0ee34fa3dbedb05570da50b285f49e52c07a241da967840432513 \ - --hash=sha256:4018904c83eab49c814e2494b4e22501a93cdb6c9f9425533fe693c3117126f9 \ - --hash=sha256:408a36ddc75e91918cb15b03460bdc8a015885025d67e68c6f78f08c3a88f522 \ - --hash=sha256:45660d26569cc22ed30adf583389d8a0d1b468f8b5e518fcf9bfe2cd298f9dd1 \ - --hash=sha256:4aa4a93a353ccff20df6e34fa855ae8fd788832c88f40a9070e3ddd3356a9f0e \ - --hash=sha256:4bca9e4c260233fba37b15c4ec2f78aa99c1a79fbf902d19dd4a763c5c3fb751 \ - --hash=sha256:514f3f363d1ebc423357d36222c37e5c8e6674b6eae8d7195ac9a64903722057 \ - --hash=sha256:54f3b2177fb892d47f8016f1087d21654b44f7fc4cf6571c1c6b3ea531ab0fcf \ - --hash=sha256:57705e8eec76c5b77130d729c0f70190a9773366c555c5457c51eace80afd873 \ - --hash=sha256:5cc22f7f5c0cc50ed69f4b15b9c51d602aabc4500b433aaa2ddd29e578f452f7 \ - --hash=sha256:61348cc488b18d1b1ff9f3e5fcd5ac43ed22d3e13e862489d2267c2337285c08 \ - --hash=sha256:64645b757d617ad5f98c08e07620bc488d4bced9ced91c6279cec418f16056fa \ - --hash=sha256:669ff2495728009a96339c5ad2612569c6d8be4474e68f3f3ac85d7c3261f5f5 \ - --hash=sha256:6bac97e51f66da2ca012adddbe9fd656b17f7368d439de30898f24b39512f40f \ - --hash=sha256:6d46aa46aa29b067f0b8b84f448fd9719aaf5f4c621cc279164d76a9dc9ab3e8 \ - --hash=sha256:71f0a5918aebbea1e7db2179a8fe87e8a8732340d9e8b8107401fb407eda446e \ - --hash=sha256:74418f718083009108dc9a42c21bf2e4802d49638a1249e13677585fcc9ca176 \ - --hash=sha256:760c25ed6ac968e24803eb5f7deb17ce026902d39865e83036bacbf5cf242aa8 \ - --hash=sha256:822ca79e28720a76a935c228d37da6579edef048a17cd98d406a2484d10eda78 \ - --hash=sha256:86605d5bac340beb030cbc35859325982a79047ebdfba1e553719c7126a2389d \ - --hash=sha256:87597e3d57ec0120a3e1d857a708f80c02c42ea6b00227c728efbc860f067c45 \ - --hash=sha256:8983c5c06ac9c990eac5eb97a9f03fe41dc7e9d7993df74d9e8682a1056f596c \ - --hash=sha256:8c659f9fb8a2f16dc4131b803fa0144c1dadcb3ab24bb7914d01a6da58ae2457 \ - --hash=sha256:8e695f25d1a425ad7a272902af8ab8c8d66c1998b177e4b5f5e7b4e215d0c88a \ - --hash=sha256:8f8ed5053ef9fb08d34f1fd80ff093ccbd1f67f147633a84cf4a7d9b09c0f089 \ - --hash=sha256:92267f865c7bbd12327a0d394666948b9bf4b51308b52947c0cc453bfa812f5d \ - --hash=sha256:98e4bbecbef8d2a60ecf731d735602feee5ac0b32117dbbc765e28b054bac912 \ - --hash=sha256:9e716f9012148a81f02f46a04fc4c663420c6fbfeacfac0b5e128cf43b4413d3 \ - --hash=sha256:9f2a6623057989ebc469cd9cc8fe436c40117a0147627568d03f84aef7854c55 \ - --hash=sha256:a218f85081fc3f70cddaed694323a1ad7db5ca028c379c214e3a7c11c0850523 \ - --hash=sha256:aa346e202e6e1ebc01fe1c69509cffe486425884b96cb9ce155c99da1ecbe0e9 \ - --hash=sha256:ad8ba80cdcea04bea7b78fcd4925bfbf408961e9d8397d2ee5d3ec121e20c08c \ - --hash=sha256:afb39550205cc5e5c935762c6bf6a2bb34f7d21a68eadb25e2db7bf3593fecc0 \ - --hash=sha256:b2922a0e8fa97395553c304edc3bd36168d8eeec26b92478e292e5d4445c1ef0 \ - --hash=sha256:b47395091e7e0ece1e6ebac9b98bf0c9084d1e3d3b2739aa566be7e56e3f7bf2 \ - --hash=sha256:c0ecf4786ad0404b072196b5df774b1bb23c8aacdcacb6c10b4128bc7b00bd01 \ - --hash=sha256:c5b31bed2c2f2fa1fcb4815b75f931121ae210dc89a3d607fb1725f5907f1437 \ - --hash=sha256:c724a884e642aa2bbad52bb0203ea40406ad742368a5f90da1b220e970384aae \ - --hash=sha256:cb92741c2b4ea63c09609b064b26f7f5d9032b61ae222558c55832ec3ad0bcaf \ - --hash=sha256:ced0925a18fddcff289ef54386b2fc230c5af3c83b11558571124bfc485b8c07 \ - --hash=sha256:cf1115207a5049d1f4b7b4b72de0e52f228d6c696803d94843907111cbf80610 \ - --hash=sha256:d3c9a07eafdc70556f8c220da4a538e715668c0c63cabcc436a026e4e89950bf \ - --hash=sha256:d7769c579663a6f8dbf34878969ac71befa42067ce6bf78e6370bf0d1194997c \ - --hash=sha256:d8f89c8d20dfa648a3f0a56861946eb00e5b00d6b00eea14bc5532b2fcfa8ef1 \ - --hash=sha256:d998b432ed9ffccc49b820e913c8f327a82026349e9c34fa3690116f6b70770f \ - --hash=sha256:dcbe48f6a03979384f27086484dc2a14959be1613cb173458bd58f714f2c48f3 \ - --hash=sha256:e17b5b42c8045867ca9d1f54af00cc2275198d38de18545edaa7833d7e9e4ac8 \ - --hash=sha256:e18875e17ef69ba7dfa9623f2f95f3eda6f70b536079ee6d5763ecdfe6cc9040 \ - --hash=sha256:e61ab234624c9ffca0248a707feffe6fac2343758a36725d8eb8a6efef0f8c30 \ - --hash=sha256:ecc2c526547eacd20cb9fbba19a5268611dbc70c346499656d6cf30fae328977 \ - --hash=sha256:f33462b19503ba68d80dac8a1354402675849259fb3ebf53b67de86421735a3a \ - --hash=sha256:fbedeb9b48614d662822ee514567d2d49a8012037fc7b4cd63f282642c2f4b7d \ - --hash=sha256:fd98cacf4e0fabcd4005c452cb8a31750258a85cab9a59fb3559e8078da408d7 \ - --hash=sha256:fdcd095b1b812d756fa5452aca93b962cf620694c0cadb192cec2bb77dcca9a2 +librt==0.7.3 ; python_version >= "3.10" and python_version < "4" \ + --hash=sha256:020c6db391268bcc8ce75105cb572df8cb659a43fd347366aaa407c366e5117a \ + --hash=sha256:0fa9ac2e49a6bee56e47573a6786cb635e128a7b12a0dc7851090037c0d397a3 \ + --hash=sha256:11ad45122bbed42cfc8b0597450660126ef28fd2d9ae1a219bc5af8406f95678 \ + --hash=sha256:120dd21d46ff875e849f1aae19346223cf15656be489242fe884036b23d39e93 \ + --hash=sha256:14569ac5dd38cfccf0a14597a88038fb16811a6fede25c67b79c6d50fc2c8fdc \ + --hash=sha256:1617bea5ab31266e152871208502ee943cb349c224846928a1173c864261375e \ + --hash=sha256:170cdb8436188347af17bf9cccf3249ba581c933ed56d926497119d4cf730cec \ + --hash=sha256:1975eda520957c6e0eb52d12968dd3609ffb7eef05d4223d097893d6daf1d8a7 \ + --hash=sha256:1fe603877e1865b5fd047a5e40379509a4a60204aa7aa0f72b16f7a41c3f0712 \ + --hash=sha256:24d70810f6e2ea853ff79338001533716b373cc0f63e2a0be5bc96129edb5fb5 \ + --hash=sha256:256793988bff98040de23c57cf36e1f4c2f2dc3dcd17537cdac031d3b681db71 \ + --hash=sha256:25711f364c64cab2c910a0247e90b51421e45dbc8910ceeb4eac97a9e132fc6f \ + --hash=sha256:2682162855a708e3270eba4b92026b93f8257c3e65278b456c77631faf0f4f7a \ + --hash=sha256:2cf9d73499486ce39eebbff5f42452518cc1f88d8b7ea4a711ab32962b176ee2 \ + --hash=sha256:2e40520c37926166c24d0c2e0f3bc3a5f46646c34bdf7b4ea9747c297d6ee809 \ + --hash=sha256:2e980cf1ed1a2420a6424e2ed884629cdead291686f1048810a817de07b5eb18 \ + --hash=sha256:2f03484b54bf4ae80ab2e504a8d99d20d551bfe64a7ec91e218010b467d77093 \ + --hash=sha256:35f1609e3484a649bb80431310ddbec81114cd86648f1d9482bc72a3b86ded2e \ + --hash=sha256:399938edbd3d78339f797d685142dd8a623dfaded023cf451033c85955e4838a \ + --hash=sha256:399bbd7bcc1633c3e356ae274a1deb8781c7bf84d9c7962cc1ae0c6e87837292 \ + --hash=sha256:3ec50cf65235ff5c02c5b747748d9222e564ad48597122a361269dd3aa808798 \ + --hash=sha256:3edbf257c40d21a42615e9e332a6b10a8bacaaf58250aed8552a14a70efd0d65 \ + --hash=sha256:440c788f707c061d237c1e83edf6164ff19f5c0f823a3bf054e88804ebf971ec \ + --hash=sha256:44b3689b040df57f492e02cd4f0bacd1b42c5400e4b8048160c9d5e866de8abe \ + --hash=sha256:4887c29cadbdc50640179e3861c276325ff2986791e6044f73136e6e798ff806 \ + --hash=sha256:5460d99ed30f043595bbdc888f542bad2caeb6226b01c33cda3ae444e8f82d42 \ + --hash=sha256:550fdbfbf5bba6a2960b27376ca76d6aaa2bd4b1a06c4255edd8520c306fcfc0 \ + --hash=sha256:56f2a47beda8409061bc1c865bef2d4bd9ff9255219402c0817e68ab5ad89aed \ + --hash=sha256:572a24fc5958c61431da456a0ef1eeea6b4989d81eeb18b8e5f1f3077592200b \ + --hash=sha256:59cb0470612d21fa1efddfa0dd710756b50d9c7fb6c1236bbf8ef8529331dc70 \ + --hash=sha256:6038ccbd5968325a5d6fd393cf6e00b622a8de545f0994b89dd0f748dcf3e19e \ + --hash=sha256:6488e69d408b492e08bfb68f20c4a899a354b4386a446ecd490baff8d0862720 \ + --hash=sha256:687403cced6a29590e6be6964463835315905221d797bc5c934a98750fe1a9af \ + --hash=sha256:6b407c23f16ccc36614c136251d6b32bf30de7a57f8e782378f1107be008ddb0 \ + --hash=sha256:6b4e7bff1d76dd2b46443078519dc75df1b5e01562345f0bb740cea5266d8218 \ + --hash=sha256:6bdd9adfca615903578d2060ee8a6eb1c24eaf54919ff0ddc820118e5718931b \ + --hash=sha256:6eb9295c730e26b849ed1f4022735f36863eb46b14b6e10604c1c39b8b5efaea \ + --hash=sha256:703456146dc2bf430f7832fd1341adac5c893ec3c1430194fdcefba00012555c \ + --hash=sha256:754a0d09997095ad764ccef050dd5bf26cbf457aab9effcba5890dad081d879e \ + --hash=sha256:7af7785f5edd1f418da09a8cdb9ec84b0213e23d597413e06525340bcce1ea4f \ + --hash=sha256:7b29e97273bd6999e2bfe9fe3531b1f4f64effd28327bced048a33e49b99674a \ + --hash=sha256:7b4f57f7a0c65821c5441d98c47ff7c01d359b1e12328219709bdd97fdd37f90 \ + --hash=sha256:8837d5a52a2d7aa9f4c3220a8484013aed1d8ad75240d9a75ede63709ef89055 \ + --hash=sha256:8ccadf260bb46a61b9c7e89e2218f6efea9f3eeaaab4e3d1f58571890e54858e \ + --hash=sha256:8d8cf653e798ee4c4e654062b633db36984a1572f68c3aa25e364a0ddfbbb910 \ + --hash=sha256:93b2a1f325fefa1482516ced160c8c7b4b8d53226763fa6c93d151fa25164207 \ + --hash=sha256:9f0e0927efe87cd42ad600628e595a1a0aa1c64f6d0b55f7e6059079a428641a \ + --hash=sha256:a59a69deeb458c858b8fea6acf9e2acd5d755d76cd81a655256bc65c20dfff5b \ + --hash=sha256:a9f9b661f82693eb56beb0605156c7fca57f535704ab91837405913417d6990b \ + --hash=sha256:abfc57cab3c53c4546aee31859ef06753bfc136c9d208129bad23e2eca39155a \ + --hash=sha256:aca73d70c3f553552ba9133d4a09e767dcfeee352d8d8d3eb3f77e38a3beb3ed \ + --hash=sha256:adeaa886d607fb02563c1f625cf2ee58778a2567c0c109378da8f17ec3076ad7 \ + --hash=sha256:b278a9248a4e3260fee3db7613772ca9ab6763a129d6d6f29555e2f9b168216d \ + --hash=sha256:b7c1239b64b70be7759554ad1a86288220bbb04d68518b527783c4ad3fb4f80b \ + --hash=sha256:bf8c7735fbfc0754111f00edda35cf9e98a8d478de6c47b04eaa9cef4300eaa7 \ + --hash=sha256:c634a0a6db395fdaba0361aa78395597ee72c3aad651b9a307a3a7eaf5efd67e \ + --hash=sha256:cad9971881e4fec00d96af7eaf4b63aa7a595696fc221808b0d3ce7ca9743258 \ + --hash=sha256:cbdb3f337c88b43c3b49ca377731912c101178be91cb5071aac48faa898e6f8e \ + --hash=sha256:cd8551aa21df6c60baa2624fd086ae7486bdde00c44097b32e1d1b1966e365e0 \ + --hash=sha256:d09f677693328503c9e492e33e9601464297c01f9ebd966ea8fc5308f3069bfd \ + --hash=sha256:d376a35c6561e81d2590506804b428fc1075fcc6298fc5bb49b771534c0ba010 \ + --hash=sha256:d39079379a9a28e74f4d57dc6357fa310a1977b51ff12239d7271ec7e71d67f5 \ + --hash=sha256:d86f94743a11873317094326456b23f8a5788bad9161fd2f0e52088c33564620 \ + --hash=sha256:d91e60ac44bbe3a77a67af4a4c13114cbe9f6d540337ce22f2c9eaf7454ca71f \ + --hash=sha256:d9883b2d819ce83f87ba82a746c81d14ada78784db431e57cc9719179847376e \ + --hash=sha256:e094e445c37c57e9ec612847812c301840239d34ccc5d153a982fa9814478c60 \ + --hash=sha256:e19acfde38cb532a560b98f473adc741c941b7a9bc90f7294bc273d08becb58b \ + --hash=sha256:e32d43610dff472eab939f4d7fbdd240d1667794192690433672ae22d7af8445 \ + --hash=sha256:ed028fc3d41adda916320712838aec289956c89b4f0a361ceadf83a53b4c047a \ + --hash=sha256:ef59c938f72bdbc6ab52dc50f81d0637fde0f194b02d636987cea2ab30f8f55a \ + --hash=sha256:f3d4801db8354436fd3936531e7f0e4feb411f62433a6b6cb32bb416e20b529f \ + --hash=sha256:f57aca20e637750a2c18d979f7096e2c2033cc40cf7ed201494318de1182f135 \ + --hash=sha256:f9da128d0edf990cf0d2ca011b02cd6f639e79286774bd5b0351245cbb5a6e51 \ + --hash=sha256:fbd7351d43b80d9c64c3cfcb50008f786cc82cba0450e8599fdd64f264320bd3 \ + --hash=sha256:fcb72249ac4ea81a7baefcbff74df7029c3cb1cf01a711113fa052d563639c9c \ + --hash=sha256:ff21c554304e8226bf80c3a7754be27c6c3549a9fec563a03c06ee8f494da8fc markupsafe==3.0.3 ; python_version >= "3.10" and python_version < "4" \ --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ diff --git a/requirements.txt b/requirements.txt index 568d5871..0f860963 100644 --- a/requirements.txt +++ b/requirements.txt @@ -121,9 +121,9 @@ charset-normalizer==3.4.4 ; python_version >= "3.10" and python_version < "4" \ cvss==3.6 ; python_version >= "3.10" and python_version < "4" \ --hash=sha256:e342c6ad9c7eb69d2aebbbc2768a03cabd57eb947c806e145de5b936219833ea \ --hash=sha256:f21d18224efcd3c01b44ff1b37dec2e3208d29a6d0ce6c87a599c73c21ee1a99 -datadog-api-client==2.46.0 ; python_version >= "3.10" and python_version < "4" \ - --hash=sha256:001719e8ea4968177d038b7f568e5c1f3fa5205896f2d999d082d751bb7bc7a8 \ - --hash=sha256:1d5c03b21fdd513d0619d62080f24a525f728e0f81c748a0eba1e5b54a73002f +datadog-api-client==2.47.0 ; python_version >= "3.10" and python_version < "4" \ + --hash=sha256:35ec8c8ee8ba9b53311465bffd476df3703cc6e48b9f1dd2d0c3a565b909f907 \ + --hash=sha256:5c5644fdf951cb306616276c7cfdf9f3e73b89a82e630976fae9100903a3e53f editor==1.6.6 ; python_version >= "3.10" and python_version < "4" \ --hash=sha256:bb6989e872638cd119db9a4fce284cd8e13c553886a1c044c6b8d8a160c871f8 \ --hash=sha256:e818e6913f26c2a81eadef503a2741d7cca7f235d20e217274a009ecd5a74abf @@ -157,9 +157,9 @@ six==1.17.0 ; python_version >= "3.10" and python_version < "4" \ typing-extensions==4.15.0 ; python_version >= "3.10" and python_version < "4" \ --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 -urllib3==2.5.0 ; python_version >= "3.10" and python_version < "4" \ - --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ - --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc +urllib3==2.6.1 ; python_version >= "3.10" and python_version < "4" \ + --hash=sha256:5379eb6e1aba4088bae84f8242960017ec8d8e3decf30480b3a1abdaa9671a3f \ + --hash=sha256:e67d06fe947c36a7ca39f4994b08d73922d40e6cca949907be05efa6fd75110b wcwidth==0.2.14 ; python_version >= "3.10" and python_version < "4" \ --hash=sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605 \ --hash=sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1 diff --git a/scfw/package_managers/npm.py b/scfw/package_managers/npm/__init__.py similarity index 73% rename from scfw/package_managers/npm.py rename to scfw/package_managers/npm/__init__.py index a3397f9f..b67b9d1b 100644 --- a/scfw/package_managers/npm.py +++ b/scfw/package_managers/npm/__init__.py @@ -3,7 +3,6 @@ """ import json -import logging import os import shutil import subprocess @@ -14,8 +13,7 @@ from scfw.ecosystem import ECOSYSTEM from scfw.package import Package from scfw.package_manager import PackageManager, UnsupportedVersionError - -_log = logging.getLogger(__name__) +from scfw.package_managers.npm.temp_project import TemporaryNpmProject MIN_NPM_VERSION = version_parse("7.0.0") @@ -93,9 +91,8 @@ def resolve_install_targets(self, command: list[str]) -> list[Package]: if `command` were run. Raises: - ValueError: - 1) The given `command` is empty or not a valid `npm` command, or 2) failed to parse - an installation target. + ValueError: The given `command` is empty or not a valid `npm` command + RuntimeError: Failed to resolve npm installation targets (with error detail) UnsupportedVersionError: The underlying `npm` executable is of an unsupported version. """ def is_install_command(command: list[str]) -> bool: @@ -105,22 +102,8 @@ def is_install_command(command: list[str]) -> bool: } return any(alias in command for alias in install_aliases) - def is_place_dep_line(line: str) -> bool: - # The "placeDep" log lines describe a new dependency added to the - # dependency tree being constructed by an installish command - return "placeDep" in line - - def line_to_dependency(line: str) -> str: - # Each added dependency is always the fifth token in its log line - return line.split()[4] - - def str_to_package(s: str) -> Package: - name, sep, version = s.rpartition('@') - if version == s or (sep and not name): - raise ValueError("Failed to parse npm installation target") - return Package(ECOSYSTEM.Npm, name, version) - - command = self._normalize_command(command) + if not command or command[0] != self.name(): + raise ValueError("Received empty or invalid npm command line") # For now, allow all non-`install` commands if not is_install_command(command): @@ -129,35 +112,15 @@ def str_to_package(s: str) -> Package: self._check_version() # On supported versions, the presence of these options prevents the command from running - if any(opt in command for opt in {"-h", "--help", "--dry-run"}): - return [] - - try: - # Compute the set of dependencies added by the install command - dry_run_command = command + ["--dry-run", "--loglevel", "silly"] - dry_run = subprocess.run(dry_run_command, check=True, text=True, capture_output=True) - dependencies = map(line_to_dependency, filter(is_place_dep_line, dry_run.stderr.strip().split('\n'))) - except subprocess.CalledProcessError: - # An erroring command does not install anything - _log.info("Encountered an error while resolving npm installation targets") + if any(opt in command for opt in {"-h", "--help", "--dry-run", "--version"}): return [] try: - # List targets already installed in the npm environment - list_command = [self.executable(), "list", "--all"] - installed = subprocess.run(list_command, check=True, text=True, capture_output=True).stdout - except subprocess.CalledProcessError: - # If this operation fails, rather than blocking, assume nothing is installed - # This has the effect of treating all dependencies like installation targets - _log.warning( - "Failed to list installed npm packages: treating all dependencies as installation targets" - ) - installed = "" - - # The installation targets are the dependencies that are not already installed - targets = filter(lambda dep: dep not in installed, dependencies) + with TemporaryNpmProject(self._executable) as temp_project: + return temp_project.resolve_install_command_targets(command) - return list(map(str_to_package, targets)) + except Exception as e: + raise RuntimeError(f"Failed to resolve npm installation targets: {e}") def list_installed_packages(self) -> list[Package]: """ @@ -185,7 +148,7 @@ def dependencies_to_packages(dependencies: dict[str, dict]) -> set[Package]: self._check_version() try: - npm_list_command = self._normalize_command(["npm", "list", "--all", "--json"]) + npm_list_command = [self._executable, "list", "--all", "--json"] npm_list = subprocess.run(npm_list_command, check=True, text=True, capture_output=True) dependencies = json.loads(npm_list.stdout.strip()).get("dependencies") return list(dependencies_to_packages(dependencies)) if dependencies else [] diff --git a/scfw/package_managers/npm/temp_project.py b/scfw/package_managers/npm/temp_project.py new file mode 100644 index 00000000..2ca8531a --- /dev/null +++ b/scfw/package_managers/npm/temp_project.py @@ -0,0 +1,252 @@ +""" +Provides a class for spinning up ephemeral npm projects to run commands in. +""" + +import functools +import json +import logging +from pathlib import Path +import shutil +import subprocess +from tempfile import TemporaryDirectory +from types import TracebackType +from typing import Any, Optional, Type +from typing_extensions import Self + +from scfw.ecosystem import ECOSYSTEM +from scfw.package import Package + +_log = logging.getLogger(__name__) + + +class TemporaryNpmProject: + """ + Prepares a temporary npm project that duplicates a given one, allowing for executing + `npm` commands in the context of that project safely and without affecting the original. + + This class implements the context manager protocol, and indeed, the temporary resources + needed by this class to run commands exist exist only while inside a context. Invoking + this class' methods outside of a context will result in error. + """ + def __init__(self, executable: str): + """ + Initialize a new `TemporaryNpmProject`. + """ + def get_project_root(executable: str) -> Optional[Path]: + npm_prefix_command = [executable, "prefix"] + npm_prefix_process = subprocess.run(npm_prefix_command, check=True, text=True, capture_output=True) + + npm_prefix = npm_prefix_process.stdout.strip() + if not npm_prefix: + raise RuntimeError("Project root resolution returned no output") + + project_root = Path(npm_prefix) + package_json_path = project_root / "package.json" + + return project_root if package_json_path.is_file() else None + + self._temp_dir: Optional[TemporaryDirectory] = None + self._executable = executable + + try: + self.project_root = get_project_root(executable) + except Exception as e: + raise RuntimeError(f"Failed to resolve npm project root: {e}") + + def __enter__(self) -> Self: + """ + Convert a `TemporaryNpmProject` into a context manager. + + Returns: + The given `TemporaryNpmProject` instance, now as a context manager. + """ + def copy_from_project_root(temp_dir_path: Path, resource: str, is_dir: bool = False): + if not self.project_root: + return + + orig_resource = self.project_root / resource + temp_resource = temp_dir_path / resource + + if is_dir and orig_resource.is_dir(): + shutil.copytree(orig_resource, temp_resource) + elif not is_dir and orig_resource.is_file(): + shutil.copy(orig_resource, temp_resource) + else: + resource_kind = "directory" if is_dir else "file" + _log.info( + f"Project root directory {self.project_root} does not contain a {resource} {resource_kind}" + ) + + self._temp_dir = TemporaryDirectory() + temp_dir_path = Path(self._temp_dir.name) + + copy_from_project_root(temp_dir_path, "package.json") + copy_from_project_root(temp_dir_path, "package-lock.json") + copy_from_project_root(temp_dir_path, "node_modules", is_dir=True) + + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ): + """ + Release the underlying `TemporaryNpmProject` resources on context manager exit. + """ + if self._temp_dir is None: + _log.warning("No handle to temporary npm project directory found on context exit") + return + + self._temp_dir.cleanup() + self._temp_dir = None + + def resolve_install_command_targets(self, install_command: list[str]) -> list[Package]: + """ + Resolve installation targets for an `npm install` command in the temporary environment. + + Args: + install_command: + The `npm install` command whose installation targets should be resolved + in the context of the temporary environment. It is the caller's responsibility + to ensure only `npm install` commands are passed to this method. + + Returns: + A `list[Package]` representing the set of installation targets that would be + installed by the given `npm install` command. + + Raises: + RuntimeError: + * This method was invoked outside of a context (i.e., with no backing resources) + * Required `package-lock.json` file was not written while resolving installation targets + KeyError: The `package-lock.json` file is malformed or missing data for installation targets + ValueError: Failed to parse installation target specification (malformed verbose log output) + """ + def is_global_command(command: list[str]) -> bool: + return any(global_opt in command for global_opt in {"-g", "--global"}) + + def extract_placed_dependencies(dry_run_log: list[str]) -> list[Package]: + placed_dependencies = [] + + # All supported npm versions adhere to this format + for line in dry_run_log: + line_tokens = line.split() + + if line_tokens[2] != "placeDep": + continue + target_spec = line_tokens[4] + + name, sep, version = target_spec.rpartition('@') + if not (name and sep): + raise ValueError(f"Failed to parse npm installation target specification '{target_spec}'") + + placed_dependencies.append(Package(ECOSYSTEM.Npm, name, version)) + + return placed_dependencies + + def extract_target_handles(dry_run_log: list[str]) -> list[str]: + target_handles = [] + + # All supported npm versions adhere to this format + for line in dry_run_log: + line_tokens = line.split() + + if line_tokens[1] in {"sill", "silly"} and line_tokens[2] in {"ADD", "CHANGE"}: + target_handles.append(line_tokens[3]) + + return target_handles + + def handle_to_package(lockfile: dict[str, Any], target_handle: str) -> Package: + # All supported npm versions adhere to this format + target_name = target_handle.rpartition("node_modules/")[2] + + if not (dependencies := lockfile.get("packages")): + raise KeyError("Missing dependencies data in package-lock.json") + if not (target_entry := dependencies.get(target_handle)): + raise KeyError( + f"Missing entry for installation target {target_name} in package-lock.json" + ) + if not (version := target_entry.get("version")): + raise KeyError( + f"Missing version data for installation target {target_name} in package-lock.json" + ) + + return Package(ECOSYSTEM.Npm, name=target_name, version=version) + + if not self._temp_dir: + raise RuntimeError("Cannot run commands in a temporary npm environment outside of a context") + + temp_dir_path = Path(self._temp_dir.name) + + # Validate and normalize `command` with respect to the given npm executable + install_command = self._normalize_command(install_command) + + # First, perform a dry-run of the installation and collect the verbose log output + try: + dry_run_command = install_command + ["--dry-run", "--loglevel", "silly"] + dry_run_process = subprocess.run( + dry_run_command, + check=True, + text=True, + capture_output=True, + cwd=temp_dir_path, + ) + except subprocess.CalledProcessError: + _log.info("Input npm install command results in error: nothing will be installed") + return [] + + dry_run_log = dry_run_process.stderr.strip().split('\n') + + # We need only look at placed dependencies for commands run outside of a project scope + if not self.project_root or is_global_command(install_command): + return extract_placed_dependencies(dry_run_log) + + # Each target handle corresponds to a (possibly duplicated) installation target + target_handles = extract_target_handles(dry_run_log) + if not target_handles: + return [] + + # Safely run the given `npm install` command to write or update the lockfile + # All supported versions of npm support these additional `install` command options + install_command = install_command + ["--package-lock-only", "--ignore-scripts"] + subprocess.run(install_command, check=True, text=True, capture_output=True, cwd=temp_dir_path) + + # Parse the updated lockfile JSON + lockfile_path = temp_dir_path / "package-lock.json" + if not lockfile_path.is_file(): + raise RuntimeError( + "Required package lockfile was not written while resolving installation targets" + ) + with open(lockfile_path) as f: + lockfile = json.load(f) + + # Read the target versions for added and changed packages out of the lockfile + install_targets: set[Package] = functools.reduce( + lambda acc, target_handle: acc | {handle_to_package(lockfile, target_handle)}, + target_handles, + set(), + ) + + return list(install_targets) + + def _normalize_command(self, command: list[str]) -> list[str]: + """ + Normalize an `npm` command. + + Args: + command: A `list[str]` containing an `npm` command line. + + Returns: + The equivalent but normalized form of `command` with the initial `"npm"` + token replaced with the local filesystem path to an `npm` executable. + + Raises: + ValueError: The given `command` is empty or not a valid `npm` command. + """ + if not command: + raise ValueError("Received empty npm command line") + if command[0] != "npm": + raise ValueError("Received invalid npm command line") + + return [self._executable] + command[1:] diff --git a/tests/package_managers/npm_fixtures.py b/tests/package_managers/npm_fixtures.py new file mode 100644 index 00000000..c83a0d55 --- /dev/null +++ b/tests/package_managers/npm_fixtures.py @@ -0,0 +1,172 @@ +""" +Provides a collection of common fixtures for the npm tests. +""" + +import os +from pathlib import Path +import shutil +import subprocess +from tempfile import TemporaryDirectory +from typing import Optional + +import pytest + +TEST_PACKAGE = "react" +TEST_PACKAGE_LATEST = "18.3.0" +TEST_PACKAGE_PREVIOUS = "18.2.0" + + +@pytest.fixture +def empty_directory(): + """ + Initialize an empty directory. + """ + tempdir = TemporaryDirectory() + + yield Path(tempdir.name) + + tempdir.cleanup() + + +@pytest.fixture +def new_npm_project(): + """ + Initialize a new npm project in an empty directory. + """ + tempdir = TemporaryDirectory() + tempdir_path = Path(tempdir.name) + init_npm_project(tempdir_path) + + yield tempdir_path + + tempdir.cleanup() + + +@pytest.fixture +def npm_project_test_package_latest(): + """ + Initialize an npm project with the `TEST_PACKAGE@TEST_PACKAGE_LATEST` installed. + """ + tempdir = TemporaryDirectory() + tempdir_path = Path(tempdir.name) + init_npm_project( + tempdir_path, + dependencies=[(TEST_PACKAGE, TEST_PACKAGE_LATEST)], + ) + + yield tempdir_path + + tempdir.cleanup() + + +@pytest.fixture +def npm_project_test_package_latest_lockfile(): + """ + Initialize an npm project with `TEST_PACKAGE@TEST_PACKAGE_LATEST` installed + and with a `package-lock.json` file. + """ + tempdir = TemporaryDirectory() + tempdir_path = Path(tempdir.name) + init_npm_project( + tempdir_path, + dependencies=[(TEST_PACKAGE, TEST_PACKAGE_LATEST)], + with_lockfile=True, + with_node_modules=False, + ) + + yield tempdir_path + + tempdir.cleanup() + + +@pytest.fixture +def npm_project_test_package_latest_lockfile_modules(): + """ + Initialize an npm project with `TEST_PACKAGE@TEST_PACKAGE_LATEST` installed + and with a `package-lock.json` file and `node_modules/` directory. + """ + tempdir = TemporaryDirectory() + tempdir_path = Path(tempdir.name) + init_npm_project( + tempdir_path, + dependencies=[(TEST_PACKAGE, TEST_PACKAGE_LATEST)], + with_lockfile=True, + with_node_modules=True, + ) + + yield tempdir_path + + tempdir.cleanup() + + +@pytest.fixture +def npm_project_test_package_previous_lockfile(): + """ + Initialize an npm project with `TEST_PACKAGE@TEST_PACKAGE_PREVIOUS` installed + and with a `package-lock.json` file. + """ + tempdir = TemporaryDirectory() + tempdir_path = Path(tempdir.name) + init_npm_project( + tempdir_path, + dependencies=[(TEST_PACKAGE, TEST_PACKAGE_PREVIOUS)], + with_lockfile=True, + ) + + yield tempdir_path + + tempdir.cleanup() + + +@pytest.fixture +def npm_project_test_package_previous_lockfile_modules(): + """ + Initialize an npm project with `TEST_PACKAGE@TEST_PACKAGE_PREVIOUS` installed + and with a `package-lock.json` file and `node_modules/` directory. + """ + tempdir = TemporaryDirectory() + tempdir_path = Path(tempdir.name) + init_npm_project( + tempdir_path, + dependencies=[(TEST_PACKAGE, TEST_PACKAGE_PREVIOUS)], + with_lockfile=True, + with_node_modules=True, + ) + + yield tempdir_path + + tempdir.cleanup() + + +def init_npm_project( + path: Path, + dependencies: Optional[list[tuple[str, str]]] = None, + with_lockfile: bool = False, + with_node_modules: bool = False, +): + """ + Initialize an npm project in `path` with the given `dependencies` and with or + without the `package-lock.json` file and `node_modules/` directory present. + + Note that setting `with_lockfile=False` always results in the `node_modules/` + directory being deleted, regardless of the value of `with_node_modules`. + """ + subprocess.run(["npm", "init", "--yes"], check=True, text=True, capture_output=True, cwd=path) + + if not dependencies: + return + + for package, version in dependencies: + subprocess.run( + ["npm", "install", f"{package}@{version}"], + check=True, + text=True, + capture_output=True, + cwd=path, + ) + + if not (with_node_modules and with_lockfile): + shutil.rmtree(path / "node_modules") + + if not with_lockfile: + os.remove(path / "package-lock.json") diff --git a/tests/package_managers/poetry_fixtures.py b/tests/package_managers/poetry_fixtures.py new file mode 100644 index 00000000..2e2eaaeb --- /dev/null +++ b/tests/package_managers/poetry_fixtures.py @@ -0,0 +1,128 @@ +""" +Provides a collection of common fixtures for the Poetry tests. +""" + +from pathlib import Path +import subprocess +import sys +from tempfile import TemporaryDirectory + +import pytest +import requests + +TEST_PROJECT_NAME = "foo" + +# Tree-sitter is a convenient test target because it never has any dependencies +# and is not part of the standard set of system Python modules +TARGET = "tree-sitter" + +# Version numbers of available Tree-sitter releases on PyPI +TARGET_RELEASES = list( + requests.get(f"https://pypi.org/pypi/{TARGET}/json", timeout=5).json()["releases"] +) + +# The latest and most recent previous versions of Tree-sitter +TARGET_LATEST = TARGET_RELEASES[-1] +TARGET_PREVIOUS = TARGET_RELEASES[-2] + + +@pytest.fixture +def new_poetry_project(): + """ + Initialize a clean Poetry project for use in testing. + """ + tempdir = TemporaryDirectory() + _init_poetry_project(tempdir.name, TEST_PROJECT_NAME) + + yield tempdir.name + + tempdir.cleanup() + + +@pytest.fixture +def poetry_project_target_latest(): + """ + Initialize a Poetry project with the latest version of `TARGET` as a dependency. + """ + tempdir = TemporaryDirectory() + _init_poetry_project(tempdir.name, TEST_PROJECT_NAME, [(TARGET, TARGET_LATEST)]) + + yield tempdir.name + + tempdir.cleanup() + + +@pytest.fixture +def poetry_project_target_previous(): + """ + Initialize a Poetry project with the previous version of `TARGET` as a dependency. + """ + tempdir = TemporaryDirectory() + _init_poetry_project(tempdir.name, TEST_PROJECT_NAME, [(TARGET, TARGET_PREVIOUS)]) + + yield tempdir.name + + tempdir.cleanup() + + +@pytest.fixture +def poetry_project_target_latest_lock_previous(): + """ + Initialize a Poetry project where the latest version of `TARGET` has been installed but + the previous version of it is an as-yet uninstalled dependency of the project. + """ + tempdir = TemporaryDirectory() + _init_poetry_project(tempdir.name, TEST_PROJECT_NAME, [(TARGET, TARGET_LATEST)]) + subprocess.run(["poetry", "add", "--lock", f"{TARGET}=={TARGET_PREVIOUS}"], check=True, cwd=tempdir.name) + + yield tempdir.name + + tempdir.cleanup() + + +@pytest.fixture +def poetry_project_target_previous_lock_latest(): + """ + Initialize a Poetry project where the previous version of `TARGET` has been installed but + the latest version of it is an as-yet uninstalled dependency of the project. + """ + tempdir = TemporaryDirectory() + _init_poetry_project(tempdir.name, TEST_PROJECT_NAME, [(TARGET, TARGET_PREVIOUS)]) + subprocess.run(["poetry", "add", "--lock", f"{TARGET}=={TARGET_LATEST}"], check=True, cwd=tempdir.name) + + yield tempdir.name + + tempdir.cleanup() + + +@pytest.fixture +def poetry_project_lock_latest(): + """ + Initialize a Poetry project where the latest version of `TARGET` is an as-yet + uninstalled dependency. + """ + tempdir = TemporaryDirectory() + _init_poetry_project(tempdir.name, TEST_PROJECT_NAME) + subprocess.run(["poetry", "add", "--lock", f"{TARGET}=={TARGET_LATEST}"], check=True, cwd=tempdir.name) + + yield tempdir.name + + tempdir.cleanup() + + +def _init_poetry_project(directory, name, dependencies = None): + """ + Initialize a fresh Poetry project in `directory` with the given `dependencies`. + """ + subprocess.run(["poetry", "init", "--no-interaction", "--name", name], check=True, cwd=directory) + subprocess.run(["poetry", "lock"], check=True, cwd=directory) + + # Create a separate venv for Poetry to use during testing + venv_path = Path(directory) / "venv" + venv_python_path = venv_path / "bin" / "python" + subprocess.run([sys.executable, "-m", "venv", venv_path], check=True) + subprocess.run(["poetry", "env", "use", venv_python_path], check=True, cwd=directory) + + if dependencies: + for package, version in dependencies: + subprocess.run(["poetry", "add", f"{package}=={version}"], check=True, cwd=directory) diff --git a/tests/package_managers/test_npm.py b/tests/package_managers/test_npm.py index 193f223b..8bc4002c 100644 --- a/tests/package_managers/test_npm.py +++ b/tests/package_managers/test_npm.py @@ -1,87 +1,350 @@ """ -Tests of npm's command line behavior. +Tests establishing the validity of Supply-Chain Firewall's assumptions about +npm's command-line options and behavior. """ +import json +import os +from pathlib import Path +import re +import subprocess + import packaging.version as version import pytest -import subprocess -from scfw.ecosystem import ECOSYSTEM +from .npm_fixtures import * -from .utils import read_top_packages, select_test_install_target + +def test_npm_version_output(): + """ + Test that `npm --version` has the required format. + """ + version_str = subprocess.run(["npm", "--version"], check=True, text=True, capture_output=True) + version.parse(version_str.stdout.strip()) -def npm_list() -> str: +def test_npm_prefix_output(new_npm_project): """ - Get the current state of packages installed via npm. + Test that the `npm prefix` command exists and has the expected behavior. """ - return subprocess.run(["npm", "list", "--all"], check=True, text=True, capture_output=True).stdout.lower() + def get_npm_prefix(path: Path) -> Path: + npm_prefix = subprocess.run( + ["npm", "prefix"], + check=True, + text=True, + capture_output=True, + cwd=path, + ) + return Path(npm_prefix.stdout.strip()) + # The conversions via `os.path.realpath()` are needed to account for a + # macOS-specific discrepancy in which the temporary directories are under + # `/private/var` but report being under only `/var` + project_realpath = os.path.realpath(new_npm_project) -INIT_NPM_STATE = npm_list() -""" -Caches the npm installation state before running any tests. -""" + assert os.path.realpath(get_npm_prefix(new_npm_project)) == project_realpath -TEST_TARGET = select_test_install_target(read_top_packages(ECOSYSTEM.Npm), INIT_NPM_STATE) -""" -A fresh (not currently installed) package target to use for testing. -""" + # Make a subdirectory and rerun the experiment from within + subdirectory = new_npm_project / "foo" + subdirectory.mkdir() + assert os.path.realpath(get_npm_prefix(subdirectory)) == project_realpath -def test_npm_version_output(): + # Now create an npm project in the subdirectory and check that the prefix changes + init_npm_project(subdirectory) + assert os.path.realpath(get_npm_prefix(subdirectory)) == os.path.realpath(subdirectory) + + +def test_npm_loglevel_override(empty_directory): """ - Test that `npm --version` has the required format. + Test that we can override the log level setting included in an npm command, + that is, that npm ignores all but the last instance of the `--loglevel` option. """ - version_str = subprocess.run(["npm", "--version"], check=True, text=True, capture_output=True) - version.parse(version_str.stdout.strip()) + command_line = [ + "npm", "--loglevel", "silent", "install", "--dry-run", TEST_PACKAGE, "--loglevel", "silly" + ] + silly_lines = _get_silly_log_lines(empty_directory, command_line) + + # We successfully overrode the `silent` log level and observe `silly` log lines + assert silly_lines + + +def test_npm_log_line_format_place_dep(empty_directory): + """ + Test that the `placeDep` log lines have the required format. + """ + target_spec = f"{TEST_PACKAGE}@{TEST_PACKAGE_LATEST}" + command_line = ["npm", "install", target_spec, "--dry-run", "--loglevel", "silly"] + + # There are `silly` log lines for this command + silly_lines = _get_silly_log_lines(empty_directory, command_line) + assert silly_lines + + # There are `placeDep` lines among the `silly` lines + place_dep_lines = list(filter(lambda l: "placeDep" in l, silly_lines)) + assert place_dep_lines + + # The `placeDep` lines have the required format + assert all(line.split()[2] == "placeDep" for line in place_dep_lines) + assert all(re.fullmatch(r"@?(.+)@(.+)", line.split()[4]) for line in place_dep_lines) + + # One of the `placeDep` lines is for our test package + assert any(line.split()[4] == target_spec for line in place_dep_lines) + + +def test_npm_log_line_format_add(empty_directory): + """ + Test that the `ADD` log lines have the required format. + """ + target_spec = f"{TEST_PACKAGE}@{TEST_PACKAGE_LATEST}" + command_line = ["npm", "install", target_spec, "--dry-run", "--loglevel", "silly"] + + # There are `silly` log lines for this command + silly_lines = _get_silly_log_lines(empty_directory, command_line) + assert silly_lines + + # There are `ADD` lines among the `silly` lines + add_lines = list(filter(lambda l: "ADD" in l, silly_lines)) + assert add_lines + + # The `ADD` lines have the required format + assert all(line.split()[2] == "ADD" for line in add_lines) + assert all(line.split()[3].startswith("node_modules/") for line in add_lines) + + # One of the `ADD` lines is for our test package + assert any(line.split()[3] == f"node_modules/{TEST_PACKAGE}" for line in add_lines) + + +def test_npm_log_line_format_change(npm_project_test_package_previous_lockfile_modules): + """ + Test that the `CHANGE` log lines have the required format. + """ + target_spec = f"{TEST_PACKAGE}@{TEST_PACKAGE_LATEST}" + command_line = ["npm", "install", target_spec, "--dry-run", "--loglevel", "silly"] + + # There are `silly` log lines for this command + silly_lines = _get_silly_log_lines(npm_project_test_package_previous_lockfile_modules, command_line) + assert silly_lines + + # There are `CHANGE` lines among the `silly` lines + change_lines = list(filter(lambda l: "CHANGE" in l, silly_lines)) + assert change_lines + + # The `CHANGE` lines have the required format + assert all(line.split()[2] == "CHANGE" for line in change_lines) + assert all(line.split()[3].startswith("node_modules/") for line in change_lines) + + # One of the `CHANGE` lines is for our test package + assert any(line.split()[3] == f"node_modules/{TEST_PACKAGE}" for line in change_lines) + + +def test_npm_install_package_lock_only_test_package_latest(npm_project_test_package_latest): + """ + Test that the `--package-lock-only` option works as expected in an npm project + with a specified dependency but no prior `package-lock.json` file. + """ + _backend_test_npm_install_package_lock_only(npm_project_test_package_latest, ["npm", "install"]) + + +def test_npm_install_package_lock_only_test_package_previous_lockfile(npm_project_test_package_previous_lockfile): + """ + Test that the `--package-lock-only` option works as expected in an npm project + with a specified dependency and a prior `package-lock.json` file. + """ + _backend_test_npm_install_package_lock_only( + npm_project_test_package_previous_lockfile, + ["npm", "install", f"{TEST_PACKAGE}@{TEST_PACKAGE_LATEST}"] + ) + + +def test_npm_install_ignore_scripts(npm_project_test_package_latest): + """ + Test that the `--ignore-scripts` option works as expected. + """ + test_script_body = "This should never execute" + scripts = { + lifecycle: f"echo \"{test_script_body}\"" + for lifecycle in {"preinstall", "install", "postinstall"} + } + + # Add lifecycle scripts to the package.json file + package_json_path = npm_project_test_package_latest / "package.json" + + with open(package_json_path) as f: + package_json = json.load(f) + + package_json["scripts"] = scripts + + with open(package_json_path, 'w') as f: + json.dump(package_json, f, indent=4) + + # Check that the scripts do run normally + p = subprocess.run( + ["npm", "install"], + check=True, + text=True, + capture_output=True, + cwd=npm_project_test_package_latest + ) + assert test_script_body in p.stdout + + # Check that the scripts do not run with `--ignore-scripts` + p = subprocess.run( + ["npm", "install", "--ignore-scripts"], + check=True, + text=True, + capture_output=True, + cwd=npm_project_test_package_latest + ) + assert test_script_body not in p.stdout @pytest.mark.parametrize( "command_line", [ - ["npm", "-h", "install", TEST_TARGET], - ["npm", "--help", "install", TEST_TARGET], - ["npm", "--dry-run", "install", TEST_TARGET], - ["npm", "install", "--dry-run", TEST_TARGET], - ["npm", "install", TEST_TARGET, "--dry-run"], - ["npm", "--dry-run", "install", "--dry-run", TEST_TARGET, "--dry-run"], - ["npm", "--non-existent-option", "install", TEST_TARGET, "--dry-run"] + ["npm", "--version", "install", TEST_PACKAGE], + ["npm", "install", "--version", TEST_PACKAGE], + ["npm", "install", TEST_PACKAGE, "--version"], + ["npm", "--version", "install", "--version", TEST_PACKAGE, "--version"], + ["npm", "-h", "install", TEST_PACKAGE], + ["npm", "install", "-h", TEST_PACKAGE], + ["npm", "install", TEST_PACKAGE, "-h"], + ["npm", "-h", "install", "-h", TEST_PACKAGE, "-h"], + ["npm", "--help", "install", TEST_PACKAGE], + ["npm", "install", "--help", TEST_PACKAGE], + ["npm", "install", TEST_PACKAGE, "--help"], + ["npm", "--help", "install", "--help", TEST_PACKAGE, "--help"], + ["npm", "--dry-run", "install", TEST_PACKAGE], + ["npm", "install", "--dry-run", TEST_PACKAGE], + ["npm", "install", TEST_PACKAGE, "--dry-run"], + ["npm", "--dry-run", "install", "--dry-run", TEST_PACKAGE, "--dry-run"], + ["npm", "--non-existent-option", "--version", "install", TEST_PACKAGE], + ["npm", "--non-existent-option", "-h", "install", TEST_PACKAGE], + ["npm", "--non-existent-option", "--help", "install", TEST_PACKAGE], + ["npm", "--non-existent-option", "--dry-run", "install", TEST_PACKAGE], ] ) -def test_npm_no_change(command_line: list[str]): +def test_options_prevent_install_empty_directory(empty_directory, command_line: list[str]): """ - Backend function for testing that an `npm` command does not encounter any - errors and does not modify the local `npm` installation state. + Test that the `-h`/`--help` and `--dry-run` options prevent an `npm install` + command from running in an empty directory. """ - subprocess.run(command_line, check=True) - assert npm_list() == INIT_NPM_STATE + _backend_test_no_change(empty_directory, command_line) @pytest.mark.parametrize( "command_line", [ - ["npm", "--non-existent-option"], - ["npm", "install", "--dry-run", "!!!a_nonexistent_p@ckage_name"] + ["npm", "--version", "install", TEST_PACKAGE], + ["npm", "install", "--version", TEST_PACKAGE], + ["npm", "install", TEST_PACKAGE, "--version"], + ["npm", "--version", "install", "--version", TEST_PACKAGE, "--version"], + ["npm", "-h", "install", TEST_PACKAGE], + ["npm", "install", "-h", TEST_PACKAGE], + ["npm", "install", TEST_PACKAGE, "-h"], + ["npm", "-h", "install", "-h", TEST_PACKAGE, "-h"], + ["npm", "--help", "install", TEST_PACKAGE], + ["npm", "install", "--help", TEST_PACKAGE], + ["npm", "install", TEST_PACKAGE, "--help"], + ["npm", "--help", "install", "--help", TEST_PACKAGE, "--help"], + ["npm", "--dry-run", "install", TEST_PACKAGE], + ["npm", "install", "--dry-run", TEST_PACKAGE], + ["npm", "install", TEST_PACKAGE, "--dry-run"], + ["npm", "--dry-run", "install", "--dry-run", TEST_PACKAGE, "--dry-run"], + ["npm", "--non-existent-option", "--version", "install", TEST_PACKAGE], + ["npm", "--non-existent-option", "-h", "install", TEST_PACKAGE], + ["npm", "--non-existent-option", "--help", "install", TEST_PACKAGE], + ["npm", "--non-existent-option", "--dry-run", "install", TEST_PACKAGE], ] ) -def test_npm_no_change_error(command_line: list[str]): +def test_options_prevent_install_new_npm_project(new_npm_project, command_line: list[str]): """ - Backend function for testing that an `npm` command raises an error and - does not modify the local `npm` installation state. + Test that the `-h`/`--help` and `--dry-run` options prevent an `npm install` + command from running in a new npm project. """ - with pytest.raises(subprocess.CalledProcessError): - subprocess.run(command_line, check=True) - assert npm_list() == INIT_NPM_STATE + _backend_test_no_change(new_npm_project, command_line) -def test_npm_loglevel_override(): +def get_npm_project_state(project_path: Path) -> str: """ - Test that all but the last instance of `--loglevel` are ignored by `npm`. + Return the current state of installed packages in the given npm project. """ - command_line = [ - "npm", "--loglevel", "silent", "install", "--dry-run", TEST_TARGET, "--loglevel", "silly" - ] - p = subprocess.run(command_line, check=True, text=True, capture_output=True) - assert p.stderr - assert "silly" in p.stderr + lockfile_path = project_path / "package-lock.json" + node_modules_path = project_path / "node_modules/" + + # On older versions of npm, the inverse of this condition results + # useful output being returned with an error code, so we disable `check` + check = not lockfile_path.is_file() or node_modules_path.is_dir() + + npm_list_command = ["npm", "list", "--all"] + npm_list_process = subprocess.run( + npm_list_command, + check=check, + text=True, + capture_output=True, + cwd=project_path + ) + + return npm_list_process.stdout.strip() + + +def _backend_test_npm_install_package_lock_only(project: Path, command_line: list[str]): + """ + Backend function for testing that the `--package-lock-only` option of the + `npm install` command works as expected. + """ + subprocess.run( + command_line + ["--package-lock-only"], + check=True, + text=True, + capture_output=True, + cwd=project, + ) + + package_json_path = project / "package.json" + lockfile_path = project / "package-lock.json" + node_modules_path = project / "node_modules/" + + assert package_json_path.is_file() + assert lockfile_path.is_file() + assert not node_modules_path.exists() + + +def _backend_test_no_change(project: Path, command_line: list[str]): + """ + Backend function for testing that running an npm command does not modify + the project state and should or should not raise an error. + """ + initial_state = get_npm_project_state(project) + + subprocess.run(command_line, check=True, cwd=project) + + assert get_npm_project_state(project) == initial_state + + +def _get_silly_log_lines(project: Path, command_line: list[str]) -> list[str]: + """ + Return the `silly` log lines output, if any, from running the given `command_line` + in the given `project`. + + If there are `silly` lines, ensure they have the expected format before returning. + """ + log_output = subprocess.run( + command_line, + check=True, + text=True, + capture_output=True, + cwd=project, + ) + + silly_lines = list( + filter(lambda l: l.startswith("npm sill"), log_output.stderr.strip().split('\n')) + ) + + # The `silly` log lines all have the expected format + assert ( + all(line.split()[1] == "sill" for line in silly_lines) + or all(line.split()[1] == "silly" for line in silly_lines) + ) + + return silly_lines diff --git a/tests/package_managers/test_npm_class.py b/tests/package_managers/test_npm_class.py index 97c80f4e..59bf4b84 100644 --- a/tests/package_managers/test_npm_class.py +++ b/tests/package_managers/test_npm_class.py @@ -2,78 +2,237 @@ Tests of `Npm`, the `PackageManager` subclass. """ +from pathlib import Path +from typing import Optional + import pytest -import subprocess -from tempfile import TemporaryDirectory from scfw.ecosystem import ECOSYSTEM from scfw.package import Package from scfw.package_managers.npm import Npm -from .test_npm import INIT_NPM_STATE, TEST_TARGET, npm_list +from .npm_fixtures import * +from .test_npm import get_npm_project_state PACKAGE_MANAGER = Npm() """ Fixed `PackageManager` to use across all tests. """ +TEST_PACKAGE_LATEST_INSTALL_TARGETS = { + Package(ECOSYSTEM.Npm, TEST_PACKAGE, TEST_PACKAGE_LATEST), + Package(ECOSYSTEM.Npm, "js-tokens", "4.0.0"), + Package(ECOSYSTEM.Npm, "loose-envify", "1.4.0"), +} +""" +Known installation targets for `TEST_PACKAGE@TEST_PACKAGE_LATEST`. +""" + +TEST_PACKAGE_PREVIOUS_INSTALL_TARGETS = { + Package(ECOSYSTEM.Npm, TEST_PACKAGE, TEST_PACKAGE_PREVIOUS), + Package(ECOSYSTEM.Npm, "js-tokens", "4.0.0"), + Package(ECOSYSTEM.Npm, "loose-envify", "1.4.0"), +} +""" +Known installation targets for `TEST_PACKAGE@TEST_PACKAGE_PREVIOUS`. +""" + @pytest.mark.parametrize( - "command_line,has_targets", - [ - (["npm", "install", TEST_TARGET], True), - (["npm", "-h", "install", TEST_TARGET], False), - (["npm", "--help", "install", TEST_TARGET], False), - (["npm", "install", "-h", TEST_TARGET], False), - (["npm", "install", "--help", TEST_TARGET], False), - (["npm", "--dry-run", "install", TEST_TARGET], False), - (["npm", "install", "--dry-run", TEST_TARGET], False), - (["npm", "--non-existent-option"], False) - ] + "command, true_targets", + [ + (["npm", "install"], None), + (["npm", "install", f"{TEST_PACKAGE}@{TEST_PACKAGE_LATEST}"], TEST_PACKAGE_LATEST_INSTALL_TARGETS), + ] ) -def test_npm_command_resolve_install_targets(command_line: list[str], has_targets: bool): +def test_resolve_install_targets_empty_directory( + monkeypatch, + empty_directory, + command: list[str], + true_targets: Optional[set[Package]], +): """ - Backend function for testing that an `Npm.resolve_install_targets` call - either does or does not have install targets and does not modify the local - npm installation state. + Test that `Npm` correctly resolves installation targets for `npm install` commands + run in an empty directory. """ - targets = PACKAGE_MANAGER.resolve_install_targets(command_line) - if has_targets: - assert targets - else: - assert not targets - assert npm_list() == INIT_NPM_STATE + _backend_test_resolve_install_targets(monkeypatch, empty_directory, command, true_targets) + + +@pytest.mark.parametrize( + "command, true_targets", + [ + (["npm", "install"], None), + (["npm", "install", f"{TEST_PACKAGE}@{TEST_PACKAGE_LATEST}"], TEST_PACKAGE_LATEST_INSTALL_TARGETS), + ] +) +def test_resolve_install_targets_new_project( + monkeypatch, + new_npm_project, + command: list[str], + true_targets: Optional[set[Package]] +): + """ + Test that `Npm` correctly resolves installation targets for `npm install` commands + run in a new, uninstalled `npm` project. + """ + _backend_test_resolve_install_targets(monkeypatch, new_npm_project, command, true_targets) -def test_npm_command_resolve_install_targets_exact(): +@pytest.mark.parametrize( + "command, true_targets", + [ + (["npm", "install"], TEST_PACKAGE_LATEST_INSTALL_TARGETS), + ( + ["npm", "install", f"{TEST_PACKAGE}@{TEST_PACKAGE_LATEST}"], + TEST_PACKAGE_LATEST_INSTALL_TARGETS, + ), + ( + ["npm", "install", f"{TEST_PACKAGE}@{TEST_PACKAGE_PREVIOUS}"], + TEST_PACKAGE_PREVIOUS_INSTALL_TARGETS, + ), + ] +) +def test_resolve_install_targets_test_package_latest_lockfile( + monkeypatch, + npm_project_test_package_latest_lockfile, + command: list[str], + true_targets: Optional[set[Package]], +): """ - Test that `Npm.resolve_install_targets` gives the right answer relative to - an exact top-level installation target and its dependencies. + Test that `Npm` correctly resolves installation targets for `npm install` commands + run in an npm project with a `package-lock.json` file but no installed dependencies + and in which we can downgrade a dependency. """ - true_targets = list( - map( - lambda p: Package(ECOSYSTEM.Npm, p[0], p[1]), - [ - ("js-tokens", "4.0.0"), - ("loose-envify", "1.4.0"), - ("react", "18.3.1") - ] - ) + _backend_test_resolve_install_targets( + monkeypatch, + npm_project_test_package_latest_lockfile, + command, + true_targets, ) - command_line = ["npm", "install", "react@18.3.1"] - targets = PACKAGE_MANAGER.resolve_install_targets(command_line) - assert len(targets) == len(true_targets) - assert all(target in true_targets for target in targets) + +@pytest.mark.parametrize( + "command, true_targets", + [ + (["npm", "install"], None), + (["npm", "install", f"{TEST_PACKAGE}@{TEST_PACKAGE_LATEST}"], None), + ( + ["npm", "install", f"{TEST_PACKAGE}@{TEST_PACKAGE_PREVIOUS}"], + {Package(ECOSYSTEM.Npm, TEST_PACKAGE, TEST_PACKAGE_PREVIOUS)}, + ), + ] +) +def test_resolve_install_targets_test_package_latest_lockfile_modules( + monkeypatch, + npm_project_test_package_latest_lockfile_modules, + command: list[str], + true_targets: Optional[set[Package]], +): + """ + Test that `Npm` correctly resolves installation targets for `npm install` commands + run in a fully installed npm project and in which we can downgrade a dependency. + """ + _backend_test_resolve_install_targets( + monkeypatch, + npm_project_test_package_latest_lockfile_modules, + command, + true_targets, + ) -def test_npm_list_installed_packages(monkeypatch): +@pytest.mark.parametrize( + "command, true_targets", + [ + (["npm", "install"], TEST_PACKAGE_PREVIOUS_INSTALL_TARGETS), + ( + ["npm", "install", f"{TEST_PACKAGE}@{TEST_PACKAGE_PREVIOUS}"], + TEST_PACKAGE_PREVIOUS_INSTALL_TARGETS, + ), + ( + ["npm", "install", f"{TEST_PACKAGE}@{TEST_PACKAGE_LATEST}"], + TEST_PACKAGE_LATEST_INSTALL_TARGETS, + ), + ] +) +def test_resolve_install_targets_test_package_previous_lockfile( + monkeypatch, + npm_project_test_package_previous_lockfile, + command: list[str], + true_targets: Optional[set[Package]], +): + """ + Test that `Npm` correctly resolves installation targets for `npm install` commands + run in an npm project with a `package-lock.json` file but no installed dependencies + and in which we can upgrade a dependency. + """ + _backend_test_resolve_install_targets( + monkeypatch, + npm_project_test_package_previous_lockfile, + command, + true_targets, + ) + + +@pytest.mark.parametrize( + "command, true_targets", + [ + (["npm", "install"], None), + (["npm", "install", f"{TEST_PACKAGE}@{TEST_PACKAGE_PREVIOUS}"], None), + ( + ["npm", "install", f"{TEST_PACKAGE}@{TEST_PACKAGE_LATEST}"], + {Package(ECOSYSTEM.Npm, TEST_PACKAGE, TEST_PACKAGE_LATEST)}, + ), + ] +) +def test_resolve_install_targets_test_package_previous_lockfile_modules( + monkeypatch, + npm_project_test_package_previous_lockfile_modules, + command: list[str], + true_targets: Optional[set[Package]], +): + """ + Test that `Npm` correctly resolves installation targets for `npm install` commands + run in a fully installed npm project where we can upgrade a dependency. + """ + _backend_test_resolve_install_targets( + monkeypatch, + npm_project_test_package_previous_lockfile_modules, + command, + true_targets, + ) + + +def test_npm_list_installed_packages(monkeypatch, npm_project_test_package_latest_lockfile_modules): """ Test that `Npm.list_installed_packages` correctly parses `npm` output. """ - target = Package(ECOSYSTEM.Npm, "react", "19.1.0") + monkeypatch.chdir(npm_project_test_package_latest_lockfile_modules) + + installed_packages = PACKAGE_MANAGER.list_installed_packages() + + assert len(installed_packages) == len(TEST_PACKAGE_LATEST_INSTALL_TARGETS) + assert set(installed_packages) == TEST_PACKAGE_LATEST_INSTALL_TARGETS + + +def _backend_test_resolve_install_targets( + monkeypatch, + project: Path, + command: list[str], + true_targets: Optional[set[Package]], +): + """ + Backend function for testing that the `Npm.resolve_install_targets()` method + correctly identifies install targets and does not modify the project state. + """ + monkeypatch.chdir(project) + initial_state = get_npm_project_state(project) + + targets = PACKAGE_MANAGER.resolve_install_targets(command) + + if true_targets is None: + assert not targets + else: + assert len(targets) == len(true_targets) + assert set(targets) == true_targets - with TemporaryDirectory() as tmp: - monkeypatch.chdir(tmp) - subprocess.run(["npm", "install", f"{target.name}@{target.version}"], check=True) - assert PACKAGE_MANAGER.list_installed_packages() == [target] + assert get_npm_project_state(project) == initial_state diff --git a/tests/package_managers/test_pip.py b/tests/package_managers/test_pip.py index 5e614988..ed562cd1 100644 --- a/tests/package_managers/test_pip.py +++ b/tests/package_managers/test_pip.py @@ -3,15 +3,13 @@ """ import json -import packaging.version as version -import pytest +import os import subprocess import sys import tempfile -from scfw.ecosystem import ECOSYSTEM - -from .utils import read_top_packages, select_test_install_target +import packaging.version as version +import pytest PIP_COMMAND_PREFIX = [sys.executable, "-m", "pip"] @@ -24,12 +22,34 @@ def pip_list() -> str: return subprocess.run(pip_list_command, check=True, text=True, capture_output=True).stdout.lower() +def select_test_install_target(installed_packages: str) -> str: + """ + Select a test target that is not in the given installed packages output. + + This allows us to be certain when testing that nothing was installed in a dry-run. + """ + def read_top_pypi_packages() -> set[str]: + test_dir = os.path.dirname(os.path.realpath(__file__, strict=True)) + top_packages_file = os.path.join(test_dir, f"top_pypi_packages.txt") + with open(top_packages_file) as f: + return set(f.read().split()) + + try: + top_packages = read_top_pypi_packages() + while (choice := top_packages.pop()) in installed_packages: + pass + return choice + + except KeyError: + raise RuntimeError("Unable to select a target package for testing") + + INIT_PIP_STATE = pip_list() """ Caches the pip installation state before running any tests. """ -TEST_TARGET = select_test_install_target(read_top_packages(ECOSYSTEM.PyPI), INIT_PIP_STATE) +TEST_TARGET = select_test_install_target(INIT_PIP_STATE) """ A fresh (not currently installed) package target to use for testing. """ diff --git a/tests/package_managers/test_poetry.py b/tests/package_managers/test_poetry.py index 80d813f1..9a1f2116 100644 --- a/tests/package_managers/test_poetry.py +++ b/tests/package_managers/test_poetry.py @@ -2,116 +2,16 @@ Tests of Poetry's command line behavior. """ -import packaging.version as version -from pathlib import Path -import pytest import re -import requests import subprocess -import sys -from tempfile import TemporaryDirectory from typing import Optional -POETRY_V2 = version.parse("2.0.0") - -TEST_PROJECT_NAME = "foo" - -# Tree-sitter is a convenient test target because it never has any dependencies -# and is not part of the standard set of system Python modules -TARGET = "tree-sitter" - -# Version numbers of available Tree-sitter releases on PyPI -TARGET_RELEASES = list( - requests.get(f"https://pypi.org/pypi/{TARGET}/json", timeout=5).json()["releases"] -) - -# The latest and most recent previous versions of Tree-sitter -TARGET_LATEST = TARGET_RELEASES[-1] -TARGET_PREVIOUS = TARGET_RELEASES[-2] - - -@pytest.fixture -def new_poetry_project(): - """ - Initialize a clean Poetry project for use in testing. - """ - tempdir = TemporaryDirectory() - _init_poetry_project(tempdir.name, TEST_PROJECT_NAME) - - yield tempdir.name - - tempdir.cleanup() - - -@pytest.fixture -def poetry_project_target_latest(): - """ - Initialize a Poetry project with the latest version of `TARGET` as a dependency. - """ - tempdir = TemporaryDirectory() - _init_poetry_project(tempdir.name, TEST_PROJECT_NAME, [(TARGET, TARGET_LATEST)]) - - yield tempdir.name - - tempdir.cleanup() - - -@pytest.fixture -def poetry_project_target_previous(): - """ - Initialize a Poetry project with the previous version of `TARGET` as a dependency. - """ - tempdir = TemporaryDirectory() - _init_poetry_project(tempdir.name, TEST_PROJECT_NAME, [(TARGET, TARGET_PREVIOUS)]) - - yield tempdir.name - - tempdir.cleanup() - - -@pytest.fixture -def poetry_project_target_latest_lock_previous(): - """ - Initialize a Poetry project where the latest version of `TARGET` has been installed but - the previous version of it is an as-yet uninstalled dependency of the project. - """ - tempdir = TemporaryDirectory() - _init_poetry_project(tempdir.name, TEST_PROJECT_NAME, [(TARGET, TARGET_LATEST)]) - subprocess.run(["poetry", "add", "--lock", f"{TARGET}=={TARGET_PREVIOUS}"], check=True, cwd=tempdir.name) - - yield tempdir.name - - tempdir.cleanup() - - -@pytest.fixture -def poetry_project_target_previous_lock_latest(): - """ - Initialize a Poetry project where the previous version of `TARGET` has been installed but - the latest version of it is an as-yet uninstalled dependency of the project. - """ - tempdir = TemporaryDirectory() - _init_poetry_project(tempdir.name, TEST_PROJECT_NAME, [(TARGET, TARGET_PREVIOUS)]) - subprocess.run(["poetry", "add", "--lock", f"{TARGET}=={TARGET_LATEST}"], check=True, cwd=tempdir.name) - - yield tempdir.name - - tempdir.cleanup() - - -@pytest.fixture -def poetry_project_lock_latest(): - """ - Initialize a Poetry project where the latest version of `TARGET` is an as-yet - uninstalled dependency. - """ - tempdir = TemporaryDirectory() - _init_poetry_project(tempdir.name, TEST_PROJECT_NAME) - subprocess.run(["poetry", "add", "--lock", f"{TARGET}=={TARGET_LATEST}"], check=True, cwd=tempdir.name) +import packaging.version as version +import pytest - yield tempdir.name +from .poetry_fixtures import * - tempdir.cleanup() +POETRY_V2 = version.parse("2.0.0") def test_poetry_version_output(): @@ -177,7 +77,10 @@ def test_poetry_sync_no_change(new_poetry_project): Test that certain `poetry sync` commands relied on by Supply-Chain Firewall not to error or modify the local installation state indeed have these properties. """ - if poetry_version() < POETRY_V2: + version = poetry_version() + assert version + + if version < POETRY_V2: return test_cases = [ @@ -288,7 +191,10 @@ def test_poetry_sync_error_no_change(new_poetry_project): Tests that certain `poetry sync` commands encounter an error and do not modify the local installation state when run in the context of a given project. """ - if poetry_version() < POETRY_V2: + version = poetry_version() + assert version + + if version < POETRY_V2: return test_cases = [ @@ -447,21 +353,3 @@ def poetry_version() -> Optional[version.Version]: return version.parse(match.group(1)) if match else None except Exception: return None - - -def _init_poetry_project(directory, name, dependencies = None): - """ - Initialize a fresh Poetry project in `directory` with the given `dependencies`. - """ - subprocess.run(["poetry", "init", "--no-interaction", "--name", name], check=True, cwd=directory) - subprocess.run(["poetry", "lock"], check=True, cwd=directory) - - # Create a separate venv for Poetry to use during testing - venv_path = Path(directory) / "venv" - venv_python_path = venv_path / "bin" / "python" - subprocess.run([sys.executable, "-m", "venv", venv_path], check=True) - subprocess.run(["poetry", "env", "use", venv_python_path], check=True, cwd=directory) - - if dependencies: - for package, version in dependencies: - subprocess.run(["poetry", "add", f"{package}=={version}"], check=True, cwd=directory) diff --git a/tests/package_managers/test_poetry_class.py b/tests/package_managers/test_poetry_class.py index a923b969..6a877f67 100644 --- a/tests/package_managers/test_poetry_class.py +++ b/tests/package_managers/test_poetry_class.py @@ -6,13 +6,8 @@ from scfw.package import Package from scfw.package_managers.poetry import Poetry -from .test_poetry import ( - POETRY_V2, TARGET, TARGET_LATEST, TARGET_PREVIOUS, TEST_PROJECT_NAME, - new_poetry_project, poetry_project_lock_latest, - poetry_project_target_latest, poetry_project_target_latest_lock_previous, - poetry_project_target_previous, poetry_project_target_previous_lock_latest, - poetry_show, poetry_version, -) +from .poetry_fixtures import * +from .test_poetry import POETRY_V2, poetry_show, poetry_version PACKAGE_MANAGER = Poetry() """ @@ -107,7 +102,10 @@ def test_poetry_command_resolve_install_targets_sync( Tests that `Poetry.resolve_install_targets()` for a `poetry sync` command correctly resolves installation targets without installing anything. """ - if poetry_version() < POETRY_V2: + version = poetry_version() + assert version + + if version < POETRY_V2: return test_cases = [ diff --git a/tests/package_managers/top_npm_packages.txt b/tests/package_managers/top_npm_packages.txt deleted file mode 100644 index 9be85309..00000000 --- a/tests/package_managers/top_npm_packages.txt +++ /dev/null @@ -1,500 +0,0 @@ -chalk -commander -debug -tslib -fs-extra -semver -glob -@types/node -typescript -lodash -yargs -axios -uuid -mkdirp -js-yaml -rimraf -node-fetch -minimist -dotenv -strip-ansi -minimatch -react -ms -execa -ws -ajv -async -@babel/runtime -@babel/core -acorn -react-dom -string-width -eslint -core-js -wrap-ansi -prop-types -qs -prettier -ora -cross-spawn -readable-stream -type-fest -source-map -escape-string-regexp -find-up -rxjs -form-data -camelcase -iconv-lite -buffer -which -ansi-regex -globby -@typescript-eslint/parser -has-flag -safe-buffer -ts-node -object-assign -moment -webpack -inherits -lru-cache -path-exists -@typescript-eslint/eslint-plugin -diff -resolve -yaml -source-map-support -brace-expansion -path-to-regexp -yargs-parser -undici-types -react-is -arg -nanoid -json5 -emoji-regex -eslint-plugin-import -color-name -js-tokens -fast-glob -argparse -signal-exit -slash -mime-types -string_decoder -through2 -body-parser -acorn-walk -mime -locate-path -jsonwebtoken -is-fullwidth-code-point -globals -graceful-fs -@babel/parser -micromatch -get-stream -pify -isarray -kind-of -https-proxy-agent -cookie -events -@babel/types -glob-parent -p-locate -next -is-stream -open -jsonfile -dayjs -is-number -picocolors -deepmerge -eslint-plugin-react -date-fns -bluebird -resolve-from -eventemitter3 -rollup -ejs -@babel/preset-env -browserslist -picomatch -log-symbols -path-key -ini -sprintf-js -json-schema-traverse -fast-deep-equal -strip-json-comments -convert-source-map -make-dir -bn.js -regenerator-runtime -@babel/traverse -eslint-config-prettier -onetime -zod -ignore -lodash.merge -shebang-regex -isexe -prompts -estraverse -@babel/generator -eslint-scope -http-errors -strip-bom -esbuild -babel-jest -function-bind -chokidar -eslint-visitor-keys -make-error -clsx -pretty-format -braces -@testing-library/jest-dom -tmp -ansi-escapes -caniuse-lite -esprima -cliui -jquery -mime-db -minipass -once -create-require -xml2js -clone -extend -shebang-command -handlebars -normalize-path -chai -y18n -v8-compile-cache-lib -whatwg-url -readdirp -tough-cookie -has-symbols -cors -vue -scheduler -escalade -util-deprecate -got -reflect-metadata -extend-shallow -is-glob -call-bind -serve-static -schema-utils -espree -is-plain-object -eslint-plugin-react-hooks -to-regex-range -yocto-queue -electron-to-chromium -isobject -webidl-conversions -magic-string -co -yn -encodeurl -is-arrayish -tr46 -parse5 -is-extglob -loader-utils -callsites -doctrine -path-parse -wrappy -bytes -xtend -big.js -entities -express -node-addon-api -inquirer -graphql -path-type -yallist -import-fresh -concat-map -parse-json -require-directory -hasown -has-property-descriptors -text-table -statuses -follow-redirects -node-releases -write-file-atomic -is-wsl -@testing-library/user-event -@testing-library/react -on-finished -base64-js -fs.realpath -is-core-module -jsdom -@babel/code-frame -indent-string -jest-worker -http-proxy-agent -hosted-git-info -has-proto -anymatch -eslint-plugin-jsx-a11y -fast-json-stable-stringify -eslint-plugin-prettier -cli-cursor -through -long -safer-buffer -binary-extensions -get-intrinsic -depd -escape-html -file-entry-cache -kleur -p-try -p-map -strip-final-newline -babel-loader -sass -import-local -jest-resolve -redux -flatted -immutable -meow -cheerio -mocha -is-plain-obj -object.assign -gopd -imurmurhash -update-browserslist-db -fastq -pump -protobufjs -decamelize -tar -define-properties -htmlparser2 -define-data-property -send -side-channel -set-function-length -es-errors -lodash-es -bignumber.js -react-router-dom -read-pkg -restore-cursor -finalhandler -is-binary-path -merge-stream -tsconfig-paths -es-define-property -object-keys -uri-js -csstype -istanbul-lib-instrument -lines-and-columns -run-parallel -@babel/template -tiny-invariant -postcss-selector-parser -@babel/helper-plugin-utils -npm-run-path -color -is-extendable -reusify -end-of-stream -progress -loose-envify -is-callable -esrecurse -word-wrap -supports-preserve-symlinks-flag -acorn-jsx -json-parse-even-better-errors -source-map-js -combined-stream -vite -queue-microtask -webpack-dev-server -fast-levenshtein -dedent -@aws-sdk/types -es-abstract -agent-base -bl -negotiator -estree-walker -fast-xml-parser -retry -get-caller-file -process-nextick-args -cli-spinners -@babel/preset-react -has-tostringtag -punycode -natural-compare -marked -@typescript-eslint/utils -ieee754 -error-ex -setprototypeof -slice-ansi -enhanced-resolve -normalize-package-data -joi -is-path-inside -@smithy/types -find-cache-dir -colorette -delayed-stream -pirates -is-regex -babel-core -prelude-ls -is-negative-zero -jest-get-type -postcss-value-parser -asynckit -is-unicode-supported -deep-is -type-detect -require-from-string -buffer-from -jest-diff -deep-equal -postcss -es-to-primitive -@sinclair/typebox -optionator -jest-util -sax -string.prototype.trimend -@smithy/util-utf8 -pako -@babel/helper-module-imports -parseurl -dir-glob -levn -concat-stream -etag -cli-width -keyv -type-check -xmlbuilder -json-stable-stringify-without-jsonify -process -tailwindcss -istanbul-lib-coverage -string.prototype.trimstart -node-gyp-build -@babel/preset-typescript -expect -is-shared-array-buffer -content-type -mute-stream -accepts -cookie-signature -compression -@emotion/react -object.values -url-parse -regexp.prototype.flags -style-loader -domutils -jest-message-util -dom-serializer -@eslint/js -range-parser -superagent -istanbul-reports -foreground-child -ansi-colors -@types/uuid -domhandler -validator -ipaddr.js -available-typed-arrays -fresh -jest-matcher-utils -utils-merge -diff-sequences -css-loader -core-util-is -js-cookie -for-each -webpack-sources -strip-indent -@typescript-eslint/typescript-estree -which-typed-array -abort-controller -object-hash -webpack-merge -react-redux -pluralize -merge-descriptors -is-bigint -npm -@babel/plugin-transform-runtime -has-bigints -nopt -dotenv-expand -flat-cache -crypto-js -http-proxy-middleware -unpipe -array-union -resolve-cwd -object-inspect -vary -@babel/plugin-syntax-jsx -regjsparser -cross-fetch -raw-body -proxy-from-env -is-typed-array -randombytes -functions-have-names -mongodb -lilconfig -internal-slot -jest-cli -content-disposition -@emotion/styled -type-is -function.prototype.name -methods -string-length -jsbn -ajv-formats -neo-async -nan -jackspeak -is-weakref -globalthis -enquirer -merge2 -esquery -json-buffer -jest-haste-map -jest-mock -regexpu-core -ee-first -validate-npm-package-name -array-flatten -eastasianwidth -string.prototype.trim diff --git a/tests/package_managers/utils.py b/tests/package_managers/utils.py deleted file mode 100644 index 69b5f2a2..00000000 --- a/tests/package_managers/utils.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -Common utilities for package manager command tests. -""" - -import os - -from scfw.ecosystem import ECOSYSTEM - - -def read_top_packages(ecosystem: ECOSYSTEM) -> set[str]: - """ - Read the top packages file for the given `ecosystem`. - """ - test_dir = os.path.dirname(os.path.realpath(__file__, strict=True)) - top_packages_file = os.path.join(test_dir, f"top_{str(ecosystem).lower()}_packages.txt") - with open(top_packages_file) as f: - return set(f.read().split()) - - -def select_test_install_target(top_packages: set[str], installed_packages: str) -> str: - """ - Select a test target from `top_packages` that is not in the given installed - packages output. - - This allows us to be certain when testing that nothing was installed in a - dry-run. - """ - try: - while (choice := top_packages.pop()) in installed_packages: - pass - return choice - except KeyError: - raise RuntimeError("Unable to select a target package for testing")