diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index d0d1c52c..71d19fdc 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -25,7 +25,7 @@ jobs: # Should match the "build" subsection's settings in docker-compose.yml context: ./apps/backend file: ./apps/backend/backend.Dockerfile - target: production + # target: production # this has been commented out due to simpler Dockerfile frontend-prod: name: Frontend Production Container runs-on: ubuntu-latest @@ -40,34 +40,36 @@ jobs: # Should match the "build" subsection's settings in docker-compose.yml context: ./apps/frontend file: ./apps/frontend/frontend.Dockerfile - target: runner + # target: runner # this has been commented out due to simpler Dockerfile - # Development containers get the settings from docker-compose.dev.yml layered on top of docker-compose.yml - backend-dev: - name: Backend Development Container - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - name: Build - uses: docker/build-push-action@v4 - with: - context: ./apps/backend - file: ./apps/backend/backend.Dockerfile - target: development - frontend-dev: - name: Frontend Development Container - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - name: Build - uses: docker/build-push-action@v4 - with: - context: ./apps/frontend - file: ./apps/frontend/frontend.Dockerfile - target: development \ No newline at end of file + # Below has been commented out due to simpler Dockerfile, as we shift towards Kubernetes + + # # Development containers get the settings from docker-compose.dev.yml layered on top of docker-compose.yml + # backend-dev: + # name: Backend Development Container + # runs-on: ubuntu-latest + # steps: + # - name: Checkout + # uses: actions/checkout@v3 + # - name: Set up Docker Buildx + # uses: docker/setup-buildx-action@v2 + # - name: Build + # uses: docker/build-push-action@v4 + # with: + # context: ./apps/backend + # file: ./apps/backend/backend.Dockerfile + # target: development + # frontend-dev: + # name: Frontend Development Container + # runs-on: ubuntu-latest + # steps: + # - name: Checkout + # uses: actions/checkout@v3 + # - name: Set up Docker Buildx + # uses: docker/setup-buildx-action@v2 + # - name: Build + # uses: docker/build-push-action@v4 + # with: + # context: ./apps/frontend + # file: ./apps/frontend/frontend.Dockerfile + # target: development \ No newline at end of file diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index 6621d970..20f515ff 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -31,7 +31,7 @@ jobs: python -m pip install coverage pip install pipenv pipenv install --dev --system --deploy --ignore-pipfile - - name: Analyze with pylint + - name: Analyze backend with pylint working-directory: ./apps/backend # https://pylint.readthedocs.io/en/v2.16.1/user_guide/configuration/all-options.html#fail-on # Uses the .pylintrc rules file in the working directory @@ -39,16 +39,36 @@ jobs: # Fail if the code quality score is below 9.0 # Recursively search the specified directories for files to analyze # Check the file app.py - # Check the directories ./modules and ./tests - run: pylint --fail-on=E --fail-under=9.0 --recursive=y app.py ./modules/ ./tests/ - - name: Test with pytest + record coverage + run: pylint --fail-on=E --fail-under=9.0 --recursive=y app.py + - name: Analyze runner with pylint + working-directory: ./apps/runner + # https://pylint.readthedocs.io/en/v2.16.1/user_guide/configuration/all-options.html#fail-on + # Uses the .pylintrc rules file in the working directory + # Fail if any Error-level notices were produced + # Fail if the code quality score is below 9.0 + # Recursively search the specified directories for files to analyze + # Check the file runner.py + # Check the ./modules and ./tests directory + run: pylint --fail-on=E --fail-under=9.0 --recursive=y runner.py ./modules/ ./tests/ + - name: Test backend with pytest + record coverage working-directory: ./apps/backend run: bash coverage.sh - - name: Zip coverage report + - name: Zip backend coverage report working-directory: ./apps/backend run: zip -r codeCoverage.zip coverageReport/ - - name: Upload coverage report + - name: Test runner with pytest + record coverage + working-directory: ./apps/runner + run: bash coverage.sh + - name: Zip runner coverage report + working-directory: ./apps/runner + run: zip -r codeCoverage.zip coverageReport/ + - name: Upload backend coverage report uses: actions/upload-artifact@v3 with: - name: code-coverage-report + name: code-coverage-report-backend path: ./apps/backend/codeCoverage.zip + - name: Upload runner coverage report + uses: actions/upload-artifact@v3 + with: + name: code-coverage-report-runner + path: ./apps/runner/codeCoverage.zip diff --git a/apps/backend/.pylintrc b/apps/backend/.pylintrc new file mode 100644 index 00000000..0a22ad3b --- /dev/null +++ b/apps/backend/.pylintrc @@ -0,0 +1,15 @@ +# pylint python linter configuration + +[MASTER] +extension-pkg-whitelist=pydantic # stop "No name 'BaseModel' in module 'pydantic'" https://github.com/pydantic/pydantic/issues/1961#issuecomment-759522422 +disable= + C0301, # line too long + C0114, # missing-module-docstring + C0116, # missing-function-docstring + C0115, # missing-class-docstring + C0103, # we chose to use camel case to be consistent with our frontend code + W1203, # we want to use fstrings for log message readability, the performance cost is not significant enough for us to care. but, don't turn off W1201 because if the dev wants to use % syntax, they should use lazy logging since the syntax readability is equivalent + +# Load our own custom plugins +load-plugins= + linters.no_print \ No newline at end of file diff --git a/apps/backend/Pipfile b/apps/backend/Pipfile index 157963df..78537cd3 100644 --- a/apps/backend/Pipfile +++ b/apps/backend/Pipfile @@ -9,5 +9,13 @@ requests = "*" kubernetes = "*" flask-cors = "*" +[dev-packages] +# libmagic dlls for windows hosts https://pypi.org/project/python-magic/ (when missing, vague errors and it crashes) +python-magic-bin = {version = "*", markers = "platform_system == 'Windows'"} +yapf = "*" +pylint = "*" +pytest = "*" +coverage = "*" + [requires] python_version = "3.8" diff --git a/apps/backend/Pipfile.lock b/apps/backend/Pipfile.lock index d74001b8..3bbf6d95 100644 --- a/apps/backend/Pipfile.lock +++ b/apps/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "313e247bf2e676662f13bc2993a15a6d66b731258541470a004563bd2e129d04" + "sha256": "38c6529f0e9bbddb7967c903554cb7ec72fe17d7211be340c1f53fe58d64a62c" }, "pipfile-spec": 6, "requires": { @@ -18,19 +18,19 @@ "default": { "blinker": { "hashes": [ - "sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9", - "sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182" + "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01", + "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83" ], "markers": "python_version >= '3.8'", - "version": "==1.7.0" + "version": "==1.8.2" }, "cachetools": { "hashes": [ - "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2", - "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1" + "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945", + "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105" ], "markers": "python_version >= '3.7'", - "version": "==5.3.2" + "version": "==5.3.3" }, "certifi": { "hashes": [ @@ -154,60 +154,59 @@ }, "flask": { "hashes": [ - "sha256:3232e0e9c850d781933cf0207523d1ece087eb8d87b23777ae38456e2fbe7c6e", - "sha256:822c03f4b799204250a7ee84b1eddc40665395333973dfb9deebfe425fefcb7d" + "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3", + "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==3.0.2" + "version": "==3.0.3" }, "flask-cors": { "hashes": [ - "sha256:bc3492bfd6368d27cfe79c7821df5a8a319e1a6d5eab277a3794be19bdc51783", - "sha256:f268522fcb2f73e2ecdde1ef45e2fd5c71cc48fe03cffb4b441c6d1b40684eb0" + "sha256:eeb69b342142fdbf4766ad99357a7f3876a2ceb77689dc10ff912aac06c389e4", + "sha256:f2a704e4458665580c074b714c4627dd5a306b333deb9074d0b1794dfa2fb677" ], "index": "pypi", - "version": "==4.0.0" + "version": "==4.0.1" }, "google-auth": { "hashes": [ - "sha256:8e4bad367015430ff253fe49d500fdc3396c1a434db5740828c728e45bcce245", - "sha256:e863a56ccc2d8efa83df7a80272601e43487fa9a728a376205c86c26aaefa821" + "sha256:672dff332d073227550ffc7457868ac4218d6c500b155fe6cc17d2b13602c360", + "sha256:d452ad095688cd52bae0ad6fafe027f6a6d6f560e810fec20914e17a09526415" ], "markers": "python_version >= '3.7'", - "version": "==2.27.0" + "version": "==2.29.0" }, "idna": { "hashes": [ - "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", - "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" + "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", + "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" ], "markers": "python_version >= '3.5'", - "version": "==3.6" + "version": "==3.7" }, "importlib-metadata": { "hashes": [ - "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e", - "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc" + "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570", + "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2" ], "markers": "python_version < '3.10'", - "version": "==7.0.1" + "version": "==7.1.0" }, "itsdangerous": { "hashes": [ - "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44", - "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a" + "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", + "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173" ], - "markers": "python_version >= '3.7'", - "version": "==2.1.2" + "markers": "python_version >= '3.8'", + "version": "==2.2.0" }, "jinja2": { "hashes": [ - "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa", - "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90" + "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", + "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" ], "markers": "python_version >= '3.7'", - "version": "==3.1.3" + "version": "==3.1.4" }, "kubernetes": { "hashes": [ @@ -215,7 +214,6 @@ "sha256:c4812e227ae74d07d53c88293e564e54b850452715a59a927e7e1bc6b9a60459" ], "index": "pypi", - "markers": "python_version >= '3.6'", "version": "==29.0.0" }, "markupsafe": { @@ -294,27 +292,27 @@ }, "pyasn1": { "hashes": [ - "sha256:4439847c58d40b1d0a573d07e3856e95333f1976294494c325775aeca506eb58", - "sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c" + "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c", + "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==0.5.1" + "markers": "python_version >= '3.8'", + "version": "==0.6.0" }, "pyasn1-modules": { "hashes": [ - "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c", - "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d" + "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6", + "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==0.3.0" + "markers": "python_version >= '3.8'", + "version": "==0.4.0" }, "python-dateutil": { "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.2" + "version": "==2.9.0.post0" }, "pyyaml": { "hashes": [ @@ -379,16 +377,15 @@ "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==2.31.0" }, "requests-oauthlib": { "hashes": [ - "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5", - "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a" + "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", + "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.3.1" + "markers": "python_version >= '3.4'", + "version": "==2.0.0" }, "rsa": { "hashes": [ @@ -408,36 +405,249 @@ }, "urllib3": { "hashes": [ - "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20", - "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224" + "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", + "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" ], "markers": "python_version >= '3.8'", - "version": "==2.2.0" + "version": "==2.2.1" }, "websocket-client": { "hashes": [ - "sha256:10e511ea3a8c744631d3bd77e61eb17ed09304c413ad42cf6ddfa4c7787e8fe6", - "sha256:f4c3d22fec12a2461427a29957ff07d35098ee2d976d3ba244e688b8b4057588" + "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", + "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da" ], "markers": "python_version >= '3.8'", - "version": "==1.7.0" + "version": "==1.8.0" }, "werkzeug": { "hashes": [ - "sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc", - "sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10" + "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18", + "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8" ], "markers": "python_version >= '3.8'", - "version": "==3.0.1" + "version": "==3.0.3" }, "zipp": { "hashes": [ - "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31", - "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0" + "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b", + "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715" ], "markers": "python_version >= '3.8'", - "version": "==3.17.0" + "version": "==3.18.1" } }, - "develop": {} + "develop": { + "astroid": { + "hashes": [ + "sha256:16ee8ca5c75ac828783028cc1f967777f0e507c6886a295ad143e0f405b975a2", + "sha256:f7f829f8506ade59f1b3c6c93d8fac5b1ebc721685fa9af23e9794daf1d450a3" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==3.2.0" + }, + "colorama": { + "hashes": [ + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" + ], + "markers": "platform_system == 'Windows'", + "version": "==0.4.6" + }, + "coverage": { + "hashes": [ + "sha256:0646599e9b139988b63704d704af8e8df7fa4cbc4a1f33df69d97f36cb0a38de", + "sha256:0cdcbc320b14c3e5877ee79e649677cb7d89ef588852e9583e6b24c2e5072661", + "sha256:0d0a0f5e06881ecedfe6f3dd2f56dcb057b6dbeb3327fd32d4b12854df36bf26", + "sha256:1434e088b41594baa71188a17533083eabf5609e8e72f16ce8c186001e6b8c41", + "sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d", + "sha256:1cc0fe9b0b3a8364093c53b0b4c0c2dd4bb23acbec4c9240b5f284095ccf7981", + "sha256:1fc81d5878cd6274ce971e0a3a18a8803c3fe25457165314271cf78e3aae3aa2", + "sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34", + "sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f", + "sha256:4a7b0ceee8147444347da6a66be737c9d78f3353b0681715b668b72e79203e4a", + "sha256:4a9ca3f2fae0088c3c71d743d85404cec8df9be818a005ea065495bedc33da35", + "sha256:4bf0655ab60d754491004a5efd7f9cccefcc1081a74c9ef2da4735d6ee4a6223", + "sha256:4cc37def103a2725bc672f84bd939a6fe4522310503207aae4d56351644682f1", + "sha256:4fc84a37bfd98db31beae3c2748811a3fa72bf2007ff7902f68746d9757f3746", + "sha256:5037f8fcc2a95b1f0e80585bd9d1ec31068a9bcb157d9750a172836e98bc7a90", + "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c", + "sha256:556cf1a7cbc8028cb60e1ff0be806be2eded2daf8129b8811c63e2b9a6c43bca", + "sha256:57e0204b5b745594e5bc14b9b50006da722827f0b8c776949f1135677e88d0b8", + "sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596", + "sha256:5c3721c2c9e4c4953a41a26c14f4cef64330392a6d2d675c8b1db3b645e31f0e", + "sha256:5fa567e99765fe98f4e7d7394ce623e794d7cabb170f2ca2ac5a4174437e90dd", + "sha256:5fd215c0c7d7aab005221608a3c2b46f58c0285a819565887ee0b718c052aa4e", + "sha256:6175d1a0559986c6ee3f7fccfc4a90ecd12ba0a383dcc2da30c2b9918d67d8a3", + "sha256:61c4bf1ba021817de12b813338c9be9f0ad5b1e781b9b340a6d29fc13e7c1b5e", + "sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312", + "sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7", + "sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572", + "sha256:796a79f63eca8814ca3317a1ea443645c9ff0d18b188de470ed7ccd45ae79428", + "sha256:79afb6197e2f7f60c4824dd4b2d4c2ec5801ceb6ba9ce5d2c3080e5660d51a4f", + "sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07", + "sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e", + "sha256:8fe7502616b67b234482c3ce276ff26f39ffe88adca2acf0261df4b8454668b4", + "sha256:9314d5678dcc665330df5b69c1e726a0e49b27df0461c08ca12674bcc19ef136", + "sha256:9735317685ba6ec7e3754798c8871c2f49aa5e687cc794a0b1d284b2389d1bd5", + "sha256:9981706d300c18d8b220995ad22627647be11a4276721c10911e0e9fa44c83e8", + "sha256:9e78295f4144f9dacfed4f92935fbe1780021247c2fabf73a819b17f0ccfff8d", + "sha256:b016ea6b959d3b9556cb401c55a37547135a587db0115635a443b2ce8f1c7228", + "sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206", + "sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa", + "sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e", + "sha256:c3e757949f268364b96ca894b4c342b41dc6f8f8b66c37878aacef5930db61be", + "sha256:ca498687ca46a62ae590253fba634a1fe9836bc56f626852fb2720f334c9e4e5", + "sha256:d1d0d98d95dd18fe29dc66808e1accf59f037d5716f86a501fc0256455219668", + "sha256:d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601", + "sha256:d7fed867ee50edf1a0b4a11e8e5d0895150e572af1cd6d315d557758bfa9c057", + "sha256:db66fc317a046556a96b453a58eced5024af4582a8dbdc0c23ca4dbc0d5b3146", + "sha256:dde0070c40ea8bb3641e811c1cfbf18e265d024deff6de52c5950677a8fb1e0f", + "sha256:df4e745a81c110e7446b1cc8131bf986157770fa405fe90e15e850aaf7619bc8", + "sha256:e2213def81a50519d7cc56ed643c9e93e0247f5bbe0d1247d15fa520814a7cd7", + "sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987", + "sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19", + "sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece" + ], + "index": "pypi", + "version": "==7.5.1" + }, + "dill": { + "hashes": [ + "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca", + "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7" + ], + "markers": "python_version < '3.11'", + "version": "==0.3.8" + }, + "exceptiongroup": { + "hashes": [ + "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad", + "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16" + ], + "markers": "python_version < '3.11'", + "version": "==1.2.1" + }, + "importlib-metadata": { + "hashes": [ + "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570", + "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2" + ], + "markers": "python_version < '3.10'", + "version": "==7.1.0" + }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "isort": { + "hashes": [ + "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", + "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==5.13.2" + }, + "mccabe": { + "hashes": [ + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "packaging": { + "hashes": [ + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" + ], + "markers": "python_version >= '3.7'", + "version": "==24.0" + }, + "platformdirs": { + "hashes": [ + "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee", + "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3" + ], + "markers": "python_version >= '3.8'", + "version": "==4.2.2" + }, + "pluggy": { + "hashes": [ + "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", + "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" + ], + "markers": "python_version >= '3.8'", + "version": "==1.5.0" + }, + "pylint": { + "hashes": [ + "sha256:9f20c05398520474dac03d7abb21ab93181f91d4c110e1e0b32bc0d016c34fa4", + "sha256:ad8baf17c8ea5502f23ae38d7c1b7ec78bd865ce34af9a0b986282e2611a8ff2" + ], + "index": "pypi", + "version": "==3.2.0" + }, + "pytest": { + "hashes": [ + "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233", + "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f" + ], + "index": "pypi", + "version": "==8.2.0" + }, + "python-magic-bin": { + "hashes": [ + "sha256:34a788c03adde7608028203e2dbb208f1f62225ad91518787ae26d603ae68892", + "sha256:7b1743b3dbf16601d6eedf4e7c2c9a637901b0faaf24ad4df4d4527e7d8f66a4", + "sha256:90be6206ad31071a36065a2fc169c5afb5e0355cbe6030e87641c6c62edc2b69" + ], + "index": "pypi", + "markers": "platform_system == 'Windows'", + "version": "==0.4.14" + }, + "tomli": { + "hashes": [ + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + ], + "markers": "python_version < '3.11'", + "version": "==2.0.1" + }, + "tomlkit": { + "hashes": [ + "sha256:af914f5a9c59ed9d0762c7b64d3b5d5df007448eb9cd2edc8a46b1eafead172f", + "sha256:eef34fba39834d4d6b73c9ba7f3e4d1c417a4e56f89a7e96e090dd0d24b8fb3c" + ], + "markers": "python_version >= '3.7'", + "version": "==0.12.5" + }, + "typing-extensions": { + "hashes": [ + "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", + "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" + ], + "markers": "python_version < '3.10'", + "version": "==4.11.0" + }, + "yapf": { + "hashes": [ + "sha256:4dab8a5ed7134e26d57c1647c7483afb3f136878b579062b786c9ba16b94637b", + "sha256:adc8b5dd02c0143108878c499284205adb258aad6db6634e5b869e7ee2bd548b" + ], + "index": "pypi", + "version": "==0.40.2" + }, + "zipp": { + "hashes": [ + "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b", + "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715" + ], + "markers": "python_version >= '3.8'", + "version": "==3.18.1" + } + } } diff --git a/apps/backend/app.py b/apps/backend/app.py index 71ab4aec..718b41b4 100644 --- a/apps/backend/app.py +++ b/apps/backend/app.py @@ -1,13 +1,10 @@ """Module that uses flask to host endpoints for the backend""" from concurrent.futures import ProcessPoolExecutor -from sys import stdout from flask import Flask, Response, request, jsonify from kubernetes import client, config -from spawn_runner import create_job, create_job_object -from flask_cors import CORS +from spawn_runner import create_job, create_job_object flaskApp = Flask(__name__) -CORS(flaskApp) config.load_incluster_config() BATCH_API = client.BatchV1Api() diff --git a/apps/backend/coverage.sh b/apps/backend/coverage.sh new file mode 100644 index 00000000..34485315 --- /dev/null +++ b/apps/backend/coverage.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# This script is used by +# - the VSCode task 'Backend Code Coverage Report' to produce a report that can be displayed in the editor +# - the CI to execute the tests and produce a report that gets uploaded as an artifact for download + +# Avoid pollution from past runs +rm -f .coverage +rm -f coverage.xml +rm -rf coverageReport/ + +# Running `python -m pytest` and not `pytest` because we need the current dir on sys.path, see https://docs.pytest.org/en/6.2.x/usage.html#calling-pytest-through-python-m-pytest +# -rA flag controls printed report details: https://docs.pytest.org/en/6.2.x/usage.html#detailed-summary-report +# --cov flags from pytest-cov plugin control contents and file output of the report https://pytest-cov.readthedocs.io/en/latest/readme.html +python -m pytest \ + -rA \ + --cov-report xml:coverage.xml \ + --cov-report html:coverageReport \ + --cov-branch \ + --cov . \ + --cov modules/ + +# Print information in the console for the user as well +# -m also prints Missed lines +coverage report -m + +echo "▶ Use the VSCode action 'Coverage Gutters: Display Coverage' to see coverage in the editor" \ No newline at end of file diff --git a/apps/backend/linters/no_print.py b/apps/backend/linters/no_print.py new file mode 100644 index 00000000..23fcc57a --- /dev/null +++ b/apps/backend/linters/no_print.py @@ -0,0 +1,35 @@ +# Based on https://stackoverflow.com/questions/71026245/how-to-add-custom-pylint-warning + +from typing import TYPE_CHECKING + +from astroid import nodes + +from pylint.checkers import BaseChecker +from pylint.checkers.utils import check_messages + +if TYPE_CHECKING: + from pylint.lint import PyLinter + +# TODO the CI can't seem to see this file (or its parent directory? so it currently only runs locally in the editor) + +class PrintUsedChecker(BaseChecker): + + name = "no_print_allowed" + msgs = { + "C9001": ( + "GLADOS: Used `print` statement, use the correct logger instead", + "glados-print-used", + "Messages that are printed aren't available in the system" + "logs nor to system users. Use the logging system.", + ) + } + + @check_messages("glados-print-used") + def visit_call(self, node: nodes.Call) -> None: + if isinstance(node.func, nodes.Name): + if node.func.name == "print": + self.add_message("glados-print-used", node=node) + + +def register(linter: "PyLinter") -> None: + linter.register_checker(PrintUsedChecker(linter)) \ No newline at end of file diff --git a/apps/runner/.pylintrc b/apps/runner/.pylintrc new file mode 100644 index 00000000..0a22ad3b --- /dev/null +++ b/apps/runner/.pylintrc @@ -0,0 +1,15 @@ +# pylint python linter configuration + +[MASTER] +extension-pkg-whitelist=pydantic # stop "No name 'BaseModel' in module 'pydantic'" https://github.com/pydantic/pydantic/issues/1961#issuecomment-759522422 +disable= + C0301, # line too long + C0114, # missing-module-docstring + C0116, # missing-function-docstring + C0115, # missing-class-docstring + C0103, # we chose to use camel case to be consistent with our frontend code + W1203, # we want to use fstrings for log message readability, the performance cost is not significant enough for us to care. but, don't turn off W1201 because if the dev wants to use % syntax, they should use lazy logging since the syntax readability is equivalent + +# Load our own custom plugins +load-plugins= + linters.no_print \ No newline at end of file diff --git a/apps/runner/tests/test_config.py b/apps/runner/tests/test_config.py new file mode 100644 index 00000000..40a0ce08 --- /dev/null +++ b/apps/runner/tests/test_config.py @@ -0,0 +1,286 @@ +from typing import Dict +import unittest + +from modules.configs import gather_parameters, generate_config_files, generate_list, get_default +from modules.data.configData import ConfigData +from modules.data.experiment import ExperimentData +from modules.data.parameters import BoolParameter, FloatParam, IntegerParam, ParamType, Parameter, StringParameter + +intDefault, intStart, intStop, intStep, intStepInvalid, intConst = 0, 0, 5, 1, 0, 5 +int_param_dict = {"default": intDefault, "min": intStart, "max": intStop, "step": intStep, "type": ParamType.INTEGER} +int_param_const_dict = {"default": intDefault, "min": intConst, "max": intConst, "step": intStep, "type": ParamType.INTEGER} +int_param = IntegerParam(**int_param_dict) +int_const_param = IntegerParam(**int_param_const_dict) + +floatDefault, floatStart, floatStop, floatStep, invalidFloatStep, floatConst = 0.0, 0.0, 0.5, 0.1, 0, 0.5 +float_param_dict = {"default": floatDefault, "min": floatStart, "max": floatStop, "step": floatStep, "type": ParamType.FLOAT} +float_param_const_dict = {"default": floatDefault, "min": floatConst, "max": floatConst, "step": floatStep, "type": ParamType.FLOAT} +float_param = FloatParam(**float_param_dict) +float_const_param = FloatParam(**float_param_const_dict) + +bool_param = BoolParameter(**{"type": ParamType.BOOL, "default": True}) +string_param = StringParameter(**{'type': ParamType.STRING, "default": "Weezer!"}) + +empty_hyperparams_dict = {} #Empty dictionary +single_int_param_dict = {"x": int_param} +single_float_param_dict = {"x": float_param} +single_int_const_param_dict = {"x": int_const_param} +single_float_const_param_dict = {"x": float_const_param} +single_bool_param_dict = {"b": bool_param} +single_string_param_dict = {"s": string_param} +mixed_param_dict = {"x": int_param, "y": float_param, 'intconst': int_const_param, 'floatconst': float_const_param, 'b': bool_param, 's': string_param} + + +class TestGatherParameters(unittest.TestCase): + + const_result_dict = {} + param_result_dict = {} + + def reset(self): + self.empty_hyperparams_dict = {} #Empty dictionary + self.const_result_dict = {} + self.param_result_dict = {} + + def test_gather_parameters_no_hyperparams(self): + self.reset() + gather_parameters(self.empty_hyperparams_dict, self.const_result_dict, self.param_result_dict) + self.assertFalse(self.const_result_dict) + self.assertFalse(self.param_result_dict) + + def test_gather_parameters_one_int_param(self): + self.reset() + gather_parameters(single_int_param_dict, self.const_result_dict, self.param_result_dict) + self.assertFalse(self.const_result_dict) + self.assertEqual(len(self.param_result_dict), 1) + self.assertTrue("x" in self.param_result_dict) + self.assertEqual(self.param_result_dict['x'], single_int_param_dict['x']) + + def test_gather_parameters_const_int_param(self): + self.reset() + gather_parameters(single_int_const_param_dict, self.const_result_dict, self.param_result_dict) + self.assertEqual(len(self.const_result_dict), 1) + self.assertEqual(len(self.param_result_dict), 0) + self.assertTrue("x" in self.const_result_dict) + self.assertTrue(self.const_result_dict['x'], intConst) + + def test_gather_parameters_one_float_param(self): + self.reset() + gather_parameters(single_float_param_dict, self.const_result_dict, self.param_result_dict) + self.assertFalse(self.const_result_dict) + self.assertEqual(len(self.param_result_dict), 1) + self.assertTrue("x" in self.param_result_dict) + self.assertEqual(self.param_result_dict['x'], single_float_param_dict['x']) + + def test_gather_parameters_const_float_param(self): + self.reset() + gather_parameters(single_float_const_param_dict, self.const_result_dict, self.param_result_dict) + self.assertEqual(len(self.const_result_dict), 1) + self.assertEqual(len(self.param_result_dict), 0) + self.assertTrue("x" in self.const_result_dict) + self.assertTrue(self.const_result_dict['x'], floatConst) + + def test_gather_parameters_bool_param(self): + self.reset() + gather_parameters(single_bool_param_dict, self.const_result_dict, self.param_result_dict) + self.assertFalse(self.const_result_dict) + self.assertEqual(len(self.param_result_dict), 1) + self.assertTrue("b" in self.param_result_dict) + self.assertTrue(self.param_result_dict['b'], single_bool_param_dict['b']) + + def test_gather_parameters_string_param(self): + self.reset() + gather_parameters(single_string_param_dict, self.const_result_dict, self.param_result_dict) + self.assertEqual(len(self.const_result_dict), 1) + self.assertEqual(len(self.param_result_dict), 0) + self.assertTrue("s" in self.const_result_dict) + self.assertTrue(self.const_result_dict['s'], bool_param.default) + + def test_gather_parameters_two_param(self): + self.reset() + gather_parameters(mixed_param_dict, self.const_result_dict, self.param_result_dict) + self.assertEqual(len(self.const_result_dict), 3) + self.assertEqual(len(self.param_result_dict), 3) + self.assertTrue('x' in self.param_result_dict) + self.assertTrue('y' in self.param_result_dict) + self.assertTrue('b' in self.param_result_dict) + self.assertTrue('intconst' in self.const_result_dict) + self.assertTrue('floatconst' in self.const_result_dict) + self.assertTrue('s' in self.const_result_dict) + self.assertEqual(self.param_result_dict['x'], mixed_param_dict['x']) + self.assertEqual(self.param_result_dict['y'], mixed_param_dict['y']) + self.assertEqual(self.param_result_dict['b'], mixed_param_dict['b']) + self.assertEqual(self.const_result_dict['intconst'], intConst) + self.assertEqual(self.const_result_dict['floatconst'], floatConst) + self.assertEqual(self.const_result_dict['s'], string_param.default) + + +class TestGenerateList(unittest.TestCase): + int_list = [('x', 0), ('x', 1), ('x', 2), ('x', 3), ('x', 4)] + float_list = [('y', 0.0), ('y', 0.1), ('y', 0.2), ('y', 0.3), ('y', 0.4)] + bool_list = [('b', True), ('b', False)] + + possible_param_list = [] + + def reset(self): + self.possible_param_list = [] + + def assertListOfFloatTuplesEqual(self, list1, list2): + for item1, item2 in zip(list1, list2): + self.assertEqual(item1[0], item2[0]) + self.assertAlmostEqual(item1[1], item2[1], delta=0.0001) + + def test_generate_int_list(self): + self.reset() + result = generate_list(int_param, 'x') + self.assertListEqual(result, self.int_list) + + def test_generate_float_list(self): + self.reset() + result = generate_list(float_param, 'y') + self.assertListOfFloatTuplesEqual(result, self.float_list) + + def test_generate_bool_list(self): + self.reset() + result = generate_list(bool_param, 'b') + self.assertListEqual(result, self.bool_list) + + +class TestGenerateConfigFiles(unittest.TestCase): + + #Has an empty Hyperparameters + exp_info = ExperimentData(**{'trialExtraFile': 'Testing Data', 'description': 'Testing Data', 'file': 'experimentV3dpcllHWPrK1Kgbyzqb', 'creator': 'U0EmxpfuqWM2fSa1LKmpFiqLj0V2', 'finished': False, 'estimatedTotalTimeMinutes': 0, 'dumbTextArea': 'dummy = dummy\na = 100', 'verbose': True, 'scatterIndVar': 'iparam', 'scatterDepVar': 'fparam', 'timeout': 18000, 'workers': 1, 'keepLogs': True, 'hyperparameters': {}, 'name': 'Testing Data', 'trialResult': 'Testing Data', 'totalExperimentRuns': 0, 'created': 1679705027850, 'scatter': True, 'expId': 'V3dpcllHWPrK1Kgbyzqb'}) + + single_int_param_hyperparams: Dict[str, Parameter] = {"x": int_param} + single_int_param_expected_configs = {'config0': ConfigData(data={'x': 0}), 'config1': ConfigData(data={'x': 1}), 'config2': ConfigData(data={'x': 2}), 'config3': ConfigData(data={'x': 3}), 'config4': ConfigData(data={'x': 4})} + + single_float_param_hyperparams: Dict[str, Parameter] = {"x": float_param} + single_float_param_expected_configs = {'config0': ConfigData(data={'x': 0.0}), 'config1': ConfigData(data={'x': 0.1}), 'config2': ConfigData(data={'x': 0.2}), 'config3': ConfigData(data={'x': 0.30000000000000004}), 'config4': ConfigData(data={'x': 0.4})} + + single_bool_param_hyperparams: Dict[str, Parameter] = {"b": bool_param} + single_bool_param_expected_configs = {'config0': ConfigData(data={'b': True}), 'config1': ConfigData(data={'b': False})} + + single_int_const_hyperparams: Dict[str, Parameter] = {"x": int_param, 'xc': int_const_param} + single_int_const_expected_configs = {'config0': ConfigData(data={'x': 0, 'xc': 5}), 'config1': ConfigData(data={'x': 1, 'xc': 5}), 'config2': ConfigData(data={'x': 2, 'xc': 5}), 'config3': ConfigData(data={'x': 3, 'xc': 5}), 'config4': ConfigData(data={'x': 4, 'xc': 5})} + + single_float_const_hyperparams: Dict[str, Parameter] = {"x": int_param, 'fc': float_const_param} + single_float_const_expected_configs = {'config0': ConfigData(data={'x': 0, 'fc': 0.5}), 'config1': ConfigData(data={'x': 1, 'fc': 0.5}), 'config2': ConfigData(data={'x': 2, 'fc': 0.5}), 'config3': ConfigData(data={'x': 3, 'fc': 0.5}), 'config4': ConfigData(data={'x': 4, 'fc': 0.5})} + + single_string_const_hyperparams: Dict[str, Parameter] = {"x": int_param, 's': string_param} + single_string_const_expected_configs = {'config0': ConfigData(data={'x': 0, 's': 'Weezer!'}), 'config1': ConfigData(data={'x': 1, 's': 'Weezer!'}), 'config2': ConfigData(data={'x': 2, 's': 'Weezer!'}), 'config3': ConfigData(data={'x': 3, 's': 'Weezer!'}), 'config4': ConfigData(data={'x': 4, 's': 'Weezer!'})} + + one_of_everything: Dict[str, Parameter] = {"x": int_param, "y": float_param, "b": bool_param, "s":string_param, "int_const": int_const_param, "float_const":float_param} + one_of_everything_expected_configs = {'config0': ConfigData(data={'x': 0, 'y': 0.0, 'b': True, 'float_const': 0.0, 's': 'Weezer!', 'int_const': 5}), 'config1': ConfigData(data={'x': 1, 'y': 0.0, 'b': True, 'float_const': 0.0, 's': 'Weezer!', 'int_const': 5}), 'config2': ConfigData(data={'x': 2, 'y': 0.0, 'b': True, 'float_const': 0.0, 's': 'Weezer!', 'int_const': 5}), 'config3': ConfigData(data={'x': 3, 'y': 0.0, 'b': True, 'float_const': 0.0, 's': 'Weezer!', 'int_const': 5}), 'config4': ConfigData(data={'x': 4, 'y': 0.0, 'b': True, 'float_const': 0.0, 's': 'Weezer!', 'int_const': 5}), 'config5': ConfigData(data={'y': 0.0, 'x': 0, 'b': True, 'float_const': 0.0, 's': 'Weezer!', 'int_const': 5}), 'config6': ConfigData(data={'y': 0.1, 'x': 0, 'b': True, 'float_const': 0.0, 's': 'Weezer!', 'int_const': 5}), 'config7': ConfigData(data={'y': 0.2, 'x': 0, 'b': True, 'float_const': 0.0, 's': 'Weezer!', 'int_const': 5}), 'config8': ConfigData(data={'y': 0.30000000000000004, 'x': 0, 'b': True, 'float_const': 0.0, 's': 'Weezer!', 'int_const': 5}), 'config9': ConfigData(data={'y': 0.4, 'x': 0, 'b': True, 'float_const': 0.0, 's': 'Weezer!', 'int_const': 5}), 'config10': ConfigData(data={'b': True, 'x': 0, 'y': 0.0, 'float_const': 0.0, 's': 'Weezer!', 'int_const': 5}), 'config11': ConfigData(data={'b': False, 'x': 0, 'y': 0.0, 'float_const': 0.0, 's': 'Weezer!', 'int_const': 5}), 'config12': ConfigData(data={'float_const': 0.0, 'x': 0, 'y': 0.0, 'b': True, 's': 'Weezer!', 'int_const': 5}), 'config13': ConfigData(data={'float_const': 0.1, 'x': 0, 'y': 0.0, 'b': True, 's': 'Weezer!', 'int_const': 5}), 'config14': ConfigData(data={'float_const': 0.2, 'x': 0, 'y': 0.0, 'b': True, 's': 'Weezer!', 'int_const': 5}), 'config15': ConfigData(data={'float_const': 0.30000000000000004, 'x': 0, 'y': 0.0, 'b': True, 's': 'Weezer!', 'int_const': 5}), 'config16': ConfigData(data={'float_const': 0.4, 'x': 0, 'y': 0.0, 'b': True, 's': 'Weezer!', 'int_const': 5})} + + def reset(self): + self.exp_info = ExperimentData(**{'trialExtraFile': 'Testing Data', 'description': 'Testing Data', 'file': 'experimentV3dpcllHWPrK1Kgbyzqb', 'creator': 'U0EmxpfuqWM2fSa1LKmpFiqLj0V2', 'finished': False, 'estimatedTotalTimeMinutes': 0, 'dumbTextArea': 'dummy = dummy\na = 100', 'verbose': True, 'scatterIndVar': 'iparam', 'scatterDepVar': 'fparam', 'timeout': 18000, 'workers': 1, 'keepLogs': True, 'hyperparameters': {}, 'name': 'Testing Data', 'trialResult': 'Testing Data', 'totalExperimentRuns': 0, 'created': 1679705027850, 'scatter': True, 'expId': 'V3dpcllHWPrK1Kgbyzqb'}) + + def assertConfigKeys(self, numConfigs, configs): + for i in range(0, numConfigs): + self.assertTrue(f'config{i}' in configs) + + def assertConfigSize(self, numItems, configs: Dict[str, ConfigData]): + for key, config in configs.items(): + self.assertEqual(numItems, len(config.data)) + + def test_single_int_variable(self): + self.reset() + self.exp_info.hyperparameters = self.single_int_param_hyperparams + generate_config_files(self.exp_info) + configs = self.exp_info.configs + self.assertEqual(len(configs), 5) + self.assertConfigKeys(5, configs) + self.assertConfigSize(len(self.single_int_param_hyperparams), configs) + self.assertDictEqual(configs, self.single_int_param_expected_configs) + + def test_single_float_variable(self): + self.reset() + self.exp_info.hyperparameters = self.single_float_param_hyperparams + generate_config_files(self.exp_info) + configs = self.exp_info.configs + self.assertEqual(len(configs), 5) + self.assertConfigKeys(5, configs) + self.assertConfigSize(len(self.single_float_param_hyperparams), configs) + self.assertDictEqual(configs, self.single_float_param_expected_configs) + + def test_single_bool_variable(self): + self.reset() + self.exp_info.hyperparameters = self.single_bool_param_hyperparams + generate_config_files(self.exp_info) + configs = self.exp_info.configs + self.assertEqual(len(configs), 2) + self.assertConfigKeys(len(self.single_bool_param_hyperparams), configs) + self.assertConfigSize(len(self.single_bool_param_hyperparams), configs) + self.assertDictEqual(configs, self.single_bool_param_expected_configs) + + def test_single_int_const(self): + self.reset() + self.exp_info.hyperparameters = self.single_int_const_hyperparams + generate_config_files(self.exp_info) + configs = self.exp_info.configs + self.assertEqual(len(configs), 5) + self.assertConfigKeys(5, configs) + self.assertConfigSize(len(self.single_int_const_hyperparams), configs) + self.assertDictEqual(configs, self.single_int_const_expected_configs) + + def test_single_float_const(self): + self.reset() + self.exp_info.hyperparameters = self.single_float_const_hyperparams + generate_config_files(self.exp_info) + configs = self.exp_info.configs + self.assertEqual(len(configs), 5) + self.assertConfigKeys(5, configs) + self.assertConfigSize(len(self.single_float_const_hyperparams), configs) + self.assertDictEqual(configs, self.single_float_const_expected_configs) + + def test_single_string_const(self): + self.reset() + self.exp_info.hyperparameters = self.single_string_const_hyperparams + generate_config_files(self.exp_info) + configs = self.exp_info.configs + self.assertEqual(len(configs), 5) + self.assertConfigKeys(5, configs) + self.assertConfigSize(len(self.single_string_const_hyperparams), configs) + self.assertDictEqual(configs, self.single_string_const_expected_configs) + + def test_one_of_everything(self): + self.reset() + self.exp_info.hyperparameters = self.one_of_everything + generate_config_files(self.exp_info) + configs = self.exp_info.configs + self.assertEqual(len(configs), 17) + self.assertConfigKeys(17, configs) + self.assertConfigSize(len(self.one_of_everything), configs) + self.assertDictEqual(configs, self.one_of_everything_expected_configs) + + +class TestGetDefault(unittest.TestCase): + + def test_get_int_default(self): + result = get_default(int_param) + self.assertEqual(result, 0) + + def test_get_float_default(self): + result = get_default(float_param) + self.assertEqual(result, 0.0) + + def test_get_bool_default(self): + result = get_default(bool_param) + self.assertTrue(result) + + def test_get_string_default(self): + result = get_default(string_param) + self.assertEqual(result, "Weezer!") + + +#Tests to write-- (generate_config_files) +#Error with default (returns None) Should we reraise the exception? +#Error making permutations ("Error while making permutations") +#Successful config generation +#Vary one variable and keep the rest (multiple) as constants/default +#Do we want to test the configFile writing? + +#Tests to write-- (get_config_paramNames) +#Test for element being/not being in res + +if __name__ == '__main__': + unittest.main(verbosity=2) \ No newline at end of file diff --git a/apps/runner/tests/test_experiment.py b/apps/runner/tests/test_experiment.py new file mode 100644 index 00000000..6ddc49b2 --- /dev/null +++ b/apps/runner/tests/test_experiment.py @@ -0,0 +1,82 @@ +import unittest +from modules.data.parameters import parseRawHyperparameterData +from modules.data.configData import ConfigData + +from modules.data.experiment import ExperimentData, ExperimentType + + +class TestExperimentData(unittest.TestCase): + params = parseRawHyperparameterData([{"name": "iparam", "default": "1", "min": "1", "max": "10", "step": "1", "type": "integer"}, {"name": "fparam", "default": "1.0", "min": "1.0", "max": "10.0", "step": "1.0", "type": "float"}, {"name": "sparam", "default": "Hi", "type": "string"}, {"name": "bparam", "default": True, "type": "bool"}]) + configs = {"config0": ConfigData(**{"data":{"key":"value"}})} + optional = ["trialExtraFile", "scatterIndVar", "scatterDepVar", "startedAtEpochMillis", "finishedAtEpochMillis"] + fields_with_default = {"file": "", "postProcess": False, "configs": {}, "totalExperimentRuns": 0, "experimentType": ExperimentType.UNKNOWN, "finished": False,"passes":0, "fails":0} + + + exp_info = {'configs':configs,'trialExtraFile': 'Testing Data', 'description': 'Testing Data', 'file': 'experimentV3dpcllHWPrK1Kgbyzqb', 'creator': 'U0EmxpfuqWM2fSa1LKmpFiqLj0V2', 'finished': False, 'estimatedTotalTimeMinutes': 0, 'dumbTextArea': 'dummy = dummy\na = 100', 'verbose': True, 'scatterIndVar': 'iparam', 'scatterDepVar': 'fparam', 'timeout': 18000, 'workers': 1, 'keepLogs': True, 'hyperparameters': params, 'name': 'Testing Data', 'trialResult': 'Testing Data', 'totalExperimentRuns': 0, 'created': 1679705027850, 'scatter': True, 'expId': 'V3dpcllHWPrK1Kgbyzqb'} + + exp_info_has_all_optional = {'trialExtraFile': 'Testing Data', 'description': 'Testing Data', 'file': 'experimentV3dpcllHWPrK1Kgbyzqb', 'creator': 'U0EmxpfuqWM2fSa1LKmpFiqLj0V2', 'finished': False, 'estimatedTotalTimeMinutes': 0, 'dumbTextArea': 'dummy = dummy\na = 100', 'verbose': True, 'scatterIndVar': 'iparam', 'scatterDepVar': 'fparam', "startedAtEpochMillis": 0, "finishedAtEpochMillis": 0, 'timeout': 18000, 'workers': 1, 'keepLogs': True, 'hyperparameters': params, 'name': 'Testing Data', 'trialResult': 'Testing Data', 'totalExperimentRuns': 0, 'created': 1679705027850, 'scatter': True, 'expId': 'V3dpcllHWPrK1Kgbyzqb'} + + exp_info_has_all_default = {'trialExtraFile': 'Testing Data', 'description': 'Testing Data', 'file': 'experimentV3dpcllHWPrK1Kgbyzqb', 'creator': 'U0EmxpfuqWM2fSa1LKmpFiqLj0V2', 'finished': False, "experimentType": ExperimentType.PYTHON, 'estimatedTotalTimeMinutes': 0, 'dumbTextArea': 'dummy = dummy\na = 100', 'verbose': True, 'scatterIndVar': 'iparam', 'scatterDepVar': 'fparam', 'timeout': 18000, 'workers': 1, 'keepLogs': True, "configs": {}, 'hyperparameters': params, 'name': 'Testing Data', 'trialResult': 'Testing Data', 'totalExperimentRuns': 0, "passes": 0, "fails": 0, 'created': 1679705027850, 'scatter': True, "postProcess": True, 'expId': 'V3dpcllHWPrK1Kgbyzqb'} + + def test_creating_object_does_not_mutate_input_fields(self): + experiment = ExperimentData(**self.exp_info) + expDict = experiment.dict().copy() + for key, value in expDict.items(): + if key in self.optional: + if key in self.exp_info: + self.assertEqual(value, self.exp_info[key]) + else: + self.assertIsNone(value) + elif key in self.fields_with_default: + if key in self.exp_info: + self.assertEqual(value, self.exp_info[key]) + else: + self.assertEqual(value, self.fields_with_default[key]) + else: + self.assertEqual(value, self.exp_info[key]) + + def test_remove_optional_does_not_error(self): + for field in self.optional: + cloned_info = self.exp_info_has_all_optional.copy() + del cloned_info[field] + experiment = ExperimentData(**cloned_info) + self.assertIsInstance(experiment, ExperimentData) + self.assertEqual(experiment.dict()[field], None) + + def test_remove_default_does_not_error(self): + for field, value in self.fields_with_default.items(): + cloned_info = self.exp_info_has_all_default.copy() + del cloned_info[field] + experiment = ExperimentData(**cloned_info) + self.assertIsInstance(experiment, ExperimentData) + self.assertEqual(experiment.dict()[field], value) + + def test_check_trialResult(self): + cloned_info = self.exp_info.copy() + del cloned_info['trialResult'] + with self.assertRaises(ValueError): + ExperimentData(**cloned_info) + + def test_check_hyperparams(self): + cloned_info = self.exp_info.copy() + invalid_hyperparameters_dict = {"field": "value"} + cloned_info['hyperparameters'] = invalid_hyperparameters_dict + with self.assertRaises(ValueError): + ExperimentData(**cloned_info) + + def test_check_configs(self): + cloned_info = self.exp_info.copy() + invalid_configs_dict = {"field": "value"} + cloned_info['configs'] = invalid_configs_dict + with self.assertRaises(ValueError): + ExperimentData(**cloned_info) + + cloned_info = self.exp_info.copy() + invalid_configs_dict = {1: "value"} + cloned_info['configs'] = invalid_configs_dict + with self.assertRaises(ValueError): + ExperimentData(**cloned_info) + + +if __name__ == '__main__': + unittest.main(verbosity=2) \ No newline at end of file diff --git a/apps/runner/tests/test_parameters.py b/apps/runner/tests/test_parameters.py new file mode 100644 index 00000000..70bbabd8 --- /dev/null +++ b/apps/runner/tests/test_parameters.py @@ -0,0 +1,132 @@ +import unittest + +from modules.data.parameters import BoolParameter, FloatParam, IntegerParam, ParamType, StringParameter + +class TestIntParameter(unittest.TestCase): + intDefault, intStart, intStop, intStep, intStepInvalid = 0, 0, 10, 1, 0 + + int_param_dict = {"default": intDefault, "min": intStart, "max": intStop, "step": intStep, "type": ParamType.INTEGER} + int_param_dict_min_equal_max = {"default": intDefault, "min": intStart, "max": intStart, "step": intStep, "type": ParamType.INTEGER} + int_param_dict_no_default = {"min": intStart, "max": intStop, "step": intStep, "type": ParamType.INTEGER} + int_param_dict_no_min = {"default": intDefault, "max": intStop, "step": intStep, "type": ParamType.INTEGER} + int_param_dict_no_max = {"default": intDefault, "min": intStart, "step": intStep, "type": ParamType.INTEGER} + int_param_dict_no_step = {"default": intDefault, "min": intStart, "max": intStop, "type": ParamType.INTEGER} + + int_param_dict_min_greater_than_max = {"default": intDefault, "min": intStop, "max": intStart, "step": intStep, "type": ParamType.INTEGER} + int_param_dict_invalid_step = {"default": intDefault, "min": intStart, "max": intStop, "step": intStepInvalid, "type": ParamType.INTEGER} + + def test_normal(self): + intParam = IntegerParam(**self.int_param_dict) + self.assertEqual(intParam.default, self.intDefault) + self.assertEqual(intParam.min, self.intStart) + self.assertEqual(intParam.max, self.intStop) + self.assertEqual(intParam.step, self.intStep) + + def test_int_equal_max(self): + intParam = IntegerParam(**self.int_param_dict_min_equal_max) + self.assertEqual(intParam.default, self.intDefault) + self.assertEqual(intParam.min, self.intStart) + self.assertEqual(intParam.max, self.intStart) + self.assertEqual(intParam.step, self.intStep) + + def test_no_default(self): + with self.assertRaises(ValueError): + IntegerParam(**self.int_param_dict_no_default) + + def test_no_min(self): + with self.assertRaises(ValueError): + IntegerParam(**self.int_param_dict_no_min) + + def test_no_max(self): + with self.assertRaises(ValueError): + IntegerParam(**self.int_param_dict_no_max) + + def test_no_step(self): + with self.assertRaises(ValueError): + IntegerParam(**self.int_param_dict_no_step) + + def test_min_greater_than_max(self): + with self.assertRaises(ValueError): + IntegerParam(**self.int_param_dict_min_greater_than_max) + + def test_invalid_step(self): + with self.assertRaises(ValueError): + IntegerParam(**self.int_param_dict_invalid_step) + + +class TestFloatParameter(unittest.TestCase): + floatDefault, floatStart, floatStop, floatStep, invalidFloatStep = 0.0, 0.1, 1, 0.1, 0 + + float_param_dict = {"default": floatDefault, "min": floatStart, "max": floatStop, "step": floatStep, "type": ParamType.FLOAT} + float_param_dict_min_equal_max = {"default": floatDefault, "min": floatStart, "max": floatStart, "step": floatStep, "type": ParamType.FLOAT} + float_param_dict_no_default = {"min": floatStart, "max": floatStop, "step": floatStep, "type": ParamType.FLOAT} + float_param_dict_no_min = {"default": floatDefault, "max": floatStop, "step": floatStep, "type": ParamType.FLOAT} + float_param_dict_no_max = {"default": floatDefault, "min": floatStart, "step": floatStep, "type": ParamType.FLOAT} + float_param_dict_no_step = {"default": floatDefault, "min": floatStart, "max": floatStop, "type": ParamType.FLOAT} + float_param_dict_min_greater_max = {"default": floatDefault, "min": floatStop, "max": floatStart, "step": invalidFloatStep, "type": ParamType.FLOAT} + float_param_dict_invalid_step = {"default": floatDefault, "min": floatStart, "max": floatStop, "step": invalidFloatStep, "type": ParamType.FLOAT} + + def test_creating_object_does_not_mutate_input_fields(self): + floatParam = FloatParam(**self.float_param_dict) + self.assertEqual(floatParam.default, self.floatDefault) + self.assertEqual(floatParam.min, self.floatStart) + self.assertEqual(floatParam.max, self.floatStop) + self.assertEqual(floatParam.step, self.floatStep) + + def test_min_equal_max(self): + floatParam = FloatParam(**self.float_param_dict_min_equal_max) + self.assertEqual(floatParam.default, self.floatDefault) + self.assertEqual(floatParam.min, self.floatStart) + self.assertEqual(floatParam.max, self.floatStart) + self.assertEqual(floatParam.step, self.floatStep) + + def test_no_default(self): + with self.assertRaises(ValueError): + FloatParam(**self.float_param_dict_no_default) + + def test_no_min(self): + with self.assertRaises(ValueError): + FloatParam(**self.float_param_dict_no_min) + + def test_no_max(self): + with self.assertRaises(ValueError): + FloatParam(**self.float_param_dict_no_max) + + def test_no_step(self): + with self.assertRaises(ValueError): + FloatParam(**self.float_param_dict_no_step) + + def test_min_greater_max(self): + with self.assertRaises(ValueError): + FloatParam(**self.float_param_dict_min_greater_max) + + def test_invalid_step(self): + with self.assertRaises(ValueError): + FloatParam(**self.float_param_dict_invalid_step) + + +class TestBoolParameter(unittest.TestCase): + bool_dict_true = {'default': True, 'type': ParamType.BOOL} + bool_dict_false = {'default': False, 'type': ParamType.BOOL} + bool_dict_no_default = {'type': ParamType.BOOL} + + def test_true_input(self): + boolParam = BoolParameter(**self.bool_dict_true) + self.assertTrue(boolParam.default) + + def test_false_input(self): + boolParam = BoolParameter(**self.bool_dict_false) + self.assertFalse(boolParam.default) + +class TestStringParameter(unittest.TestCase): + testString = "Hello World" + string_dict = {'default': testString, 'type': ParamType.STRING} + string_dict_no_default = {'type': ParamType.STRING} + + def test_true_input(self): + strParam = StringParameter(**self.string_dict) + self.assertEqual(strParam.default, self.testString) + + +if __name__ == '__main__': + unittest.main(verbosity=2) \ No newline at end of file diff --git a/apps/runner/tests/type_testing.py b/apps/runner/tests/type_testing.py new file mode 100644 index 00000000..047ee57c --- /dev/null +++ b/apps/runner/tests/type_testing.py @@ -0,0 +1,42 @@ +import json +from modules.data.parameters import parseRawHyperparameterData +from modules.configs import generate_config_files +from modules.data.experiment import ExperimentType, ExperimentData + +# This file shows how to construct an ExperimentData object from a JSON string +# This approach can help in writing tests for that functionality, after that, this file can be deleted +if __name__ == "__main__": + print("hello world") + + # cspell:disable # disable spellchecker + expInfo = { + 'trialExtraFile': 'dummy', + 'description': '', + 'type': ExperimentType.PYTHON.value, + 'file': 'experimentV3dpcllHWPrK1Kgbyzqb', + 'creator': 'U0EmxpfuqWM2fSa1LKmpFiqLj0V2', + 'finished': False, + 'estimatedTotalTimeMinutes': 0, + 'dumbTextArea': 'dummy = dummy\na = 100', + 'verbose': True, + 'scatterIndVar': 'iparam', + 'scatterDepVar': 'fparam', + 'timeout': 18000, + 'workers': 1, + 'keepLogs': True, + 'hyperparameters': '{"hyperparameters":[{"name":"iparam","default":"1","min":"1","max":"10","step":"1","type":"integer"},{"name":"fparam","default":"1.0","min":"1.0","max":"10.0","step":"1.0","type":"float"},{"name":"sparam","default":"Hi","type":"string"},{"name":"bparam","default":true,"type":"bool"}]}', + 'name': 'Just to get the datA', + 'trialResult': 'dummy', + 'totalExperimentRuns': 0, + 'created': 1679705027850, + 'scatter': True, + 'expId': 'V3dpcllHWPrK1Kgbyzqb' + } + # cspell:enable + hyperparameters = parseRawHyperparameterData(json.loads(expInfo['hyperparameters'])['hyperparameters']) + expInfo['hyperparameters'] = hyperparameters + experiment = ExperimentData(**expInfo) + generate_config_files(experiment) + + for configId, config in experiment.configs.items(): + print(f'{configId}: {config.data}\n') \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 3eb8d4e4..fe3ddc1d 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,3 +1,5 @@ +# As of May 2024, docker-compose is not used to build GLADOS. This can be removed in the future. + # Use this docker-compose file in addition to the base one in order to set up development configurations for the containers # https://docs.docker.com/compose/extends/ diff --git a/docker-compose.yml b/docker-compose.yml index 0aa42bd5..8a1b65c7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,5 @@ +# As of May 2024, docker-compose is not used to build GLADOS. This can be removed in the future. + # Usage # Start: docker compose up # With helpers: docker compose -f docker-compose.yml -f ./.devcontainer/docker-compose.dev.yml up