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

Use testcontainers to improve platform coverage #408

Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.coverage
.crate-docs
.python-version
.idea/
.installed.cfg
.mypy_cache/
Expand Down
2 changes: 2 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Changes for crash
Unreleased
==========

- Use Python Testcontainers for integration testing

2024/02/08 0.31.2
=================

Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ def read(path):
extras_require=dict(
test=[
'crate[test]',
'zc.customdoctests<2'
'zc.customdoctests<2',
'cratedb-toolkit[test]',
],
devel=[
'coverage<8',
Expand Down
138 changes: 89 additions & 49 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
import os
import ssl
import sys
Expand All @@ -7,6 +8,8 @@
from unittest import SkipTest, TestCase
from unittest.mock import Mock, patch

from cratedb_toolkit.testing.testcontainers.cratedb import CrateDBTestAdapter
from cratedb_toolkit.util.common import setup_logging
from urllib3.exceptions import LocationParseError

from crate.client.exceptions import ProgrammingError
Expand All @@ -22,32 +25,57 @@
from crate.crash.commands import Command
from crate.crash.outputs import _val_len as val_len
from crate.crash.printer import ColorPrinter
from crate.testing.layer import CrateLayer
from tests import ftouch

if sys.platform != "linux":
raise SkipTest("Integration tests only supported on Linux")

crate_version = os.getenv("CRATEDB_VERSION", "5.5.0")
crate_http_port = 44209
crate_transport_port = 44309
crate_settings = {
'cluster.name': 'Testing44209',
'node.name': 'crate',
'psql.port': 45441,
'lang.js.enabled': True,
'http.port': crate_http_port,
'transport.tcp.port': crate_transport_port
}
node = CrateLayer.from_uri(
f'https://cdn.crate.io/downloads/releases/cratedb/x64_linux/crate-{crate_version}.tar.gz',
'crate',
settings=crate_settings
)
logger = logging.getLogger(__name__)


def _skip_tests_in_ci() -> bool:
"""
Return true if integration tests cannot be run in the CI pipeline
"""
# GitHub Runners have some default envs set, let's use them as an indicator of the CI run
# https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
inside_ci = (os.environ.get("CI") == "true") and ("GITHUB_RUN_ID" in os.environ)

# Due to licensing issues GitHub Runners with MacOS doesn't support Docker
# https://github.com/orgs/community/discussions/25777
os_not_supported = sys.platform == "darwin"

logger.debug("Inside CI: '%s', OS: '%s'", inside_ci, sys.platform)
return inside_ci and os_not_supported


if _skip_tests_in_ci():
raise SkipTest("Platform is not supported")


class EntrypointOpts:
version = os.getenv("CRATEDB_VERSION", "5.4.5")
psql_port = 45441
http_port = 44209
transport_port = 44309
settings = {
"cluster.name": "Testing44209",
"node.name": "crate",
"lang.js.enabled": True,
"psql.port": psql_port,
"http.port": http_port,
"transport.tcp.port": transport_port,
}


node = CrateDBTestAdapter(crate_version=EntrypointOpts.version)


def setUpModule():
node.start()
node.start(
cmd_opts=EntrypointOpts.settings,
# all ports inside container to be bound to the randomly generated ports on the host
ports={EntrypointOpts.http_port: None,
EntrypointOpts.psql_port: None,
EntrypointOpts.transport_port: None})
node.reset()


def tearDownModule():
Expand Down Expand Up @@ -92,6 +120,8 @@ def test_connect(self):


class CommandTest(TestCase):
def setUp(self):
node.reset()

def _output_format(self, format, func, query="select name from sys.cluster"):
orig_argv = sys.argv[:]
Expand Down Expand Up @@ -193,7 +223,7 @@ def test_pprint_duplicate_keys(self):
"| name | name |",
"+------+------+",
"+------+------+\n"])
with CrateShell() as cmd:
with CrateShell(crate_hosts=[node.http_url]) as cmd:
with patch('sys.stdout', new_callable=StringIO) as output:
cmd.pprint([], ['name', 'name'])
self.assertEqual(expected, output.getvalue())
Expand All @@ -205,7 +235,7 @@ def test_pprint_dont_guess_type(self):
"+---------+",
"| 0.50 |",
"+---------+\n"])
with CrateShell() as cmd:
with CrateShell(crate_hosts=[node.http_url]) as cmd:
with patch('sys.stdout', new_callable=StringIO) as output:
cmd.pprint([["0.50"]], ['version'])
self.assertEqual(expected, output.getvalue())
Expand Down Expand Up @@ -286,7 +316,7 @@ def test_multiple_hosts(self):
output = output.getvalue()
lines = output.split('\n')
self.assertRegex(lines[3], r'^\| http://[\d\.:]+ .*\| NULL .*\| FALSE .*\| Server not available')
self.assertRegex(lines[4], r'^\| http://[\d\.:]+. *\| crate .*\| TRUE .*\| OK')
self.assertRegex(lines[4], r'^\| http://.*@?localhost:\d+ *\| crate .*\| TRUE .*\| OK')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this always localhost because of testcontainers now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it really depends on two things:

  1. if you pass host explicitly or rely on the default None value, when invoking get_connection_url method of the CrateDBTestAdapter
  2. where the code is running, on the host or Docker-in-Docker, see the logic here. In our case, both for local development and in the CI pipelines, it will always be localhost

