diff --git a/.gitea/workflows/backend_run_test.yaml b/.gitea/workflows/backend_run_test.yaml index 7ad3df0..fe28f1f 100644 --- a/.gitea/workflows/backend_run_test.yaml +++ b/.gitea/workflows/backend_run_test.yaml @@ -29,5 +29,11 @@ jobs: working-directory: backend - name: Run Tests - run: pipenv run pytest src + run: pipenv run pytest src --html=report.html --self-contained-html working-directory: backend + + - name: Upload HTML report + uses: https://gitea.com/actions/upload-artifact@v3 + with: + name: pytest-html-report + path: report.html diff --git a/backend/Pipfile b/backend/Pipfile index 6d2f5cb..165b760 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -9,6 +9,7 @@ pytest = "*" tomli = "*" httpx = "*" exceptiongroup = "*" +pytest-html = "*" [packages] numpy = "*" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 12634b4..b7b8e8f 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "4f5f640531306a5b25610a89686ea4cc61c1381a42442e09edc89dc2c65e9798" + "sha256": "2b63ee95d92465adc00e3404e19b55c21604867bcdb0f4e349c595e891a895b1" }, "pipfile-spec": 6, "requires": {}, @@ -1300,26 +1300,26 @@ }, "starlette": { "hashes": [ - "sha256:9834fd799d1a87fd346deb76158668cfa0b0d56f85caefe8268e2d97c3468b62", - "sha256:fbc189474b4731cf30fcef52f18a8d070e3f3b46c6a04c97579e85e6ffca942d" + "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835", + "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7" ], "markers": "python_version >= '3.8'", - "version": "==0.41.2" + "version": "==0.41.3" }, "typer": { "hashes": [ - "sha256:d85fe0b777b2517cc99c8055ed735452f2659cd45e451507c76f48ce5c1d00e2", - "sha256:f1c7198347939361eec90139ffa0fd8b3df3a2259d5852a0f7400e476d95985c" + "sha256:5b59580fd925e89463a29d363e0a43245ec02765bde9fb77d39e5d0f29dd7157", + "sha256:9d444cb96cc268ce6f8b94e13b4335084cef4c079998a9f4851a90229a3bd25c" ], "markers": "python_version >= '3.7'", - "version": "==0.13.0" + "version": "==0.13.1" }, "typing-extensions": { "hashes": [ "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], - "markers": "python_version >= '3.8'", + "markers": "python_version < '3.13'", "version": "==4.12.2" }, "tzdata": { @@ -1737,6 +1737,81 @@ "markers": "python_full_version >= '3.8.0'", "version": "==5.13.2" }, + "jinja2": { + "hashes": [ + "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", + "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.4" + }, + "markupsafe": { + "hashes": [ + "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", + "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", + "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", + "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", + "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", + "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", + "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", + "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", + "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", + "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", + "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", + "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", + "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", + "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", + "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", + "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", + "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", + "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", + "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", + "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", + "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", + "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", + "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", + "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", + "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", + "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", + "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", + "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", + "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", + "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", + "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", + "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", + "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", + "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", + "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", + "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", + "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", + "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", + "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", + "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", + "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", + "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", + "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", + "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", + "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", + "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", + "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", + "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", + "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", + "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", + "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", + "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", + "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", + "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", + "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", + "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", + "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", + "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", + "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", + "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", + "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" + ], + "markers": "python_version >= '3.9'", + "version": "==3.0.2" + }, "mccabe": { "hashes": [ "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", @@ -1787,6 +1862,23 @@ "markers": "python_version >= '3.8'", "version": "==8.3.3" }, + "pytest-html": { + "hashes": [ + "sha256:70a01e8ae5800f4a074b56a4cb1025c8f4f9b038bba5fe31e3c98eb996686f07", + "sha256:c8152cea03bd4e9bee6d525573b67bbc6622967b72b9628dda0ea3e2a0b5dd71" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==4.1.1" + }, + "pytest-metadata": { + "hashes": [ + "sha256:c8e0844db684ee1c798cfa38908d20d67d0463ecb6137c72e91f418558dd5f4b", + "sha256:d2a29b0355fbc03f168aa96d41ff88b1a3b44a3b02acbe491801c98a048017c8" + ], + "markers": "python_version >= '3.8'", + "version": "==3.1.1" + }, "sniffio": { "hashes": [ "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", diff --git a/backend/conftest.py b/backend/conftest.py new file mode 100644 index 0000000..21aa09f --- /dev/null +++ b/backend/conftest.py @@ -0,0 +1,47 @@ +import pytest + +pytest_plugins = ["pytest_html"] + +def pytest_html_report_title(report): + """modifying the title of html report""" + report.title = "Backend Testing Report" + +def pytest_html_results_table_header(cells): + cells.insert(2, "<th>Detailed trip</th>") + cells.insert(3, "<th>Trip Duration</th>") + cells.insert(4, "<th>Target Duration</th>") + cells[5] = "<th>Execution time</th>" # rename the column containing execution times to avoid confusion + + +def pytest_html_results_table_row(report, cells): + trip_details = getattr(report, "trip_details", "N/A") # Default to "N/A" if no trip data + trip_duration = getattr(report, "trip_duration", "N/A") # Default to "N/A" if no trip data + target_duration = getattr(report, "target_duration", "N/A") # Default to "N/A" if no trip data + cells.insert(2, f"<td>{trip_details}</td>") + cells.insert(3, f"<td>{trip_duration}</td>") + cells.insert(4, f"<td>{target_duration}</td>") + + +@pytest.hookimpl(hookwrapper=True) +def pytest_runtest_makereport(item, call): + outcome = yield + report = outcome.get_result() + report.description = str(item.function.__doc__) + + # Attach trip_details if it exists + if hasattr(item, "trip_details"): + report.trip_details = " - ".join(item.trip_details) # Convert list to string + else: + report.trip_details = "N/A" # Default if trip_string is not set + + # Attach trip_duration if it exists + if hasattr(item, "trip_duration"): + report.trip_duration = item.trip_duration + " min" + else: + report.trip_duration = "N/A" # Default if duration is not set + + # Attach target_duration if it exists + if hasattr(item, "target_duration"): + report.target_duration = item.target_duration + " min" + else: + report.target_duration = "N/A" # Default if duration is not set \ No newline at end of file diff --git a/backend/report.html b/backend/report.html new file mode 100644 index 0000000..19f894c --- /dev/null +++ b/backend/report.html @@ -0,0 +1,1094 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"/> + <title id="head-title">Backend Testing Report</title> + <style type="text/css">body { + font-family: Helvetica, Arial, sans-serif; + font-size: 12px; + /* do not increase min-width as some may use split screens */ + min-width: 800px; + color: #999; +} + +h1 { + font-size: 24px; + color: black; +} + +h2 { + font-size: 16px; + color: black; +} + +p { + color: black; +} + +a { + color: #999; +} + +table { + border-collapse: collapse; +} + +/****************************** + * SUMMARY INFORMATION + ******************************/ +#environment td { + padding: 5px; + border: 1px solid #e6e6e6; + vertical-align: top; +} +#environment tr:nth-child(odd) { + background-color: #f6f6f6; +} +#environment ul { + margin: 0; + padding: 0 20px; +} + +/****************************** + * TEST RESULT COLORS + ******************************/ +span.passed, +.passed .col-result { + color: green; +} + +span.skipped, +span.xfailed, +span.rerun, +.skipped .col-result, +.xfailed .col-result, +.rerun .col-result { + color: orange; +} + +span.error, +span.failed, +span.xpassed, +.error .col-result, +.failed .col-result, +.xpassed .col-result { + color: red; +} + +.col-links__extra { + margin-right: 3px; +} + +/****************************** + * RESULTS TABLE + * + * 1. Table Layout + * 2. Extra + * 3. Sorting items + * + ******************************/ +/*------------------ + * 1. Table Layout + *------------------*/ +#results-table { + border: 1px solid #e6e6e6; + color: #999; + font-size: 12px; + width: 100%; +} +#results-table th, +#results-table td { + padding: 5px; + border: 1px solid #e6e6e6; + text-align: left; +} +#results-table th { + font-weight: bold; +} + +/*------------------ + * 2. Extra + *------------------*/ +.logwrapper { + max-height: 230px; + overflow-y: scroll; + background-color: #e6e6e6; +} +.logwrapper.expanded { + max-height: none; +} +.logwrapper.expanded .logexpander:after { + content: "collapse [-]"; +} +.logwrapper .logexpander { + z-index: 1; + position: sticky; + top: 10px; + width: max-content; + border: 1px solid; + border-radius: 3px; + padding: 5px 7px; + margin: 10px 0 10px calc(100% - 80px); + cursor: pointer; + background-color: #e6e6e6; +} +.logwrapper .logexpander:after { + content: "expand [+]"; +} +.logwrapper .logexpander:hover { + color: #000; + border-color: #000; +} +.logwrapper .log { + min-height: 40px; + position: relative; + top: -50px; + height: calc(100% + 50px); + border: 1px solid #e6e6e6; + color: black; + display: block; + font-family: "Courier New", Courier, monospace; + padding: 5px; + padding-right: 80px; + white-space: pre-wrap; +} + +div.media { + border: 1px solid #e6e6e6; + float: right; + height: 240px; + margin: 0 5px; + overflow: hidden; + width: 320px; +} + +.media-container { + display: grid; + grid-template-columns: 25px auto 25px; + align-items: center; + flex: 1 1; + overflow: hidden; + height: 200px; +} + +.media-container--fullscreen { + grid-template-columns: 0px auto 0px; +} + +.media-container__nav--right, +.media-container__nav--left { + text-align: center; + cursor: pointer; +} + +.media-container__viewport { + cursor: pointer; + text-align: center; + height: inherit; +} +.media-container__viewport img, +.media-container__viewport video { + object-fit: cover; + width: 100%; + max-height: 100%; +} + +.media__name, +.media__counter { + display: flex; + flex-direction: row; + justify-content: space-around; + flex: 0 0 25px; + align-items: center; +} + +.collapsible td:not(.col-links) { + cursor: pointer; +} +.collapsible td:not(.col-links):hover::after { + color: #bbb; + font-style: italic; + cursor: pointer; +} + +.col-result { + width: 130px; +} +.col-result:hover::after { + content: " (hide details)"; +} + +.col-result.collapsed:hover::after { + content: " (show details)"; +} + +#environment-header h2:hover::after { + content: " (hide details)"; + color: #bbb; + font-style: italic; + cursor: pointer; + font-size: 12px; +} + +#environment-header.collapsed h2:hover::after { + content: " (show details)"; + color: #bbb; + font-style: italic; + cursor: pointer; + font-size: 12px; +} + +/*------------------ + * 3. Sorting items + *------------------*/ +.sortable { + cursor: pointer; +} +.sortable.desc:after { + content: " "; + position: relative; + left: 5px; + bottom: -12.5px; + border: 10px solid #4caf50; + border-bottom: 0; + border-left-color: transparent; + border-right-color: transparent; +} +.sortable.asc:after { + content: " "; + position: relative; + left: 5px; + bottom: 12.5px; + border: 10px solid #4caf50; + border-top: 0; + border-left-color: transparent; + border-right-color: transparent; +} + +.hidden, .summary__reload__button.hidden { + display: none; +} + +.summary__data { + flex: 0 0 550px; +} +.summary__reload { + flex: 1 1; + display: flex; + justify-content: center; +} +.summary__reload__button { + flex: 0 0 300px; + display: flex; + color: white; + font-weight: bold; + background-color: #4caf50; + text-align: center; + justify-content: center; + align-items: center; + border-radius: 3px; + cursor: pointer; +} +.summary__reload__button:hover { + background-color: #46a049; +} +.summary__spacer { + flex: 0 0 550px; +} + +.controls { + display: flex; + justify-content: space-between; +} + +.filters, +.collapse { + display: flex; + align-items: center; +} +.filters button, +.collapse button { + color: #999; + border: none; + background: none; + cursor: pointer; + text-decoration: underline; +} +.filters button:hover, +.collapse button:hover { + color: #ccc; +} + +.filter__label { + margin-right: 10px; +} + + </style> + + </head> + <body> + <h1 id="title">Backend Testing Report</h1> + <p>Report generated on 20-Nov-2024 at 16:43:04 by <a href="https://pypi.python.org/pypi/pytest-html">pytest-html</a> + v4.1.1</p> + <div id="environment-header"> + <h2>Environment</h2> + </div> + <table id="environment"></table> + <!-- TEMPLATES --> + <template id="template_environment_row"> + <tr> + <td></td> + <td></td> + </tr> + </template> + <template id="template_results-table__body--empty"> + <tbody class="results-table-row"> + <tr id="not-found-message"> + <td colspan="7">No results found. Check the filters.</th> + </tr> + </template> + <template id="template_results-table__tbody"> + <tbody class="results-table-row"> + <tr class="collapsible"> + </tr> + <tr class="extras-row"> + <td class="extra" colspan="7"> + <div class="extraHTML"></div> + <div class="media"> + <div class="media-container"> + <div class="media-container__nav--left"><</div> + <div class="media-container__viewport"> + <img src="" /> + <video controls> + <source src="" type="video/mp4"> + </video> + </div> + <div class="media-container__nav--right">></div> + </div> + <div class="media__name"></div> + <div class="media__counter"></div> + </div> + <div class="logwrapper"> + <div class="logexpander"></div> + <div class="log"></div> + </div> + </td> + </tr> + </tbody> + </template> + <!-- END TEMPLATES --> + <div class="summary"> + <div class="summary__data"> + <h2>Summary</h2> + <div class="additional-summary prefix"> + </div> + <p class="run-count">3 tests took 00:00:09.</p> + <p class="filter">(Un)check the boxes to filter the results.</p> + <div class="summary__reload"> + <div class="summary__reload__button hidden" onclick="location.reload()"> + <div>There are still tests running. <br />Reload this page to get the latest results!</div> + </div> + </div> + <div class="summary__spacer"></div> + <div class="controls"> + <div class="filters"> + <input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="failed" /> + <span class="failed">1 Failed,</span> + <input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="passed" /> + <span class="passed">2 Passed,</span> + <input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="skipped" disabled/> + <span class="skipped">0 Skipped,</span> + <input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="xfailed" disabled/> + <span class="xfailed">0 Expected failures,</span> + <input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="xpassed" disabled/> + <span class="xpassed">0 Unexpected passes,</span> + <input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="error" disabled/> + <span class="error">0 Errors,</span> + <input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="rerun" disabled/> + <span class="rerun">0 Reruns</span> + </div> + <div class="collapse"> + <button id="show_all_details">Show all details</button> / <button id="hide_all_details">Hide all details</button> + </div> + </div> + </div> + <div class="additional-summary summary"> + </div> + <div class="additional-summary postfix"> + </div> + </div> + <table id="results-table"> + <thead id="results-table-head"> + <tr> + <th class="sortable" data-column-type="result">Result</th> + <th class="sortable" data-column-type="testId">Test</th> + <th>Detailed trip</th> + <th>Trip Duration</th> + <th>Target Duration</th> + <th>Execution time</th> + <th>Links</th> + </tr> + </thead> + </table> + </body> + <footer> + <div id="data-container" data-jsonblob="{"environment": {"Python": "3.12.3", "Platform": "Linux-6.8.0-48-generic-x86_64-with-glibc2.39", "Packages": {"pytest": "8.3.3", "pluggy": "1.5.0"}, "Plugins": {"html": "4.1.1", "metadata": "3.1.1", "anyio": "4.6.2.post1"}}, "tests": {"src/tests/test_main.py::test_new_trip_invalid_prefs": [{"extras": [], "result": "Passed", "testId": "src/tests/test_main.py::test_new_trip_invalid_prefs", "resultsTableRow": ["<td class=\"col-result\">Passed</td>", "<td class=\"col-testId\">src/tests/test_main.py::test_new_trip_invalid_prefs</td>", "<td>N/A</td>", "<td>N/A</td>", "<td>N/A</td>", "<td class=\"col-duration\">12 ms</td>", "<td class=\"col-links\"></td>"], "log": "No log output captured."}], "src/tests/test_main.py::test_turckheim": [{"extras": [], "result": "Passed", "testId": "src/tests/test_main.py::test_turckheim", "resultsTableRow": ["<td class=\"col-result\">Passed</td>", "<td class=\"col-testId\">src/tests/test_main.py::test_turckheim</td>", "<td>start (0) - 5 - Porte de France (257) - 5 - finish (0) - 0</td>", "<td>15 min</td>", "<td>15 min</td>", "<td class=\"col-duration\">18 ms</td>", "<td class=\"col-links\"></td>"], "log": "No log output captured."}], "src/tests/test_main.py::test_bellecour": [{"extras": [], "result": "Failed", "testId": "src/tests/test_main.py::test_bellecour", "resultsTableRow": ["<td class=\"col-result\">Failed</td>", "<td class=\"col-testId\">src/tests/test_main.py::test_bellecour</td>", "<td>start (0) - 3 - M\u00e9morial Lyonnais du G\u00e9nocide Arm\u00e9nien (265) - 4 - \u00c9glise Saint-Fran\u00e7ois-de-Sales (211) - 7 - Basilique Saint-Martin d'Ainay (333) - 11 - finish (0) - 0</td>", "<td>40 min</td>", "<td>35 min</td>", "<td class=\"col-duration\">00:00:08</td>", "<td class=\"col-links\"></td>"], "log": "client = &lt;starlette.testclient.TestClient object at 0x7bca5bce57c0&gt;\nrequest = &lt;FixtureRequest for &lt;Function test_bellecour&gt;&gt;\n\n def test_bellecour(client, request) :\n duration_minutes = 35\n response = client.post(\n &quot;/trip/new&quot;,\n json={\n &quot;preferences&quot;: {&quot;sightseeing&quot;: {&quot;type&quot;: &quot;sightseeing&quot;, &quot;score&quot;: 5}, &quot;nature&quot;: {&quot;type&quot;: &quot;nature&quot;, &quot;score&quot;: 5}, &quot;shopping&quot;: {&quot;type&quot;: &quot;shopping&quot;, &quot;score&quot;: 5}, &quot;max_time_minute&quot;: duration_minutes, &quot;detour_tolerance_minute&quot;: 0},\n &quot;start&quot;: [45.7576485, 4.8330241]\n }\n )\n result = response.json()\n landmarks = load_trip_landmarks(client, result[&#x27;first_landmark_uuid&#x27;])\n osm_ids = landmarks_to_osmid(landmarks)\n \n # Create the trip string\n trip_string = [f&quot;{landmark.name} ({landmark.attractiveness}) - {landmark.time_to_reach_next}&quot; for landmark in landmarks]\n \n # Pass additional info to pytest for reporting\n request.node.trip_details = trip_string\n request.node.trip_duration = str(result[&#x27;total_time&#x27;])\n request.node.target_duration = str(duration_minutes)\n \n # checks :\n assert response.status_code == 200 # check for successful planning\n assert duration_minutes*0.8 &lt; int(result[&#x27;total_time&#x27;]) &lt; duration_minutes*1.2\n&gt; assert 136200148 in osm_ids # check for Cath\u00e9drale St. Jean in trip\nE assert 136200148 in [0, 265922306, 82260098, 82254733, 0]\n\nsrc/tests/test_main.py:78: AssertionError\n"}]}, "renderCollapsed": ["passed"], "initialSort": "result", "title": "Backend Testing Report"}"></div> + <script> + (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){ +const { getCollapsedCategory, setCollapsedIds } = require('./storage.js') + +class DataManager { + setManager(data) { + const collapsedCategories = [...getCollapsedCategory(data.renderCollapsed)] + const collapsedIds = [] + const tests = Object.values(data.tests).flat().map((test, index) => { + const collapsed = collapsedCategories.includes(test.result.toLowerCase()) + const id = `test_${index}` + if (collapsed) { + collapsedIds.push(id) + } + return { + ...test, + id, + collapsed, + } + }) + const dataBlob = { ...data, tests } + this.data = { ...dataBlob } + this.renderData = { ...dataBlob } + setCollapsedIds(collapsedIds) + } + + get allData() { + return { ...this.data } + } + + resetRender() { + this.renderData = { ...this.data } + } + + setRender(data) { + this.renderData.tests = [...data] + } + + toggleCollapsedItem(id) { + this.renderData.tests = this.renderData.tests.map((test) => + test.id === id ? { ...test, collapsed: !test.collapsed } : test, + ) + } + + set allCollapsed(collapsed) { + this.renderData = { ...this.renderData, tests: [...this.renderData.tests.map((test) => ( + { ...test, collapsed } + ))] } + } + + get testSubset() { + return [...this.renderData.tests] + } + + get environment() { + return this.renderData.environment + } + + get initialSort() { + return this.data.initialSort + } +} + +module.exports = { + manager: new DataManager(), +} + +},{"./storage.js":8}],2:[function(require,module,exports){ +const mediaViewer = require('./mediaviewer.js') +const templateEnvRow = document.getElementById('template_environment_row') +const templateResult = document.getElementById('template_results-table__tbody') + +function htmlToElements(html) { + const temp = document.createElement('template') + temp.innerHTML = html + return temp.content.childNodes +} + +const find = (selector, elem) => { + if (!elem) { + elem = document + } + return elem.querySelector(selector) +} + +const findAll = (selector, elem) => { + if (!elem) { + elem = document + } + return [...elem.querySelectorAll(selector)] +} + +const dom = { + getStaticRow: (key, value) => { + const envRow = templateEnvRow.content.cloneNode(true) + const isObj = typeof value === 'object' && value !== null + const values = isObj ? Object.keys(value).map((k) => `${k}: ${value[k]}`) : null + + const valuesElement = htmlToElements( + values ? `<ul>${values.map((val) => `<li>${val}</li>`).join('')}<ul>` : `<div>${value}</div>`)[0] + const td = findAll('td', envRow) + td[0].textContent = key + td[1].appendChild(valuesElement) + + return envRow + }, + getResultTBody: ({ testId, id, log, extras, resultsTableRow, tableHtml, result, collapsed }) => { + const resultBody = templateResult.content.cloneNode(true) + resultBody.querySelector('tbody').classList.add(result.toLowerCase()) + resultBody.querySelector('tbody').id = testId + resultBody.querySelector('.collapsible').dataset.id = id + + resultsTableRow.forEach((html) => { + const t = document.createElement('template') + t.innerHTML = html + resultBody.querySelector('.collapsible').appendChild(t.content) + }) + + if (log) { + // Wrap lines starting with "E" with span.error to color those lines red + const wrappedLog = log.replace(/^E.*$/gm, (match) => `<span class="error">${match}</span>`) + resultBody.querySelector('.log').innerHTML = wrappedLog + } else { + resultBody.querySelector('.log').remove() + } + + if (collapsed) { + resultBody.querySelector('.collapsible > td')?.classList.add('collapsed') + resultBody.querySelector('.extras-row').classList.add('hidden') + } else { + resultBody.querySelector('.collapsible > td')?.classList.remove('collapsed') + } + + const media = [] + extras?.forEach(({ name, format_type, content }) => { + if (['image', 'video'].includes(format_type)) { + media.push({ path: content, name, format_type }) + } + + if (format_type === 'html') { + resultBody.querySelector('.extraHTML').insertAdjacentHTML('beforeend', `<div>${content}</div>`) + } + }) + mediaViewer.setup(resultBody, media) + + // Add custom html from the pytest_html_results_table_html hook + tableHtml?.forEach((item) => { + resultBody.querySelector('td[class="extra"]').insertAdjacentHTML('beforeend', item) + }) + + return resultBody + }, +} + +module.exports = { + dom, + htmlToElements, + find, + findAll, +} + +},{"./mediaviewer.js":6}],3:[function(require,module,exports){ +const { manager } = require('./datamanager.js') +const { doSort } = require('./sort.js') +const storageModule = require('./storage.js') + +const getFilteredSubSet = (filter) => + manager.allData.tests.filter(({ result }) => filter.includes(result.toLowerCase())) + +const doInitFilter = () => { + const currentFilter = storageModule.getVisible() + const filteredSubset = getFilteredSubSet(currentFilter) + manager.setRender(filteredSubset) +} + +const doFilter = (type, show) => { + if (show) { + storageModule.showCategory(type) + } else { + storageModule.hideCategory(type) + } + + const currentFilter = storageModule.getVisible() + const filteredSubset = getFilteredSubSet(currentFilter) + manager.setRender(filteredSubset) + + const sortColumn = storageModule.getSort() + doSort(sortColumn, true) +} + +module.exports = { + doFilter, + doInitFilter, +} + +},{"./datamanager.js":1,"./sort.js":7,"./storage.js":8}],4:[function(require,module,exports){ +const { redraw, bindEvents, renderStatic } = require('./main.js') +const { doInitFilter } = require('./filter.js') +const { doInitSort } = require('./sort.js') +const { manager } = require('./datamanager.js') +const data = JSON.parse(document.getElementById('data-container').dataset.jsonblob) + +function init() { + manager.setManager(data) + doInitFilter() + doInitSort() + renderStatic() + redraw() + bindEvents() +} + +init() + +},{"./datamanager.js":1,"./filter.js":3,"./main.js":5,"./sort.js":7}],5:[function(require,module,exports){ +const { dom, find, findAll } = require('./dom.js') +const { manager } = require('./datamanager.js') +const { doSort } = require('./sort.js') +const { doFilter } = require('./filter.js') +const { + getVisible, + getCollapsedIds, + setCollapsedIds, + getSort, + getSortDirection, + possibleFilters, +} = require('./storage.js') + +const removeChildren = (node) => { + while (node.firstChild) { + node.removeChild(node.firstChild) + } +} + +const renderStatic = () => { + const renderEnvironmentTable = () => { + const environment = manager.environment + const rows = Object.keys(environment).map((key) => dom.getStaticRow(key, environment[key])) + const table = document.getElementById('environment') + removeChildren(table) + rows.forEach((row) => table.appendChild(row)) + } + renderEnvironmentTable() +} + +const addItemToggleListener = (elem) => { + elem.addEventListener('click', ({ target }) => { + const id = target.parentElement.dataset.id + manager.toggleCollapsedItem(id) + + const collapsedIds = getCollapsedIds() + if (collapsedIds.includes(id)) { + const updated = collapsedIds.filter((item) => item !== id) + setCollapsedIds(updated) + } else { + collapsedIds.push(id) + setCollapsedIds(collapsedIds) + } + redraw() + }) +} + +const renderContent = (tests) => { + const sortAttr = getSort(manager.initialSort) + const sortAsc = JSON.parse(getSortDirection()) + const rows = tests.map(dom.getResultTBody) + const table = document.getElementById('results-table') + const tableHeader = document.getElementById('results-table-head') + + const newTable = document.createElement('table') + newTable.id = 'results-table' + + // remove all sorting classes and set the relevant + findAll('.sortable', tableHeader).forEach((elem) => elem.classList.remove('asc', 'desc')) + tableHeader.querySelector(`.sortable[data-column-type="${sortAttr}"]`)?.classList.add(sortAsc ? 'desc' : 'asc') + newTable.appendChild(tableHeader) + + if (!rows.length) { + const emptyTable = document.getElementById('template_results-table__body--empty').content.cloneNode(true) + newTable.appendChild(emptyTable) + } else { + rows.forEach((row) => { + if (!!row) { + findAll('.collapsible td:not(.col-links', row).forEach(addItemToggleListener) + find('.logexpander', row).addEventListener('click', + (evt) => evt.target.parentNode.classList.toggle('expanded'), + ) + newTable.appendChild(row) + } + }) + } + + table.replaceWith(newTable) +} + +const renderDerived = () => { + const currentFilter = getVisible() + possibleFilters.forEach((result) => { + const input = document.querySelector(`input[data-test-result="${result}"]`) + input.checked = currentFilter.includes(result) + }) +} + +const bindEvents = () => { + const filterColumn = (evt) => { + const { target: element } = evt + const { testResult } = element.dataset + + doFilter(testResult, element.checked) + const collapsedIds = getCollapsedIds() + const updated = manager.renderData.tests.map((test) => { + return { + ...test, + collapsed: collapsedIds.includes(test.id), + } + }) + manager.setRender(updated) + redraw() + } + + const header = document.getElementById('environment-header') + header.addEventListener('click', () => { + const table = document.getElementById('environment') + table.classList.toggle('hidden') + header.classList.toggle('collapsed') + }) + + findAll('input[name="filter_checkbox"]').forEach((elem) => { + elem.addEventListener('click', filterColumn) + }) + + findAll('.sortable').forEach((elem) => { + elem.addEventListener('click', (evt) => { + const { target: element } = evt + const { columnType } = element.dataset + doSort(columnType) + redraw() + }) + }) + + document.getElementById('show_all_details').addEventListener('click', () => { + manager.allCollapsed = false + setCollapsedIds([]) + redraw() + }) + document.getElementById('hide_all_details').addEventListener('click', () => { + manager.allCollapsed = true + const allIds = manager.renderData.tests.map((test) => test.id) + setCollapsedIds(allIds) + redraw() + }) +} + +const redraw = () => { + const { testSubset } = manager + + renderContent(testSubset) + renderDerived() +} + +module.exports = { + redraw, + bindEvents, + renderStatic, +} + +},{"./datamanager.js":1,"./dom.js":2,"./filter.js":3,"./sort.js":7,"./storage.js":8}],6:[function(require,module,exports){ +class MediaViewer { + constructor(assets) { + this.assets = assets + this.index = 0 + } + + nextActive() { + this.index = this.index === this.assets.length - 1 ? 0 : this.index + 1 + return [this.activeFile, this.index] + } + + prevActive() { + this.index = this.index === 0 ? this.assets.length - 1 : this.index -1 + return [this.activeFile, this.index] + } + + get currentIndex() { + return this.index + } + + get activeFile() { + return this.assets[this.index] + } +} + + +const setup = (resultBody, assets) => { + if (!assets.length) { + resultBody.querySelector('.media').classList.add('hidden') + return + } + + const mediaViewer = new MediaViewer(assets) + const container = resultBody.querySelector('.media-container') + const leftArrow = resultBody.querySelector('.media-container__nav--left') + const rightArrow = resultBody.querySelector('.media-container__nav--right') + const mediaName = resultBody.querySelector('.media__name') + const counter = resultBody.querySelector('.media__counter') + const imageEl = resultBody.querySelector('img') + const sourceEl = resultBody.querySelector('source') + const videoEl = resultBody.querySelector('video') + + const setImg = (media, index) => { + if (media?.format_type === 'image') { + imageEl.src = media.path + + imageEl.classList.remove('hidden') + videoEl.classList.add('hidden') + } else if (media?.format_type === 'video') { + sourceEl.src = media.path + + videoEl.classList.remove('hidden') + imageEl.classList.add('hidden') + } + + mediaName.innerText = media?.name + counter.innerText = `${index + 1} / ${assets.length}` + } + setImg(mediaViewer.activeFile, mediaViewer.currentIndex) + + const moveLeft = () => { + const [media, index] = mediaViewer.prevActive() + setImg(media, index) + } + const doRight = () => { + const [media, index] = mediaViewer.nextActive() + setImg(media, index) + } + const openImg = () => { + window.open(mediaViewer.activeFile.path, '_blank') + } + if (assets.length === 1) { + container.classList.add('media-container--fullscreen') + } else { + leftArrow.addEventListener('click', moveLeft) + rightArrow.addEventListener('click', doRight) + } + imageEl.addEventListener('click', openImg) +} + +module.exports = { + setup, +} + +},{}],7:[function(require,module,exports){ +const { manager } = require('./datamanager.js') +const storageModule = require('./storage.js') + +const genericSort = (list, key, ascending, customOrder) => { + let sorted + if (customOrder) { + sorted = list.sort((a, b) => { + const aValue = a.result.toLowerCase() + const bValue = b.result.toLowerCase() + + const aIndex = customOrder.findIndex((item) => item.toLowerCase() === aValue) + const bIndex = customOrder.findIndex((item) => item.toLowerCase() === bValue) + + // Compare the indices to determine the sort order + return aIndex - bIndex + }) + } else { + sorted = list.sort((a, b) => a[key] === b[key] ? 0 : a[key] > b[key] ? 1 : -1) + } + + if (ascending) { + sorted.reverse() + } + return sorted +} + +const durationSort = (list, ascending) => { + const parseDuration = (duration) => { + if (duration.includes(':')) { + // If it's in the format "HH:mm:ss" + const [hours, minutes, seconds] = duration.split(':').map(Number) + return (hours * 3600 + minutes * 60 + seconds) * 1000 + } else { + // If it's in the format "nnn ms" + return parseInt(duration) + } + } + const sorted = list.sort((a, b) => parseDuration(a['duration']) - parseDuration(b['duration'])) + if (ascending) { + sorted.reverse() + } + return sorted +} + +const doInitSort = () => { + const type = storageModule.getSort(manager.initialSort) + const ascending = storageModule.getSortDirection() + const list = manager.testSubset + const initialOrder = ['Error', 'Failed', 'Rerun', 'XFailed', 'XPassed', 'Skipped', 'Passed'] + + storageModule.setSort(type) + storageModule.setSortDirection(ascending) + + if (type?.toLowerCase() === 'original') { + manager.setRender(list) + } else { + let sortedList + switch (type) { + case 'duration': + sortedList = durationSort(list, ascending) + break + case 'result': + sortedList = genericSort(list, type, ascending, initialOrder) + break + default: + sortedList = genericSort(list, type, ascending) + break + } + manager.setRender(sortedList) + } +} + +const doSort = (type, skipDirection) => { + const newSortType = storageModule.getSort(manager.initialSort) !== type + const currentAsc = storageModule.getSortDirection() + let ascending + if (skipDirection) { + ascending = currentAsc + } else { + ascending = newSortType ? false : !currentAsc + } + storageModule.setSort(type) + storageModule.setSortDirection(ascending) + + const list = manager.testSubset + const sortedList = type === 'duration' ? durationSort(list, ascending) : genericSort(list, type, ascending) + manager.setRender(sortedList) +} + +module.exports = { + doInitSort, + doSort, +} + +},{"./datamanager.js":1,"./storage.js":8}],8:[function(require,module,exports){ +const possibleFilters = [ + 'passed', + 'skipped', + 'failed', + 'error', + 'xfailed', + 'xpassed', + 'rerun', +] + +const getVisible = () => { + const url = new URL(window.location.href) + const settings = new URLSearchParams(url.search).get('visible') + const lower = (item) => { + const lowerItem = item.toLowerCase() + if (possibleFilters.includes(lowerItem)) { + return lowerItem + } + return null + } + return settings === null ? + possibleFilters : + [...new Set(settings?.split(',').map(lower).filter((item) => item))] +} + +const hideCategory = (categoryToHide) => { + const url = new URL(window.location.href) + const visibleParams = new URLSearchParams(url.search).get('visible') + const currentVisible = visibleParams ? visibleParams.split(',') : [...possibleFilters] + const settings = [...new Set(currentVisible)].filter((f) => f !== categoryToHide).join(',') + + url.searchParams.set('visible', settings) + window.history.pushState({}, null, unescape(url.href)) +} + +const showCategory = (categoryToShow) => { + if (typeof window === 'undefined') { + return + } + const url = new URL(window.location.href) + const currentVisible = new URLSearchParams(url.search).get('visible')?.split(',').filter(Boolean) || + [...possibleFilters] + const settings = [...new Set([categoryToShow, ...currentVisible])] + const noFilter = possibleFilters.length === settings.length || !settings.length + + noFilter ? url.searchParams.delete('visible') : url.searchParams.set('visible', settings.join(',')) + window.history.pushState({}, null, unescape(url.href)) +} + +const getSort = (initialSort) => { + const url = new URL(window.location.href) + let sort = new URLSearchParams(url.search).get('sort') + if (!sort) { + sort = initialSort || 'result' + } + return sort +} + +const setSort = (type) => { + const url = new URL(window.location.href) + url.searchParams.set('sort', type) + window.history.pushState({}, null, unescape(url.href)) +} + +const getCollapsedCategory = (renderCollapsed) => { + let categories + if (typeof window !== 'undefined') { + const url = new URL(window.location.href) + const collapsedItems = new URLSearchParams(url.search).get('collapsed') + switch (true) { + case !renderCollapsed && collapsedItems === null: + categories = ['passed'] + break + case collapsedItems?.length === 0 || /^["']{2}$/.test(collapsedItems): + categories = [] + break + case /^all$/.test(collapsedItems) || collapsedItems === null && /^all$/.test(renderCollapsed): + categories = [...possibleFilters] + break + default: + categories = collapsedItems?.split(',').map((item) => item.toLowerCase()) || renderCollapsed + break + } + } else { + categories = [] + } + return categories +} + +const getSortDirection = () => JSON.parse(sessionStorage.getItem('sortAsc')) || false +const setSortDirection = (ascending) => sessionStorage.setItem('sortAsc', ascending) + +const getCollapsedIds = () => JSON.parse(sessionStorage.getItem('collapsedIds')) || [] +const setCollapsedIds = (list) => sessionStorage.setItem('collapsedIds', JSON.stringify(list)) + +module.exports = { + getVisible, + hideCategory, + showCategory, + getCollapsedIds, + setCollapsedIds, + getSort, + setSort, + getSortDirection, + setSortDirection, + getCollapsedCategory, + possibleFilters, +} + +},{}]},{},[4]); + </script> + </footer> +</html> \ No newline at end of file diff --git a/backend/src/tests/test_main.py b/backend/src/tests/test_main.py index f4600e5..e7c3aa6 100644 --- a/backend/src/tests/test_main.py +++ b/backend/src/tests/test_main.py @@ -22,8 +22,8 @@ def test_new_trip_invalid_prefs(client): assert response.status_code == 422 -# Test no. 1 -def test_turckheim(client): +# Test no. 2 +def test_turckheim(client, request): duration_minutes = 15 response = client.post( "/trip/new", @@ -35,6 +35,14 @@ def test_turckheim(client): result = response.json() landmarks = load_trip_landmarks(client, result['first_landmark_uuid']) + # Create the trip string + trip_string = [f"{landmark.name} ({landmark.attractiveness}) - {landmark.time_to_reach_next}" for landmark in landmarks] + + # Pass additional info to pytest for reporting + request.node.trip_details = trip_string + request.node.trip_duration = str(result['total_time']) + request.node.target_duration = str(duration_minutes) + # checks : assert response.status_code == 200 # check for successful planning assert isinstance(landmarks, list) # check that the return type is a list @@ -42,8 +50,8 @@ def test_turckheim(client): assert len(landmarks) > 2 # check that there is something to visit -# Test no. 2 -def test_bellecour(client) : +# Test no. 3 +def test_bellecour(client, request) : duration_minutes = 35 response = client.post( "/trip/new", @@ -56,6 +64,14 @@ def test_bellecour(client) : landmarks = load_trip_landmarks(client, result['first_landmark_uuid']) osm_ids = landmarks_to_osmid(landmarks) + # Create the trip string + trip_string = [f"{landmark.name} ({landmark.attractiveness}) - {landmark.time_to_reach_next}" for landmark in landmarks] + + # Pass additional info to pytest for reporting + request.node.trip_details = trip_string + request.node.trip_duration = str(result['total_time']) + request.node.target_duration = str(duration_minutes) + # checks : assert response.status_code == 200 # check for successful planning assert duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2 @@ -63,7 +79,7 @@ def test_bellecour(client) : -def landmarks_to_osmid(landmarks: List[Landmark]) -> list : +def landmarks_to_osmid(landmarks: List[Landmark]) -> List[int] : """ Convert the list of landmarks into a list containing their osm ids for quick landmark checking. @@ -79,7 +95,7 @@ def landmarks_to_osmid(landmarks: List[Landmark]) -> list : return ids -def fetch_landmark(client, landmark_uuid): +def fetch_landmark(client, landmark_uuid: str): """ Fetch landmark data from the API based on the landmark UUID. @@ -102,7 +118,7 @@ def fetch_landmark(client, landmark_uuid): return json_data -def load_trip_landmarks(client, first_uuid): +def load_trip_landmarks(client, first_uuid: str) -> List[Landmark]: """ Load all landmarks for a trip using the response from the API. @@ -117,6 +133,11 @@ def load_trip_landmarks(client, first_uuid): while next_uuid is not None: landmark_data = fetch_landmark(client, next_uuid) + # # Convert UUIDs to strings explicitly + # landmark_data = { + # key: str(value) if isinstance(value, UUID) else value + # for key, value in landmark_data.items() + # } landmarks.append(Landmark(**landmark_data)) # Create Landmark objects next_uuid = landmark_data.get('next_uuid') # Prepare for the next iteration