Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion web/server/codechecker_server/cli/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,20 @@ def is_localhost(address):
return address in valid_values


def config_db_has_products(cfg_sql_server) -> bool:
"""
Returns whether the configuration database already has at least one
product registered in it.
"""
engine = cfg_sql_server.create_engine()
sess = sessionmaker(bind=engine)()
try:
return sess.query(ORMProduct).first() is not None
finally:
sess.close()
engine.dispose()


def server_init_start(args):
"""
Start or manage a CodeChecker report server.
Expand Down Expand Up @@ -976,8 +990,19 @@ def server_init_start(args):
# command line.
workspace_dir = os.path.abspath(args.workspace)
default_product_path = os.path.join(workspace_dir, 'Default.sqlite')

# The 'Default' product should only be auto-created when the config
# database has no products in it at all. Relying only on the
# 'Default.sqlite' file's absence is not enough: if that file was
# deleted (e.g. manually, or as leftover from an unrelated product)
# while the config database still has product(s) registered - be it
# the previous 'Default' entry or any other product - creating a new
# 'Default' product would either collide with an existing one or,
# since 'add_initial_run_database' refuses to run on a non-empty
# config database, crash the server on startup (see #1386).
create_default_product = 'sqlite' in args and \
not os.path.exists(default_product_path)
not os.path.exists(default_product_path) and \
not config_db_has_products(cfg_sql_server)

if create_default_product:
# Create a default product and add it to the configuration database.
Expand Down
100 changes: 100 additions & 0 deletions web/server/tests/unit/test_config_db_has_products.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# -------------------------------------------------------------------------
#
# Part of the CodeChecker project, under the Apache License v2.0 with
# LLVM Exceptions. See LICENSE for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#
# -------------------------------------------------------------------------

"""
Regression tests for
https://github.com/Ericsson/codechecker/issues/1386 (and its duplicate,
https://github.com/Ericsson/codechecker/issues/3810) - starting the server
after 'Default.sqlite' was removed, while the config database still has
product(s) registered, must not attempt to (re-)create the initial
'Default' product.

Previously, whether to auto-create the 'Default' product was decided
purely from 'Default.sqlite' file's absence on disk:

create_default_product = 'sqlite' in args and \\
not os.path.exists(default_product_path)

If that file was deleted while the config database still had product(s)
registered (the old 'Default' entry, or any other product), the server
would still try to create a new initial 'Default' product, which
'add_initial_run_database' explicitly refuses to do on a non-empty config
database - crashing the server on startup.
"""

import unittest

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from codechecker_server.cli.server import config_db_has_products
from codechecker_server.database.config_db_model import Base, Product


class _FakeSqlServer:
"""
Minimal stand-in for a 'codechecker_server.database.database.SQLServer'
subclass, exposing only what 'config_db_has_products' actually uses.
"""

def __init__(self, engine):
self.__engine = engine

def create_engine(self):
return self.__engine


class ConfigDbHasProductsTest(unittest.TestCase):
def setUp(self):
self.engine = create_engine('sqlite:///:memory:')
Base.metadata.create_all(self.engine)
self.sql_server = _FakeSqlServer(self.engine)

def tearDown(self):
self.engine.dispose()

def test_empty_config_db_has_no_products(self):
"""
A brand new, empty config database (e.g. on first-ever server
start) must report that it has no products, so the initial
'Default' product can still be auto-created.
"""
self.assertFalse(config_db_has_products(self.sql_server))

def test_config_db_with_existing_default_product(self):
"""
This is the exact scenario from #1386/#3810: 'Default.sqlite' was
deleted, but the config database still has the 'Default' product
registered. The helper must report this as non-empty, so the
caller knows not to attempt creating a new initial product.
"""
session = sessionmaker(bind=self.engine)()
session.add(Product(
'Default', 'sqlite_step://Default.sqlite', 'Default',
"Default product created at server start."))
session.commit()
session.close()

self.assertTrue(config_db_has_products(self.sql_server))

def test_config_db_with_unrelated_product(self):
"""
Any registered product - not just one literally named 'Default' -
must prevent auto-creation of a new initial 'Default' product.
"""
session = sessionmaker(bind=self.engine)()
session.add(Product(
'my_project', 'postgresql://...', 'My Project', None))
session.commit()
session.close()

self.assertTrue(config_db_has_products(self.sql_server))


if __name__ == "__main__":
unittest.main()
Loading