so, I'd say, it's safe to assume localhost won't break anything, unless testcontainers upstream or CI/CD pipelines in crash repo will be changed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, thx!

finally:
try:
os.remove(tmphistory)
Expand Down Expand Up @@ -354,7 +384,7 @@ def test_tabulate_null_int_column(self):
'| 1 |',
'| NULL |',
'+------+\n'])
with CrateShell() as cmd:
with CrateShell(crate_hosts=[node.http_url]) as cmd:
with patch('sys.stdout', new_callable=StringIO) as output:
cmd.pprint(rows, cols=['x'])
self.assertEqual(expected, output.getvalue())
Expand All @@ -370,7 +400,7 @@ def test_tabulate_boolean_int_column(self):
'| FALSE |',
'| 1 |',
'+-------+\n'])
with CrateShell() as cmd:
with CrateShell(crate_hosts=[node.http_url]) as cmd:
with patch('sys.stdout', new_callable=StringIO) as output:
cmd.pprint(rows, cols=['x'])
self.assertEqual(expected, output.getvalue())
Expand All @@ -387,7 +417,7 @@ def test_multiline_header(self):
'| FALSE |',
'| 1 |',
'+-------+\n'])
with CrateShell() as cmd:
with CrateShell(crate_hosts=[node.http_url]) as cmd:
with patch('sys.stdout', new_callable=StringIO) as output:
cmd.pprint(rows, cols=['x\ny'])
self.assertEqual(expected, output.getvalue())
Expand All @@ -406,7 +436,7 @@ def test_multiline_row(self):
'| name string | | |',
'| ) | | |',
'+-----------------------+-----+---+\n'])
with CrateShell() as cmd:
with CrateShell(crate_hosts=[node.http_url]) as cmd:
with patch('sys.stdout', new_callable=StringIO) as output:
cmd.pprint(rows, cols=['show create table foo', 'a', 'b'])
self.assertEqual(expected, output.getvalue())
Expand All @@ -428,7 +458,7 @@ def test_tabulate_empty_line(self):
'| | Planet |',
'+------------------------------------+-------------+\n'])

with CrateShell() as cmd:
with CrateShell(crate_hosts=[node.http_url]) as cmd:
with patch('sys.stdout', new_callable=StringIO) as output:
cmd.pprint(rows, cols=['min(name)', 'kind'])
# assert 0
Expand All @@ -451,7 +481,7 @@ def test_empty_line_first_row_first_column(self):
'| Galactic Sector QQ7 Active J Gamma | Galaxy |',
'+------------------------------------+-------------+\n'])

