diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..7280bd89 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +[run] +concurrency = multiprocessing, thread +parallel = true +sigterm = true +omit = tests/**/*.py, docs/**/*.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 019b532a..00f96e92 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,6 +5,7 @@ on: jobs: base_coverage: + continue-on-error: true runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -17,7 +18,7 @@ jobs: python-version: 3.12 - name: Install Dependencies - run: pip install -e .[dev,server] + run: pip install -e . -r dev-requirements.txt - name: Test with pytest run: | @@ -111,11 +112,16 @@ jobs: name: coverage-3.12 - name: Download code coverage report for base branch + id: download-base-coverage + continue-on-error: true uses: actions/download-artifact@v4 with: name: base-coverage.lcov - name: Generate Code Coverage report + # Note, due to continue on error (to make job pass) we need to check the + # Status of the step directly not just use success() or failure() + if: steps.download-base-coverage.outcome == 'success' id: code-coverage uses: barecheck/code-coverage-action@v1 with: @@ -124,4 +130,15 @@ jobs: base-lcov-file: "./base-coverage.lcov" minimum-ratio: 0 send-summary-comment: true - show-annotations: "warning" \ No newline at end of file + show-annotations: "warning" + + - name: Generate Code Coverage report if base job fails + if: steps.download-base-coverage.outcome == 'failure' + id: code-coverage-without-base + uses: barecheck/code-coverage-action@v1 + with: + barecheck-github-app-token: ${{ secrets.BARECHECK_GITHUB_APP_TOKEN }} + lcov-file: "./coverage.lcov" + minimum-ratio: 0 + send-summary-comment: true + show-annotations: "warning" diff --git a/pyproject.toml b/pyproject.toml index b154a054..c4cc5131 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "labthings-fastapi" -version = "0.0.7" +version = "0.0.8" authors = [ { name="Richard Bowman", email="richard.bowman@cantab.net" }, ] @@ -13,7 +13,7 @@ classifiers = [ "Operating System :: OS Independent", ] dependencies = [ - "pydantic>=2.0.0", + "pydantic ~= 2.10.6", "numpy>=1.20", "jsonschema", "typing_extensions", diff --git a/src/labthings_fastapi/server/fallback.py b/src/labthings_fastapi/server/fallback.py index db224407..f9cf9109 100644 --- a/src/labthings_fastapi/server/fallback.py +++ b/src/labthings_fastapi/server/fallback.py @@ -1,4 +1,5 @@ import json +from traceback import format_exception from fastapi import FastAPI from fastapi.responses import HTMLResponse from starlette.responses import RedirectResponse @@ -10,6 +11,7 @@ def __init__(self, *args, **kwargs): self.labthings_config = None self.labthings_server = None self.labthings_error = None + self.log_history = None app = FallbackApp() @@ -32,6 +34,9 @@ def __init__(self, *args, **kwargs):

Your configuration:

{{config}}
+

Traceback

+
{{traceback}}
+ {{logginginfo}} """ @@ -40,6 +45,9 @@ def __init__(self, *args, **kwargs): @app.get("/") async def root(): error_message = f"{app.labthings_error!r}" + # use traceback.format_exception to get full traceback as list + # this ends in newlines, but needs joining to be a single string + error_w_trace = "".join(format_exception(app.labthings_error)) things = "" if app.labthings_server: for path, thing in app.labthings_server.things.items(): @@ -49,6 +57,14 @@ async def root(): content = content.replace("{{error}}", error_message) content = content.replace("{{things}}", things) content = content.replace("{{config}}", json.dumps(app.labthings_config, indent=2)) + content = content.replace("{{traceback}}", error_w_trace) + + if app.log_history is None: + logging_info = "

No logging info available

" + else: + logging_info = f"

Logging info

\n
{app.log_history}
" + + content = content.replace("{{logginginfo}}", logging_info) return HTMLResponse(content=content, status_code=500) diff --git a/tests/test_fallback.py b/tests/test_fallback.py new file mode 100644 index 00000000..9e285d47 --- /dev/null +++ b/tests/test_fallback.py @@ -0,0 +1,64 @@ +from fastapi.testclient import TestClient +from labthings_fastapi.server import server_from_config +from labthings_fastapi.server.fallback import app + + +def test_fallback_empty(): + with TestClient(app) as client: + response = client.get("/") + html = response.text + # test that something when wrong is shown + assert "Something went wrong" in html + assert "No logging info available" in html + + +def test_fallback_with_config(): + app.labthings_config = {"hello": "goodbye"} + with TestClient(app) as client: + response = client.get("/") + html = response.text + assert "Something went wrong" in html + assert "No logging info available" in html + assert '"hello": "goodbye"' in html + + +def test_fallback_with_error(): + app.labthings_error = RuntimeError("Custom error message") + with TestClient(app) as client: + response = client.get("/") + html = response.text + assert "Something went wrong" in html + assert "No logging info available" in html + assert "RuntimeError" in html + assert "Custom error message" in html + + +def test_fallback_with_server(): + config = { + "things": { + "thing1": "labthings_fastapi.example_things:MyThing", + "thing2": { + "class": "labthings_fastapi.example_things:MyThing", + "kwargs": {}, + }, + } + } + app.labthings_server = server_from_config(config) + with TestClient(app) as client: + response = client.get("/") + html = response.text + assert "Something went wrong" in html + assert "No logging info available" in html + assert "thing1/" in html + assert "thing2/" in html + + +def test_fallback_with_log(): + app.log_history = "Fake log conetent" + with TestClient(app) as client: + response = client.get("/") + html = response.text + assert "Something went wrong" in html + assert "No logging info available" not in html + assert "

Logging info

" in html + assert "Fake log conetent" in html diff --git a/tests/test_server_cli.py b/tests/test_server_cli.py index 404b9067..49bd5e23 100644 --- a/tests/test_server_cli.py +++ b/tests/test_server_cli.py @@ -119,8 +119,8 @@ def test_serve_with_no_config(): check_serve_from_cli([]) -def test_invalid_thing_and_fallback(): - """Check it fails for invalid things, and test the fallback option""" +def test_invalid_thing(): + """Check it fails for invalid things""" config_json = json.dumps( { "things": { @@ -130,8 +130,21 @@ def test_invalid_thing_and_fallback(): ) with raises(ImportError): check_serve_from_cli(["-j", config_json]) - ## the line below should start a dummy server with an error page - - ## it terminates happily once the server starts. + + +def test_fallback(): + """test the fallback option + + startd a dummy server with an error page - + it terminates once the server starts. + """ + config_json = json.dumps( + { + "things": { + "broken": "labthings_fastapi.example_things:MissingThing", + } + } + ) check_serve_from_cli(["-j", config_json, "--fallback"])