Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve testing #321

Closed
tcompa opened this issue Oct 19, 2023 · 27 comments
Closed

Improve testing #321

tcompa opened this issue Oct 19, 2023 · 27 comments
Labels
testing Tests, including both vitest (unit) and playwrigth

Comments

@tcompa
Copy link
Collaborator

tcompa commented Oct 19, 2023

#320 implements a first complete version of the testing framework: unit tests (vitest), end-to-end tests (playwright), global coverage, and appropriate github actions.

Let's list here placeholders of future updates, and then move to specific issues when necessary.

A few first thoughts:

  1. Add safari to playwright config
  2. Add SSR to coverage (see Set up end-to-end testing #8 (comment))
  3. Assess whether some caching may be useful (see Set up end-to-end testing #8 (comment))
  4. Add a lot more e2e tests, in general
  5. Assess whether it's useful to create a few modular "fixtures", for operations that are repeated often (e.g. create a project at the beginning of a test). This is just me coming from pytest, but perhaps there is a better/different approach in playwright.
  6. Upload the coverage report of main somewhere, so that we can view it from the repo page (and not just from the PRs)
  7. Automatically test fractal-tasks-core task-argument JSON Schemas #161
  8. We may want to expose the fractal-server version as an env variable, so that in principle we could ask GitHub actions to iterate over e.g. the two latest versions (in parallel).
@tcompa tcompa added the testing Tests, including both vitest (unit) and playwrigth label Oct 19, 2023
@zonia3000
Copy link
Collaborator

Regarding to "Add SSR to coverage"

I report the comment from issue #8:

the server side of playwright end-to-end tests -> this is currently ignored. Just for the record, would sveltejs/kit#5145 (reply in thread) work? I'm not suggesting we go in that direction (as it requires a change in the code base which is only done in view of testing), but it may be part of the ongoing exploration.

It's not so easy to disable ssr in our case, because we are using the *.server.js files, that are explicitly designed to be run on server and the logic written on those files can't be automatically rewritten as client-side logic by Svelte. For example there are some Node dependencies that can work only on the server and not inside the browser. It would be a very different application. In that example they are using just page.js and it's very a different situation.

What we need is a way to attach istanbul instrumentation to the node process that is starting the server, in playwright.config.js webServer list:

command: 'npm run build && ORIGIN=http://localhost:5173 PORT=5173 node build',

A similar use case is described here: istanbuljs/nyc#520, where they would like to compute code coverage for manual testing. They are suggesting 2 possible tools, but both have some problems:

  • babel-plugin-istanbul, depends on babel build tool, that we are not using, moreover the last commit is from 2021, so it could be an unmaintained project.
  • istanbul-middleware: last commit is from 2015, so it's not maintained for sure.

Istanbul documentation says that the command nyc instrument can be used to instrument source files outside of the context of your unit-tests and I think that this may be the way to go. I tried but I had 0% coverage, so its feasibility should be investigated.

@tcompa
Copy link
Collaborator Author

tcompa commented Oct 24, 2023

As briefly mentioned in the discussion in #324, we may want to include some benchmarks (AKA testing for performance regressions) in our test suites.

To be clear: I don't think it makes sense to have it as a broadly-scoped feature of our test suite, nor I think it's a priority.
Let's just keep it in mind when developing components with relevant amount of internal logic: if we are worried about a possible future performance hit, we can at least encode current performance expectations in a benchmark (by hand, or via dedicated tools).

@tcompa
Copy link
Collaborator Author

tcompa commented Nov 7, 2023

A quick comment (based on a quick look at main...more-tests#diff-cb5fff2cfb1045f7d933c94ceef3671d2407b3f0a5abd691197979c6f00f1817R18): great to have tests for task collection!
If collecting fractal-tasks-core takes too long, we can easily set up a lightweight task package (which e.g. does not require pytorch install). Or, otherwise, you could just skip including the fractal-tasks extra. The tasks will not be functional, but the task collection should still work and be testable.

@zonia3000
Copy link
Collaborator

we can easily set up a lightweight task package (which e.g. does not require pytorch install). Or, otherwise, you could just skip including the fractal-tasks extra. The tasks will not be functional, but the task collection should still work and be testable.

Thanks! I've removed fractal-tasks extra and it is faster now! Performing a task collection is a required step of other test cases, but I don't think we need working tasks.

@zonia3000
Copy link
Collaborator

zonia3000 commented Nov 7, 2023

It seems that when playwright creates 2 projects concurrently (in different tests) a SQL constraint fails:

image

Complete error:

(sqlite3.IntegrityError) UNIQUE constraint failed: linkuserproject.project_id, linkuserproject.user_id
[SQL: INSERT INTO linkuserproject (project_id, user_id) VALUES (?, ?)]\n[parameters: (2, 1)]
(Background on this error at: https://sqlalche.me/e/14/gkpj)

Any idea about this? I didn't try to reproduce it outside the tests yet, but I've already encountered it multiple times. I'm setting up a test that uses a "create project" fixture and it seems to clash with the create/delete project test.

The 2 tests pass when I run them separately.

@tcompa
Copy link
Collaborator Author

tcompa commented Nov 8, 2023

Any idea about this?

We can look into it together, if it's useful, but for the moment I have no idea of why this is happening.

Some very preliminary thoughts:

  1. Do you get access to the fractal-server logs, while running through playwright? It'd be useful to see the logs of the API calls. I guess there an call with 201-Created response, and then a 500-internal-error one. Is it the case?
  2. Are the two calls made at the very same time? Can you add a small sleep time (a few hundred ms maybe) in front of one of them?

@tcompa
Copy link
Collaborator Author

tcompa commented Nov 8, 2023

Something more that should be tested at some point is that resource order is somewhat predictable, both in POST and PATCH API calls.

Ref:

@zonia3000
Copy link
Collaborator

I've collected some logs of other failures. I'm still not sure about what is the exact sequence to reproduce this, but it seems that reading, creating and deleting projects concurrently causes it. It could be caused by SQLite (I'll do some tests with PostgreSQL) or by something that should be handled using transaction in the backend.

In a highly concurrent environment errors can happen also during a simple select (project listing):

[WebServer] INFO:     127.0.0.1:48092 - "GET /api/v1/project/ HTTP/1.1" 500 Internal Server Error
[WebServer] Client unable to fetch projects list
Error: Unable to list projects
    at listProjects (file:///home/xonya/code/fractal-web/build/server/chunks/project_api-ec5961e6.js:13:9)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async loadProjects (file:///home/xonya/code/fractal-web/build/server/chunks/6-728fc528.js:8:10)
    at async load (file:///home/xonya/code/fractal-web/build/server/chunks/6-728fc528.js:15:20)
    at async load_server_data (file:///home/xonya/code/fractal-web/build/server/index.js:1893:18)
    at async file:///home/xonya/code/fractal-web/build/server/index.js:3264:18
[WebServer] ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/engine/base.py", line 1900, in _execute_context
    self.dialect.do_execute(
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/engine/default.py", line 736, in do_execute
    cursor.execute(statement, parameters)
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/dialects/sqlite/aiosqlite.py", line 100, in execute
    self._adapt_connection._handle_exception(error)
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/dialects/sqlite/aiosqlite.py", line 228, in _handle_exception
    raise error
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/dialects/sqlite/aiosqlite.py", line 89, in execute
    self._rows = self.await_(_cursor.fetchall())
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 68, in await_only
    return current.driver.switch(awaitable)
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 121, in greenlet_spawn
    value = await result
  File "/home/xonya/.local/lib/python3.10/site-packages/aiosqlite/cursor.py", line 65, in fetchall
    return await self._execute(self._cursor.fetchall)
  File "/home/xonya/.local/lib/python3.10/site-packages/aiosqlite/cursor.py", line 31, in _execute
    return await self._conn._execute(fn, *args, **kwargs)
  File "/home/xonya/.local/lib/python3.10/site-packages/aiosqlite/core.py", line 129, in _execute
    return await future
  File "/home/xonya/.local/lib/python3.10/site-packages/aiosqlite/core.py", line 102, in run
    result = function()
sqlite3.InterfaceError: Cursor needed to be reset because of commit/rollback and can no longer be fetched from.

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/xonya/.local/lib/python3.10/site-packages/uvicorn/protocols/http/h11_impl.py", line 407, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
  File "/home/xonya/.local/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__
    return await self.app(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/fastapi/applications.py", line 276, in __call__
    await super().__call__(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/applications.py", line 122, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/errors.py", line 184, in __call__
    raise exc
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/errors.py", line 162, in __call__
    await self.app(scope, receive, _send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/cors.py", line 91, in __call__
    await self.simple_response(scope, receive, send, request_headers=headers)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/cors.py", line 146, in simple_response
    await self.app(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 79, in __call__
    raise exc
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 68, in __call__
    await self.app(scope, receive, sender)
  File "/home/xonya/.local/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 21, in __call__
    raise e
  File "/home/xonya/.local/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__
    await self.app(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/routing.py", line 718, in __call__
    await route.handle(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/routing.py", line 276, in handle
    await self.app(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/routing.py", line 66, in app
    response = await func(request)
  File "/home/xonya/.local/lib/python3.10/site-packages/fastapi/routing.py", line 237, in app
    raw_response = await run_endpoint_function(
  File "/home/xonya/.local/lib/python3.10/site-packages/fastapi/routing.py", line 163, in run_endpoint_function
    return await dependant.call(**values)
  File "/home/xonya/.local/lib/python3.10/site-packages/fractal_server/app/api/v1/project.py", line 55, in get_list_project
    res = await db.execute(stm)
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/ext/asyncio/session.py", line 215, in execute
    result = await greenlet_spawn(
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 126, in greenlet_spawn
    result = context.throw(*sys.exc_info())
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/orm/session.py", line 1712, in execute
    result = conn._execute_20(statement, params or {}, execution_options)
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/engine/base.py", line 1705, in _execute_20
    return meth(self, args_10style, kwargs_10style, execution_options)
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/sql/elements.py", line 333, in _execute_on_connection
    return connection._execute_clauseelement(
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/engine/base.py", line 1572, in _execute_clauseelement
    ret = self._execute_context(
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/engine/base.py", line 1943, in _execute_context
    self._handle_dbapi_exception(
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/engine/base.py", line 2124, in _handle_dbapi_exception
    util.raise_(
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/util/compat.py", line 208, in raise_
    raise exception
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/engine/base.py", line 1900, in _execute_context
    self.dialect.do_execute(
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/engine/default.py", line 736, in do_execute
    cursor.execute(statement, parameters)
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/dialects/sqlite/aiosqlite.py", line 100, in execute
    self._adapt_connection._handle_exception(error)
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/dialects/sqlite/aiosqlite.py", line 228, in _handle_exception
    raise error
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/dialects/sqlite/aiosqlite.py", line 89, in execute
    self._rows = self.await_(_cursor.fetchall())
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 68, in await_only
    return current.driver.switch(awaitable)
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 121, in greenlet_spawn
    value = await result
  File "/home/xonya/.local/lib/python3.10/site-packages/aiosqlite/cursor.py", line 65, in fetchall
    return await self._execute(self._cursor.fetchall)
  File "/home/xonya/.local/lib/python3.10/site-packages/aiosqlite/cursor.py", line 31, in _execute
    return await self._conn._execute(fn, *args, **kwargs)
  File "/home/xonya/.local/lib/python3.10/site-packages/aiosqlite/core.py", line 129, in _execute
    return await future
  File "/home/xonya/.local/lib/python3.10/site-packages/aiosqlite/core.py", line 102, in run
    result = function()
sqlalchemy.exc.InterfaceError: (sqlite3.InterfaceError) Cursor needed to be reset because of commit/rollback and can no longer be fetched from.
[SQL: SELECT project.name, project.read_only, project.id 
FROM project JOIN linkuserproject ON project.id = linkuserproject.project_id 
WHERE linkuserproject.user_id = ?]
[parameters: (1,)]
(Background on this error at: https://sqlalche.me/e/14/rvf5)
[WebServer] INFO:     127.0.0.1:54842 - "POST /api/v1/project/15/dataset/ HTTP/1.1" 500 Internal Server Error
[WebServer] ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "/home/xonya/.local/lib/python3.10/site-packages/uvicorn/protocols/http/h11_impl.py", line 407, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
  File "/home/xonya/.local/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__
    return await self.app(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/fastapi/applications.py", line 276, in __call__
    await super().__call__(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/applications.py", line 122, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/errors.py", line 184, in __call__
    raise exc
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/errors.py", line 162, in __call__
    await self.app(scope, receive, _send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/cors.py", line 83, in __call__
    await self.app(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 79, in __call__
    raise exc
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 68, in __call__
    await self.app(scope, receive, sender)
  File "/home/xonya/.local/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 21, in __call__
    raise e
  File "/home/xonya/.local/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__
    await self.app(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/routing.py", line 718, in __call__
    await route.handle(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/routing.py", line 276, in handle
    await self.app(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/routing.py", line 66, in app
    response = await func(request)
  File "/home/xonya/.local/lib/python3.10/site-packages/fastapi/routing.py", line 237, in app
    raw_response = await run_endpoint_function(
  File "/home/xonya/.local/lib/python3.10/site-packages/fastapi/routing.py", line 163, in run_endpoint_function
    return await dependant.call(**values)
  File "/home/xonya/.local/lib/python3.10/site-packages/fractal_server/app/api/v1/dataset.py", line 51, in create_dataset
    await db.refresh(db_dataset)
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/ext/asyncio/session.py", line 157, in refresh
    return await greenlet_spawn(
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 128, in greenlet_spawn
    result = context.switch(value)
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/orm/session.py", line 2348, in refresh
    raise sa_exc.InvalidRequestError(
sqlalchemy.exc.InvalidRequestError: Could not refresh instance '<Dataset at 0x7fb325cc3440>'

But with the delete+insert something very bad can happen (since it leaves the database in a broken state and manual fix on the table is needed):

[WebServer] Load projects page
[WebServer] Including cookie into request to http://localhost:8000/api/v1/project/, via handleFetch
[WebServer] INFO:     127.0.0.1:45248 - "GET /api/alive/ HTTP/1.1" 200 OK
[WebServer] Server info loaded: Alive true - 1.3.4
[WebServer] Including cookie into request to http://localhost:8000/auth/whoami, via handleFetch
[WebServer] INFO:     127.0.0.1:45236 - "GET /auth/whoami HTTP/1.1" 200 OK
[WebServer] INFO:     127.0.0.1:45264 - "POST /api/v1/project/ HTTP/1.1" 500 Internal Server Error
[WebServer] ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "/home/xonya/.local/lib/python3.10/site-packages/uvicorn/protocols/http/h11_impl.py", line 407, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
  File "/home/xonya/.local/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__
    return await self.app(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/fastapi/applications.py", line 276, in __call__
    await super().__call__(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/applications.py", line 122, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/errors.py", line 184, in __call__
    raise exc
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/errors.py", line 162, in __call__
    await self.app(scope, receive, _send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/cors.py", line 83, in __call__
    await self.app(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 79, in __call__
    raise exc
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 68, in __call__
    await self.app(scope, receive, sender)
  File "/home/xonya/.local/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 21, in __call__
    raise e
  File "/home/xonya/.local/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__
    await self.app(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/routing.py", line 718, in __call__
    await route.handle(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/routing.py", line 276, in handle
    await self.app(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/routing.py", line 66, in app
    response = await func(request)
  File "/home/xonya/.local/lib/python3.10/site-packages/fastapi/routing.py", line 237, in app
    raw_response = await run_endpoint_function(
  File "/home/xonya/.local/lib/python3.10/site-packages/fastapi/routing.py", line 163, in run_endpoint_function
    return await dependant.call(**values)
  File "/home/xonya/.local/lib/python3.10/site-packages/fractal_server/app/api/v1/project.py", line 91, in create_project
    await db.refresh(db_project)
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/ext/asyncio/session.py", line 157, in refresh
    return await greenlet_spawn(
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 128, in greenlet_spawn
    result = context.switch(value)
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/orm/session.py", line 2348, in refresh
    raise sa_exc.InvalidRequestError(
sqlalchemy.exc.InvalidRequestError: Could not refresh instance '<Project at 0x7f7d2b8bf940>'
[WebServer] INFO:     127.0.0.1:43604 - "POST /api/v1/project/ HTTP/1.1" 500 Internal Server Error
[WebServer] ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "/home/xonya/.local/lib/python3.10/site-packages/uvicorn/protocols/http/h11_impl.py", line 407, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
  File "/home/xonya/.local/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__
    return await self.app(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/fastapi/applications.py", line 276, in __call__
    await super().__call__(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/applications.py", line 122, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/errors.py", line 184, in __call__
    raise exc
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/errors.py", line 162, in __call__
    await self.app(scope, receive, _send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/cors.py", line 83, in __call__
    await self.app(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 79, in __call__
    raise exc
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 68, in __call__
    await self.app(scope, receive, sender)
  File "/home/xonya/.local/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 21, in __call__
    raise e
  File "/home/xonya/.local/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__
    await self.app(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/routing.py", line 718, in __call__
  ✓  5 [chromium] › login_logout.spec.js:6:1 › Login and Logout (1.4s)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/routing.py", line 276, in handle
    await self.app(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/routing.py", line 66, in app
    response = await func(request)
  File "/home/xonya/.local/lib/python3.10/site-packages/fastapi/routing.py", line 237, in app
    raw_response = await run_endpoint_function(
  File "/home/xonya/.local/lib/python3.10/site-packages/fastapi/routing.py", line 163, in run_endpoint_function
    return await dependant.call(**values)
  File "/home/xonya/.local/lib/python3.10/site-packages/fractal_server/app/api/v1/project.py", line 91, in create_project
    await db.refresh(db_project)
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/ext/asyncio/session.py", line 157, in refresh
    return await greenlet_spawn(
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 128, in greenlet_spawn
    result = context.switch(value)
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/orm/session.py", line 2348, in refresh
    raise sa_exc.InvalidRequestError(
sqlalchemy.exc.InvalidRequestError: Could not refresh instance '<Project at 0x7f7d2a6c9600>'
[WebServer] INFO:     127.0.0.1:45256 - "GET /api/v1/project/ HTTP/1.1" 200 OK

After this the database is in an inconsistent state and it is not possible to create projects anymore:

[WebServer] 2023-11-08 10:49:05,137 - create_project - ERROR - (sqlite3.IntegrityError) UNIQUE constraint failed: linkuserproject.project_id, linkuserproject.user_id
[SQL: INSERT INTO linkuserproject (project_id, user_id) VALUES (?, ?)]
[parameters: (38, 1)]
(Background on this error at: https://sqlalche.me/e/14/gkpj)
[WebServer] INFO:     127.0.0.1:38604 - "POST /api/v1/project/ HTTP/1.1" 422 Unprocessable Entity

Inside the database I have the following:

SELECT * FROM project WHERE id > 35;
83qams|0|36
w6zmvb|0|37
 SELECT * FROM linkuserproject WHERE project_id > 35;
36|1
37|1
38|1
39|1

In this state, when I try to create a project its id would be 38, however 38 is already inside the linkuserproject table, so the insert fails.

@tcompa
Copy link
Collaborator Author

tcompa commented Nov 8, 2023

Quick comment again: as of fractal-server 0.3.13, your own PR for using WAL in sqlite is part of fractal-server. I don't know if this matters at all.

@zonia3000
Copy link
Collaborator

I'll also try without WAL.

Here is another one happening during dataset resource creation:

[WebServer] Load projects page
[WebServer] INFO:     127.0.0.1:38406 - "GET /api/alive/ HTTP/1.1" 200 OK
[WebServer] Server info loaded: Alive true - 1.3.4
Including cookie into request to http://localhost:8000/auth/whoami, via handleFetch
[WebServer] INFO:     127.0.0.1:38402 - "POST /api/v1/project/51/dataset/63/resource/ HTTP/1.1" 500 Internal Server Error
[WebServer] ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "/home/xonya/.local/lib/python3.10/site-packages/uvicorn/protocols/http/h11_impl.py", line 407, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
  File "/home/xonya/.local/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__
    return await self.app(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/fastapi/applications.py", line 276, in __call__
    await super().__call__(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/applications.py", line 122, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/errors.py", line 184, in __call__
    raise exc
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/errors.py", line 162, in __call__
    await self.app(scope, receive, _send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/cors.py", line 83, in __call__
    await self.app(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 79, in __call__
    raise exc
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 68, in __call__
    await self.app(scope, receive, sender)
  File "/home/xonya/.local/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 21, in __call__
    raise e
  File "/home/xonya/.local/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__
    await self.app(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/routing.py", line 718, in __call__
    await route.handle(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/routing.py", line 276, in handle
    await self.app(scope, receive, send)
  File "/home/xonya/.local/lib/python3.10/site-packages/starlette/routing.py", line 66, in app
    response = await func(request)
  File "/home/xonya/.local/lib/python3.10/site-packages/fastapi/routing.py", line 237, in app
    raw_response = await run_endpoint_function(
  File "/home/xonya/.local/lib/python3.10/site-packages/fastapi/routing.py", line 163, in run_endpoint_function
    return await dependant.call(**values)
  File "/home/xonya/.local/lib/python3.10/site-packages/fractal_server/app/api/v1/dataset.py", line 179, in create_resource
    await db.refresh(db_resource)
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/ext/asyncio/session.py", line 157, in refresh
    return await greenlet_spawn(
  File "/home/xonya/.local/lib/python3.10/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 128, in greenlet_spawn
    result = context.switch(value)
    raise sa_exc.InvalidRequestError(
sqlalchemy.exc.InvalidRequestError: Could not refresh instance '<Resource at 0x7f4f9d01abc0>'
[WebServer] INFO:     127.0.0.1:38390 - "GET /auth/whoami HTTP/1.1" 200 OK
[WebServer] INFO:     127.0.0.1:38408 - "GET /api/v1/project/ HTTP/1.1" 200 OK
[WebServer] [DELETE] - /api/v1/project/50

@zonia3000
Copy link
Collaborator

Quick comment again: as of fractal-server 0.3.13, your own PR for using WAL in sqlite is part of fractal-server. I don't know if this matters at all.

Just happened with 0.3.12

@zonia3000
Copy link
Collaborator

@tcompa I'm not able to reproduce these errors using PostgreSQL, so I assume it's caused by SQLite. Do you see any problem in configuring the CI pipeline to run with PostgreSQL?

@tcompa
Copy link
Collaborator Author

tcompa commented Nov 8, 2023

I'm not able to reproduce these errors using PostgreSQL, so I assume it's caused by SQLite.

That's good to know!

Do you see any problem in configuring the CI pipeline to run with PostgreSQL?

Short answer: go ahead!

Still, I'd like to understand what goes wrong at some point. We do test the sqlite use case in fractal-server, but apparently this test suite is somewhat different.

@zonia3000
Copy link
Collaborator

Still, I'd like to understand what goes wrong at some point. We do test the sqlite use case in fractal-server, but apparently this test suite is somewhat different.

I think it's only because Playwright executes a lot of calls in parallel and SQLite is not the right tool for these cases:

https://docs.sqlalchemy.org/en/20/dialects/sqlite.html#database-locking-behavior-concurrency

SQLite is not designed for a high level of write concurrency.

This behavior becomes more critical when used in conjunction with the SQLAlchemy ORM. SQLAlchemy’s Session object by default runs within a transaction, and with its autoflush model, may emit DML preceding any SELECT statement. This may lead to a SQLite database that locks more quickly than is expected. The locking mode of SQLite and the pysqlite driver can be manipulated to some degree, however it should be noted that achieving a high degree of write-concurrency with SQLite is a losing battle.

@tcompa
Copy link
Collaborator Author

tcompa commented Nov 8, 2023

Just to have it clear (and only if doesn't take too much effort to run a check):
If you disable parallelism in playright (ref https://playwright.dev/docs/test-parallel#disable-parallelism), would the tests still fail?


This is only as a clarification, but running tests with postgres is perfectly fine..

@tcompa
Copy link
Collaborator Author

tcompa commented Nov 8, 2023

This is only as a clarification, but running tests with postgres is perfectly fine..

In case it's useful, this is how we handle this choice in fractal-server GitHub actions: https://github.com/fractal-analytics-platform/fractal-server/blob/main/.github/workflows/ci.yml. We run both sqlite and postgresql tests, and for postgresql we include the corresponding service in the yaml file.

@zonia3000
Copy link
Collaborator

I confirm that tests are successful with SQLite when disabling parallelism. At the moment the difference in the execution time is not very big, but I assume it will increase when we will add more tests:

  • PostgreSQL with parallelism: 34.1s
  • SQLite without parallelism: 49.3s

Consider that currently most of the time is spent by the initial tasks collection (19.5s).

@zonia3000
Copy link
Collaborator

I'm not sure about the impact on the pipeline total execution time. Even if tests are faster we will spend more time in starting the postgres service and installing the additional dependencies. Maybe for the moment we can just continue with SQLite and parallelism disabled. What do you think?

@tcompa
Copy link
Collaborator Author

tcompa commented Nov 9, 2023

Maybe for the moment we can just continue with SQLite and parallelism disabled. What do you think?

Let's keep it like this for the moment, and switch to postgresql once the number of tests grows.
For the record, starting postgres service in github actions may take aroun 24 seconds.

@zonia3000 zonia3000 mentioned this issue Nov 9, 2023
1 task
@tcompa
Copy link
Collaborator Author

tcompa commented Jan 18, 2024

Upload the coverage report of main somewhere, so that we can view it from the repo page (and not just from the PRs)

Concerning this (low priority!) aspect, an option would be to use this approach: https://github.com/fractal-analytics-platform/fractal-server/blob/main/.github/workflows/benchmarks.yaml#L53-L60.

@zonia3000
Copy link
Collaborator

Before that we have to fix our current coverage setup, that unfortunately it doesn't seem to work correctly. It's some time that I'm looking at it and it has 2 problems:

For example, this is a result from our last PR:

BooleanProperty.svelte               |   11.11 |     3.57 |       0 |   10.34 | 11,18,35-203

It says that the coverage is 11.11%, however running only the unit tests the coverage is at 77%. If you notice at the uncovered lines (the last column) it says that lines 35-203 are not covered, but that file has only 35 lines!

I would like to try with a different code coverage tool (e.g. you can use @vitest/coverage-v8 instead of @vitest/coverage-istanbul), but that investigation will take some time.

@zonia3000
Copy link
Collaborator

Also notice that this seems to depend by Svelte compiler: sveltejs/svelte#7824

And according to the user that opened the issue there might be no solution for this at the moment:

image

@zonia3000
Copy link
Collaborator

Considerations about coverage

For JavaScript there are 2 possible coverage tools: v8 (the JavaScript engine, that provides some native coverage features) and istanbul (a library that instrument the code before running the tests).

While istanbul produces a human readable result, v8 itself simply generates a coverage file in JSON format, but is not able to generate a human readable coverage report. For that tools like c8 or v8-to-istanbul are used.

Unit tests

In the past vitest used c8 as the default code coverage report generator, but now it uses v8 with v8-to-istanbul, and the default coverage method is v8. We are using vite with istanbul (both for the coverage detection and for the report generation) and I wanted to see if something improved using v8 and the new vitest.

So, in order to update vitest from 0.32.4 to 1.x.x I've updated Svelte from 3.58.0 to 4.x.x and Svelte Kit from 1.15.7 to 2.x.x.

Then I configured the tests to run with v8. The 2 tools compute the coverage in different ways and v8 shows many more hits for statements, while branches results seems to be slightly more similar. In any case the results are not very reliable.

Here is a coverage computed using v8:

image

image

image

Here is the same using istanbul:

image

image

image

In general istanbul output looks more useful, since v8 applies a 1x on all the lines just for having loaded the file, however both produces misleading reports, for example adding many "branch not covered" annotations on locations that are not a branch.

Playwright coverage

Playwright coverage can track only the code that runs on the browser (so ssr coverage should be taken in a different way).

Our current Playwright coverage setup is based on this article, that uses istanbul. It produces very weird results, showing line numbers that simply don't exist.

Official Playwright documentation suggests a different setup, using v8 and supporting only Chromium based browsers. I tried with that but the results are useless in the same way.

I'm not sure about what's going on exactly, however, looking at the generated files I suspect that there is something wrong with the source map paths and there are some svelte kit issues about source maps.

Moreover, a standard raw v8 report contains a results array (containing the list of the detected coverage hits) and a source-map-cache fields (used to map the sources). However, the coverage produced by Playwright only contains the results array and there is no way to retrieve the property, that is used by c8 during the generation of the reports.

It seems that someone is building its own Playwright Coverage tool for Svelte, but is a WIP and I wasn't able to make it working: https://github.com/jill64/svelte-playwright-coverage

Backend coverage

Technically, it should be possible to generate a coverage of the backend code with v8 starting the server in this way:

NODE_V8_COVERAGE=coverage-output-dir node build

The application needs to exit gracefully on Ctrl+C, otherwise the coverage process is terminated too. So we need to add the following lines to fractal-web, somewhere:

process.on('SIGINT', () => {
  process.exit(0);
});

The coverage report is built, however both c8 and v8-to-istanbul produces broken reports. I suspect that this is caused by the nested level of source mapping that happens in this case. For example:

  • build/server/chunks/ConfirmActionButton-t3ixxnbe.js
  • -> .svelte-kit/adapter-node/chunks/ConfirmActionButton.js
  • -> src/lib/components/common/ConfirmActionButton.svelte

There are some tools, like https://github.com/jvilk/merge-source-maps, that are able to merge a map of map, but is not clear how to apply this to the coverage reporter when using a raw v8 report.

Moreover, it seems that there are source map problems also for the ssr part: sveltejs/kit#9608

@tcompa
Copy link
Collaborator Author

tcompa commented Jan 26, 2024

Thanks a lot @zonia3000 for this deep exploration!

As we already discussed this morning, let's put coverage-fixing on hold for a while, because of how complex it is. But this detailed log will be useful in the future, if we try once more to get it right (hopefully after some of the existing tools also improve).

@cenfun
Copy link

cenfun commented Mar 5, 2024

@zonia3000 I have built a tool that can generate coverage reports directly from v8 coverage data. It can also be integrated with testing frameworks like Playwright, Vitest, Jest, etc., but it hasn't been tested with svelte too much. You could give it a try when you have the chance.
see https://github.com/cenfun/monocart-coverage-reports

@tcompa
Copy link
Collaborator Author

tcompa commented May 2, 2024

After reviewing this issue, I'd say that what is still relevant is only:

  1. Move the tests to use postgresql

(while there is no current need of working on the coverage)

@tcompa
Copy link
Collaborator Author

tcompa commented May 14, 2024

After reviewing this issue, I'd say that what is still relevant is only:

1. Move the tests to use postgresql

(while there is no current need of working on the coverage)

Now done with #484

@tcompa tcompa closed this as completed May 14, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
testing Tests, including both vitest (unit) and playwrigth
Projects
Development

No branches or pull requests

3 participants