with CrateShell() as cmd:
with CrateShell(crate_hosts=[node.http_url]) as cmd:
with patch('sys.stdout', new_callable=StringIO) as output:
cmd.pprint(rows, cols=['min(name)', 'kind'])
self.assertEqual(expected, output.getvalue())
Expand All @@ -475,7 +505,7 @@ def test_empty_first_row(self):
'| Alpha Centauri | Alpha - Centauri |',
'+---------------------+-----------------------+\n'])

with CrateShell() as cmd:
with CrateShell(crate_hosts=[node.http_url]) as cmd:
with patch('sys.stdout', new_callable=StringIO) as output:
cmd.pprint(rows, cols=['name', 'replaced'])
self.assertEqual(expected, output.getvalue())
Expand All @@ -497,7 +527,7 @@ def test_any_empty(self):
'| Features and conformance views | FALSE | 3 | SQL_LANGUAGES view |',
'+--------------------------------+--------------+----------------+--------------------+\n'])

with CrateShell() as cmd:
with CrateShell(crate_hosts=[node.http_url]) as cmd:
with patch('sys.stdout', new_callable=StringIO) as output:
cmd.pprint(rows, cols=['feature_name', 'is_supported', 'sub_feature_id', 'sub_feature_name'])
self.assertEqual(expected, output.getvalue())
Expand Down Expand Up @@ -533,7 +563,7 @@ def test_first_column_first_row_empty(self):
'| NULL | 1.0 |',
'+------------------------------------+--------+\n'])

with CrateShell() as cmd:
with CrateShell(crate_hosts=[node.http_url]) as cmd:
with patch('sys.stdout', new_callable=StringIO) as output:
cmd.pprint(rows, cols=['name', '_score'])
self.assertEqual(expected, output.getvalue())
Expand All @@ -552,7 +582,7 @@ def test_error_exit_code(self):
self.assertEqual(e.code, 1)

def test_verbose_with_error_trace(self):
with CrateShell(error_trace=True) as cmd:
with CrateShell(crate_hosts=[node.http_url], error_trace=True) as cmd:
cmd.logger = Mock()
cmd.cursor.execute = Mock(side_effect=ProgrammingError(msg="the error message",
error_trace="error trace"))
Expand All @@ -561,7 +591,7 @@ def test_verbose_with_error_trace(self):
cmd.logger.critical.assert_called_with("\nerror trace")

def test_verbose_no_error_trace(self):
with CrateShell(error_trace=True) as cmd:
with CrateShell(crate_hosts=[node.http_url], error_trace=True) as cmd:
cmd.logger = Mock()
cmd.cursor.execute = Mock(side_effect=ProgrammingError(msg="the error message",
error_trace=None))
Expand All @@ -577,7 +607,7 @@ def test_rendering_object(self):
'+-------------------------------+',
'| {"age": 42, "name": "Arthur"} |',
'+-------------------------------+\n'])
with CrateShell() as cmd:
with CrateShell(crate_hosts=[node.http_url]) as cmd:
with patch('sys.stdout', new_callable=StringIO) as output:
cmd.pprint([[user]], ['user'])
self.assertEqual(expected, output.getvalue())
Expand All @@ -590,7 +620,7 @@ def test_rendering_array(self):
'+--------------------+',
'| ["Arthur", "Ford"] |',
'+--------------------+\n'])
with CrateShell() as cmd:
with CrateShell(crate_hosts=[node.http_url]) as cmd:
with patch('sys.stdout', new_callable=StringIO) as output:
cmd.pprint([[names]], ['names'])
self.assertEqual(expected, output.getvalue())
Expand All @@ -603,14 +633,14 @@ def test_rendering_float(self):
'| 3.1415926535 |',
'| 42.0 |',
'+---------------+\n'])
with CrateShell() as cmd:
with CrateShell(crate_hosts=[node.http_url]) as cmd:
with patch('sys.stdout', new_callable=StringIO) as output:
cmd.pprint([[3.1415926535], [42.0]], ['number'])
self.assertEqual(expected, output.getvalue())

def test_help_command(self):
"""Test output of help command"""
command = CrateShell(is_tty=False)
command = CrateShell(crate_hosts=[node.http_url], is_tty=False)
expected = "\n".join([
'\\? print this help',
'\\autocapitalize toggle automatic capitalization of SQL keywords',
Expand All @@ -630,7 +660,7 @@ def test_help_command(self):
help_ = command.commands['?']
self.assertTrue(isinstance(help_, Command))
self.assertEqual(expected, help_(command))
with CrateShell(is_tty=False) as cmd:
with CrateShell(crate_hosts=[node.http_url], is_tty=False) as cmd:
output = StringIO()
cmd.logger = ColorPrinter(False, stream=output)
text = help_(cmd, 'arg1', 'arg2')
Expand Down Expand Up @@ -671,7 +701,7 @@ def test_wrong_host_format(self):
_create_shell(crate_hosts, False, None, False, args)

def test_command_timeout(self):
with CrateShell(node.http_url) as crash:
with CrateShell(crate_hosts=[node.http_url]) as crash:
crash.process("""
CREATE FUNCTION fib(long)
RETURNS LONG
Expand All @@ -686,24 +716,31 @@ def test_command_timeout(self):
slow_query = "SELECT fib(35)"

# without verbose
with CrateShell(node.http_url,
with CrateShell(crate_hosts=[node.http_url],
error_trace=False,
timeout=timeout) as crash:
crash.logger = Mock()
crash.process(slow_query)
crash.logger.warn.assert_any_call("Use \\connect <server> to connect to one or more servers first.")

# with verbose
with CrateShell(node.http_url,
with CrateShell(crate_hosts=[node.http_url],
error_trace=True,
timeout=timeout) as crash:
crash.logger = Mock()
crash.process(slow_query)
crash.logger.warn.assert_any_call("No more Servers available, exception from last server: HTTPConnectionPool(host='127.0.0.1', port=44209): Read timed out. (read timeout=0.1)")

# Get randomly generated host port bound to the predefined HTTP Interface port inside container
node.cratedb._container.reload()
host_port = node.cratedb._container.ports.get("{}/tcp".format(EntrypointOpts.http_port), [])[0].get("HostPort")

crash.logger.warn.assert_any_call(
"No more Servers available, exception from last server: "
"HTTPConnectionPool(host='localhost', port={}): Read timed out. (read timeout=0.1)".format(host_port))
crash.logger.warn.assert_any_call("Use \\connect <server> to connect to one or more servers first.")

def test_username_param(self):
with CrateShell(node.http_url,
with CrateShell(crate_hosts=[node.http_url],
username='crate') as crash:
self.assertEqual(crash.username, "crate")
self.assertEqual(crash.connection.client.username, "crate")
Expand All @@ -718,7 +755,7 @@ def test_ssl_params(self):
ftouch(key_filename)
ftouch(ca_cert_filename)

with CrateShell(node.http_url,
with CrateShell(crate_hosts=[node.http_url],
verify_ssl=False,
cert_file=cert_filename,
key_file=key_filename,
Expand Down Expand Up @@ -762,7 +799,7 @@ def test_ssl_params_wrong_permision_file(self):
parser.parse_args(argv)

def test_close_shell(self):
crash = CrateShell(node.http_url)
crash = CrateShell(crate_hosts=[node.http_url])
self.assertFalse(crash.is_closed())
self.assertTrue(crash.is_conn_available())

Expand All @@ -777,7 +814,7 @@ def test_close_shell(self):
ctx.exception.message)

def test_connect_info(self):
with CrateShell(node.http_url,
with CrateShell(crate_hosts=[node.http_url],
username='crate',
schema='test') as crash:
self.assertEqual(crash.connect_info.user, "crate")
Expand Down Expand Up @@ -826,8 +863,11 @@ def test_connect_info(self):
@patch.object(CrateShell, "is_conn_available")
def test_connect_info_not_available(self, is_conn_available):
is_conn_available.return_value = False
with CrateShell(node.http_url,
with CrateShell(crate_hosts=[node.http_url],
username='crate',
schema='test') as crash:
self.assertEqual(crash.connect_info.user, None)
self.assertEqual(crash.connect_info.schema, None)


setup_logging(level=logging.INFO)